341 lines
8.3 KiB
JavaScript
341 lines
8.3 KiB
JavaScript
const Thread = require('../engine/thread');
|
|
const Timer = require('../util/timer');
|
|
|
|
var jump = (id) => {
|
|
IMMEDIATE = THREAD.functionJumps[id];
|
|
};
|
|
|
|
var jumpLazy = (id) => {
|
|
if (THREAD.warp) {
|
|
jump(id);
|
|
} else {
|
|
THREAD.fn = THREAD.functionJumps[id];
|
|
}
|
|
};
|
|
|
|
var call = (procedureCode, args, resume) => {
|
|
THREAD.callStack.push(THREAD.call);
|
|
THREAD.call = {
|
|
args,
|
|
resume,
|
|
};
|
|
// TODO: check recursion
|
|
const procedure = THREAD.procedures[procedureCode];
|
|
if (procedure.warp || THREAD.warp) {
|
|
THREAD.warp++;
|
|
}
|
|
jump(procedure.label);
|
|
};
|
|
|
|
var end = () => {
|
|
if (THREAD.callStack.length) {
|
|
jump(THREAD.call.resume);
|
|
if (THREAD.warp) {
|
|
THREAD.warp--;
|
|
}
|
|
THREAD.call = THREAD.callStack.pop();
|
|
} else {
|
|
retire();
|
|
}
|
|
};
|
|
|
|
var 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) => {
|
|
// The only falsey values that Number can return is 0 and NaN, both of which are treated as 0.
|
|
return Number(value) || 0;
|
|
};
|
|
|
|
/**
|
|
* Convert a number to something other than NaN.
|
|
* @param {number} number The value to convert.
|
|
* @returns {number}
|
|
*/
|
|
const toNotNaN = (number) => {
|
|
return number || 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 Boolean(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 = Number(v1);
|
|
let n2 = Number(v2);
|
|
if (n1 === 0 && isWhiteSpace(v1)) {
|
|
n1 = NaN;
|
|
} else if (n2 === 0 && isWhiteSpace(v2)) {
|
|
n2 = NaN;
|
|
}
|
|
if (isNaN(n1) || isNaN(n2)) {
|
|
const s1 = String(v1).toLowerCase();
|
|
const s2 = String(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;
|
|
};
|
|
|
|
const ioQuery = (runtime, device, func, args) => {
|
|
// We will assume that the device always exists.
|
|
const devObject = runtime.ioDevices[device];
|
|
return devObject[func].apply(devObject, args);
|
|
};
|
|
|
|
/**
|
|
* Create and start a 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 toListIndex = (index, length) => {
|
|
if (typeof index !== 'number') {
|
|
if (index === 'last') {
|
|
if (length > 0) {
|
|
return length;
|
|
}
|
|
return -1;
|
|
} else if (index === 'random' || index === 'any') {
|
|
if (length > 0) {
|
|
return 1 + Math.floor(Math.random() * length);
|
|
}
|
|
return -1;
|
|
}
|
|
}
|
|
index = Math.floor(toNumber(index));
|
|
if (index < 1 || index > length) {
|
|
return -1;
|
|
}
|
|
return index - 1;
|
|
};
|
|
|
|
const getListItem = (list, idx) => {
|
|
const index = toListIndex(idx, list.value.length);
|
|
if (index === -1) {
|
|
return '';
|
|
}
|
|
return list.value[index];
|
|
};
|
|
|
|
var replaceItemOfList = (list, idx, value) => {
|
|
const index = toListIndex(idx, list.value.length);
|
|
if (index === -1) {
|
|
return;
|
|
}
|
|
list.value[index] = value;
|
|
list._monitorUpToDate = false;
|
|
};
|
|
|
|
var insertAtList = (list, idx, value) => {
|
|
const index = toListIndex(idx, list.value.length + 1);
|
|
if (index === -1) {
|
|
return;
|
|
}
|
|
list.value.splice(index - 1, 0, value);
|
|
list._monitorUpToDate = false;
|
|
};
|
|
|
|
var deleteOfList = (list, idx) => {
|
|
if (idx === 'all') {
|
|
list.value = [];
|
|
return;
|
|
}
|
|
const index = toListIndex(idx, list.value.length);
|
|
if (index === -1) {
|
|
return;
|
|
}
|
|
list.value.splice(index - 1, 1);
|
|
list._monitorUpToDate = false;
|
|
};
|
|
|
|
var mod = (n, modulus) => {
|
|
let result = n % modulus;
|
|
if (result / modulus < 0) result += modulus;
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* If set, the executor will immediately start executing this function when the current function returns.
|
|
* @type {Function}
|
|
*/
|
|
var IMMEDIATE;
|
|
/**
|
|
* The currently running thread.
|
|
* @type {Thread}
|
|
*/
|
|
var THREAD;
|
|
|
|
/**
|
|
* Step a compiled thread.
|
|
* @param {Thread} thread
|
|
*/
|
|
const execute = function (thread) {
|
|
THREAD = thread;
|
|
|
|
thread.fn();
|
|
|
|
while (IMMEDIATE) {
|
|
var fn = IMMEDIATE;
|
|
IMMEDIATE = null;
|
|
fn();
|
|
}
|
|
};
|
|
|
|
const evalCompiledScript = (compiler, _source) => {
|
|
// TODO: this is something that definitely deserves unit tests
|
|
|
|
// Create some of the data that the script will need to execute.
|
|
const thread = compiler.thread;
|
|
const target = compiler.target;
|
|
const runtime = target.runtime;
|
|
const stage = runtime.getTargetForStage();
|
|
|
|
// no reason to access compiler anymore
|
|
compiler = null;
|
|
|
|
// eval will grab references to all variables in this context
|
|
return eval(_source);
|
|
};
|
|
|
|
var createContinuation = (compiler, source) => {
|
|
// TODO: optimize, refactor
|
|
// TODO: support more than just "} else {"
|
|
// TODO: this is something that definitely deserves unit tests
|
|
var result = '(function continuation() {\n';
|
|
var brackets = 0;
|
|
var delBrackets = 0;
|
|
var shouldDelete = false;
|
|
var here = 0;
|
|
var length = source.length;
|
|
while (here < length) {
|
|
var i = source.indexOf('{', here);
|
|
var j = source.indexOf('}', here);
|
|
var k = source.indexOf('return;', here);
|
|
if (k === -1) k = length;
|
|
if (i === -1 && j === -1) {
|
|
if (!shouldDelete) {
|
|
result += source.slice(here, k);
|
|
}
|
|
break;
|
|
}
|
|
if (i === -1) i = length;
|
|
if (j === -1) j = length;
|
|
if (shouldDelete) {
|
|
if (i < j) {
|
|
delBrackets++;
|
|
here = i + 1;
|
|
} else {
|
|
delBrackets--;
|
|
if (!delBrackets) {
|
|
shouldDelete = false;
|
|
}
|
|
here = j + 1;
|
|
}
|
|
} else {
|
|
if (brackets === 0 && k < i && k < j) {
|
|
result += source.slice(here, k);
|
|
break;
|
|
}
|
|
if (i < j) {
|
|
result += source.slice(here, i + 1);
|
|
brackets++;
|
|
here = i + 1;
|
|
} else {
|
|
result += source.slice(here, j);
|
|
here = j + 1;
|
|
if (source.substr(j, 8) === '} else {') {
|
|
if (brackets > 0) {
|
|
result += '} else {';
|
|
here = j + 8;
|
|
} else {
|
|
shouldDelete = true;
|
|
delBrackets = 0;
|
|
}
|
|
} else {
|
|
if (brackets > 0) {
|
|
result += '}';
|
|
brackets--;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
result += '})';
|
|
return evalCompiledScript(compiler, result);
|
|
};
|
|
|
|
execute.createContinuation = createContinuation;
|
|
|
|
module.exports = execute;
|