From b4cf64009fb4205022d14caf177683a0316aefbf Mon Sep 17 00:00:00 2001 From: Tim Mickel Date: Tue, 23 Aug 2016 18:12:32 -0400 Subject: [PATCH] General-purpose hat implementation --- src/engine/execute.js | 105 ++++++++++++++++++------- src/engine/runtime.js | 173 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 232 insertions(+), 46 deletions(-) diff --git a/src/engine/execute.js b/src/engine/execute.js index d885a6c58..3ab78752d 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -1,5 +1,14 @@ var Thread = require('./thread'); +/** + * 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. + */ +var isPromise = function (value) { + return value && value.then && typeof value.then === 'function'; +}; + /** * Execute a block. * @param {!Sequencer} sequencer Which sequencer is executing. @@ -21,9 +30,28 @@ var execute = function (sequencer, thread) { } var blockFunction = runtime.getOpcodeFunction(opcode); - if (!blockFunction) { - console.warn('Could not get implementation for opcode: ' + opcode); - return; + var isHat = runtime.getIsHat(opcode); + + // Hats are implemented slightly differently from regular blocks. + // If they have an associated block function, it's treated as a predicate; + // if not, execution will proceed right through it (as a no-op). + if (isHat) { + var nextBlock = target.blocks.getNextBlock(currentBlockId); + if (!nextBlock) { + // Hat with no next block - don't try to evaluate it. + sequencer.retireThread(thread); + return; + } + if (!blockFunction) { + // No predicate for the hat - just continue to next block. + sequencer.proceedThread(thread); + return; + } + } else { + if (!blockFunction) { + console.warn('Could not get implementation for opcode: ' + opcode); + return; + } } // Generate values for arguments (inputs). @@ -63,6 +91,8 @@ var execute = function (sequencer, thread) { var primitiveReportedValue = null; primitiveReportedValue = blockFunction(argValues, { + stackFrame: currentStackFrame.executionContext, + target: target, yield: function() { thread.setStatus(Thread.STATUS_YIELD); }, @@ -73,11 +103,14 @@ var execute = function (sequencer, thread) { thread.setStatus(Thread.STATUS_RUNNING); sequencer.proceedThread(thread); }, - stackFrame: currentStackFrame.executionContext, startBranch: function (branchNum) { sequencer.stepToBranch(thread, branchNum); }, - target: target, + triggerHats: function(requestedHat, opt_matchFields, opt_target) { + return ( + runtime.triggerHats(requestedHat, opt_matchFields, opt_target) + ); + }, ioQuery: function (device, func, args) { // Find the I/O device and execute the query/function call. if (runtime.ioDevices[device] && runtime.ioDevices[device][func]) { @@ -87,28 +120,53 @@ var execute = function (sequencer, thread) { } }); - // Deal with any reported value. + /** + * Handle any reported value from the primitive, either directly returned + * or after a promise resolves. + * @param {*} resolvedValue Value eventually returned from the primitive. + */ + var handleReport = function (resolvedValue) { + thread.pushReportedValue(resolvedValue); + if (isHat) { + // Hat predicate was evaluated. + if (runtime.getIsEdgeTriggeredHat(opcode)) { + // If this is an edge-triggered hat, only proceed if + // the value is true and used to be false. + var oldEdgeValue = runtime.updateEdgeTriggeredValue( + currentBlockId, + resolvedValue + ); + var edgeWasTriggered = !oldEdgeValue && resolvedValue; + if (!edgeWasTriggered) { + sequencer.retireThread(thread); + } + } else { + // Not an edge-triggered hat: retire the thread + // if predicate was false. + 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()) { + runtime.visualReport(currentBlockId, resolvedValue); + } + // Finished any yields. + thread.setStatus(Thread.STATUS_RUNNING); + } + }; + // If it's a promise, wait until promise resolves. - var isPromise = ( - primitiveReportedValue && - primitiveReportedValue.then && - typeof primitiveReportedValue.then === 'function' - ); - if (isPromise) { + if (isPromise(primitiveReportedValue)) { if (thread.status === Thread.STATUS_RUNNING) { // Primitive returned a promise; automatically yield thread. thread.setStatus(Thread.STATUS_YIELD); } // Promise handlers primitiveReportedValue.then(function(resolvedValue) { - // Promise resolved: the primitive reported a value. - thread.pushReportedValue(resolvedValue); - // Report the value visually if necessary. - if (typeof resolvedValue !== 'undefined' && - thread.peekStack() === thread.topBlock) { - runtime.visualReport(thread.peekStack(), resolvedValue); - } - thread.setStatus(Thread.STATUS_RUNNING); + handleReport(resolvedValue); sequencer.proceedThread(thread); }, function(rejectionReason) { // Promise rejected: the primitive had some error. @@ -118,12 +176,7 @@ var execute = function (sequencer, thread) { sequencer.proceedThread(thread); }); } else if (thread.status === Thread.STATUS_RUNNING) { - thread.pushReportedValue(primitiveReportedValue); - // Report the value visually if necessary. - if (typeof primitiveReportedValue !== 'undefined' && - thread.peekStack() === thread.topBlock) { - runtime.visualReport(thread.peekStack(), primitiveReportedValue); - } + handleReport(primitiveReportedValue); } }; diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 8a347a0c2..62a083216 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -47,6 +47,8 @@ function Runtime (targets) { * @type {Object.} */ this._primitives = {}; + this._hats = {}; + this._edgeTriggeredHatValues = {}; this._registerBlockPackages(); this.ioDevices = { @@ -109,11 +111,23 @@ Runtime.prototype._registerBlockPackages = function () { if (defaultBlockPackages.hasOwnProperty(packageName)) { // @todo pass a different runtime depending on package privilege? var packageObject = new (defaultBlockPackages[packageName])(this); - var packageContents = packageObject.getPrimitives(); - for (var op in packageContents) { - if (packageContents.hasOwnProperty(op)) { - this._primitives[op] = - packageContents[op].bind(packageObject); + // Collect primitives from package. + if (packageObject.getPrimitives) { + var packagePrimitives = packageObject.getPrimitives(); + for (var op in packagePrimitives) { + if (packagePrimitives.hasOwnProperty(op)) { + this._primitives[op] = + packagePrimitives[op].bind(packageObject); + } + } + } + // Collect hat metadata from package. + if (packageObject.getHats) { + var packageHats = packageObject.getHats(); + for (var hatName in packageHats) { + if (packageHats.hasOwnProperty(hatName)) { + this._hats[hatName] = packageHats[hatName]; + } } } } @@ -132,6 +146,46 @@ Runtime.prototype.getOpcodeFunction = function (opcode) { // ----------------------------------------------------------------------------- // ----------------------------------------------------------------------------- +/** + * Return whether an opcode represents a hat block. + * @param {!string} opcode The opcode to look up. + * @return {Boolean} True if the op is known to be a hat. + */ +Runtime.prototype.getIsHat = function (opcode) { + return opcode in this._hats; +}; + +/** + * Return whether an opcode represents an edge-triggered hat block. + * @param {!string} opcode The opcode to look up. + * @return {Boolean} True if the op is known to be a edge-triggered hat. + */ +Runtime.prototype.getIsEdgeTriggeredHat = function (opcode) { + return opcode in this._hats && this._hats[opcode].edgeTriggered; +}; + +/** + * Update an edge-triggered hat block value. + * @param {!string} blockId ID of hat to store value for. + * @param {*} newValue Value to store for edge-triggered hat. + * @return {*} The old value for the edge-triggered hat. + */ +Runtime.prototype.updateEdgeTriggeredValue = function (blockId, newValue) { + var oldValue = this._edgeTriggeredHatValues[blockId]; + this._edgeTriggeredHatValues[blockId] = newValue; + return oldValue; +}; + +/** + * Clear all edge-triggered hat values. + */ +Runtime.prototype.clearEdgeTriggeredValues = function () { + this._edgeTriggeredHatValues = {}; +}; + +// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- + /** * Create a thread and push it to the list of threads. * @param {!string} id ID of block that starts the stack @@ -150,6 +204,7 @@ Runtime.prototype._pushThread = function (id) { * @param {?Thread} thread Thread object to remove from actives */ Runtime.prototype._removeThread = function (thread) { + thread.setStatus(Thread.STATUS_DONE); var i = this.threads.indexOf(thread); if (i > -1) { this.glowScript(thread.topBlock, false); @@ -174,28 +229,99 @@ Runtime.prototype.toggleScript = function (topBlockId) { }; /** - * Green flag, which stops currently running threads - * and adds all top-level scripts that start with the green flag + * Run a function `f` for all scripts in a workspace. + * `f` will be called with two parameters: + * -the top block ID of each script + * -the opcode of that block, for convenience. + * -fields on that block, for convenience. + * @param {!Function} f Function to call for each script. + * @param {Target=} opt_target Optionally, a target to restrict to. */ -Runtime.prototype.greenFlag = function () { - // Remove all existing threads - for (var i = 0; i < this.threads.length; i++) { - this._removeThread(this.threads[i]); +Runtime.prototype.allScriptsDo = function (f, opt_target) { + var targets = this.targets; + if (opt_target) { + targets = [opt_target]; } - // Add all top scripts with green flag - for (var t = 0; t < this.targets.length; t++) { - var target = this.targets[t]; + for (var t = 0; t < targets.length; t++) { + var target = targets[t]; var scripts = target.blocks.getScripts(); for (var j = 0; j < scripts.length; j++) { var topBlock = scripts[j]; - if (target.blocks.getBlock(topBlock).opcode === - 'event_whenflagclicked') { - this._pushThread(scripts[j]); - } + var topOpcode = target.blocks.getBlock(topBlock).opcode; + var topFields = target.blocks.getFields(topBlock); + f(topBlock, topOpcode, topFields); } } }; +/** + * Trigger all relevant hats. + * @param {!string} requestedHat Name of hat to trigger. + * @param {Object=} opt_matchFields Optionally, fields to match on the hat. + * @param {Target=} opt_target Optionally, a target to restrict to. + * @return {Array.} List of threads started by this trigger. + */ +Runtime.prototype.triggerHats = function (requestedHat, + opt_matchFields, opt_target) { + var instance = this; + var newThreads = []; + // Consider all scripts, looking for hats named `requestedHat`. + this.allScriptsDo(function(topBlockId, topOpcode, topFields) { + if (topOpcode !== requestedHat) { + // Not the right hat. + return; + } + // Match any requested fields. + // For example: ensures that broadcasts match. + // This needs to happen before the block is evaluated + // (i.e., before the predicate can be run) because "broadcast and wait" + // needs to have a precise collection of triggered threads. + if (opt_matchFields) { + for (var matchField in opt_matchFields) { + if (topFields[matchField].value !== + opt_matchFields[matchField]) { + // Field mismatch. + return; + } + } + } + if (instance._hats.hasOwnProperty(topOpcode)) { + // Look up metadata for the relevant hat. + var hatMeta = instance._hats[topOpcode]; + if (hatMeta.restartExistingThreads) { + // If `restartExistingThreads` is true, this trigger + // should stop any existing threads starting with the top block. + for (var i = 0; i < instance.threads.length; i++) { + if (instance.threads[i].topBlock === topBlockId) { + instance._removeThread(instance.threads[i]); + } + } + } else { + // If `restartExistingThreads` is false, this trigger + // should give up if any threads with the top block are running. + for (var j = 0; j < instance.threads.length; j++) { + if (instance.threads[j].topBlock === topBlockId) { + // Some thread is already running. + return; + } + } + } + // Start the thread with this top block. + newThreads.push(instance._pushThread(topBlockId)); + } + }, opt_target); + return newThreads; +}; + +/** + * Start all threads that start with the green flag. + */ +Runtime.prototype.greenFlag = function () { + this.ioDevices.clock.resetProjectTimer(); + this.clearEdgeTriggeredValues(); + this.triggerHats('event_whenflagclicked'); +}; + /** * Stop "everything" */ @@ -217,9 +343,16 @@ Runtime.prototype.stopAll = function () { * inactive threads after each iteration. */ Runtime.prototype._step = function () { + // Find all edge-triggered hats, and add them to threads to be evaluated. + for (var hatType in this._hats) { + var hat = this._hats[hatType]; + if (hat.edgeTriggered) { + this.triggerHats(hatType); + } + } var inactiveThreads = this.sequencer.stepThreads(this.threads); - for (var i = 0; i < inactiveThreads.length; i++) { - this._removeThread(inactiveThreads[i]); + for (var j = 0; j < inactiveThreads.length; j++) { + this._removeThread(inactiveThreads[j]); } };