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, currentBlockId, opcode, 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 oldEdgeValue = sequencer.runtime.updateEdgeActivatedValue( currentBlockId, resolvedValue ); const edgeWasActivated = !oldEdgeValue && 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 (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: String(resolvedValue) })); } } // Finished any yields. thread.status = Thread.STATUS_RUNNING; } }; /** * A convenience constant to help make use of the recursiveCall argument easier * to read. * @const {boolean} */ const RECURSIVE = true; // With the help of the Blocks class create a cached copy of values from // Blocks and the derived values execute needs. These values can be produced // one time during the first execution of a block so that later executions // are faster by using these cached values. This helps turn most costly // javascript operations like testing if the fields for a block has a // certain key like VARIABLE into a test that done once is saved on the // cache object to _isFieldVariable. This reduces the cost later in execute // when that will determine how execute creates the argValues for the // blockFunction. // // With Blocks providing this private function for execute to use, any time // Blocks is modified in the editor these cached objects will be cleaned up // and new cached copies can be created. This lets us optimize this critical // path while keeping up to date with editor changes to a project. const BlockCached = function (blockContainer, cached) { this.opcode = cached.opcode; this.fields = cached.fields; this.inputs = cached.inputs; this.mutation = cached.mutation; this._isHat = false; this._blockFunction = null; this._definedBlockFunction = false; this._isShadowBlock = false; this._shadowValue = null; this._fields = null; this._argValues = null; this._inputs = null; 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'; const fieldKeys = Object.keys(fields); // Store the current shadow value if there is a shadow value. this._isShadowBlock = fieldKeys.length === 1 && Object.keys(inputs).length === 0; this._shadowValue = this._isShadowBlock && fields[fieldKeys[0]].value; // Store a fields copy. this._fields = Object.assign({}, this.fields); // Create a argValues instance for all executions of this block. Fields are // static, store them on args. this._argValues = { mutation: this.mutation }; for (const fieldName in fields) { if (fieldName === 'VARIABLE') { this._argValues.VARIABLE = { id: fields.VARIABLE.id, name: fields.VARIABLE.value }; } else if (fieldName === 'LIST') { this._argValues.LIST = { id: fields.LIST.id, name: fields.LIST.value }; } else if (fieldName === 'BROADCAST_OPTION') { this._argValues.BROADCAST_OPTION = { id: fields.BROADCAST_OPTION.id, name: fields.BROADCAST_OPTION.value }; } else { this._argValues[fieldName] = fields[fieldName].value; } } // Store a modified inputs. This assures the keys are its own properties // and that custom_block will not be evaluated. this._inputs = Object.assign({}, this.inputs); delete this._inputs.custom_block; if ('BROADCAST_INPUT' in this._inputs) { this._argValues.BROADCAST_OPTION = { id: null, name: null }; 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.name; delete this._inputs.BROADCAST_INPUT; } } }; /** * Execute a block. * @param {!Sequencer} sequencer Which sequencer is executing. * @param {!Thread} thread Thread which to read and execute. * @param {boolean} recursiveCall is execute called from another execute call? */ const execute = function (sequencer, thread, recursiveCall) { const runtime = sequencer.runtime; // sequencer and thread are the same objects during a recursive set of // execute operations. if (recursiveCall !== RECURSIVE) { 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 opcode = blockCached.opcode; const inputs = blockCached._inputs; const blockFunction = blockCached._blockFunction; const isHat = blockCached._isHat; // 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. if (!blockCached._definedBlockFunction) { if (!opcode) { log.warn(`Could not get opcode for block: ${currentBlockId}`); return; } if (recursiveCall === RECURSIVE && blockCached._isShadowBlock) { // One field and no inputs - treat as arg. thread.pushReportedValue(blockCached._shadowValue); thread.status = Thread.STATUS_RUNNING; } else if (isHat) { // Skip through the block (hat with no predicate). return; } else { log.warn(`Could not get implementation for opcode: ${opcode}`); } thread.requestScriptGlowInFrame = true; return; } // Update values for arguments (inputs). const argValues = blockCached._argValues; // Fields are set during blockCached initialization. // Recursively evaluate input blocks. for (const inputName in inputs) { const input = inputs[inputName]; const inputBlockId = input.block; // Is there no value for this input waiting in the stack frame? if (inputBlockId !== null && currentStackFrame.waitingReporter === null) { // 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, RECURSIVE); if (thread.status === Thread.STATUS_PROMISE_WAIT) { // Waiting for the block to resolve, store the current argValues // onto a member of the currentStackFrame that can be used once // the nested block resolves to rebuild argValues up to this // point. for (const _inputName in inputs) { // We are waiting on the nested block at inputName so we // don't need to store any more inputs. if (_inputName === inputName) break; if (_inputName === 'BROADCAST_INPUT') { currentStackFrame.reported[_inputName] = argValues.BROADCAST_OPTION.name; } else { currentStackFrame.reported[_inputName] = argValues[_inputName]; } } return; } // Execution returned immediately, // and presumably a value was reported, so pop the stack. currentStackFrame.waitingReporter = null; thread.popStack(); } let inputValue; if (currentStackFrame.waitingReporter === null) { inputValue = currentStackFrame.justReported; currentStackFrame.justReported = null; } else if (currentStackFrame.waitingReporter === inputName) { inputValue = currentStackFrame.justReported; currentStackFrame.waitingReporter = null; currentStackFrame.justReported = null; // We have rebuilt argValues with all the stored values in the // currentStackFrame from the nested block's promise resolving. // Using the reported value from the block we waited on, reset the // storage member of currentStackFrame so the next execute call at // this level can use it in a clean state. currentStackFrame.reported = {}; } else if (typeof currentStackFrame.reported[inputName] !== 'undefined') { inputValue = currentStackFrame.reported[inputName]; } if (inputName === 'BROADCAST_INPUT') { const broadcastInput = inputs[inputName]; // Check if something is plugged into the broadcast block, or // if the shadow dropdown menu is being used. if (broadcastInput.block !== broadcastInput.shadow) { // 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; } } let primitiveReportedValue = null; if (runtime.profiler !== null) { if (blockFunctionProfilerId === -1) { blockFunctionProfilerId = runtime.profiler.idByName(blockFunctionProfilerFrame); } // The method commented below has its code inlined underneath to reduce // the bias recorded for the profiler's calls in this time sensitive // execute function. // // runtime.profiler.start(blockFunctionProfilerId, opcode); runtime.profiler.records.push( runtime.profiler.START, blockFunctionProfilerId, opcode, performance.now()); } primitiveReportedValue = blockFunction(argValues, blockUtility); if (runtime.profiler !== null) { // runtime.profiler.stop(blockFunctionProfilerId); runtime.profiler.records.push(runtime.profiler.STOP, performance.now()); } if (recursiveCall !== RECURSIVE && 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. if (isPromise(primitiveReportedValue)) { 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, currentBlockId, opcode, isHat); 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. 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(); } }, 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(); }); } else if (thread.status === Thread.STATUS_RUNNING) { if (recursiveCall === RECURSIVE) { // In recursive calls (where execute calls execute) handleReport // simplifies to just calling thread.pushReportedValue. thread.pushReportedValue(primitiveReportedValue); } else { handleReport(primitiveReportedValue, sequencer, thread, currentBlockId, opcode, isHat); } } }; module.exports = execute;