From 2c9d301da2e9746afbb04e9a4e7e4e5a15878792 Mon Sep 17 00:00:00 2001 From: Thomas Weber Date: Fri, 17 Jan 2025 23:16:30 -0600 Subject: [PATCH] ir: Fix variable cache collision when variable or list field has null ID Fixes https://github.com/TurboWarp/scratch-vm/issues/239 --- src/compiler/irgen.js | 8 ++-- .../tw-block-with-null-for-variable-id.sb3 | Bin 0 -> 3610 bytes ...-with-null-for-variable-id.sb3.tw-snapshot | 39 ++++++++++++++++++ ...-with-null-for-variable-id.sb3.tw-snapshot | 39 ++++++++++++++++++ 4 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 test/fixtures/execute/tw-block-with-null-for-variable-id.sb3 create mode 100644 test/snapshot/__snapshots__/tw-block-with-null-for-variable-id.sb3.tw-snapshot create mode 100644 test/snapshot/__snapshots__/warp-timer/tw-block-with-null-for-variable-id.sb3.tw-snapshot diff --git a/src/compiler/irgen.js b/src/compiler/irgen.js index b1b8ded5..ca3d7f31 100644 --- a/src/compiler/irgen.js +++ b/src/compiler/irgen.js @@ -1229,17 +1229,19 @@ class ScriptTreeGenerator { const variable = block.fields[fieldName]; const id = variable.id; - if (Object.prototype.hasOwnProperty.call(this.variableCache, id)) { + if (id && Object.prototype.hasOwnProperty.call(this.variableCache, id)) { return this.variableCache[id]; } const data = this._descendVariable(id, variable.value, type); - this.variableCache[id] = data; + // If variable ID was null, this might do some unnecessary updates, but that is a rare + // edge case and it won't have any adverse effects anyways. + this.variableCache[data.id] = data; return data; } /** - * @param {string} id The ID of the variable. + * @param {string|null} id The ID of the variable. * @param {string} name The name of the variable. * @param {''|'list'} type The variable type. * @private diff --git a/test/fixtures/execute/tw-block-with-null-for-variable-id.sb3 b/test/fixtures/execute/tw-block-with-null-for-variable-id.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..68b151ee32dce6812521f664a61035fd7e26c307 GIT binary patch literal 3610 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 K&l0SEs(%2^bad(f literal 0 HcmV?d00001 diff --git a/test/snapshot/__snapshots__/tw-block-with-null-for-variable-id.sb3.tw-snapshot b/test/snapshot/__snapshots__/tw-block-with-null-for-variable-id.sb3.tw-snapshot new file mode 100644 index 00000000..17631a6d --- /dev/null +++ b/test/snapshot/__snapshots__/tw-block-with-null-for-variable-id.sb3.tw-snapshot @@ -0,0 +1,39 @@ +// TW Snapshot +// Input SHA-256: 99bb22ac4fb2542ce3c21fa7d2025f03d15392a33850585a4d12f4ec558663ac + +// Sprite1 script +(function factoryXYZ(thread) { const target = thread.target; const runtime = target.runtime; const stage = runtime.getTargetForStage(); +const b0 = runtime.getOpcodeFunction("looks_say"); +const b1 = stage.variables["`jEk@4|i[#Fk?(8x)AV.-my variable"]; +const b2 = stage.variables[")|GMR5fz;%F_H,c0wGVM"]; +return function* genXYZ () { +yield* executeInCompatibilityLayer({"MESSAGE":"plan 2",}, b0, false, false, "c", null); +b1.value = 1; +b2.value = 2; +yield* waitThreads(startHats("event_whenbroadcastreceived", { BROADCAST_OPTION: "check 1" })); +yield* waitThreads(startHats("event_whenbroadcastreceived", { BROADCAST_OPTION: "check 2" })); +yield* executeInCompatibilityLayer({"MESSAGE":"end",}, b0, false, false, "k", null); +retire(); return; +}; }) + +// Sprite1 script +(function factoryXYZ(thread) { const target = thread.target; const runtime = target.runtime; const stage = runtime.getTargetForStage(); +const b0 = stage.variables["`jEk@4|i[#Fk?(8x)AV.-my variable"]; +const b1 = runtime.getOpcodeFunction("looks_say"); +return function* genXYZ () { +if (((+b0.value || 0) === 1)) { +yield* executeInCompatibilityLayer({"MESSAGE":"pass variable 1",}, b1, false, false, "m", null); +} +retire(); return; +}; }) + +// Sprite1 script +(function factoryXYZ(thread) { const target = thread.target; const runtime = target.runtime; const stage = runtime.getTargetForStage(); +const b0 = stage.variables[")|GMR5fz;%F_H,c0wGVM"]; +const b1 = runtime.getOpcodeFunction("looks_say"); +return function* genXYZ () { +if (((+b0.value || 0) === 2)) { +yield* executeInCompatibilityLayer({"MESSAGE":"pass variable 2",}, b1, false, false, "q", null); +} +retire(); return; +}; }) diff --git a/test/snapshot/__snapshots__/warp-timer/tw-block-with-null-for-variable-id.sb3.tw-snapshot b/test/snapshot/__snapshots__/warp-timer/tw-block-with-null-for-variable-id.sb3.tw-snapshot new file mode 100644 index 00000000..17631a6d --- /dev/null +++ b/test/snapshot/__snapshots__/warp-timer/tw-block-with-null-for-variable-id.sb3.tw-snapshot @@ -0,0 +1,39 @@ +// TW Snapshot +// Input SHA-256: 99bb22ac4fb2542ce3c21fa7d2025f03d15392a33850585a4d12f4ec558663ac + +// Sprite1 script +(function factoryXYZ(thread) { const target = thread.target; const runtime = target.runtime; const stage = runtime.getTargetForStage(); +const b0 = runtime.getOpcodeFunction("looks_say"); +const b1 = stage.variables["`jEk@4|i[#Fk?(8x)AV.-my variable"]; +const b2 = stage.variables[")|GMR5fz;%F_H,c0wGVM"]; +return function* genXYZ () { +yield* executeInCompatibilityLayer({"MESSAGE":"plan 2",}, b0, false, false, "c", null); +b1.value = 1; +b2.value = 2; +yield* waitThreads(startHats("event_whenbroadcastreceived", { BROADCAST_OPTION: "check 1" })); +yield* waitThreads(startHats("event_whenbroadcastreceived", { BROADCAST_OPTION: "check 2" })); +yield* executeInCompatibilityLayer({"MESSAGE":"end",}, b0, false, false, "k", null); +retire(); return; +}; }) + +// Sprite1 script +(function factoryXYZ(thread) { const target = thread.target; const runtime = target.runtime; const stage = runtime.getTargetForStage(); +const b0 = stage.variables["`jEk@4|i[#Fk?(8x)AV.-my variable"]; +const b1 = runtime.getOpcodeFunction("looks_say"); +return function* genXYZ () { +if (((+b0.value || 0) === 1)) { +yield* executeInCompatibilityLayer({"MESSAGE":"pass variable 1",}, b1, false, false, "m", null); +} +retire(); return; +}; }) + +// Sprite1 script +(function factoryXYZ(thread) { const target = thread.target; const runtime = target.runtime; const stage = runtime.getTargetForStage(); +const b0 = stage.variables[")|GMR5fz;%F_H,c0wGVM"]; +const b1 = runtime.getOpcodeFunction("looks_say"); +return function* genXYZ () { +if (((+b0.value || 0) === 2)) { +yield* executeInCompatibilityLayer({"MESSAGE":"pass variable 2",}, b1, false, false, "q", null); +} +retire(); return; +}; })