scratch-vm/src/engine/execute.js
2020-05-12 15:30:20 -04:00

580 lines
21 KiB
JavaScript

const BlockUtility = require('./block-utility');
const BlocksExecuteCache = require('./blocks-execute-cache');
const log = require('../util/log');
const Thread = require('./thread');
const {Map} = require('immutable');
const cast = require('../util/cast');
/**
* Single BlockUtility instance reused by execute for every pritimive ran.
* @const
*/
const blockUtility = new BlockUtility();
/**
* Profiler frame name for block functions.
* @const {string}
*/
const blockFunctionProfilerFrame = 'blockFunction';
/**
* Profiler frame ID for 'blockFunction'.
* @type {number}
*/
let blockFunctionProfilerId = -1;
/**
* Utility function to determine if a value is a Promise.
* @param {*} value Value to check for a Promise.
* @return {boolean} True if the value appears to be a Promise.
*/
const isPromise = function (value) {
return (
value !== null &&
typeof value === 'object' &&
typeof value.then === 'function'
);
};
/**
* Handle any reported value from the primitive, either directly returned
* or after a promise resolves.
* @param {*} resolvedValue Value eventually returned from the primitive.
* @param {!Sequencer} sequencer Sequencer stepping the thread for the ran
* primitive.
* @param {!Thread} thread Thread containing the primitive.
* @param {!string} currentBlockId Id of the block in its thread for value from
* the primitive.
* @param {!string} opcode opcode used to identify a block function primitive.
* @param {!boolean} isHat Is the current block a hat?
*/
// @todo move this to callback attached to the thread when we have performance
// metrics (dd)
const handleReport = function (resolvedValue, sequencer, thread, blockCached, lastOperation) {
const currentBlockId = blockCached.id;
const opcode = blockCached.opcode;
const isHat = blockCached._isHat;
thread.pushReportedValue(resolvedValue);
if (isHat) {
// Hat predicate was evaluated.
if (sequencer.runtime.getIsEdgeActivatedHat(opcode)) {
// If this is an edge-activated hat, only proceed if the value is
// true and used to be false, or the stack was activated explicitly
// via stack click
if (!thread.stackClick) {
const hasOldEdgeValue = thread.target.hasEdgeActivatedValue(currentBlockId);
const oldEdgeValue = thread.target.updateEdgeActivatedValue(
currentBlockId,
resolvedValue
);
const edgeWasActivated = hasOldEdgeValue ? (!oldEdgeValue && resolvedValue) : resolvedValue;
if (!edgeWasActivated) {
sequencer.retireThread(thread);
}
}
} else if (!resolvedValue) {
// Not an edge-activated hat: retire the thread
// if predicate was false.
sequencer.retireThread(thread);
}
} else {
// In a non-hat, report the value visually if necessary if
// at the top of the thread stack.
if (lastOperation && typeof resolvedValue !== 'undefined' && thread.atStackTop()) {
if (thread.stackClick) {
sequencer.runtime.visualReport(currentBlockId, resolvedValue);
}
if (thread.updateMonitor) {
const targetId = sequencer.runtime.monitorBlocks.getBlock(currentBlockId).targetId;
if (targetId && !sequencer.runtime.getTargetById(targetId)) {
// Target no longer exists
return;
}
sequencer.runtime.requestUpdateMonitor(Map({
id: currentBlockId,
spriteName: targetId ? sequencer.runtime.getTargetById(targetId).getName() : null,
value: resolvedValue
}));
}
}
// Finished any yields.
thread.status = Thread.STATUS_RUNNING;
}
};
const handlePromise = (primitiveReportedValue, sequencer, thread, blockCached, lastOperation) => {
if (thread.status === Thread.STATUS_RUNNING) {
// Primitive returned a promise; automatically yield thread.
thread.status = Thread.STATUS_PROMISE_WAIT;
}
// Promise handlers
primitiveReportedValue.then(resolvedValue => {
handleReport(resolvedValue, sequencer, thread, blockCached, lastOperation);
// If it's a command block or a top level reporter in a stackClick.
if (lastOperation) {
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.
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);
}
}, rejectionReason => {
// Promise rejected: the primitive had some error.
// Log it and proceed.
log.warn('Primitive rejected promise: ', rejectionReason);
thread.status = Thread.STATUS_RUNNING;
thread.popStack();
});
};
/**
* A execute.js internal representation of a block to reduce the time spent in
* execute as the same blocks are called the most.
*
* With the help of the Blocks class create a mutable copy of block
* information. The members of BlockCached derived values of block information
* that does not need to be reevaluated until a change in Blocks. Since Blocks
* handles where the cache instance is stored, it drops all cache versions of a
* block when any change happens to it. This way we can quickly execute blocks
* and keep perform the right action according to the current block information
* in the editor.
*
* @param {Blocks} blockContainer the related Blocks instance
* @param {object} cached default set of cached values
*/
class BlockCached {
constructor (blockContainer, cached) {
/**
* Block id in its parent set of blocks.
* @type {string}
*/
this.id = cached.id;
/**
* Block operation code for this block.
* @type {string}
*/
this.opcode = cached.opcode;
/**
* Original block object containing argument values for static fields.
* @type {object}
*/
this.fields = cached.fields;
/**
* Original block object containing argument values for executable inputs.
* @type {object}
*/
this.inputs = cached.inputs;
/**
* Procedure mutation.
* @type {?object}
*/
this.mutation = cached.mutation;
/**
* The profiler the block is configured with.
* @type {?Profiler}
*/
this._profiler = null;
/**
* Profiler information frame.
* @type {?ProfilerFrame}
*/
this._profilerFrame = null;
/**
* Is the opcode a hat (event responder) block.
* @type {boolean}
*/
this._isHat = false;
/**
* The block opcode's implementation function.
* @type {?function}
*/
this._blockFunction = null;
/**
* Is the block function defined for this opcode?
* @type {boolean}
*/
this._definedBlockFunction = false;
/**
* Is this block a block with no function but a static value to return.
* @type {boolean}
*/
this._isShadowBlock = false;
/**
* The static value of this block if it is a shadow block.
* @type {?any}
*/
this._shadowValue = null;
/**
* A copy of the block's fields that may be modified.
* @type {object}
*/
this._fields = Object.assign({}, this.fields);
/**
* A copy of the block's inputs that may be modified.
* @type {object}
*/
this._inputs = Object.assign({}, this.inputs);
/**
* An arguments object for block implementations. All executions of this
* specific block will use this objecct.
* @type {object}
*/
this._argValues = {
mutation: this.mutation
};
/**
* The inputs key the parent refers to this BlockCached by.
* @type {string}
*/
this._parentKey = null;
/**
* The target object where the parent wants the resulting value stored
* with _parentKey as the key.
* @type {object}
*/
this._parentValues = null;
/**
* A sequence of non-shadow operations that can must be performed. This
* list recreates the order this block and its children are executed.
* Since the order is always the same we can safely store that order
* and iterate over the operations instead of dynamically walking the
* tree every time.
* @type {Array<BlockCached>}
*/
this._ops = [];
const {runtime} = blockUtility.sequencer;
const {opcode, fields, inputs} = this;
// Assign opcode isHat and blockFunction data to avoid dynamic lookups.
this._isHat = runtime.getIsHat(opcode);
this._blockFunction = runtime.getOpcodeFunction(opcode);
this._definedBlockFunction = typeof this._blockFunction !== 'undefined';
// Store the current shadow value if there is a shadow value.
const fieldKeys = Object.keys(fields);
this._isShadowBlock = (
!this._definedBlockFunction &&
fieldKeys.length === 1 &&
Object.keys(inputs).length === 0
);
this._shadowValue = this._isShadowBlock && fields[fieldKeys[0]].value;
// Store the static fields onto _argValues.
for (const fieldName in fields) {
if (
fieldName === 'VARIABLE' ||
fieldName === 'LIST' ||
fieldName === 'BROADCAST_OPTION'
) {
this._argValues[fieldName] = {
id: fields[fieldName].id,
name: fields[fieldName].value
};
} else {
this._argValues[fieldName] = fields[fieldName].value;
}
}
// Remove custom_block. It is not part of block execution.
delete this._inputs.custom_block;
if ('BROADCAST_INPUT' in this._inputs) {
// BROADCAST_INPUT is called BROADCAST_OPTION in the args and is an
// object with an unchanging shape.
this._argValues.BROADCAST_OPTION = {
id: null,
name: null
};
// We can go ahead and compute BROADCAST_INPUT if it is a shadow
// value.
const broadcastInput = this._inputs.BROADCAST_INPUT;
if (broadcastInput.block === broadcastInput.shadow) {
// Shadow dropdown menu is being used.
// Get the appropriate information out of it.
const shadow = blockContainer.getBlock(broadcastInput.shadow);
const broadcastField = shadow.fields.BROADCAST_OPTION;
this._argValues.BROADCAST_OPTION.id = broadcastField.id;
this._argValues.BROADCAST_OPTION.name = broadcastField.value;
// Evaluating BROADCAST_INPUT here we do not need to do so
// later.
delete this._inputs.BROADCAST_INPUT;
}
}
// Cache all input children blocks in the operation lists. The
// operations can later be run in the order they appear in correctly
// executing the operations quickly in a flat loop instead of needing to
// recursivly iterate them.
for (const inputName in this._inputs) {
const input = this._inputs[inputName];
if (input.block) {
const inputCached = BlocksExecuteCache.getCached(blockContainer, input.block, BlockCached);
if (inputCached._isHat) {
continue;
}
this._ops.push(...inputCached._ops);
inputCached._parentKey = inputName;
inputCached._parentValues = this._argValues;
// Shadow values are static and do not change, go ahead and
// store their value on args.
if (inputCached._isShadowBlock) {
this._argValues[inputName] = inputCached._shadowValue;
}
}
}
// The final operation is this block itself. At the top most block is a
// command block or a block that is being run as a monitor.
if (this._definedBlockFunction) {
this._ops.push(this);
}
}
}
/**
* Initialize a BlockCached instance so its command/hat
* block and reporters can be profiled during execution.
* @param {Profiler} profiler - The profiler that is currently enabled.
* @param {BlockCached} blockCached - The blockCached instance to profile.
*/
const _prepareBlockProfiling = function (profiler, blockCached) {
blockCached._profiler = profiler;
if (blockFunctionProfilerId === -1) {
blockFunctionProfilerId = profiler.idByName(blockFunctionProfilerFrame);
}
const ops = blockCached._ops;
for (let i = 0; i < ops.length; i++) {
ops[i]._profilerFrame = profiler.frame(blockFunctionProfilerId, ops[i].opcode);
}
};
/**
* Execute a block.
* @param {!Sequencer} sequencer Which sequencer is executing.
* @param {!Thread} thread Thread which to read and execute.
*/
const execute = function (sequencer, thread) {
const runtime = sequencer.runtime;
// store sequencer and thread so block functions can access them through
// convenience methods.
blockUtility.sequencer = sequencer;
blockUtility.thread = thread;
// Current block to execute is the one on the top of the stack.
const currentBlockId = thread.peekStack();
const currentStackFrame = thread.peekStackFrame();
let blockContainer = thread.blockContainer;
let blockCached = BlocksExecuteCache.getCached(blockContainer, currentBlockId, BlockCached);
if (blockCached === null) {
blockContainer = runtime.flyoutBlocks;
blockCached = BlocksExecuteCache.getCached(blockContainer, currentBlockId, BlockCached);
// Stop if block or target no longer exists.
if (blockCached === null) {
// No block found: stop the thread; script no longer exists.
sequencer.retireThread(thread);
return;
}
}
const ops = blockCached._ops;
const length = ops.length;
let i = 0;
if (currentStackFrame.reported !== null) {
const reported = currentStackFrame.reported;
// Reinstate all the previous values.
for (; i < reported.length; i++) {
const {opCached: oldOpCached, inputValue} = reported[i];
const opCached = ops.find(op => op.id === oldOpCached);
if (opCached) {
const inputName = opCached._parentKey;
const argValues = opCached._parentValues;
if (inputName === 'BROADCAST_INPUT') {
// Something is plugged into the broadcast input.
// Cast it to a string. We don't need an id here.
argValues.BROADCAST_OPTION.id = null;
argValues.BROADCAST_OPTION.name = cast.toString(inputValue);
} else {
argValues[inputName] = inputValue;
}
}
}
// Find the last reported block that is still in the set of operations.
// This way if the last operation was removed, we'll find the next
// candidate. If an earlier block that was performed was removed then
// we'll find the index where the last operation is now.
if (reported.length > 0) {
const lastExisting = reported.reverse().find(report => ops.find(op => op.id === report.opCached));
if (lastExisting) {
i = ops.findIndex(opCached => opCached.id === lastExisting.opCached) + 1;
} else {
i = 0;
}
}
// The reporting block must exist and must be the next one in the sequence of operations.
if (thread.justReported !== null && ops[i] && ops[i].id === currentStackFrame.reporting) {
const opCached = ops[i];
const inputValue = thread.justReported;
thread.justReported = null;
const inputName = opCached._parentKey;
const argValues = opCached._parentValues;
if (inputName === 'BROADCAST_INPUT') {
// Something is plugged into the broadcast input.
// Cast it to a string. We don't need an id here.
argValues.BROADCAST_OPTION.id = null;
argValues.BROADCAST_OPTION.name = cast.toString(inputValue);
} else {
argValues[inputName] = inputValue;
}
i += 1;
}
currentStackFrame.reporting = null;
currentStackFrame.reported = null;
}
const start = i;
for (; i < length; i++) {
const lastOperation = i === length - 1;
const opCached = ops[i];
const blockFunction = opCached._blockFunction;
// Update values for arguments (inputs).
const argValues = opCached._argValues;
// Fields are set during opCached initialization.
// Blocks should glow when a script is starting,
// not after it has finished (see #1404).
// Only blocks in blockContainers that don't forceNoGlow
// should request a glow.
if (!blockContainer.forceNoGlow) {
thread.requestScriptGlowInFrame = true;
}
// Inputs are set during previous steps in the loop.
const primitiveReportedValue = blockFunction(argValues, blockUtility);
// If it's a promise, wait until promise resolves.
if (isPromise(primitiveReportedValue)) {
handlePromise(primitiveReportedValue, sequencer, thread, opCached, lastOperation);
// Store the already reported values. They will be thawed into the
// future versions of the same operations by block id. The reporting
// operation if it is promise waiting will set its parent value at
// that time.
thread.justReported = null;
currentStackFrame.reporting = ops[i].id;
currentStackFrame.reported = ops.slice(0, i).map(reportedCached => {
const inputName = reportedCached._parentKey;
const reportedValues = reportedCached._parentValues;
if (inputName === 'BROADCAST_INPUT') {
return {
opCached: reportedCached.id,
inputValue: reportedValues[inputName].BROADCAST_OPTION.name
};
}
return {
opCached: reportedCached.id,
inputValue: reportedValues[inputName]
};
});
// We are waiting for a promise. Stop running this set of operations
// and continue them later after thawing the reported values.
break;
} else if (thread.status === Thread.STATUS_RUNNING) {
if (lastOperation) {
handleReport(primitiveReportedValue, sequencer, thread, opCached, lastOperation);
} else {
// By definition a block that is not last in the list has a
// parent.
const inputName = opCached._parentKey;
const parentValues = opCached._parentValues;
if (inputName === 'BROADCAST_INPUT') {
// Something is plugged into the broadcast input.
// Cast it to a string. We don't need an id here.
parentValues.BROADCAST_OPTION.id = null;
parentValues.BROADCAST_OPTION.name = cast.toString(primitiveReportedValue);
} else {
parentValues[inputName] = primitiveReportedValue;
}
}
}
}
if (runtime.profiler !== null) {
if (blockCached._profiler !== runtime.profiler) {
_prepareBlockProfiling(runtime.profiler, blockCached);
}
// Determine the index that is after the last executed block. `i` is
// currently the block that was just executed. `i + 1` will be the block
// after that. `length` with the min call makes sure we don't try to
// reference an operation outside of the set of operations.
const end = Math.min(i + 1, length);
for (let p = start; p < end; p++) {
ops[p]._profilerFrame.count += 1;
}
}
};
module.exports = execute;