Skip to content
79 changes: 79 additions & 0 deletions src/__tests__/__snapshots__/dynamic-repeat.test.ts.snap
Original file line number Diff line number Diff line change
@@ -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);
}
}
"
`;
Binary file added src/__tests__/dynamic-repeat.sb3
Binary file not shown.
14 changes: 14 additions & 0 deletions src/__tests__/dynamic-repeat.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Project } from "..";

import * as fs from "fs";
import * as path from "path";

async function loadProject(filename: string): Promise<Project> {
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();
});
43 changes: 39 additions & 4 deletions src/io/leopard/toLeopard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,12 @@ export default function toLeopard(
// Leopard names (JS function arguments, which are identifiers).
let customBlockArgNameMap: Map<Script, { [key: string]: string }> = 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<Script, (name: string) => 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
Expand Down Expand Up @@ -482,6 +488,8 @@ export default function toLeopard(
}
}
}

uniqueLocalVarNameMap.set(script, uniqueNameFactory(Object.values(argNameMap)));
}
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}
Expand Down
Loading