scratch-vm/src/engine/execute.js

282 lines
11 KiB
JavaScript
Raw Normal View History

2017-04-17 15:10:04 -04:00
const log = require('../util/log');
const Thread = require('./thread');
2016-08-23 18:12:32 -04:00
/**
* Utility function to determine if a value is a Promise.
* @param {*} value Value to check for a Promise.
2017-02-01 15:59:50 -05:00
* @return {boolean} True if the value appears to be a Promise.
2016-08-23 18:12:32 -04:00
*/
2017-04-17 15:10:04 -04:00
const isPromise = function (value) {
2016-08-23 18:12:32 -04:00
return value && value.then && typeof value.then === 'function';
};
/**
* Execute a block.
* @param {!Sequencer} sequencer Which sequencer is executing.
* @param {!Thread} thread Thread which to read and execute.
*/
const execute = function (sequencer, thread) {
2017-04-17 15:10:04 -04:00
const runtime = sequencer.runtime;
const target = thread.target;
2016-10-13 17:15:49 -04:00
// Stop if block or target no longer exists.
2017-02-16 05:54:17 -05:00
if (target === null) {
// No block found: stop the thread; script no longer exists.
sequencer.retireThread(thread);
return;
}
2016-10-13 17:15:49 -04:00
2017-02-11 09:26:23 -05:00
// Current block to execute is the one on the top of the stack.
2017-04-17 15:10:04 -04:00
const currentBlockId = thread.peekStack();
const currentStackFrame = thread.peekStackFrame();
2017-02-11 09:26:23 -05:00
let blockContainer;
if (thread.updateMonitor) {
blockContainer = runtime.monitorBlocks;
} else {
blockContainer = target.blocks;
}
2017-04-17 15:10:04 -04:00
let block = blockContainer.getBlock(currentBlockId);
2017-02-16 05:54:17 -05:00
if (typeof block === 'undefined') {
2016-10-13 17:15:49 -04:00
blockContainer = runtime.flyoutBlocks;
2017-02-11 09:26:23 -05:00
block = blockContainer.getBlock(currentBlockId);
// Stop if block or target no longer exists.
2017-02-16 05:54:17 -05:00
if (typeof block === 'undefined') {
2017-02-11 09:26:23 -05:00
// No block found: stop the thread; script no longer exists.
sequencer.retireThread(thread);
return;
}
2016-10-13 17:15:49 -04:00
}
2017-02-11 09:26:23 -05:00
2017-04-17 15:10:04 -04:00
const opcode = blockContainer.getOpcode(block);
const fields = blockContainer.getFields(block);
const inputs = blockContainer.getInputs(block);
const blockFunction = runtime.getOpcodeFunction(opcode);
const isHat = runtime.getIsHat(opcode);
2016-10-13 17:15:49 -04:00
if (!opcode) {
2017-04-17 15:10:04 -04:00
log.warn(`Could not get opcode for block: ${currentBlockId}`);
return;
}
/**
* Handle any reported value from the primitive, either directly returned
* or after a promise resolves.
* @param {*} resolvedValue Value eventually returned from the primitive.
*/
// @todo move this to callback attached to the thread when we have performance
// metrics (dd)
2017-04-17 15:10:04 -04:00
const handleReport = function (resolvedValue) {
thread.pushReportedValue(resolvedValue);
if (isHat) {
// Hat predicate was evaluated.
if (runtime.getIsEdgeActivatedHat(opcode)) {
// If this is an edge-activated hat, only proceed if
// the value is true and used to be false.
2017-04-17 15:10:04 -04:00
const oldEdgeValue = runtime.updateEdgeActivatedValue(
currentBlockId,
resolvedValue
);
2017-04-17 15:10:04 -04:00
const edgeWasActivated = !oldEdgeValue && resolvedValue;
if (!edgeWasActivated) {
sequencer.retireThread(thread);
}
2016-10-24 10:28:31 -04:00
} else {
// Not an edge-activated hat: retire the thread
// if predicate was false.
2016-10-24 10:28:31 -04:00
if (!resolvedValue) {
sequencer.retireThread(thread);
}
}
} else {
// In a non-hat, report the value visually if necessary if
// at the top of the thread stack.
if (typeof resolvedValue !== 'undefined' && thread.atStackTop()) {
if (thread.showVisualReport) {
runtime.visualReport(currentBlockId, resolvedValue);
}
if (thread.updateMonitor) {
runtime.requestUpdateMonitor({
id: currentBlockId,
2017-05-10 17:00:08 -04:00
value: String(resolvedValue)
2017-05-11 15:23:11 -04:00
});
}
}
// Finished any yields.
thread.status = Thread.STATUS_RUNNING;
}
};
2016-08-23 18:12:32 -04:00
// Hats and single-field shadows are implemented slightly differently
// from regular blocks.
// For hats: if they have an associated block function,
// it's treated as a predicate; if not, execution will proceed as a no-op.
// For single-field shadows: If the block has a single field, and no inputs,
// immediately return the value of the field.
2017-02-16 05:54:17 -05:00
if (typeof blockFunction === 'undefined') {
if (isHat) {
// Skip through the block (hat with no predicate).
return;
2017-02-11 09:26:23 -05:00
}
2017-04-17 15:10:04 -04:00
const keys = Object.keys(fields);
2017-02-11 09:26:23 -05:00
if (keys.length === 1 && Object.keys(inputs).length === 0) {
// One field and no inputs - treat as arg.
handleReport(fields[keys[0]].value);
} else {
2017-04-17 15:10:04 -04:00
log.warn(`Could not get implementation for opcode: ${opcode}`);
2016-08-23 18:12:32 -04:00
}
2017-02-11 09:26:23 -05:00
thread.requestScriptGlowInFrame = true;
return;
}
// Generate values for arguments (inputs).
2017-04-17 15:10:04 -04:00
const argValues = {};
// Add all fields on this block to the argValues.
2017-04-17 15:10:04 -04:00
for (const fieldName in fields) {
2017-02-09 21:12:47 -05:00
if (!fields.hasOwnProperty(fieldName)) continue;
argValues[fieldName] = fields[fieldName].value;
}
// Recursively evaluate input blocks.
2017-04-17 15:10:04 -04:00
for (const inputName in inputs) {
2017-02-09 21:12:47 -05:00
if (!inputs.hasOwnProperty(inputName)) continue;
2017-04-17 15:10:04 -04:00
const input = inputs[inputName];
const inputBlockId = input.block;
2016-06-20 14:24:00 -04:00
// Is there no value for this input waiting in the stack frame?
2017-02-16 05:54:17 -05:00
if (inputBlockId !== null && typeof currentStackFrame.reported[inputName] === 'undefined') {
2016-06-20 14:24:00 -04:00
// If there's not, we need to evaluate the block.
// Push to the stack to evaluate the reporter block.
thread.pushStack(inputBlockId);
// Save name of input for `Thread.pushReportedValue`.
currentStackFrame.waitingReporter = inputName;
// Actually execute the block.
execute(sequencer, thread);
if (thread.status === Thread.STATUS_PROMISE_WAIT) {
return;
}
2017-02-11 09:26:23 -05:00
// Execution returned immediately,
// and presumably a value was reported, so pop the stack.
currentStackFrame.waitingReporter = null;
thread.popStack();
}
argValues[inputName] = currentStackFrame.reported[inputName];
}
// Add any mutation to args (e.g., for procedures).
2017-04-17 15:10:04 -04:00
const mutation = blockContainer.getMutation(block);
2017-02-11 09:26:23 -05:00
if (mutation !== null) {
argValues.mutation = mutation;
}
// If we've gotten this far, all of the input blocks are evaluated,
// and `argValues` is fully populated. So, execute the block primitive.
// First, clear `currentStackFrame.reported`, so any subsequent execution
2016-08-10 11:27:21 -04:00
// (e.g., on return from a branch) gets fresh inputs.
currentStackFrame.reported = {};
2017-04-17 15:10:04 -04:00
let primitiveReportedValue = null;
primitiveReportedValue = blockFunction(argValues, {
2016-08-23 18:12:32 -04:00
stackFrame: currentStackFrame.executionContext,
target: target,
yield: function () {
thread.status = Thread.STATUS_YIELD;
2016-07-01 10:44:43 -04:00
},
startBranch: function (branchNum, isLoop) {
sequencer.stepToBranch(thread, branchNum, isLoop);
},
stopAll: function () {
runtime.stopAll();
},
stopOtherTargetThreads: function () {
runtime.stopForTarget(target, thread);
},
stopThisScript: function () {
thread.stopThisScript();
},
startProcedure: function (procedureCode) {
sequencer.stepToProcedure(thread, procedureCode);
},
getProcedureParamNames: function (procedureCode) {
return blockContainer.getProcedureParamNames(procedureCode);
2016-10-13 13:11:26 -04:00
},
pushParam: function (paramName, paramValue) {
thread.pushParam(paramName, paramValue);
},
getParam: function (paramName) {
return thread.getParam(paramName);
},
startHats: function (requestedHat, optMatchFields, optTarget) {
2016-08-23 18:12:32 -04:00
return (
runtime.startHats(requestedHat, optMatchFields, optTarget)
2016-08-23 18:12:32 -04:00
);
},
ioQuery: function (device, func, args) {
// Find the I/O device and execute the query/function call.
if (runtime.ioDevices[device] && runtime.ioDevices[device][func]) {
2017-04-17 15:10:04 -04:00
const devObject = runtime.ioDevices[device];
// @todo Figure out why eslint complains about no-useless-call
// no-useless-call can't tell if the call is useless for dynamic
// expressions... or something. Not exactly sure why it
// complains here.
// eslint-disable-next-line no-useless-call
2016-10-24 14:07:38 -04:00
return devObject[func].call(devObject, args);
}
}
});
if (typeof primitiveReportedValue === 'undefined') {
// No value reported - potentially a command block.
// Edge-activated hats don't request a glow; all commands do.
thread.requestScriptGlowInFrame = true;
}
// If it's a promise, wait until promise resolves.
2016-08-23 18:12:32 -04:00
if (isPromise(primitiveReportedValue)) {
if (thread.status === Thread.STATUS_RUNNING) {
// Primitive returned a promise; automatically yield thread.
thread.status = Thread.STATUS_PROMISE_WAIT;
}
// Promise handlers
2017-04-17 15:10:04 -04:00
primitiveReportedValue.then(resolvedValue => {
2016-08-23 18:12:32 -04:00
handleReport(resolvedValue);
if (typeof resolvedValue === 'undefined') {
let stackFrame;
let nextBlockId;
do {
// In the case that the promise is the last block in the current thread stack
// We need to pop out repeatedly until we find the next block.
2017-04-17 15:10:04 -04:00
const popped = thread.popStack();
if (popped === null) {
return;
}
nextBlockId = thread.target.blocks.getNextBlock(popped);
if (nextBlockId !== null) {
// A next block exists so break out this loop
break;
}
// Investigate the next block and if not in a loop,
// then repeat and pop the next item off the stack frame
stackFrame = thread.peekStackFrame();
} while (stackFrame !== null && !stackFrame.isLoop);
thread.pushStack(nextBlockId);
} else {
thread.popStack();
}
2017-04-17 15:10:04 -04:00
}, rejectionReason => {
2016-06-24 11:19:38 -04:00
// Promise rejected: the primitive had some error.
// Log it and proceed.
2016-10-23 18:01:11 -04:00
log.warn('Primitive rejected promise: ', rejectionReason);
thread.status = Thread.STATUS_RUNNING;
thread.popStack();
});
} else if (thread.status === Thread.STATUS_RUNNING) {
2016-08-23 18:12:32 -04:00
handleReport(primitiveReportedValue);
}
};
module.exports = execute;