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

Extension loop/conditional support for the compiler #141

Closed
wants to merge 10 commits into from
26 changes: 23 additions & 3 deletions src/compiler/irgen.js
Original file line number Diff line number Diff line change
Expand Up @@ -1219,7 +1219,11 @@ class ScriptTreeGenerator {
const blockInfo = this.getBlockInfo(block.opcode);
if (blockInfo) {
const type = blockInfo.info.blockType;
if (type === BlockType.COMMAND) {
if (
type === BlockType.COMMAND ||
type === BlockType.CONDITIONAL ||
type === BlockType.LOOP
) {
return this.descendCompatLayer(block);
}
}
Expand Down Expand Up @@ -1380,17 +1384,33 @@ class ScriptTreeGenerator {
this.script.yields = true;
const inputs = {};
const fields = {};
const substacks = {};
for (const name of Object.keys(block.inputs)) {
inputs[name] = this.descendInputOfBlock(block, name);
// Substacks are processed differently, into the
// substacks object.
if (!name.startsWith('SUBSTACK')) {
inputs[name] = this.descendInputOfBlock(block, name);
}
}
for (const name of Object.keys(block.fields)) {
fields[name] = block.fields[name].value;
}
const blockInfo = this.getBlockInfo(block.opcode);
const blockType = blockInfo?.info?.blockType ?? BlockType.COMMAND;
if (blockInfo) {
const branchCount = blockInfo.info.branchCount;
for (let i = 1; i <= branchCount; i++) {
const inputName = i === 1 ? 'SUBSTACK' : `SUBSTACK${i}`;
substacks[i] = this.descendSubstack(block, inputName);
}
}
return {
kind: 'compat',
opcode: block.opcode,
inputs,
fields
fields,
substacks,
blockType
};
}

Expand Down
6 changes: 3 additions & 3 deletions src/compiler/jsexecute.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,12 @@ const isPromise = value => (
typeof value === 'object' &&
typeof value.then === 'function'
);
const executeInCompatibilityLayer = function*(inputs, blockFunction, isWarp, useFlags, blockId) {
const executeInCompatibilityLayer = function*(inputs, blockFunction, isWarp, useFlags, blockId, resetStackFrame) {
const thread = globalState.thread;

// reset the stackframe
// reset the stackframe if the block requests it
// we only ever use one stackframe at a time, so this shouldn't cause issues
thread.stackFrames[thread.stackFrames.length - 1].reuse(isWarp);
if (resetStackFrame) thread.stackFrames[thread.stackFrames.length - 1].reuse(isWarp);

const executeBlock = () => {
const blockUtility = globalState.blockUtility;
Expand Down
30 changes: 26 additions & 4 deletions src/compiler/jsgen.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const Cast = require('../util/cast');
const VariablePool = require('./variable-pool');
const jsexecute = require('./jsexecute');
const environment = require('./environment');
const BlockType = require('../extension-support/block-type');

// Imported for JSDoc types, not to actually use
// eslint-disable-next-line no-unused-vars
Expand Down Expand Up @@ -435,7 +436,7 @@ class JSGenerator {

case 'compat':
// Compatibility layer inputs never use flags.
return new TypedInput(`(${this.generateCompatibilityLayerCall(node, false)})`, TYPE_UNKNOWN);
return new TypedInput(`(${this.generateCompatibilityLayerCall(node, false, true)})`, TYPE_UNKNOWN);

case 'constant':
return this.safeConstantInput(node.value);
Expand Down Expand Up @@ -725,7 +726,27 @@ class JSGenerator {
// If the last command in a loop returns a promise, immediately continue to the next iteration.
// If you don't do this, the loop effectively yields twice per iteration and will run at half-speed.
const isLastInLoop = this.isLastBlockInLoop();
this.source += `${this.generateCompatibilityLayerCall(node, isLastInLoop)};\n`;
if (node.blockType === BlockType.COMMAND) {
this.source += `${this.generateCompatibilityLayerCall(node, isLastInLoop, true)};\n`;
} else if (node.blockType === BlockType.LOOP) {
// Only reset the stack frame once (at the start of the loop)
// for interpreter compatibility
// (useful for C blocks that maintain state, like repeat ())
this.source += `thread.stackFrames[thread.stackFrames.length - 1].reuse(this.isWarp);`;
this.source += `while (${this.generateCompatibilityLayerCall(node, isLastInLoop, false)}) {`;
this.descendStack(node.substacks[1] ?? [], new Frame(true));
this.yieldLoop();
this.source += `}\n`;
} else if (node.blockType === BlockType.CONDITIONAL) {
this.source += `switch (${this.generateCompatibilityLayerCall(node, isLastInLoop, true)}) {\n`;
// substackId is always a number
for (const substackId in node.substacks) {
this.source += `case ${substackId}:\n`;
this.descendStack(node.substacks[substackId] ?? [], new Frame(false));
this.source += `break;\n`;
}
this.source += `}\n`;
}
if (isLastInLoop) {
this.source += 'if (hasResumedFromPromise) {hasResumedFromPromise = false;continue;}\n';
}
Expand Down Expand Up @@ -1211,9 +1232,10 @@ class JSGenerator {
* Generate a call into the compatibility layer.
* @param {*} node The "compat" kind node to generate from.
* @param {boolean} setFlags Whether flags should be set describing how this function was processed.
* @param {boolean} resetStackFrame Whether the stack frame should be reset.
* @returns {string} The JS of the call.
*/
generateCompatibilityLayerCall (node, setFlags) {
generateCompatibilityLayerCall (node, setFlags, resetStackFrame) {
const opcode = node.opcode;

let result = 'yield* executeInCompatibilityLayer({';
Expand All @@ -1228,7 +1250,7 @@ class JSGenerator {
result += `"${sanitize(fieldName)}":"${sanitize(field)}",`;
}
const opcodeFunction = this.evaluateOnce(`runtime.getOpcodeFunction("${sanitize(opcode)}")`);
result += `}, ${opcodeFunction}, ${this.isWarp}, ${setFlags}, null)`;
result += `}, ${opcodeFunction}, ${this.isWarp}, ${setFlags}, null, ${resetStackFrame ? 'true' : 'false'})`;

return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ yield;
thread.timer = null;
b0.value = ((+b0.value || 0) + 1);
if (((b0.value || 0) === 1)) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass order is correct (1)",}, b2, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"pass order is correct (1)",}, b2, false, false, null, true);
} else {
yield* executeInCompatibilityLayer({"MESSAGE":("fail order is incorrect 1 != " + ("" + b0.value)),}, b2, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":("fail order is incorrect 1 != " + ("" + b0.value)),}, b2, false, false, null, true);
}
retire();
}; })
Expand All @@ -30,7 +30,7 @@ retire();
const b0 = runtime.getOpcodeFunction("looks_say");
const b1 = stage.variables["p]KODv+)+:l=%NT~j3/d-wait"];
return function* genXYZ () {
yield* executeInCompatibilityLayer({"MESSAGE":"plan 2",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"plan 2",}, b0, false, false, null, true);
thread.timer = timer();
var a0 = Math.max(0, 1000 * (+b1.value || 0));
runtime.requestRedraw();
Expand All @@ -55,7 +55,7 @@ while (thread.timer.timeElapsed() < a2) {
yield;
}
thread.timer = null;
yield* executeInCompatibilityLayer({"MESSAGE":"end",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"end",}, b0, false, false, null, true);
retire();
}; })

Expand All @@ -76,9 +76,9 @@ yield;
thread.timer = null;
b0.value = ((+b0.value || 0) + 1);
if (((b0.value || 0) === 2)) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass order is correct (2)",}, b2, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"pass order is correct (2)",}, b2, false, false, null, true);
} else {
yield* executeInCompatibilityLayer({"MESSAGE":("fail order is incorrect 2 != " + ("" + b0.value)),}, b2, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":("fail order is incorrect 2 != " + ("" + b0.value)),}, b2, false, false, null, true);
}
retire();
}; })
12 changes: 6 additions & 6 deletions test/snapshot/__snapshots__/order-library.sb3.tw-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
const b0 = runtime.getOpcodeFunction("looks_say");
const b1 = stage.variables["):/PVGTvoVRvq(ikGwRE-wait"];
return function* genXYZ () {
yield* executeInCompatibilityLayer({"MESSAGE":"plan 2",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"plan 2",}, b0, false, false, null, true);
thread.timer = timer();
var a0 = Math.max(0, 1000 * (+b1.value || 0));
runtime.requestRedraw();
Expand All @@ -31,7 +31,7 @@ while (thread.timer.timeElapsed() < a2) {
yield;
}
thread.timer = null;
yield* executeInCompatibilityLayer({"MESSAGE":"end",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"end",}, b0, false, false, null, true);
retire();
}; })

Expand All @@ -52,9 +52,9 @@ yield;
thread.timer = null;
b0.value = ((+b0.value || 0) + 1);
if (((b0.value || 0) === 1)) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass order is correct (1)",}, b2, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"pass order is correct (1)",}, b2, false, false, null, true);
} else {
yield* executeInCompatibilityLayer({"MESSAGE":("fail order is incorrect 1 != " + ("" + b0.value)),}, b2, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":("fail order is incorrect 1 != " + ("" + b0.value)),}, b2, false, false, null, true);
}
retire();
}; })
Expand All @@ -76,9 +76,9 @@ yield;
thread.timer = null;
b0.value = ((+b0.value || 0) + 1);
if (((b0.value || 0) === 2)) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass order is correct (2)",}, b2, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"pass order is correct (2)",}, b2, false, false, null, true);
} else {
yield* executeInCompatibilityLayer({"MESSAGE":("fail order is incorrect 2 != " + ("" + b0.value)),}, b2, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":("fail order is incorrect 2 != " + ("" + b0.value)),}, b2, false, false, null, true);
}
retire();
}; })
44 changes: 22 additions & 22 deletions test/snapshot/__snapshots__/tw-NaN.sb3.tw-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -5,67 +5,67 @@
(function factoryXYZ(thread) { const target = thread.target; const runtime = target.runtime; const stage = runtime.getTargetForStage();
const b0 = runtime.getOpcodeFunction("looks_say");
return function* genXYZ () {
yield* executeInCompatibilityLayer({"MESSAGE":"plan 20",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"plan 20",}, b0, false, false, null, true);
if ((("" + (0 * Infinity)).toLowerCase() === "NaN".toLowerCase())) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null, true);
}
if (compareEqual((((0 * Infinity) || 0) * 1), 0)) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null, true);
}
if ((("" + ((Math.acos(1.01) * 180) / Math.PI)).toLowerCase() === "NaN".toLowerCase())) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null, true);
}
if (compareEqual(((((Math.acos(1.01) * 180) / Math.PI) || 0) * 1), 0)) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null, true);
}
if ((("" + ((Math.asin(1.01) * 180) / Math.PI)).toLowerCase() === "NaN".toLowerCase())) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null, true);
}
if (compareEqual(((((Math.asin(1.01) * 180) / Math.PI) || 0) * 1), 0)) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null, true);
}
if ((("" + (0 / 0)).toLowerCase() === "NaN".toLowerCase())) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null, true);
}
if (compareEqual((((0 / 0) || 0) * 1), 0)) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null, true);
}
if ((("" + Math.sqrt(-1)).toLowerCase() === "NaN".toLowerCase())) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null, true);
}
if (compareEqual(((Math.sqrt(-1) || 0) * 1), 0)) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null, true);
}
if ((("" + mod(0, 0)).toLowerCase() === "NaN".toLowerCase())) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null, true);
}
if (compareEqual(((mod(0, 0) || 0) * 1), 0)) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null, true);
}
if ((("" + Math.log(-1)).toLowerCase() === "NaN".toLowerCase())) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null, true);
}
if (compareEqual(((Math.log(-1) || 0) * 1), 0)) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null, true);
}
if ((("" + (Math.log(-1) / Math.LN10)).toLowerCase() === "NaN".toLowerCase())) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null, true);
}
if (compareEqual((((Math.log(-1) / Math.LN10) || 0) * 1), 0)) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null, true);
}
if (compareEqual((((Math.round(Math.sin((Math.PI * ((1 / 0) || 0)) / 180) * 1e10) / 1e10) || 0) * 1), 0)) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null, true);
}
if (compareEqual((((Math.round(Math.cos((Math.PI * ((1 / 0) || 0)) / 180) * 1e10) / 1e10) || 0) * 1), 0)) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null, true);
}
if (compareEqual(((tan(((1 / 0) || 0)) || 0) * 1), 0)) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null, true);
}
if (compareEqual(((runtime.ext_scratch3_operators._random((-1 / 0), (1 / 0)) || 0) * 1), 0)) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null, true);
}
yield* executeInCompatibilityLayer({"MESSAGE":"end",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"end",}, b0, false, false, null, true);
retire();
}; })
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
(function factoryXYZ(thread) { const target = thread.target; const runtime = target.runtime; const stage = runtime.getTargetForStage();
const b0 = runtime.getOpcodeFunction("looks_say");
return function* genXYZ () {
yield* executeInCompatibilityLayer({"MESSAGE":"plan 1",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"plan 1",}, b0, false, false, null, true);
if (!((Infinity + -Infinity) <= 0)) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null, true);
}
yield* executeInCompatibilityLayer({"MESSAGE":"end",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"end",}, b0, false, false, null, true);
retire();
}; })
6 changes: 3 additions & 3 deletions test/snapshot/__snapshots__/tw-all-at-once.sb3.tw-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
(function factoryXYZ(thread) { const target = thread.target; const runtime = target.runtime; const stage = runtime.getTargetForStage();
const b0 = runtime.getOpcodeFunction("looks_say");
return function* genXYZ () {
yield* executeInCompatibilityLayer({"MESSAGE":"plan 1",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"plan 1",}, b0, false, false, null, true);
if (true) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null, true);
}
yield* executeInCompatibilityLayer({"MESSAGE":"end",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"end",}, b0, false, false, null, true);
retire();
}; })
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@
(function factoryXYZ(thread) { const target = thread.target; const runtime = target.runtime; const stage = runtime.getTargetForStage();
const b0 = runtime.getOpcodeFunction("looks_say");
return function* genXYZ () {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null, true);
retire();
}; })

// Sprite1 script
(function factoryXYZ(thread) { const target = thread.target; const runtime = target.runtime; const stage = runtime.getTargetForStage();
const b0 = runtime.getOpcodeFunction("looks_say");
return function* genXYZ () {
yield* executeInCompatibilityLayer({"MESSAGE":"plan 1",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"plan 1",}, b0, false, false, null, true);
yield* waitThreads(startHats("event_whenbroadcastreceived", { BROADCAST_OPTION: "message1" }));
yield* executeInCompatibilityLayer({"MESSAGE":"end",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"end",}, b0, false, false, null, true);
retire();
}; })
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
const b0 = runtime.getOpcodeFunction("looks_say");
const b1 = stage.variables["`jEk@4|i[#Fk?(8x)AV.-my variable"];
return function* genXYZ () {
yield* executeInCompatibilityLayer({"MESSAGE":"plan 1",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"plan 1",}, b0, false, false, null, true);
target.setSize(96);
b1.value = 0;
while (!(100 === Math.round(target.size))) {
Expand All @@ -15,8 +15,8 @@ target.setSize(target.size + ((((100 - Math.round(target.size)) || 0) / 10) || 0
yield;
}
if (((+b1.value || 0) === 20)) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null, true);
}
yield* executeInCompatibilityLayer({"MESSAGE":"end",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"end",}, b0, false, false, null, true);
retire();
}; })
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
const b0 = runtime.getOpcodeFunction("looks_say");
const b1 = stage.variables["`jEk@4|i[#Fk?(8x)AV.-my variable"];
return function* genXYZ () {
yield* executeInCompatibilityLayer({"MESSAGE":"plan 1",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"plan 1",}, b0, false, false, null, true);
b1.value = "#22388a";
if ((("" + b1.value).toLowerCase() === "#22388a".toLowerCase())) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, null, true);
}
yield* executeInCompatibilityLayer({"MESSAGE":"end",}, b0, false, false, null);
yield* executeInCompatibilityLayer({"MESSAGE":"end",}, b0, false, false, null, true);
retire();
}; })
Loading