From 92da52e4fc6aca03bf973fcf5db99d37d78a13e5 Mon Sep 17 00:00:00 2001 From: Thomas Weber Date: Tue, 9 Jun 2020 20:00:17 -0500 Subject: [PATCH] This is progress - if compilation errors, fallback to interpreter - retire compiled threads once done - add control_repeat - add math_whole_number --- src/compiler/blocks/compiler_natives.js | 21 +- .../blocks/compiler_scratch3_control.js | 33 +++- src/compiler/blocks/compiler_scratch3_data.js | 11 ++ .../blocks/compiler_scratch3_events.js | 11 ++ .../blocks/compiler_scratch3_looks.js | 16 +- .../blocks/compiler_scratch3_motion.js | 11 ++ .../blocks/compiler_scratch3_operators.js | 11 ++ .../blocks/compiler_scratch3_procedures.js | 11 ++ .../blocks/compiler_scratch3_sensing.js | 11 ++ .../blocks/compiler_scratch3_sounds.js | 11 ++ src/compiler/compiler.js | 31 ++- src/compiler/executor.js | 9 + src/engine/execute.js | 185 +++++++++++++++++- src/engine/runtime.js | 10 +- src/engine/sequencer.js | 109 ++++++++++- src/engine/thread.js | 2 + 16 files changed, 446 insertions(+), 47 deletions(-) create mode 100644 src/compiler/blocks/compiler_scratch3_data.js create mode 100644 src/compiler/blocks/compiler_scratch3_events.js create mode 100644 src/compiler/blocks/compiler_scratch3_motion.js create mode 100644 src/compiler/blocks/compiler_scratch3_operators.js create mode 100644 src/compiler/blocks/compiler_scratch3_procedures.js create mode 100644 src/compiler/blocks/compiler_scratch3_sensing.js create mode 100644 src/compiler/blocks/compiler_scratch3_sounds.js create mode 100644 src/compiler/executor.js diff --git a/src/compiler/blocks/compiler_natives.js b/src/compiler/blocks/compiler_natives.js index d984eebe..92c7bce0 100644 --- a/src/compiler/blocks/compiler_natives.js +++ b/src/compiler/blocks/compiler_natives.js @@ -1,17 +1,18 @@ module.exports.getStatements = () => { - return { - - } + return { + + }; }; module.exports.getInputs = () => { - return { - math_number: number, - }; + return { + math_number: number, + math_whole_number: number, + }; }; const number = (util) => { - const NUM = util.getFieldUnsafe('NUM'); - const number = Number(NUM); - return number; -}; \ No newline at end of file + const NUM = util.getFieldUnsafe('NUM'); + const number = Number(NUM); + return number; +}; diff --git a/src/compiler/blocks/compiler_scratch3_control.js b/src/compiler/blocks/compiler_scratch3_control.js index e2fe11a4..c007a180 100644 --- a/src/compiler/blocks/compiler_scratch3_control.js +++ b/src/compiler/blocks/compiler_scratch3_control.js @@ -1,19 +1,32 @@ module.exports.getStatements = () => { - return { - control_forever: forever, - } + return { + control_repeat: repeat, + control_forever: forever, + }; }; module.exports.getInputs = () => { - return { + return { + + }; +}; - }; +const repeat = (util) => { + const TIMES = util.getInput('TIMES'); + const SUBSTACK = util.compileSubstack('SUBSTACK'); + const times = util.nextVariable(); + util.writeLn(`var ${times} = ${TIMES};`); + util.writeLn(`while (${times} >= 0.5) {`); + util.writeLn(`${times}--;`); + util.write(SUBSTACK); + util.yieldLoop(); + util.writeLn(`}`); }; const forever = (util) => { - const SUBSTACK = util.compileSubstack('SUBSTACK'); - util.writeLn('while (true) {'); - util.write(SUBSTACK); - util.yieldLoop(); - util.writeLn('}'); + const SUBSTACK = util.compileSubstack('SUBSTACK'); + util.writeLn('while (true) {'); + util.write(SUBSTACK); + util.yieldLoop(); + util.writeLn('}'); }; diff --git a/src/compiler/blocks/compiler_scratch3_data.js b/src/compiler/blocks/compiler_scratch3_data.js new file mode 100644 index 00000000..d312919c --- /dev/null +++ b/src/compiler/blocks/compiler_scratch3_data.js @@ -0,0 +1,11 @@ +module.exports.getStatements = () => { + return { + + }; +}; + +module.exports.getInputs = () => { + return { + + }; +}; diff --git a/src/compiler/blocks/compiler_scratch3_events.js b/src/compiler/blocks/compiler_scratch3_events.js new file mode 100644 index 00000000..d312919c --- /dev/null +++ b/src/compiler/blocks/compiler_scratch3_events.js @@ -0,0 +1,11 @@ +module.exports.getStatements = () => { + return { + + }; +}; + +module.exports.getInputs = () => { + return { + + }; +}; diff --git a/src/compiler/blocks/compiler_scratch3_looks.js b/src/compiler/blocks/compiler_scratch3_looks.js index 376f7a19..c37a5c1f 100644 --- a/src/compiler/blocks/compiler_scratch3_looks.js +++ b/src/compiler/blocks/compiler_scratch3_looks.js @@ -1,16 +1,16 @@ module.exports.getStatements = () => { - return { - looks_changeeffectby: changeEffect, - } + return { + looks_changeeffectby: changeEffect, + }; }; module.exports.getInputs = () => { - return { - - }; + return { + + }; }; const changeEffect = (util) => { - const CHANGE = util.getInput('CHANGE'); - util.writeLn(`target.setEffect("color", target.effects.color + ${CHANGE});`); + const CHANGE = util.getInput('CHANGE'); + util.writeLn(`target.setEffect("color", target.effects.color + ${CHANGE});`); }; diff --git a/src/compiler/blocks/compiler_scratch3_motion.js b/src/compiler/blocks/compiler_scratch3_motion.js new file mode 100644 index 00000000..d312919c --- /dev/null +++ b/src/compiler/blocks/compiler_scratch3_motion.js @@ -0,0 +1,11 @@ +module.exports.getStatements = () => { + return { + + }; +}; + +module.exports.getInputs = () => { + return { + + }; +}; diff --git a/src/compiler/blocks/compiler_scratch3_operators.js b/src/compiler/blocks/compiler_scratch3_operators.js new file mode 100644 index 00000000..d312919c --- /dev/null +++ b/src/compiler/blocks/compiler_scratch3_operators.js @@ -0,0 +1,11 @@ +module.exports.getStatements = () => { + return { + + }; +}; + +module.exports.getInputs = () => { + return { + + }; +}; diff --git a/src/compiler/blocks/compiler_scratch3_procedures.js b/src/compiler/blocks/compiler_scratch3_procedures.js new file mode 100644 index 00000000..d312919c --- /dev/null +++ b/src/compiler/blocks/compiler_scratch3_procedures.js @@ -0,0 +1,11 @@ +module.exports.getStatements = () => { + return { + + }; +}; + +module.exports.getInputs = () => { + return { + + }; +}; diff --git a/src/compiler/blocks/compiler_scratch3_sensing.js b/src/compiler/blocks/compiler_scratch3_sensing.js new file mode 100644 index 00000000..d312919c --- /dev/null +++ b/src/compiler/blocks/compiler_scratch3_sensing.js @@ -0,0 +1,11 @@ +module.exports.getStatements = () => { + return { + + }; +}; + +module.exports.getInputs = () => { + return { + + }; +}; diff --git a/src/compiler/blocks/compiler_scratch3_sounds.js b/src/compiler/blocks/compiler_scratch3_sounds.js new file mode 100644 index 00000000..d312919c --- /dev/null +++ b/src/compiler/blocks/compiler_scratch3_sounds.js @@ -0,0 +1,11 @@ +module.exports.getStatements = () => { + return { + + }; +}; + +module.exports.getInputs = () => { + return { + + }; +}; diff --git a/src/compiler/compiler.js b/src/compiler/compiler.js index fa293297..6b9cd757 100644 --- a/src/compiler/compiler.js +++ b/src/compiler/compiler.js @@ -6,8 +6,15 @@ const inputs = {}; const defaultExtensions = [ require('./blocks/compiler_natives'), - require('./blocks/compiler_scratch3_control'), + require('./blocks/compiler_scratch3_motion'), require('./blocks/compiler_scratch3_looks'), + require('./blocks/compiler_scratch3_sounds'), + require('./blocks/compiler_scratch3_events'), + require('./blocks/compiler_scratch3_control'), + require('./blocks/compiler_scratch3_sensing'), + require('./blocks/compiler_scratch3_operators'), + require('./blocks/compiler_scratch3_data'), + require('./blocks/compiler_scratch3_procedures'), ]; defaultExtensions.forEach((ext) => { @@ -26,7 +33,7 @@ defaultExtensions.forEach((ext) => { } }); -class BLockUtil { +class BlockUtil { constructor(compiler, block) { this.compiler = compiler; this.block = block; @@ -41,13 +48,13 @@ class BLockUtil { } } -class InputUtil extends BLockUtil { +class InputUtil extends BlockUtil { constructor(compiler, block) { super(compiler, block); } } -class StatementUtil extends BLockUtil { +class StatementUtil extends BlockUtil { constructor(compiler, block) { super(compiler, block); this.source = ''; @@ -65,20 +72,26 @@ class StatementUtil extends BLockUtil { this.source += s; } + nextVariable() { + this.compiler.variables++; + return 'var' + this.compiler.variables; + } + compileSubstack(inputName) { const inputValue = this.block.inputs[inputName]; + if (!inputValue) { + // empty substack + return ''; + } const substack = inputValue.block; return this.compiler.compileStack(substack); } } class Compiler { - /** - * - * @param {Thread} thread - */ constructor(thread) { this.thread = thread; + this.variables = 0; } compileInput(parentBlock, inputName) { @@ -110,6 +123,7 @@ class Compiler { const util = new StatementUtil(this, block); let compiler = statements[block.opcode]; if (!compiler) { + log.error('unknown opcode', block); throw new Error('unknown opcode: ' + block.opcode); } @@ -126,6 +140,7 @@ class Compiler { const topBlockId = this.thread.topBlock; const topBlock = this.thread.target.blocks.getBlock(topBlockId); + if (!topBlock) throw new Error('not a hat'); const script = this.compileStack(topBlock.next); diff --git a/src/compiler/executor.js b/src/compiler/executor.js new file mode 100644 index 00000000..8465c343 --- /dev/null +++ b/src/compiler/executor.js @@ -0,0 +1,9 @@ +const execute = function (sequencer, thread) { + const result = thread.generator.next(); + + if (result.done) { + sequencer.retireThread(thread); + } +}; + +module.exports = execute; diff --git a/src/engine/execute.js b/src/engine/execute.js index d3b7c687..df802d69 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -4,6 +4,7 @@ const log = require('../util/log'); const Thread = require('./thread'); const {Map} = require('immutable'); const cast = require('../util/cast'); +const compilerExecute = require('../compiler/executor'); /** * Single BlockUtility instance reused by execute for every pritimive ran. @@ -397,7 +398,189 @@ const _prepareBlockProfiling = function (profiler, blockCached) { * @param {!Thread} thread Thread which to read and execute. */ const execute = function (sequencer, thread) { - thread.generator.next(); + if (thread.isCompiled) { + compilerExecute(sequencer, thread); + return; + } + + const runtime = sequencer.runtime; + + // store sequencer and thread so block functions can access them through + // convenience methods. + blockUtility.sequencer = sequencer; + blockUtility.thread = thread; + + // Current block to execute is the one on the top of the stack. + const currentBlockId = thread.peekStack(); + const currentStackFrame = thread.peekStackFrame(); + + let blockContainer = thread.blockContainer; + let blockCached = BlocksExecuteCache.getCached(blockContainer, currentBlockId, BlockCached); + if (blockCached === null) { + blockContainer = runtime.flyoutBlocks; + blockCached = BlocksExecuteCache.getCached(blockContainer, currentBlockId, BlockCached); + // Stop if block or target no longer exists. + if (blockCached === null) { + // No block found: stop the thread; script no longer exists. + sequencer.retireThread(thread); + return; + } + } + + const ops = blockCached._ops; + const length = ops.length; + let i = 0; + + if (currentStackFrame.reported !== null) { + const reported = currentStackFrame.reported; + // Reinstate all the previous values. + for (; i < reported.length; i++) { + const {opCached: oldOpCached, inputValue} = reported[i]; + + const opCached = ops.find(op => op.id === oldOpCached); + + if (opCached) { + const inputName = opCached._parentKey; + const argValues = opCached._parentValues; + + if (inputName === 'BROADCAST_INPUT') { + // Something is plugged into the broadcast input. + // Cast it to a string. We don't need an id here. + argValues.BROADCAST_OPTION.id = null; + argValues.BROADCAST_OPTION.name = cast.toString(inputValue); + } else { + argValues[inputName] = inputValue; + } + } + } + + // Find the last reported block that is still in the set of operations. + // This way if the last operation was removed, we'll find the next + // candidate. If an earlier block that was performed was removed then + // we'll find the index where the last operation is now. + if (reported.length > 0) { + const lastExisting = reported.reverse().find(report => ops.find(op => op.id === report.opCached)); + if (lastExisting) { + i = ops.findIndex(opCached => opCached.id === lastExisting.opCached) + 1; + } else { + i = 0; + } + } + + // The reporting block must exist and must be the next one in the sequence of operations. + if (thread.justReported !== null && ops[i] && ops[i].id === currentStackFrame.reporting) { + const opCached = ops[i]; + const inputValue = thread.justReported; + + thread.justReported = null; + + const inputName = opCached._parentKey; + const argValues = opCached._parentValues; + + if (inputName === 'BROADCAST_INPUT') { + // Something is plugged into the broadcast input. + // Cast it to a string. We don't need an id here. + argValues.BROADCAST_OPTION.id = null; + argValues.BROADCAST_OPTION.name = cast.toString(inputValue); + } else { + argValues[inputName] = inputValue; + } + + i += 1; + } + + currentStackFrame.reporting = null; + currentStackFrame.reported = null; + } + + const start = i; + + for (; i < length; i++) { + const lastOperation = i === length - 1; + const opCached = ops[i]; + + const blockFunction = opCached._blockFunction; + + // Update values for arguments (inputs). + const argValues = opCached._argValues; + + // Fields are set during opCached initialization. + + // Blocks should glow when a script is starting, + // not after it has finished (see #1404). + // Only blocks in blockContainers that don't forceNoGlow + // should request a glow. + if (!blockContainer.forceNoGlow) { + thread.requestScriptGlowInFrame = true; + } + + // Inputs are set during previous steps in the loop. + + const primitiveReportedValue = blockFunction(argValues, blockUtility); + + // If it's a promise, wait until promise resolves. + if (isPromise(primitiveReportedValue)) { + handlePromise(primitiveReportedValue, sequencer, thread, opCached, lastOperation); + + // Store the already reported values. They will be thawed into the + // future versions of the same operations by block id. The reporting + // operation if it is promise waiting will set its parent value at + // that time. + thread.justReported = null; + currentStackFrame.reporting = ops[i].id; + currentStackFrame.reported = ops.slice(0, i).map(reportedCached => { + const inputName = reportedCached._parentKey; + const reportedValues = reportedCached._parentValues; + + if (inputName === 'BROADCAST_INPUT') { + return { + opCached: reportedCached.id, + inputValue: reportedValues[inputName].BROADCAST_OPTION.name + }; + } + return { + opCached: reportedCached.id, + inputValue: reportedValues[inputName] + }; + }); + + // We are waiting for a promise. Stop running this set of operations + // and continue them later after thawing the reported values. + break; + } else if (thread.status === Thread.STATUS_RUNNING) { + if (lastOperation) { + handleReport(primitiveReportedValue, sequencer, thread, opCached, lastOperation); + } else { + // By definition a block that is not last in the list has a + // parent. + const inputName = opCached._parentKey; + const parentValues = opCached._parentValues; + + if (inputName === 'BROADCAST_INPUT') { + // Something is plugged into the broadcast input. + // Cast it to a string. We don't need an id here. + parentValues.BROADCAST_OPTION.id = null; + parentValues.BROADCAST_OPTION.name = cast.toString(primitiveReportedValue); + } else { + parentValues[inputName] = primitiveReportedValue; + } + } + } + } + + if (runtime.profiler !== null) { + if (blockCached._profiler !== runtime.profiler) { + _prepareBlockProfiling(runtime.profiler, blockCached); + } + // Determine the index that is after the last executed block. `i` is + // currently the block that was just executed. `i + 1` will be the block + // after that. `length` with the min call makes sure we don't try to + // reference an operation outside of the set of operations. + const end = Math.min(i + 1, length); + for (let p = start; p < end; p++) { + ops[p]._profilerFrame.count += 1; + } + } }; module.exports = execute; diff --git a/src/engine/runtime.js b/src/engine/runtime.js index f2467160..1035f3ff 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -1591,7 +1591,15 @@ class Runtime extends EventEmitter { thread.pushStack(id); this.threads.push(thread); - thread.compile(); + // Attempt to compile the script + // If compilation fails, fallback to the standard scratch-vm interpreter + try { + thread.compile(); + thread.isCompiled = true; + } catch (e) { + log.error('Cannot compile thread', e); + thread.isCompiled = false; + } return thread; } diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js index 471765aa..2b6244f8 100644 --- a/src/engine/sequencer.js +++ b/src/engine/sequencer.js @@ -1,6 +1,7 @@ const Timer = require('../util/timer'); const Thread = require('./thread'); const execute = require('./execute.js'); +const compilerExecute = require('../compiler/executor'); /** * Profiler frame name for stepping a single thread. @@ -177,17 +178,107 @@ class Sequencer { * @param {!Thread} thread Thread object to step. */ stepThread (thread) { - if (this.runtime.profiler !== null) { - if (executeProfilerId === -1) { - executeProfilerId = this.runtime.profiler.idByName(executeProfilerFrame); - } - - // Increment the number of times execute is called. - this.runtime.profiler.increment(executeProfilerId); + if (thread.isCompiled) { + compilerExecute(this, thread); + return; } - execute(this, thread); - // TODO: all of the control flow for turbo, warp, ... + let currentBlockId = thread.peekStack(); + if (!currentBlockId) { + // A "null block" - empty branch. + thread.popStack(); + + // Did the null follow a hat block? + if (thread.stack.length === 0) { + thread.status = Thread.STATUS_DONE; + return; + } + } + // Save the current block ID to notice if we did control flow. + while ((currentBlockId = thread.peekStack())) { + let isWarpMode = thread.peekStackFrame().warpMode; + if (isWarpMode && !thread.warpTimer) { + // Initialize warp-mode timer if it hasn't been already. + // This will start counting the thread toward `Sequencer.WARP_TIME`. + thread.warpTimer = new Timer(); + thread.warpTimer.start(); + } + // Execute the current block. + if (this.runtime.profiler !== null) { + if (executeProfilerId === -1) { + executeProfilerId = this.runtime.profiler.idByName(executeProfilerFrame); + } + + // Increment the number of times execute is called. + this.runtime.profiler.increment(executeProfilerId); + } + if (thread.target === null) { + this.retireThread(thread); + } else { + execute(this, thread); + } + thread.blockGlowInFrame = currentBlockId; + // If the thread has yielded or is waiting, yield to other threads. + if (thread.status === Thread.STATUS_YIELD) { + // Mark as running for next iteration. + thread.status = Thread.STATUS_RUNNING; + // In warp mode, yielded blocks are re-executed immediately. + if (isWarpMode && + thread.warpTimer.timeElapsed() <= Sequencer.WARP_TIME) { + continue; + } + return; + } else if (thread.status === Thread.STATUS_PROMISE_WAIT) { + // A promise was returned by the primitive. Yield the thread + // until the promise resolves. Promise resolution should reset + // thread.status to Thread.STATUS_RUNNING. + return; + } else if (thread.status === Thread.STATUS_YIELD_TICK) { + // stepThreads will reset the thread to Thread.STATUS_RUNNING + return; + } + // If no control flow has happened, switch to next block. + if (thread.peekStack() === currentBlockId) { + thread.goToNextBlock(); + } + // If no next block has been found at this point, look on the stack. + while (!thread.peekStack()) { + thread.popStack(); + + if (thread.stack.length === 0) { + // No more stack to run! + thread.status = Thread.STATUS_DONE; + return; + } + + const stackFrame = thread.peekStackFrame(); + isWarpMode = stackFrame.warpMode; + + if (stackFrame.isLoop) { + // The current level of the stack is marked as a loop. + // Return to yield for the frame/tick in general. + // Unless we're in warp mode - then only return if the + // warp timer is up. + if (!isWarpMode || + thread.warpTimer.timeElapsed() > Sequencer.WARP_TIME) { + // Don't do anything to the stack, since loops need + // to be re-executed. + return; + } + // Don't go to the next block for this level of the stack, + // since loops need to be re-executed. + continue; + + } else if (stackFrame.waitingReporter) { + // This level of the stack was waiting for a value. + // This means a reporter has just returned - so don't go + // to the next block for this level of the stack. + return; + } + // Get next block of existing block on the stack. + thread.goToNextBlock(); + } + } } /** diff --git a/src/engine/thread.js b/src/engine/thread.js index c6a6a5a1..908f8bcd 100644 --- a/src/engine/thread.js +++ b/src/engine/thread.js @@ -189,6 +189,8 @@ class Thread { this.warpTimer = null; this.justReported = null; + + this.isCompiled = false; } /**