WIP "compatibility layer" to use some blocks from scratch-vm in compiled scripts

This commit is contained in:
Thomas Weber committed 2020-07-13 17:16:31 -05:00
1 parent fa5cf0bad3
commit fd0795d285
5 files changed
+163 -34

No files matched your search

+75
View File
@@ -0,0 +1,75 @@
const { BlockUtil, InputUtil, StatementUtil, CompiledInput } = require('../compiler');
/**
* @typedef {Object} CompatInfo
* @property {string} extension
*/
/**
* @type {Object.<string, CompatInfo>}
*/
const statements = {
sound_play: {
extension: 'scratch3_sound',
},
};
const inputs = {
sound_volume: {
extension: 'scratch3_sound',
},
};
/**
* @returns {Object.<string, (util: StatementUtil) => void>}
*/
module.exports.getStatements = () => {
/** @type {Object.<string, (util: StatementUtil) => void>} */
const result = {};
for (const statement of Object.keys(statements)) {
result[statement] = statementCompat;
}
return result;
};
/**
* @returns {Object.<string, (util: InputUtil) => CompiledInput>}
*/
module.exports.getInputs = () => {
/** @type {Object.<string, (util: InputUtil) => 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]));
};
@@ -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');
};
+13
View File
@@ -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;
+20 -7
View File
@@ -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});`);
}
/**
+50 -26
View File
@@ -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;