Files
scratch-vm/src/compiler/execute.js
T
Thomas Weber 2a12e70dce Fix some more compiler bugs
- add data_itemnumoflist
- fix compatibility layer operator precedence issues
- don't cache procedures until compilation completed
2020-07-14 12:45:38 -05:00

439 lines
12 KiB
JavaScript

const Thread = require('../engine/thread');
const Timer = require('../util/timer');
const log = require('../util/log');
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.
/**
* End a procedure call.
*/
const endCall = () => {
if (thread.warp) {
thread.warp--;
}
};
/**
* Start hats by opcode.
* @param {string} requestedHat The opcode of the hat to start.
* @param {*} optMatchFields Fields to match.
* @returns {Array} A list of threads that were started.
*/
const startHats = (requestedHat, optMatchFields) => {
const threads = thread.target.runtime.startHats(requestedHat, optMatchFields, undefined);
return threads;
};
/**
* Implements "thread waiting", where scripts are halted until all the scripts have finished executing.
* @param {Array} threads The list of threads.
*/
const waitThreads = function*(threads) {
const runtime = thread.target.runtime;
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) {
// all threads are finished, can resume
return;
}
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;
}
};
/**
* Wait until a Promise resolves or rejects before continuing.
* @param {Promise} promise The promise to wait for.
* @returns {*} the value that the promise resolves to, otherwise undefined if the promise rejects
*/
const waitPromise = function*(promise) {
// TODO: there's quite a lot going on in engine/execute.js, we should see how much of that matters to us
const _thread = thread; // need to store reference to current thread, as promise handlers won't be called from the tick loop
let returnValue = undefined;
promise
.then((value) => {
returnValue = value;
_thread.status = Thread.STATUS_RUNNING;
})
.catch((error) => {
_thread.status = Thread.STATUS_RUNNING;
log.warn('Promise rejected in compiled script:', error);
});
// enter STATUS_PROMISE_WAIT and yield, this will stop script execution until the promise handlers reset the thread status
thread.status = Thread.STATUS_PROMISE_WAIT;
yield;
return returnValue;
};
/**
* Execute a scratch-vm primitive.
* @param {*} inputs The inputs to pass to the block.
* @param {function} blockFunction The primitive's function.
* @returns {*} the value returned by the block, if any.
*/
const executeInCompatibilityLayer = function*(inputs, blockFunction) {
// 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();
const executeBlock = () => {
compatibilityLayerBlockUtility.thread = thread;
compatibilityLayerBlockUtility.sequencer = thread.target.runtime.sequencer;
return blockFunction(inputs, compatibilityLayerBlockUtility);
};
const isPromise = (value) => {
// see engine/execute.js
return (
value !== null &&
typeof value === 'object' &&
typeof value.then === 'function'
);
};
let returnValue = executeBlock();
if (isPromise(returnValue)) {
return yield* waitPromise(returnValue);
}
while (thread.status === Thread.STATUS_YIELD || thread.status === Thread.STATUS_YIELD_TICK) {
// TODO: warp mode
yield;
thread.status = Thread.STATUS_RUNNING;
returnValue = executeBlock();
}
return returnValue;
};
/**
* End the current script.
*/
const retire = () => {
thread.target.runtime.sequencer.retireThread(thread);
};
/**
* Scratch cast to number.
* Similar to Cast.toNumber()
* @param {*} value The value to cast
* @returns {number}
*/
const toNumber = (value) => {
return +value || 0;
};
/**
* Scratch cast to boolean.
* Similar to Cast.toBoolean()
* @param {*} value The value to cast
* @returns {boolean}
*/
const toBoolean = (value) => {
if (typeof value === 'boolean') {
return value;
}
if (typeof value === 'string') {
if (value === '' || value === '0' || value.toLowerCase() === 'false') {
return false;
}
return true;
}
return !!value;
};
/**
* Scratch cast to string.
* Similar to Cast.toString()
* @param {*} value The value to cast
* @returns {string}
*/
const toString = (value) => {
return '' + value;
};
/**
* Check if a value is considered whitespace.
* Similar to Cast.isWhiteSpace()
* @param {*} val Value to check
* @returns {boolean}
*/
const isWhiteSpace = (val) => {
return val === null || (typeof val === 'string' && val.trim().length === 0);
}
/**
* Compare two values using Scratch casting.
* Similar to Cast.compare()
* @param {*} v1 First value to compare.
* @param {*} v2 Second value to compare.
* @returns {number} Negative if v1 < v2, 0 if equal, positive if v1 > v2
*/
const compare = (v1, v2) => {
let n1 = +v1;
let n2 = +v2;
if (n1 === 0 && isWhiteSpace(v1)) {
n1 = NaN;
} else if (n2 === 0 && isWhiteSpace(v2)) {
n2 = NaN;
}
if (isNaN(n1) || isNaN(n2)) {
const s1 = ('' + v1).toLowerCase();
const s2 = ('' + v2).toLowerCase();
if (s1 < s2) {
return -1;
} else if (s1 > s2) {
return 1;
}
return 0;
}
if (
(n1 === Infinity && n2 === Infinity) ||
(n1 === -Infinity && n2 === -Infinity)
) {
return 0;
}
return n1 - n2;
};
/**
* Perform an IO query
* @param {string} device
* @param {string} func
* @param {*} args
* @returns {*}
*/
const ioQuery = (device, func, args) => {
// We will assume that the device always exists.
const devObject = thread.target.runtime.ioDevices[device];
return devObject[func].apply(devObject, args);
};
/**
* Create and start a timer.
* @returns {Timer} A started timer
*/
const timer = () => {
const timer = new Timer();
timer.start();
return timer;
};
/**
* Convert a Scratch list index to a JavaScript list index.
* "all" is not considered as a list index.
* Similar to Cast.toListIndex()
* @param {number} index Scratch list index.
* @param {number} length Length of the list.
* @returns {number} 0 based list index, or -1 if invalid.
*/
var listIndex = (index, length) => {
if (typeof index !== 'number') {
if (index === 'last') {
if (length > 0) {
return length - 1;
}
return -1;
} else if (index === 'random' || index === '*') {
if (length > 0) {
return Math.floor(Math.random() * length);
}
return -1;
}
index = toNumber(index);
}
index = Math.floor(index);
if (index < 1 || index > length) {
return -1;
}
return index - 1;
};
/**
* Get a value from a list.
* @param {import('../engine/variable')} list The list
* @param {*} idx The 1-indexed index in the list.
* @returns The list item, otherwise empty string if it does not exist.
*/
const listGet = (list, idx) => {
const index = listIndex(idx, list.value.length);
if (index === -1) {
return '';
}
return list.value[index];
};
/**
* Replace a value in a list.
* @param {import('../engine/variable')} list The list
* @param {*} idx List index, Scratch style.
* @param {*} value The new value.
*/
const listReplace = (list, idx, value) => {
const index = listIndex(idx, list.value.length);
if (index === -1) {
return;
}
list.value[index] = value;
list._monitorUpToDate = false;
};
/**
* Insert a value in a list.
* @param {import('../engine/variable')} list The list.
* @param {*} idx The Scratch index in the list.
* @param {*} value The value to insert.
*/
const listInsert = (list, idx, value) => {
const index = listIndex(idx, list.value.length + 1);
if (index === -1) {
return;
}
list.value.splice(index, 0, value);
list._monitorUpToDate = false;
};
/**
* Delete a value from a list.
* @param {import('../engine/variable')} list The list.
* @param {*} idx The Scratch index in the list.
*/
const listDelete = (list, idx) => {
if (idx === 'all') {
list.value = [];
return;
}
const index = listIndex(idx, list.value.length);
if (index === -1) {
return;
}
list.value.splice(index, 1);
list._monitorUpToDate = false;
};
/**
* Return whether a list contains a value.
* @param {import('../engine/variable')} list The list.
* @param {*} item The value to search for.
* @returns {boolean} True if the list contains the item
*/
const listContains = (list, item) => {
// TODO: evaluate whether indexOf is worthwhile here
if (list.value.indexOf(item) !== -1) {
return true;
}
for (let i = 0; i < list.value.length; i++) {
if (compare(list.value[i], item) === 0) {
return true;
}
}
return false;
};
/**
* Find the 1-indexed index of an item in a list.
* @param {import('../engine/variable')} list The list.
* @param {*} item The item to search for
* @returns {number} The 1-indexed index of the item in the list, otherwise 0
*/
const listIndexOf = (list, item) => {
for (var i = 0; i < list.value.length; i++) {
if (compare(list.value[i], item) === 0) {
return i + 1;
}
}
return 0;
};
/**
* Implements Scratch modulo (floored division instead of truncated division)
* @param {number} n
* @param {number} modulus
* @returns {number}
*/
const mod = (n, modulus) => {
let result = n % modulus;
if (result / modulus < 0) result += modulus;
return result;
};
/**
* The currently running thread.
* @type {Thread}
*/
var thread;
/**
* Step a compiled thread.
* @param {Thread} _thread
*/
const execute = (_thread) => {
thread = _thread;
_thread.generator.next();
};
/**
* Evaluate a continuation from its source code.
* Prepares the necessary environment.
* @param {string} source
* @param {Compiler} compiler
*/
const createScriptFactory = (_source, compiler) => {
let source = `(function f(target) {
var runtime = target.runtime;
var stage = runtime.getTargetForStage();\n`;
for (const data of Object.keys(compiler.factoryVariables)) {
const varName = compiler.factoryVariables[data];
source += `var ${varName} = ${data};\n`;
}
source += `return ${_source}; });`;
// try to get the GC to drop references to these variables, they have no reason to stick around at runtime and can be rather large
// TODO: see if this works or is necessary
_source = null;
compiler = null;
try {
return eval(source);
} catch (e) {
console.error('was unable to compile script', source);
throw e;
}
};
execute.createScriptFactory = createScriptFactory;
module.exports = execute;