From fd0795d28545860cdb2f2d29ff97c842c44cd723 Mon Sep 17 00:00:00 2001 From: Thomas Weber Date: Mon, 13 Jul 2020 17:16:31 -0500 Subject: [PATCH] WIP "compatibility layer" to use some blocks from scratch-vm in compiled scripts --- src/compiler/blocks/compiler_compat.js | 75 ++++++++++++++++++ ...3_sounds.js => compiler_scratch3_sound.js} | 6 +- src/compiler/compat-block-utility.js | 13 ++++ src/compiler/compiler.js | 27 +++++-- src/compiler/execute.js | 76 ++++++++++++------- 5 files changed, 163 insertions(+), 34 deletions(-) create mode 100644 src/compiler/blocks/compiler_compat.js rename src/compiler/blocks/{compiler_scratch3_sounds.js => compiler_scratch3_sound.js} (70%) create mode 100644 src/compiler/compat-block-utility.js diff --git a/src/compiler/blocks/compiler_compat.js b/src/compiler/blocks/compiler_compat.js new file mode 100644 index 00000000..53ea8cf0 --- /dev/null +++ b/src/compiler/blocks/compiler_compat.js @@ -0,0 +1,75 @@ +const { BlockUtil, InputUtil, StatementUtil, CompiledInput } = require('../compiler'); + +/** + * @typedef {Object} CompatInfo + * @property {string} extension + */ + +/** + * @type {Object.} + */ +const statements = { + sound_play: { + extension: 'scratch3_sound', + }, +}; + +const inputs = { + sound_volume: { + extension: 'scratch3_sound', + }, +}; + +/** + * @returns {Object. void>} + */ +module.exports.getStatements = () => { + /** @type {Object. void>} */ + const result = {}; + for (const statement of Object.keys(statements)) { + result[statement] = statementCompat; + } + return result; +}; + +/** + * @returns {Object. CompiledInput>} + */ +module.exports.getInputs = () => { + /** @type {Object. CompiledInput>} */ + const result = {}; + for (const input of Object.keys(inputs)) { + result[input] = inputCompat; + } + return result; +}; + +/** + * @param {BlockUtil} util + * @param {CompatInfo} data + */ +const generateCompatCall = (util, data) => { + const opcode = util.opcode; + const inputNames = util.allInputs(); + const extensionReference = `runtime.ext_${data.extension}`; + + let result = 'yield* executeInCompatibilityLayer({'; + for (const inputName of inputNames) { + const compiledInput = util.input(inputName); + result += `"${util.safe(inputName)}": ${compiledInput},`; + } + result += '}, '; + result += `${extensionReference}.getPrimitives()["${util.safe(opcode)}"], `; // TODO: statically compile, support extensions, etc. + result += `${extensionReference}`; + result += `)`; // no semicolon here: that breaks inputs + + return result; +}; + +const statementCompat = /** @param {StatementUtil} util */ (util) => { + util.writeLn(generateCompatCall(util, statements[util.opcode]) + ';'); +}; + +const inputCompat = /** @param {InputUtil} util */ (util) => { + return util.unknown(generateCompatCall(util, inputs[util.opcode])); +}; diff --git a/src/compiler/blocks/compiler_scratch3_sounds.js b/src/compiler/blocks/compiler_scratch3_sound.js similarity index 70% rename from src/compiler/blocks/compiler_scratch3_sounds.js rename to src/compiler/blocks/compiler_scratch3_sound.js index ae4af813..4742be52 100644 --- a/src/compiler/blocks/compiler_scratch3_sounds.js +++ b/src/compiler/blocks/compiler_scratch3_sound.js @@ -14,6 +14,10 @@ module.exports.getStatements = () => { */ module.exports.getInputs = () => { return { - + sound_sounds_menu: soundsMenu, }; }; + +const soundsMenu = /** @param {InputUtil} util */ (util) => { + return util.fieldString('SOUND_MENU'); +}; diff --git a/src/compiler/compat-block-utility.js b/src/compiler/compat-block-utility.js new file mode 100644 index 00000000..3e813ee3 --- /dev/null +++ b/src/compiler/compat-block-utility.js @@ -0,0 +1,13 @@ +const BlockUtility = require('../engine/block-utility'); + +class CompatibilityLayerBlockUtility extends BlockUtility { + constructor() { + super(null, null); + } + + startBranch() { + throw new Error('startBranch is not supported on this BlockUtility'); + } +} + +module.exports = CompatibilityLayerBlockUtility; diff --git a/src/compiler/compiler.js b/src/compiler/compiler.js index e69bcc27..1ff908dc 100644 --- a/src/compiler/compiler.js +++ b/src/compiler/compiler.js @@ -14,13 +14,14 @@ const defaultExtensions = [ require('./blocks/compiler_natives'), require('./blocks/compiler_scratch3_motion'), require('./blocks/compiler_scratch3_looks'), - require('./blocks/compiler_scratch3_sounds'), + require('./blocks/compiler_scratch3_sound'), require('./blocks/compiler_scratch3_event'), 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'), + require('./blocks/compiler_compat'), // TODO: do not load extensions immediately require('./extensions/compiler_pen'), ]; @@ -98,6 +99,14 @@ class BlockUtil { return this.compiler.hints; } + /** + * The block's opcode. + * @type {string} + */ + get opcode() { + return this.block.opcode; + } + /** * Compile an input of this block. * @param {string} name The name of the input. (CONDITION, VALUE, etc.) @@ -118,6 +127,14 @@ class BlockUtil { return this.block.inputs.hasOwnProperty(name) && this.block.inputs[name].block !== null; } + /** + * Get the name of all inputs in this block. + * @returns {string[]} + */ + allInputs() { + return Object.keys(this.block.inputs); + } + /** * Get the field data object. * @param {string} name The name of the field. (VARIABLE, TEXT, etc.) @@ -148,9 +165,7 @@ class BlockUtil { .replace(/'/g, '\\\'') .replace(/"/g, '\\"') .replace(/\n/g, '\\n') - .replace(/\r/g, '\\r') - .replace(/\{/g, '\\x7b') - .replace(/\}/g, '\\x7d'); + .replace(/\r/g, '\\r'); } } @@ -201,9 +216,7 @@ class StatementUtil extends BlockUtil { * @param {string} threads Threads to wait for, should be a call to startHats() */ waitUntilThreadsComplete(threads) { - const v = this.var(); - this.writeLn(`var ${v} = ${threads};`); - this.writeLn(`while (waitThreads(${v})) yield;`); + this.writeLn(`yield* waitThreads(${threads});`); } /** diff --git a/src/compiler/execute.js b/src/compiler/execute.js index 9b8adf35..a51f4232 100644 --- a/src/compiler/execute.js +++ b/src/compiler/execute.js @@ -1,6 +1,10 @@ const Thread = require('../engine/thread'); const Timer = require('../util/timer'); +const CompatibilityLayerBlockUtility = require('./compat-block-utility'); +// single instance used for compatibility layer execution +const compatibilityLayerBlockUtility = new CompatibilityLayerBlockUtility(); + // All the functions defined here will be available to compiled scripts. // The JSDoc annotations define the function's contract. // Most of these functions are only used at runtime by generated scripts. Despite what your editor may say, they are not unused. @@ -27,38 +31,53 @@ const startHats = (requestedHat, optMatchFields) => { /** * Implements "thread waiting", where scripts are halted until all the scripts have finished executing. - * Threads are considered "active" if they are still in the thread list, even if they have STATUS_DONE. - * The current thread's status may be changed to STATUS_YIELD_TICK if all active threads are waiting. + * This is a generator, yield* into it to use it. * @param {Array} threads The list of threads. - * @returns {boolean} true if the script should keep waiting on threads to complete */ -const waitThreads = (threads) => { +const waitThreads = function*(threads) { const runtime = thread.target.runtime; - // determine whether any threads are running - var anyRunning = false; - for (var i = 0; i < threads.length; i++) { - if (runtime.threads.indexOf(threads[i]) !== -1) { - anyRunning = true; - break; + while (true) { + // determine whether any threads are running + var anyRunning = false; + for (var i = 0; i < threads.length; i++) { + if (runtime.threads.indexOf(threads[i]) !== -1) { + anyRunning = true; + break; + } } - } - if (!anyRunning) { - return false; - } - - var allWaiting = true; - for (var i = 0; i < threads.length; i++) { - if (!runtime.isWaitingThread(threads[i])) { - allWaiting = false; - break; + if (!anyRunning) { + // all threads are finished, can resume + return; } - } - if (allWaiting) { - thread.status = 3; // STATUS_YIELD_TICK - } - return true; + var allWaiting = true; + for (var i = 0; i < threads.length; i++) { + if (!runtime.isWaitingThread(threads[i])) { + allWaiting = false; + break; + } + } + if (allWaiting) { + thread.status = 3; // STATUS_YIELD_TICK + } + + yield; + } +}; + +/** + * @param {*} inputs The inputs to pass to the block. + * @returns {*} the value returned by the block, if any. + */ +const executeInCompatibilityLayer = function*(inputs, primMethod, extensionObj) { + compatibilityLayerBlockUtility.thread = thread; + compatibilityLayerBlockUtility.sequencer = thread.target.runtime.sequencer; + var returnValue = primMethod.call(extensionObj, inputs, compatibilityLayerBlockUtility); + if (thread.status !== 0) { + // thread was yielded, TODO + } + return returnValue; }; /** @@ -335,7 +354,12 @@ var targetVariables = target.variables; return ${source}; });`; - return eval(source); + try { + return eval(source); + } catch (e) { + console.error('was unable to compile script', source); + throw e; + } }; execute.createScriptFactory = createScriptFactory;