Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/compiler/compat-block-utility.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
const BlockUtility = require('../engine/block-utility');

class CompatibilityLayerBlockUtility extends BlockUtility {
constructor () {
super();
this._stackFrame = {};
}

get stackFrame () {
return this._stackFrame;
}

// Branching operations are not supported.
startBranch () {
throw new Error('startBranch is not supported by this BlockUtility');
Expand All @@ -20,9 +29,10 @@ class CompatibilityLayerBlockUtility extends BlockUtility {
throw new Error('getParam is not supported by this BlockUtility');
}

init (thread, fakeBlockId) {
init (thread, fakeBlockId, stackFrame) {
this.thread = thread;
this.sequencer = thread.target.runtime.sequencer;
this._stackFrame = stackFrame;
thread.stack[0] = fakeBlockId;
}
}
Expand Down
26 changes: 22 additions & 4 deletions src/compiler/irgen.js
Original file line number Diff line number Diff line change
Expand Up @@ -1145,7 +1145,7 @@ 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 @@ -1386,19 +1386,37 @@ class ScriptTreeGenerator {
*/
descendCompatLayer (block) {
this.script.yields = true;

const inputs = {};
const fields = {};
for (const name of Object.keys(block.inputs)) {
inputs[name] = this.descendInputOfBlock(block, name);
if (!name.startsWith('SUBSTACK')) {
inputs[name] = this.descendInputOfBlock(block, name);
}
}

const fields = {};
for (const name of Object.keys(block.fields)) {
fields[name] = block.fields[name].value;
}

const blockInfo = this.getBlockInfo(block.opcode);
const blockType = (blockInfo && blockInfo.info && blockInfo.info.blockType) || BlockType.COMMAND;
const substacks = [];
if (blockType === BlockType.CONDITIONAL || blockType === BlockType.LOOP) {
const branchCount = blockInfo.info.branchCount;
for (let i = 0; i < branchCount; i++) {
const inputName = i === 0 ? 'SUBSTACK' : `SUBSTACK${i + 1}`;
substacks.push(this.descendSubstack(block, inputName));
}
}

return {
kind: 'compat',
opcode: block.opcode,
blockType,
inputs,
fields
fields,
substacks
};
}

Expand Down
17 changes: 10 additions & 7 deletions src/compiler/jsexecute.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ runtimeFunctions.waitThreads = `const waitThreads = function*(threads) {
* @param {function} blockFunction The primitive's function.
* @param {boolean} useFlags Whether to set flags (hasResumedFromPromise)
* @param {string} blockId Block ID to set on the emulated block utility.
* @param {*|null} stackFrame Object to use as stack frame.
* @returns {*} the value returned by the block, if any.
*/
runtimeFunctions.executeInCompatibilityLayer = `let hasResumedFromPromise = false;
Expand Down Expand Up @@ -131,16 +132,13 @@ 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, stackFrame) {
const thread = globalState.thread;
// reset the stackframe
// we only ever use one stackframe at a time, so this shouldn't cause issues
thread.stackFrames[thread.stackFrames.length - 1].reuse(isWarp);
const blockUtility = globalState.blockUtility;
if (!stackFrame) stackFrame = {};
const executeBlock = () => {
const blockUtility = globalState.blockUtility;
blockUtility.init(thread, blockId);
blockUtility.init(thread, blockId, stackFrame);
return blockFunction(inputs, blockUtility);
};
Expand Down Expand Up @@ -195,6 +193,11 @@ const executeInCompatibilityLayer = function*(inputs, blockFunction, isWarp, use
return returnValue;
}`;

/**
* @returns {unknown} An object to use as a stack frame.
*/
runtimeFunctions.persistentStackFrame = `const persistentStackFrame = () => ({});`;

/**
* End the current script.
*/
Expand Down
28 changes: 25 additions & 3 deletions src/compiler/jsgen.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const log = require('../util/log');
const Cast = require('../util/cast');
const BlockType = require('../extension-support/block-type');
const VariablePool = require('./variable-pool');
const jsexecute = require('./jsexecute');
const environment = require('./environment');
Expand Down Expand Up @@ -760,7 +761,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`;

const blockType = node.blockType;
if (blockType === BlockType.COMMAND || blockType === BlockType.HAT) {
this.source += `${this.generateCompatibilityLayerCall(node, isLastInLoop)};\n`;
} else if (blockType === BlockType.CONDITIONAL) {
this.source += `switch (Math.round(${this.generateCompatibilityLayerCall(node, isLastInLoop)})) {\n`;
for (let i = 0; i < node.substacks.length; i++) {
this.source += `case ${i + 1}: {\n`;
this.descendStack(node.substacks[i], new Frame(false));
this.source += `break;\n`;
this.source += `}\n`;
}
this.source += `}\n`;
} else if (node.blockType === BlockType.LOOP) {
const stackFrameName = this.localVariables.next();
this.source += `const ${stackFrameName} = persistentStackFrame();\n`;
this.source += `while (toBoolean(${this.generateCompatibilityLayerCall(node, isLastInLoop, stackFrameName)})) {\n`;
this.descendStack(node.substacks[0], new Frame(true));
this.source += '}\n';
}

if (isLastInLoop) {
this.source += 'if (hasResumedFromPromise) {hasResumedFromPromise = false;continue;}\n';
}
Expand Down Expand Up @@ -1292,9 +1313,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 {string|null} [frameName] Name of the stack frame variable, if any
* @returns {string} The JS of the call.
*/
generateCompatibilityLayerCall (node, setFlags) {
generateCompatibilityLayerCall (node, setFlags, frameName = null) {
const opcode = node.opcode;

let result = 'yield* executeInCompatibilityLayer({';
Expand All @@ -1309,7 +1331,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, ${frameName})`;

return result;
}
Expand Down
11 changes: 11 additions & 0 deletions src/engine/execute.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ const handleReport = function (resolvedValue, sequencer, thread, blockCached, la
const currentBlockId = blockCached.id;
const opcode = blockCached.opcode;
const isHat = blockCached._isHat;
const isConditional = blockCached._isConditional;
const isLoop = blockCached._isLoop;

thread.pushReportedValue(resolvedValue);
if (isHat) {
Expand Down Expand Up @@ -83,6 +85,11 @@ const handleReport = function (resolvedValue, sequencer, thread, blockCached, la
// Predicate returned false: do not allow script to run
sequencer.retireThread(thread);
}
} else if (isConditional) {
const branch = Math.round(resolvedValue);
sequencer.stepToBranch(thread, branch, false);
} else if (isLoop && cast.toBoolean(resolvedValue)) {
sequencer.stepToBranch(thread, 1, true);
} else {
// In a non-hat, report the value visually if necessary if
// at the top of the thread stack.
Expand Down Expand Up @@ -292,6 +299,10 @@ class BlockCached {
this._blockFunction = runtime.getOpcodeFunction(opcode);
this._definedBlockFunction = typeof this._blockFunction !== 'undefined';

const flowing = runtime._flowing[opcode];
this._isConditional = !!(flowing && flowing.conditional);
this._isLoop = !!(flowing && flowing.loop);

// Store the current shadow value if there is a shadow value.
const fieldKeys = Object.keys(fields);
this._isShadowBlock = (
Expand Down
17 changes: 17 additions & 0 deletions src/engine/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,13 @@ class Runtime extends EventEmitter {
*/
this._hats = {};

/**
* Map of opcode to information about whether the block's return value should be interpreted
* for control flow purposes.
* @type {Record<string, {conditional: boolean}>}
*/
this._flowing = {};

/**
* A list of script block IDs that were glowing during the previous frame.
* @type {!Array.<!string>}
Expand Down Expand Up @@ -1120,6 +1127,16 @@ class Runtime extends EventEmitter {
edgeActivated: blockInfo.isEdgeActivated,
restartExistingThreads: blockInfo.shouldRestartExistingThreads
};
} else if (blockInfo.blockType === BlockType.CONDITIONAL) {
this._flowing[opcode] = {
conditional: true,
loop: false
};
} else if (blockInfo.blockType === BlockType.LOOP) {
this._flowing[opcode] = {
conditional: false,
loop: true
};
}
}
} catch (e) {
Expand Down
Binary file added test/fixtures/tw-conditional.sb3
Binary file not shown.
Binary file added test/fixtures/tw-loop.sb3
Binary file not shown.
133 changes: 133 additions & 0 deletions test/integration/tw_conditional_and_loop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
const fs = require('fs');
const path = require('path');
const {test} = require('tap');
const VirtualMachine = require('../../src/virtual-machine');
const Scratch = require('../../src/extension-support/tw-extension-api-common');

// Based on https://github.com/TurboWarp/scratch-vm/pull/141
class LoopsAndThings {
getInfo() {
return {
id: "loopsAndThings",
name: "Loops and things test",
blocks: [
{
opcode: "conditional",
blockType: Scratch.BlockType.CONDITIONAL,
text: "run branch [BRANCH] of",
arguments: {
BRANCH: {
type: Scratch.ArgumentType.NUMBER,
defaultValue: 1
}
},
branchCount: 3
},
{
opcode: "loop",
blockType: Scratch.BlockType.LOOP,
text: "my repeat [TIMES]",
arguments: {
TIMES: {
type: Scratch.ArgumentType.NUMBER,
defaultValue: 10
}
},
},
'---',
{
opcode: "testPromise",
blockType: Scratch.BlockType.REPORTER,
text: "return [VALUE] in a Promise",
arguments: {
VALUE: {
type: Scratch.ArgumentType.STRING,
defaultValue: ''
}
}
}
]
};
}

conditional({BRANCH}, util) {
return Scratch.Cast.toNumber(BRANCH);
}

loop({TIMES}, util) {
const times = Math.round(Scratch.Cast.toNumber(TIMES));
if (typeof util.stackFrame.loopCounter === "undefined") {
util.stackFrame.loopCounter = times;
}
util.stackFrame.loopCounter--;
if (util.stackFrame.loopCounter >= 0) {
return true;
}
}

testPromise({VALUE}) {
return Promise.resolve(VALUE);
}
}

const compilerAndInterpreter = (name, callback) => {
test(`${name} - interpreted`, t => {
callback(t, {
enabled: false
});
});
test(`${name} - compiled`, t => {
callback(t, {
enabled: true
});
});
};

compilerAndInterpreter('CONDITIONAL', (t, co) => {
t.plan(1);

const vm = new VirtualMachine();
vm.setCompilerOptions(co);
vm.extensionManager.addBuiltinExtension('loopsAndThings', LoopsAndThings);
vm.runtime.on('COMPILE_ERROR', () => {
t.fail('Compile error');
});

vm.loadProject(fs.readFileSync(path.join(__dirname, '../fixtures/tw-conditional.sb3'))).then(() => {
let okayCount = 0;
vm.runtime.on('SAY', (target, type, text) => {
if (text === 'OK!') {
okayCount++;
} else if (text === 'end') {
vm.stop();
t.equal(okayCount, 5);
t.end();
}
});

vm.greenFlag();
vm.start();
});
});

compilerAndInterpreter('LOOP', (t, co) => {
t.plan(1);

const vm = new VirtualMachine();
vm.setCompilerOptions(co);
vm.extensionManager.addBuiltinExtension('loopsAndThings', LoopsAndThings);
vm.runtime.on('COMPILE_ERROR', () => {
t.fail('Compile error');
});

vm.loadProject(fs.readFileSync(path.join(__dirname, '../fixtures/tw-loop.sb3'))).then(() => {
vm.runtime.on('SAY', (target, type, text) => {
vm.stop();
t.equal(text, 'a 5 b 50 c 200');
t.end();
});

vm.greenFlag();
vm.start();
});
});
Loading