diff --git a/src/__tests__/__snapshots__/dynamic-repeat.test.ts.snap b/src/__tests__/__snapshots__/dynamic-repeat.test.ts.snap new file mode 100644 index 0000000..1048760 --- /dev/null +++ b/src/__tests__/__snapshots__/dynamic-repeat.test.ts.snap @@ -0,0 +1,79 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dynamic-repeat.sb3 -> leopard 1`] = ` +"import { + Sprite, + Trigger, + Watcher, + Costume, + Color, + Sound, +} from "https://unpkg.com/leopard@^1/dist/index.esm.js"; + +export default class Tests extends Sprite { + constructor(...args) { + super(...args); + + this.costumes = [ + new Costume("Gobo-a", "./Tests/costumes/Gobo-a.svg", { x: 47, y: 55 }), + ]; + + this.sounds = []; + + this.triggers = [ + new Trigger(Trigger.GREEN_FLAG, this.whenGreenFlagClicked), + ]; + } + + *avoidAbscuring(times, i) { + for (let i2 = 0; i2 < 1; i2++) { + for ( + let i3 = 0, times2 = this.x + 6 * this.toNumber(i); + i3 < times2; + i3++ + ) { + this.x += this.toNumber(times); + yield; + } + yield; + } + } + + *avoidAbscuring2(times1, times2, times3, times, i3, i2, i1, i) { + for ( + let i4 = 0, + times4 = + this.x + + (this.toNumber(i) + + this.toNumber(i1) + + (this.toNumber(i2) + this.toNumber(i3))); + i4 < times4; + i4++ + ) { + this.x += + this.toNumber(times1) + + this.toNumber(times2) + + (this.toNumber(times3) + this.toNumber(times)); + yield; + } + } + + *whenGreenFlagClicked() { + this.x = 0; + for (let i = 0; i < 2; i++) { + for (let i2 = 0, times = 2 + 2; i2 < times; i2++) { + this.x += 1; + yield; + } + yield; + } + for (let i3 = 0, times2 = this.x + 4; i3 < times2; i3++) { + this.x += 1; + yield; + } + yield* this.avoidAbscuring(1, 1); + yield* this.avoidAbscuring2(0.15, 0.3, 0.25, 0.3, 1, 1, 2, 4); + } +} +" +`; diff --git a/src/__tests__/dynamic-repeat.sb3 b/src/__tests__/dynamic-repeat.sb3 new file mode 100644 index 0000000..117cb50 Binary files /dev/null and b/src/__tests__/dynamic-repeat.sb3 differ diff --git a/src/__tests__/dynamic-repeat.test.ts b/src/__tests__/dynamic-repeat.test.ts new file mode 100644 index 0000000..c66d86b --- /dev/null +++ b/src/__tests__/dynamic-repeat.test.ts @@ -0,0 +1,14 @@ +import { Project } from ".."; + +import * as fs from "fs"; +import * as path from "path"; + +async function loadProject(filename: string): Promise { + const file = fs.readFileSync(path.join(__dirname, filename)); + return Project.fromSb3(file); +} + +test("dynamic-repeat.sb3 -> leopard", async () => { + const project = await loadProject("dynamic-repeat.sb3"); + expect(project.toLeopard()["Tests/Tests.js"]).toMatchSnapshot(); +}); diff --git a/src/io/leopard/toLeopard.ts b/src/io/leopard/toLeopard.ts index d1820ed..fc88aec 100644 --- a/src/io/leopard/toLeopard.ts +++ b/src/io/leopard/toLeopard.ts @@ -433,6 +433,12 @@ export default function toLeopard( // Leopard names (JS function arguments, which are identifiers). let customBlockArgNameMap: Map = new Map(); + // Maps scripts to a function (from uniqueNameFactory) which produces unique + // names based on the provided default names. This is for "unqualified" + // identifiers, which basically means ordinary variables. That namespace is + // shared with custom block arguments! + let uniqueLocalVarNameMap: Map string> = new Map(); + // Maps variables and lists' Scratch IDs to corresponding Leopard names // (JS properties on `this.vars`). This is shared across all sprites, so // that global (stage) variables' IDs map to the same name regardless what @@ -482,6 +488,8 @@ export default function toLeopard( } } } + + uniqueLocalVarNameMap.set(script, uniqueNameFactory(Object.values(argNameMap))); } } @@ -590,6 +598,17 @@ export default function toLeopard( } function blockToJSWithContext(block: Block, target: Target, script?: Script): string { + // This will be shared by all contained blockToJS calls, which should include + // all following/descendant relative to the provided one. Officially local names + // should be unique to the script, but if we don't have a script, they will still + // be unique to these "nearby" blocks (part of the same blockToJSWithContext call). + let uniqueLocalVarName: (name: string) => string; + if (script && uniqueLocalVarNameMap.has(script)) { + uniqueLocalVarName = uniqueLocalVarNameMap.get(script)!; + } else { + uniqueLocalVarName = uniqueNameFactory(); + } + return blockToJS(block); function increase(leftSide: string, input: BlockInput.Any, allowIncrementDecrement: boolean): string { @@ -1324,13 +1343,29 @@ export default function toLeopard( case OpCode.control_repeat: { satisfiesInputShape = InputShape.Stack; + const timesIsStatic = block.inputs.TIMES.type === "number"; + + // Of course we convert blocks in a descending recursive hierarchy, + // but we still need to make sure we get the relevant local var names + // *before* processing the substack - which might include more "repeat" + // blocks! + const iVar = uniqueLocalVarName("i"); + const timesVar = timesIsStatic ? null : uniqueLocalVarName("times"); + const times = inputToJS(block.inputs.TIMES, InputShape.Number); const substack = inputToJS(block.inputs.SUBSTACK, InputShape.Stack); - blockSource = `for (let i = 0; i < ${times}; i++) { - ${substack}; - ${warp ? "" : "yield;"} - }`; + if (timesIsStatic) { + blockSource = `for (let ${iVar} = 0; ${iVar} < ${times}; ${iVar}++) { + ${substack}; + ${warp ? "" : "yield;"} + }`; + } else { + blockSource = `for (let ${iVar} = 0, ${timesVar} = ${times}; ${iVar} < ${timesVar}; ${iVar}++) { + ${substack}; + ${warp ? "" : "yield;"} + }`; + } break; }