WIP "compatibility layer" to use some blocks from scratch-vm in compiled scripts
This commit is contained in:
5 files changed
+163
-34
No files matched your search
@@ -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]));
|
||||
};
|
||||
+5
-1
@@ -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');
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user