From 94e389c8fb61c7a38ff62bed23e01f8fbc2155e4 Mon Sep 17 00:00:00 2001 From: Tim Mickel Date: Tue, 23 Aug 2016 15:47:21 -0400 Subject: [PATCH 01/13] Refactor script glowing into its own runtime function --- src/engine/runtime.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index d244544aa..8a347a0c2 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -135,12 +135,14 @@ Runtime.prototype.getOpcodeFunction = function (opcode) { /** * Create a thread and push it to the list of threads. * @param {!string} id ID of block that starts the stack + * @return {!Thread} The newly created thread. */ Runtime.prototype._pushThread = function (id) { - this.emit(Runtime.STACK_GLOW_ON, id); var thread = new Thread(id); + this.glowScript(id, true); thread.pushStack(id); this.threads.push(thread); + return thread; }; /** @@ -150,7 +152,7 @@ Runtime.prototype._pushThread = function (id) { Runtime.prototype._removeThread = function (thread) { var i = this.threads.indexOf(thread); if (i > -1) { - this.emit(Runtime.STACK_GLOW_OFF, thread.topBlock); + this.glowScript(thread.topBlock, false); this.threads.splice(i, 1); } }; @@ -234,6 +236,19 @@ Runtime.prototype.glowBlock = function (blockId, isGlowing) { } }; +/** + * Emit feedback for script glowing. + * @param {?string} topBlockId ID for the top block to update glow + * @param {boolean} isGlowing True to turn on glow; false to turn off. + */ +Runtime.prototype.glowScript = function (topBlockId, isGlowing) { + if (isGlowing) { + this.emit(Runtime.STACK_GLOW_ON, topBlockId); + } else { + this.emit(Runtime.STACK_GLOW_OFF, topBlockId); + } +}; + /** * Emit value for reporter to show in the blocks. * @param {string} blockId ID for the block. From 43f3b59f7c0c8e5d3b12305a57fcae915fbf3b9c Mon Sep 17 00:00:00 2001 From: Tim Mickel Date: Tue, 23 Aug 2016 15:53:34 -0400 Subject: [PATCH 02/13] Add `retireThread` to seqeuencer --- src/engine/sequencer.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js index 73126a227..11b134b53 100644 --- a/src/engine/sequencer.js +++ b/src/engine/sequencer.js @@ -166,4 +166,14 @@ Sequencer.prototype.proceedThread = function (thread) { } }; +/** + * Retire a thread in the middle, without considering further blocks. + * @param {!Thread} thread Thread object to retire. + */ +Sequencer.prototype.retireThread = function (thread) { + thread.stack = []; + thread.stackFrame = []; + thread.setStatus(Thread.STATUS_DONE); +}; + module.exports = Sequencer; From 39fdbaf98354c6807e772178092f72cad95d3e29 Mon Sep 17 00:00:00 2001 From: Tim Mickel Date: Tue, 23 Aug 2016 18:12:19 -0400 Subject: [PATCH 03/13] Add atStackTop helper to Thread --- src/engine/thread.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/engine/thread.js b/src/engine/thread.js index d1bd73fc0..07a98b862 100644 --- a/src/engine/thread.js +++ b/src/engine/thread.js @@ -123,6 +123,14 @@ Thread.prototype.pushReportedValue = function (value) { } }; +/** + * Whether the current execution of a thread is at the top of the stack. + * @return {Boolean} True if execution is at top of the stack. + */ +Thread.prototype.atStackTop = function () { + return this.peekStack() === this.topBlock; +}; + /** * Set thread status. * @param {number} status Enum representing thread status. From b4cf64009fb4205022d14caf177683a0316aefbf Mon Sep 17 00:00:00 2001 From: Tim Mickel Date: Tue, 23 Aug 2016 18:12:32 -0400 Subject: [PATCH 04/13] 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]); } }; From fe2ba2a5363e7da9472c668dbb276700604db8e2 Mon Sep 17 00:00:00 2001 From: Tim Mickel Date: Tue, 23 Aug 2016 18:14:05 -0400 Subject: [PATCH 05/13] Implementation of timer > _, broadcast, broadcast and wait --- src/blocks/scratch3_event.js | 74 +++++++++++++++++++++++++++++++----- 1 file changed, 64 insertions(+), 10 deletions(-) diff --git a/src/blocks/scratch3_event.js b/src/blocks/scratch3_event.js index 18a5ab621..607923454 100644 --- a/src/blocks/scratch3_event.js +++ b/src/blocks/scratch3_event.js @@ -1,3 +1,5 @@ +var Thread = require('../engine/thread'); + function Scratch3EventBlocks(runtime) { /** * The runtime instantiating this block package. @@ -12,23 +14,75 @@ function Scratch3EventBlocks(runtime) { */ Scratch3EventBlocks.prototype.getPrimitives = function() { return { - 'event_whenflagclicked': this.whenFlagClicked, - 'event_whenbroadcastreceived': this.whenBroadcastReceived, - 'event_broadcast': this.broadcast + 'event_broadcast': this.broadcast, + 'event_broadcastandwait': this.broadcastAndWait, + 'event_whengreaterthan': this.hatGreaterThanPredicate }; }; - -Scratch3EventBlocks.prototype.whenFlagClicked = function() { - // No-op +Scratch3EventBlocks.prototype.getHats = function () { + return { + 'event_whenflagclicked': { + restartExistingThreads: true + }, + /*'event_whenkeypressed': { + restartExistingThreads: false + }, + 'event_whenthisspriteclicked': { + restartExistingThreads: true + }, + 'event_whenbackdropswitchesto': { + restartExistingThreads: true + },*/ + 'event_whengreaterthan': { + restartExistingThreads: false, + edgeTriggered: true + }, + 'event_whenbroadcastreceived': { + restartExistingThreads: true + } + }; }; -Scratch3EventBlocks.prototype.whenBroadcastReceived = function() { - // No-op +Scratch3EventBlocks.prototype.hatGreaterThanPredicate = function (args, util) { + // @todo: Other cases :) + if (args.WHENGREATERTHANMENU == 'TIMER') { + return util.ioQuery('clock', 'projectTimer') > args.VALUE; + } + return false; }; -Scratch3EventBlocks.prototype.broadcast = function() { - // @todo +Scratch3EventBlocks.prototype.broadcast = function(args, util) { + util.triggerHats('event_whenbroadcastreceived', { + 'BROADCAST_OPTION': args.BROADCAST_OPTION + }); +}; + +Scratch3EventBlocks.prototype.broadcastAndWait = function (args, util) { + // Have we run before, triggering threads? + if (!util.stackFrame.triggeredThreads) { + // No - trigger hats for this broadcast. + util.stackFrame.triggeredThreads = util.triggerHats( + 'event_whenbroadcastreceived', { + 'BROADCAST_OPTION': args.BROADCAST_OPTION + } + ); + if (util.stackFrame.triggeredThreads.length == 0) { + // Nothing was started. + return; + } + } + // We've run before; check if the wait is still going on. + var waiting = false; + for (var i = 0; i < util.stackFrame.triggeredThreads.length; i++) { + var thread = util.stackFrame.triggeredThreads[i]; + if (thread.status !== Thread.STATUS_DONE) { + waiting = true; + } + } + if (waiting) { + util.yieldFrame(); + } }; module.exports = Scratch3EventBlocks; From 4f2cccf279dba389cf6caf4239885f8a1b47ec8f Mon Sep 17 00:00:00 2001 From: Tim Mickel Date: Tue, 23 Aug 2016 18:37:03 -0400 Subject: [PATCH 06/13] Fix issue when broadcasting in a when-broadcast --- src/blocks/scratch3_event.js | 3 ++- src/engine/runtime.js | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/blocks/scratch3_event.js b/src/blocks/scratch3_event.js index 607923454..b47ba014b 100644 --- a/src/blocks/scratch3_event.js +++ b/src/blocks/scratch3_event.js @@ -76,7 +76,8 @@ Scratch3EventBlocks.prototype.broadcastAndWait = function (args, util) { var waiting = false; for (var i = 0; i < util.stackFrame.triggeredThreads.length; i++) { var thread = util.stackFrame.triggeredThreads[i]; - if (thread.status !== Thread.STATUS_DONE) { + var activeThreads = this.runtime.threads; + if (activeThreads.indexOf(thread) > -1) { // @todo: A cleaner way? waiting = true; } } diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 62a083216..1d591c7ac 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -204,7 +204,6 @@ 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); From 4f81033762a8212827a392eaf3dc6269a704092a Mon Sep 17 00:00:00 2001 From: Tim Mickel Date: Tue, 23 Aug 2016 18:46:54 -0400 Subject: [PATCH 07/13] Remove extra Thread require --- src/blocks/scratch3_event.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/blocks/scratch3_event.js b/src/blocks/scratch3_event.js index b47ba014b..c03472698 100644 --- a/src/blocks/scratch3_event.js +++ b/src/blocks/scratch3_event.js @@ -1,5 +1,3 @@ -var Thread = require('../engine/thread'); - function Scratch3EventBlocks(runtime) { /** * The runtime instantiating this block package. From 29887e24c9f96f95df89afe550f0fa9cfbbbb3c0 Mon Sep 17 00:00:00 2001 From: Tim Mickel Date: Wed, 24 Aug 2016 11:04:23 -0400 Subject: [PATCH 08/13] Simplify `execute` hat check. In case a reporter has side-effects, we'd probably like to run hat predicates even if there is no next block. --- src/engine/execute.js | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/engine/execute.js b/src/engine/execute.js index 3ab78752d..1d4ec6149 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -35,23 +35,13 @@ var execute = function (sequencer, thread) { // 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) { + if (!blockFunction) { + if (!isHat) { console.warn('Could not get implementation for opcode: ' + opcode); - return; } + // Skip through the block. + // (either hat with no predicate, or missing op). + return; } // Generate values for arguments (inputs). From 64b82f4dc23dd44be177629c02b3ca42dcc6e2ed Mon Sep 17 00:00:00 2001 From: Tim Mickel Date: Mon, 29 Aug 2016 09:52:34 -0400 Subject: [PATCH 09/13] Switch back j->i --- src/engine/runtime.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 1d591c7ac..afdb63cf2 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -350,8 +350,8 @@ Runtime.prototype._step = function () { } } var inactiveThreads = this.sequencer.stepThreads(this.threads); - for (var j = 0; j < inactiveThreads.length; j++) { - this._removeThread(inactiveThreads[j]); + for (var i = 0; i < inactiveThreads.length; i++) { + this._removeThread(inactiveThreads[i]); } }; From 40c90bbcc76291c589fc50f375b51bb80746a322 Mon Sep 17 00:00:00 2001 From: Tim Mickel Date: Mon, 29 Aug 2016 10:01:31 -0400 Subject: [PATCH 10/13] Add `isActiveThread` and simplify broadcast-and-wait accordingly --- src/blocks/scratch3_event.js | 12 ++++-------- src/engine/runtime.js | 9 +++++++++ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/blocks/scratch3_event.js b/src/blocks/scratch3_event.js index c03472698..f3f6e62a3 100644 --- a/src/blocks/scratch3_event.js +++ b/src/blocks/scratch3_event.js @@ -71,14 +71,10 @@ Scratch3EventBlocks.prototype.broadcastAndWait = function (args, util) { } } // We've run before; check if the wait is still going on. - var waiting = false; - for (var i = 0; i < util.stackFrame.triggeredThreads.length; i++) { - var thread = util.stackFrame.triggeredThreads[i]; - var activeThreads = this.runtime.threads; - if (activeThreads.indexOf(thread) > -1) { // @todo: A cleaner way? - waiting = true; - } - } + var instance = this; + var waiting = util.stackFrame.triggeredThreads.some(function(thread) { + return instance.runtime.isActiveThread(thread); + }); if (waiting) { util.yieldFrame(); } diff --git a/src/engine/runtime.js b/src/engine/runtime.js index afdb63cf2..8283bdf5e 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -211,6 +211,15 @@ Runtime.prototype._removeThread = function (thread) { } }; +/** + * Return whether a thread is currently active/running. + * @param {?Thread} thread Thread object to check. + * @return {Boolean} True if the thread is active/running. + */ +Runtime.prototype.isActiveThread = function (thread) { + return this.threads.indexOf(thread) > -1; +}; + /** * Toggle a script. * @param {!string} topBlockId ID of block that starts the script. From 3ccfdf3df0fd82a675fc7ed8e45647198a07ce8f Mon Sep 17 00:00:00 2001 From: Tim Mickel Date: Mon, 29 Aug 2016 10:03:21 -0400 Subject: [PATCH 11/13] Use `hasOwnProperty` in `getIsHat`/`getIsEdgeTriggeredHat` --- src/engine/runtime.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 8283bdf5e..9ddf20509 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -152,7 +152,7 @@ Runtime.prototype.getOpcodeFunction = function (opcode) { * @return {Boolean} True if the op is known to be a hat. */ Runtime.prototype.getIsHat = function (opcode) { - return opcode in this._hats; + return this._hats.hasOwnProperty(opcode); }; /** @@ -161,7 +161,8 @@ Runtime.prototype.getIsHat = function (opcode) { * @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; + return this._hats.hasOwnProperty(opcode) && + this._hats[opcode].edgeTriggered; }; /** From 1098a0698588b7c0d65ad5477779301fb8da3be3 Mon Sep 17 00:00:00 2001 From: Tim Mickel Date: Mon, 29 Aug 2016 10:18:49 -0400 Subject: [PATCH 12/13] Various renames for hat opcodes/top blocks, `allScriptsDo` --- src/engine/runtime.js | 69 ++++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 9ddf20509..17985c285 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -240,9 +240,8 @@ Runtime.prototype.toggleScript = function (topBlockId) { /** * 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. + * - the top block ID of the script. + * - the target that owns the script. * @param {!Function} f Function to call for each script. * @param {Target=} opt_target Optionally, a target to restrict to. */ @@ -255,28 +254,31 @@ Runtime.prototype.allScriptsDo = function (f, opt_target) { var target = targets[t]; var scripts = target.blocks.getScripts(); for (var j = 0; j < scripts.length; j++) { - var topBlock = scripts[j]; - var topOpcode = target.blocks.getBlock(topBlock).opcode; - var topFields = target.blocks.getFields(topBlock); - f(topBlock, topOpcode, topFields); + var topBlockId = scripts[j]; + f(topBlockId, target); } } }; /** * Trigger all relevant hats. - * @param {!string} requestedHat Name of hat to trigger. + * @param {!string} requestedHatOpcode Opcode 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, +Runtime.prototype.triggerHats = function (requestedHatOpcode, opt_matchFields, opt_target) { + if (!this._hats.hasOwnProperty(requestedHatOpcode)) { + // No known hat with this opcode. + return; + } var instance = this; var newThreads = []; - // Consider all scripts, looking for hats named `requestedHat`. - this.allScriptsDo(function(topBlockId, topOpcode, topFields) { - if (topOpcode !== requestedHat) { + // Consider all scripts, looking for hats with opcode `requestedHatOpcode`. + this.allScriptsDo(function(topBlockId, target) { + var potentialHatOpcode = target.blocks.getBlock(topBlockId).opcode; + if (potentialHatOpcode !== requestedHatOpcode) { // Not the right hat. return; } @@ -285,39 +287,38 @@ Runtime.prototype.triggerHats = function (requestedHat, // 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. + var hatFields = target.blocks.getFields(topBlockId); if (opt_matchFields) { for (var matchField in opt_matchFields) { - if (topFields[matchField].value !== + if (hatFields[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; - } + // Look up metadata for the relevant hat. + var hatMeta = instance._hats[requestedHatOpcode]; + 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)); } + // Start the thread with this top block. + newThreads.push(instance._pushThread(topBlockId)); }, opt_target); return newThreads; }; From bdc95cffc08c91c31763c68c89a668782f2f2531 Mon Sep 17 00:00:00 2001 From: Tim Mickel Date: Mon, 29 Aug 2016 10:26:26 -0400 Subject: [PATCH 13/13] Rename trigger->activate/start --- src/blocks/scratch3_event.js | 16 +++++------ src/engine/execute.js | 16 +++++------ src/engine/runtime.js | 56 ++++++++++++++++++------------------ 3 files changed, 44 insertions(+), 44 deletions(-) diff --git a/src/blocks/scratch3_event.js b/src/blocks/scratch3_event.js index f3f6e62a3..71326d494 100644 --- a/src/blocks/scratch3_event.js +++ b/src/blocks/scratch3_event.js @@ -34,7 +34,7 @@ Scratch3EventBlocks.prototype.getHats = function () { },*/ 'event_whengreaterthan': { restartExistingThreads: false, - edgeTriggered: true + edgeActivated: true }, 'event_whenbroadcastreceived': { restartExistingThreads: true @@ -51,28 +51,28 @@ Scratch3EventBlocks.prototype.hatGreaterThanPredicate = function (args, util) { }; Scratch3EventBlocks.prototype.broadcast = function(args, util) { - util.triggerHats('event_whenbroadcastreceived', { + util.startHats('event_whenbroadcastreceived', { 'BROADCAST_OPTION': args.BROADCAST_OPTION }); }; Scratch3EventBlocks.prototype.broadcastAndWait = function (args, util) { - // Have we run before, triggering threads? - if (!util.stackFrame.triggeredThreads) { - // No - trigger hats for this broadcast. - util.stackFrame.triggeredThreads = util.triggerHats( + // Have we run before, starting threads? + if (!util.stackFrame.startedThreads) { + // No - start hats for this broadcast. + util.stackFrame.startedThreads = util.startHats( 'event_whenbroadcastreceived', { 'BROADCAST_OPTION': args.BROADCAST_OPTION } ); - if (util.stackFrame.triggeredThreads.length == 0) { + if (util.stackFrame.startedThreads.length == 0) { // Nothing was started. return; } } // We've run before; check if the wait is still going on. var instance = this; - var waiting = util.stackFrame.triggeredThreads.some(function(thread) { + var waiting = util.stackFrame.startedThreads.some(function(thread) { return instance.runtime.isActiveThread(thread); }); if (waiting) { diff --git a/src/engine/execute.js b/src/engine/execute.js index 1d4ec6149..5efe9bebd 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -96,9 +96,9 @@ var execute = function (sequencer, thread) { startBranch: function (branchNum) { sequencer.stepToBranch(thread, branchNum); }, - triggerHats: function(requestedHat, opt_matchFields, opt_target) { + startHats: function(requestedHat, opt_matchFields, opt_target) { return ( - runtime.triggerHats(requestedHat, opt_matchFields, opt_target) + runtime.startHats(requestedHat, opt_matchFields, opt_target) ); }, ioQuery: function (device, func, args) { @@ -119,19 +119,19 @@ var execute = function (sequencer, thread) { thread.pushReportedValue(resolvedValue); if (isHat) { // Hat predicate was evaluated. - if (runtime.getIsEdgeTriggeredHat(opcode)) { - // If this is an edge-triggered hat, only proceed if + if (runtime.getIsEdgeActivatedHat(opcode)) { + // If this is an edge-activated hat, only proceed if // the value is true and used to be false. - var oldEdgeValue = runtime.updateEdgeTriggeredValue( + var oldEdgeValue = runtime.updateEdgeActivatedValue( currentBlockId, resolvedValue ); - var edgeWasTriggered = !oldEdgeValue && resolvedValue; - if (!edgeWasTriggered) { + var edgeWasActivated = !oldEdgeValue && resolvedValue; + if (!edgeWasActivated) { sequencer.retireThread(thread); } } else { - // Not an edge-triggered hat: retire the thread + // Not an edge-activated hat: retire the thread // if predicate was false. if (!resolvedValue) { sequencer.retireThread(thread); diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 17985c285..921839497 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -48,7 +48,7 @@ function Runtime (targets) { */ this._primitives = {}; this._hats = {}; - this._edgeTriggeredHatValues = {}; + this._edgeActivatedHatValues = {}; this._registerBlockPackages(); this.ioDevices = { @@ -156,32 +156,32 @@ Runtime.prototype.getIsHat = function (opcode) { }; /** - * Return whether an opcode represents an edge-triggered hat block. + * Return whether an opcode represents an edge-activated hat block. * @param {!string} opcode The opcode to look up. - * @return {Boolean} True if the op is known to be a edge-triggered hat. + * @return {Boolean} True if the op is known to be a edge-activated hat. */ -Runtime.prototype.getIsEdgeTriggeredHat = function (opcode) { +Runtime.prototype.getIsEdgeActivatedHat = function (opcode) { return this._hats.hasOwnProperty(opcode) && - this._hats[opcode].edgeTriggered; + this._hats[opcode].edgeActivated; }; /** - * Update an edge-triggered hat block value. + * Update an edge-activated 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. + * @param {*} newValue Value to store for edge-activated hat. + * @return {*} The old value for the edge-activated hat. */ -Runtime.prototype.updateEdgeTriggeredValue = function (blockId, newValue) { - var oldValue = this._edgeTriggeredHatValues[blockId]; - this._edgeTriggeredHatValues[blockId] = newValue; +Runtime.prototype.updateEdgeActivatedValue = function (blockId, newValue) { + var oldValue = this._edgeActivatedHatValues[blockId]; + this._edgeActivatedHatValues[blockId] = newValue; return oldValue; }; /** - * Clear all edge-triggered hat values. + * Clear all edge-activaed hat values. */ -Runtime.prototype.clearEdgeTriggeredValues = function () { - this._edgeTriggeredHatValues = {}; +Runtime.prototype.clearEdgeActivatedValues = function () { + this._edgeActivatedHatValues = {}; }; // ----------------------------------------------------------------------------- @@ -261,13 +261,13 @@ Runtime.prototype.allScriptsDo = function (f, opt_target) { }; /** - * Trigger all relevant hats. - * @param {!string} requestedHatOpcode Opcode of hat to trigger. + * Start all relevant hats. + * @param {!string} requestedHatOpcode Opcode of hats to start. * @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. + * @return {Array.} List of threads started by this function. */ -Runtime.prototype.triggerHats = function (requestedHatOpcode, +Runtime.prototype.startHats = function (requestedHatOpcode, opt_matchFields, opt_target) { if (!this._hats.hasOwnProperty(requestedHatOpcode)) { // No known hat with this opcode. @@ -286,7 +286,7 @@ Runtime.prototype.triggerHats = function (requestedHatOpcode, // 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. + // needs to have a precise collection of started threads. var hatFields = target.blocks.getFields(topBlockId); if (opt_matchFields) { for (var matchField in opt_matchFields) { @@ -300,16 +300,16 @@ Runtime.prototype.triggerHats = function (requestedHatOpcode, // Look up metadata for the relevant hat. var hatMeta = instance._hats[requestedHatOpcode]; if (hatMeta.restartExistingThreads) { - // If `restartExistingThreads` is true, this trigger - // should stop any existing threads starting with the top block. + // If `restartExistingThreads` is true, we 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. + // If `restartExistingThreads` is false, we 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. @@ -328,8 +328,8 @@ Runtime.prototype.triggerHats = function (requestedHatOpcode, */ Runtime.prototype.greenFlag = function () { this.ioDevices.clock.resetProjectTimer(); - this.clearEdgeTriggeredValues(); - this.triggerHats('event_whenflagclicked'); + this.clearEdgeActivatedValues(); + this.startHats('event_whenflagclicked'); }; /** @@ -353,11 +353,11 @@ 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. + // Find all edge-activated 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); + if (hat.edgeActivated) { + this.startHats(hatType); } } var inactiveThreads = this.sequencer.stepThreads(this.threads);