Skip to content

Commit af3b751

Browse files
committed
ir: Match vanilla quirks when handling variable or list fields with null ID
1 parent 2c9d301 commit af3b751

File tree

5 files changed

+112
-16
lines changed

5 files changed

+112
-16
lines changed

src/compiler/irgen.js

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,12 @@ const LIST_TYPE = 'list';
1919
*/
2020

2121
/**
22-
* Create a variable codegen object.
23-
* @param {'target'|'stage'} scope The scope of this variable -- which object owns it.
24-
* @param {import('../engine/variable.js')} varObj The Scratch Variable
25-
* @returns {*} A variable codegen object.
22+
* @typedef DescendedVariable
23+
* @property {'target'|'stage'} scope
24+
* @property {string} id
25+
* @property {string} name
26+
* @property {boolean} isCloud
2627
*/
27-
const createVariableData = (scope, varObj) => ({
28-
scope,
29-
id: varObj.id,
30-
name: varObj.name,
31-
isCloud: varObj.isCloud
32-
});
3328

3429
/**
3530
* @param {string} code
@@ -1245,21 +1240,33 @@ class ScriptTreeGenerator {
12451240
* @param {string} name The name of the variable.
12461241
* @param {''|'list'} type The variable type.
12471242
* @private
1248-
* @returns {*} A parsed variable object.
1243+
* @returns {DescendedVariable} A parsed variable object.
12491244
*/
12501245
_descendVariable (id, name, type) {
12511246
const target = this.target;
12521247
const stage = this.stage;
12531248

12541249
// Look for by ID in target...
12551250
if (Object.prototype.hasOwnProperty.call(target.variables, id)) {
1256-
return createVariableData('target', target.variables[id]);
1251+
const currVar = target.variables[id];
1252+
return {
1253+
scope: 'target',
1254+
id: currVar.id,
1255+
name: currVar.name,
1256+
isCloud: currVar.isCloud
1257+
};
12571258
}
12581259

12591260
// Look for by ID in stage...
12601261
if (!target.isStage) {
12611262
if (stage && Object.prototype.hasOwnProperty.call(stage.variables, id)) {
1262-
return createVariableData('stage', stage.variables[id]);
1263+
const currVar = stage.variables[id];
1264+
return {
1265+
scope: 'stage',
1266+
id: currVar.id,
1267+
name: currVar.name,
1268+
isCloud: currVar.isCloud
1269+
};
12631270
}
12641271
}
12651272

@@ -1268,7 +1275,12 @@ class ScriptTreeGenerator {
12681275
if (Object.prototype.hasOwnProperty.call(target.variables, varId)) {
12691276
const currVar = target.variables[varId];
12701277
if (currVar.name === name && currVar.type === type) {
1271-
return createVariableData('target', currVar);
1278+
return {
1279+
scope: 'target',
1280+
id: currVar.id,
1281+
name: currVar.name,
1282+
isCloud: currVar.isCloud
1283+
};
12721284
}
12731285
}
12741286
}
@@ -1279,14 +1291,22 @@ class ScriptTreeGenerator {
12791291
if (Object.prototype.hasOwnProperty.call(stage.variables, varId)) {
12801292
const currVar = stage.variables[varId];
12811293
if (currVar.name === name && currVar.type === type) {
1282-
return createVariableData('stage', currVar);
1294+
return {
1295+
scope: 'stage',
1296+
id: currVar.id,
1297+
name: currVar.name,
1298+
isCloud: currVar.isCloud
1299+
};
12831300
}
12841301
}
12851302
}
12861303
}
12871304

12881305
// Create it locally...
12891306
const newVariable = new Variable(id, name, type, false);
1307+
1308+
// Intentionally not using newVariable.id so that this matches vanilla Scratch quirks regarding
1309+
// handling of null variable IDs.
12901310
target.variables[id] = newVariable;
12911311

12921312
if (target.sprite) {
@@ -1300,7 +1320,14 @@ class ScriptTreeGenerator {
13001320
}
13011321
}
13021322

1303-
return createVariableData('target', newVariable);
1323+
return {
1324+
scope: 'target',
1325+
// If the given ID was null, this won't match the .id property of the Variable object.
1326+
// This is intentional to match vanilla Scratch quirks.
1327+
id,
1328+
name: newVariable.name,
1329+
isCloud: newVariable.isCloud
1330+
};
13041331
}
13051332

13061333
descendProcedure (block) {
Binary file not shown.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
const {test} = require('tap');
4+
const VM = require('../../src/virtual-machine');
5+
6+
for (const compilerEnabled of [false, true]) {
7+
const prefix = compilerEnabled ? 'compiler' : 'interpreter';
8+
test(`${prefix} - quirks when block field has literal null for variable ID`, t => {
9+
const vm = new VM();
10+
vm.setCompilerOptions({
11+
enabled: compilerEnabled
12+
});
13+
t.equal(vm.runtime.compilerOptions.enabled, compilerEnabled, 'compiler options sanity check');
14+
15+
// The execute tests ensure that this fixture compiles and runs fine and the snapshot test ensures
16+
// it compiles correctly. This additional test will ensure that the internal variable objects are
17+
// being created with the expected properties.
18+
const fixturePath = path.join(
19+
__dirname,
20+
'../fixtures/execute/tw-automatic-variable-creation-literal-null-id.sb3'
21+
);
22+
23+
vm.loadProject(fs.readFileSync(fixturePath)).then(() => {
24+
vm.greenFlag();
25+
vm.runtime._step();
26+
27+
// Variable does not exist, should get made as local variable in sprite
28+
const variables = vm.runtime.targets[1].variables;
29+
t.equal(Object.keys(variables).length, 1, 'created 1 new variable');
30+
31+
// Scratch quirk - the entry in .variables should have key "null"
32+
const newVariableKey = Object.keys(variables)[0];
33+
t.equal(newVariableKey, 'null', 'key is "null"');
34+
35+
// Scratch quirk - the actual variable.id should be the random string
36+
const newVariable = Object.values(variables)[0];
37+
t.notEqual(newVariable.id, 'null', 'variable.id is not "null"');
38+
t.type(newVariable.id, 'string', 'variable.id is a string');
39+
40+
t.end();
41+
});
42+
});
43+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// TW Snapshot
2+
// Input SHA-256: 4764ae15e39b22b1a071c9ac79f8758f24ef41855802db8674e200fd26139ed0
3+
4+
// Sprite1 script
5+
(function factoryXYZ(thread) { const target = thread.target; const runtime = target.runtime; const stage = runtime.getTargetForStage();
6+
const b0 = runtime.getOpcodeFunction("looks_say");
7+
const b1 = target.variables["null"];
8+
return function* genXYZ () {
9+
yield* executeInCompatibilityLayer({"MESSAGE":"plan 0",}, b0, false, false, "a", null);
10+
b1.value = 5;
11+
yield* executeInCompatibilityLayer({"MESSAGE":"end",}, b0, false, false, "d", null);
12+
retire(); return;
13+
}; })
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// TW Snapshot
2+
// Input SHA-256: 4764ae15e39b22b1a071c9ac79f8758f24ef41855802db8674e200fd26139ed0
3+
4+
// Sprite1 script
5+
(function factoryXYZ(thread) { const target = thread.target; const runtime = target.runtime; const stage = runtime.getTargetForStage();
6+
const b0 = runtime.getOpcodeFunction("looks_say");
7+
const b1 = target.variables["null"];
8+
return function* genXYZ () {
9+
yield* executeInCompatibilityLayer({"MESSAGE":"plan 0",}, b0, false, false, "a", null);
10+
b1.value = 5;
11+
yield* executeInCompatibilityLayer({"MESSAGE":"end",}, b0, false, false, "d", null);
12+
retire(); return;
13+
}; })

0 commit comments

Comments
 (0)