Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

toLeopard: Store # of times to repeat statically when it's a block #122

Merged
merged 8 commits into from
Jul 9, 2024
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