From 190208b620276837b06d7b0865f67a53fda8243a Mon Sep 17 00:00:00 2001 From: Tim Mickel <tim.mickel@gmail.com> Date: Fri, 17 Jun 2016 14:36:36 -0400 Subject: [PATCH 01/27] Clean up yield-timers: support multiple, move logic to Threads. --- src/engine/execute.js | 14 +------------- src/engine/sequencer.js | 8 ++++---- src/engine/thread.js | 31 +++++++++++++++++++++++++++++-- src/util/yieldtimers.js | 19 ------------------- 4 files changed, 34 insertions(+), 38 deletions(-) diff --git a/src/engine/execute.js b/src/engine/execute.js index 0d1dbc3cc..6c58cd156 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -1,5 +1,3 @@ -var YieldTimers = require('../util/yieldtimers.js'); - /** * If set, block calls, args, and return values will be logged to the console. * @const {boolean} @@ -13,11 +11,6 @@ var execute = function (sequencer, thread) { var currentBlockId = thread.peekStack(); var currentStackFrame = thread.peekStackFrame(); - // Save the yield timer ID, in case a primitive makes a new one - // @todo hack - perhaps patch this to allow more than one timer per - // primitive, for example... - var oldYieldTimerId = YieldTimers.timerId; - var opcode = runtime.blocks.getOpcode(currentBlockId); // Generate values for arguments (inputs). @@ -64,17 +57,12 @@ var execute = function (sequencer, thread) { done: function() { sequencer.proceedThread(thread); }, - timeout: YieldTimers.timeout, + timeout: thread.addTimeout.bind(thread), stackFrame: currentStackFrame, startSubstack: function (substackNum) { sequencer.stepToSubstack(thread, substackNum); } }); - // Update if the thread has set a yield timer ID - // @todo hack - if (YieldTimers.timerId > oldYieldTimerId) { - thread.yieldTimerId = YieldTimers.timerId; - } if (DEBUG_BLOCK_CALLS) { console.log('ending stack frame: ', currentStackFrame); console.log('returned: ', primitiveReturnValue); diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js index deaa15266..1541d1c6f 100644 --- a/src/engine/sequencer.js +++ b/src/engine/sequencer.js @@ -53,10 +53,10 @@ Sequencer.prototype.stepThreads = function (threads) { // Normal-mode thread: step. this.startThread(activeThread); } else if (activeThread.status === Thread.STATUS_YIELD) { - // Yield-mode thread: check if the time has passed. - if (!YieldTimers.resolve(activeThread.yieldTimerId)) { - // Thread is still yielding - // if YieldTimers.resolve returns false. + // Yield-mode thread: resolve timers. + activeThread.resolveTimeouts(); + if (activeThread.status === Thread.STATUS_YIELD) { + // Still yielding. numYieldingThreads++; } } else if (activeThread.status === Thread.STATUS_DONE) { diff --git a/src/engine/thread.js b/src/engine/thread.js index c98efab48..bd991db27 100644 --- a/src/engine/thread.js +++ b/src/engine/thread.js @@ -1,3 +1,5 @@ +var YieldTimers = require('../util/yieldtimers.js'); + /** * A thread is a running stack context and all the metadata needed. * @param {?string} firstBlock First block to execute in the thread. @@ -30,10 +32,10 @@ function Thread (firstBlock) { this.status = 0; /* Thread.STATUS_RUNNING */ /** - * Yield timer ID (for checking when the thread should unyield). + * Execution-synced timeouts. * @type {number} */ - this.yieldTimerId = -1; + this.timeoutIds = []; } /** @@ -104,4 +106,29 @@ Thread.prototype.yield = function () { this.status = Thread.STATUS_YIELD; }; +/** + * Add an execution-synced timeouts for this thread. + * See also: util/yieldtimers.js:timeout + * @param {!Function} callback To be called when the timer is done. + * @param {number} timeDelta Time to wait, in ms. + */ +Thread.prototype.addTimeout = function (callback, timeDelta) { + var timeoutId = YieldTimers.timeout(callback, timeDelta); + this.timeoutIds.push(timeoutId); +}; + +/** + * Attempt to resolve all execution-synced timeouts on this thread. + */ +Thread.prototype.resolveTimeouts = function () { + var newTimeouts = []; + for (var i = 0; i < this.timeoutIds.length; i++) { + var resolved = YieldTimers.resolve(this.timeoutIds[i]); + if (!resolved) { + newTimeouts.push(this.timeoutIds[i]); + } + } + this.timeoutIds = newTimeouts; +}; + module.exports = Thread; diff --git a/src/util/yieldtimers.js b/src/util/yieldtimers.js index 45e244eaf..84c6d3259 100644 --- a/src/util/yieldtimers.js +++ b/src/util/yieldtimers.js @@ -68,23 +68,4 @@ YieldTimers.resolve = function (id) { return true; }; -/** - * Reject a timer so the callback never executes. - * @param {number} id Timer ID to reject. - */ -YieldTimers.reject = function (id) { - if (YieldTimers.timers[id]) { - delete YieldTimers.timers[id]; - } -}; - -/** - * Reject all timers currently stored. - * Especially useful for a Scratch "stop." - */ -YieldTimers.rejectAll = function () { - YieldTimers.timers = {}; - YieldTimers.timerId = 0; -}; - module.exports = YieldTimers; From 34659c9b7b2aa4a66664591460a52712e1ea96c5 Mon Sep 17 00:00:00 2001 From: Tim Mickel <tim.mickel@gmail.com> Date: Fri, 17 Jun 2016 15:08:54 -0400 Subject: [PATCH 02/27] Allow console timers --- .eslintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintrc b/.eslintrc index bcd88942f..fcb231522 100644 --- a/.eslintrc +++ b/.eslintrc @@ -8,7 +8,7 @@ "max-len": [2, 80, 4], "semi": [2, "always"], "strict": [2, "never"], - "no-console": [2, {"allow": ["log", "warn", "error", "groupCollapsed", "groupEnd"]}], + "no-console": [2, {"allow": ["log", "warn", "error", "groupCollapsed", "groupEnd", "time", "timeEnd"]}], "valid-jsdoc": ["error", {"requireReturn": false}] }, "env": { From 97f7571c6faa5454579b9776b6e51b8cfbcc9cf2 Mon Sep 17 00:00:00 2001 From: Tim Mickel <tim.mickel@gmail.com> Date: Fri, 17 Jun 2016 15:10:12 -0400 Subject: [PATCH 03/27] Prototype implementation of yielding reporters --- src/engine/execute.js | 53 ++++++++++++++++++++++++++++++++++------- src/engine/sequencer.js | 3 +-- src/engine/thread.js | 17 ++++++++++++- 3 files changed, 61 insertions(+), 12 deletions(-) diff --git a/src/engine/execute.js b/src/engine/execute.js index 6c58cd156..add9c9909 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -1,10 +1,19 @@ +var Thread = require('./thread'); + /** * If set, block calls, args, and return values will be logged to the console. * @const {boolean} */ var DEBUG_BLOCK_CALLS = true; -var execute = function (sequencer, thread) { +/** + * Execute a block. + * @param {!Sequencer} sequencer Which sequencer is executing. + * @param {!Thread} thread Thread which to read and execute. + * @param {string=} opt_waitingInputName If evaluating an input, its name. + * @return {?Any} Reported value, if available immediately. + */ +var execute = function (sequencer, thread, opt_waitingInputName) { var runtime = sequencer.runtime; // Current block to execute is the one on the top of the stack. @@ -27,11 +36,29 @@ var execute = function (sequencer, thread) { for (var inputName in inputs) { var input = inputs[inputName]; var inputBlockId = input.block; - // Push to the stack to evaluate this input. - thread.pushStack(inputBlockId); - var result = execute(sequencer, thread); - thread.popStack(); - argValues[input.name] = result; + // Is there a value for this input waiting in the stack frame? + if (currentStackFrame.reported && + currentStackFrame.reported[inputName]) { + // Use that value. + argValues[inputName] = currentStackFrame.reported[inputName]; + } else { + // Otherwise, we need to evaluate the block. + // Push to the stack to evaluate this input. + thread.pushStack(inputBlockId); + if (DEBUG_BLOCK_CALLS) { + console.time('Yielding reporter evaluation'); + } + var result = execute(sequencer, thread, inputName); + // Did the reporter yield? + if (thread.status === Thread.STATUS_YIELD) { + // Reporter yielded; don't pop stack and wait for it to unyield. + // The value will be populated once the reporter unyields, + // and passed up to the currentStackFrame on next execution. + return; + } + thread.popStack(); + argValues[inputName] = result; + } } if (!opcode) { @@ -51,21 +78,29 @@ var execute = function (sequencer, thread) { console.log('and stack frame: ', currentStackFrame); } var primitiveReturnValue = null; - // @todo deal with the return value primitiveReturnValue = blockFunction(argValues, { yield: thread.yield.bind(thread), done: function() { sequencer.proceedThread(thread); }, + report: function(reportedValue) { + thread.pushReportedValue(opt_waitingInputName, reportedValue); + if (DEBUG_BLOCK_CALLS) { + console.log('Reported: ', reportedValue, + ' for ', opt_waitingInputName); + console.timeEnd('Yielding reporter evaluation'); + } + sequencer.proceedThread(thread); + }, timeout: thread.addTimeout.bind(thread), - stackFrame: currentStackFrame, + stackFrame: currentStackFrame.executionContext, startSubstack: function (substackNum) { sequencer.stepToSubstack(thread, substackNum); } }); if (DEBUG_BLOCK_CALLS) { console.log('ending stack frame: ', currentStackFrame); - console.log('returned: ', primitiveReturnValue); + console.log('returned immediately: ', primitiveReturnValue); console.groupEnd(); } return primitiveReturnValue; diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js index 1541d1c6f..e82d4054c 100644 --- a/src/engine/sequencer.js +++ b/src/engine/sequencer.js @@ -1,6 +1,5 @@ var Timer = require('../util/timer'); var Thread = require('./thread'); -var YieldTimers = require('../util/yieldtimers.js'); var execute = require('./execute.js'); function Sequencer (runtime) { @@ -101,7 +100,7 @@ Sequencer.prototype.startThread = function (thread) { // move to done. if (thread.status === Thread.STATUS_RUNNING && thread.peekStack() === currentBlockId) { - this.proceedThread(thread, currentBlockId); + this.proceedThread(thread); } }; diff --git a/src/engine/thread.js b/src/engine/thread.js index bd991db27..65b251648 100644 --- a/src/engine/thread.js +++ b/src/engine/thread.js @@ -69,7 +69,10 @@ Thread.prototype.pushStack = function (blockId) { // Push an empty stack frame, if we need one. // Might not, if we just popped the stack. if (this.stack.length > this.stackFrames.length) { - this.stackFrames.push({}); + this.stackFrames.push({ + reported: {}, // Collects reported input values. + executionContext: {} // A context passed to block implementations. + }); } }; @@ -99,6 +102,18 @@ Thread.prototype.peekStackFrame = function () { return this.stackFrames[this.stackFrames.length - 1]; }; +/** + * Push a reported value to the parent of the current stack frame. + * @param {!string} inputName Name of input reported. + * @param {!Any} value Reported value to push. + */ +Thread.prototype.pushReportedValue = function (inputName, value) { + var parentStackFrame = this.stackFrames[this.stackFrames.length - 2]; + if (parentStackFrame) { + parentStackFrame.reported[inputName] = value; + } +}; + /** * Yields the thread. */ From 7ef3807b18bbe258cb3f318fa7ef4a5c6d6c3e11 Mon Sep 17 00:00:00 2001 From: Tim Mickel <tim.mickel@gmail.com> Date: Fri, 17 Jun 2016 15:10:28 -0400 Subject: [PATCH 04/27] Example of a yielding reporter (returns random number after 1s) --- src/blocks/scratch3_operators.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/blocks/scratch3_operators.js b/src/blocks/scratch3_operators.js index 94329b5a3..0b4471c35 100644 --- a/src/blocks/scratch3_operators.js +++ b/src/blocks/scratch3_operators.js @@ -15,7 +15,8 @@ Scratch3OperatorsBlocks.prototype.getPrimitives = function() { 'math_number': this.number, 'text': this.text, 'operator_add': this.add, - 'operator_equals': this.equals + 'operator_equals': this.equals, + 'operator_random': this.random }; }; @@ -35,4 +36,15 @@ Scratch3OperatorsBlocks.prototype.equals = function (args) { return args.OPERAND1 == args.OPERAND2; }; +Scratch3OperatorsBlocks.prototype.random = function (args, util) { + // As a demo, this implementation of random returns after 1 second of yield. + // @todo Match Scratch 2.0 implementation with int-truncation. + // See: http://bit.ly/1Qc0GzC + util.yield(); + setTimeout(function() { + var randomValue = (Math.random() * (args.TO - args.FROM)) + args.FROM; + util.report(randomValue); + }, 1000); +}; + module.exports = Scratch3OperatorsBlocks; From d15c93af053452c8818059001947caf17e10e8cd Mon Sep 17 00:00:00 2001 From: Tim Mickel <tim.mickel@gmail.com> Date: Fri, 17 Jun 2016 15:53:58 -0400 Subject: [PATCH 05/27] Keep "waiting reporter name" on the stack frame. Also add highlighting for inputs. --- src/engine/execute.js | 14 ++++++++------ src/engine/thread.js | 8 +++++--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/engine/execute.js b/src/engine/execute.js index add9c9909..6657a3e15 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -10,10 +10,9 @@ var DEBUG_BLOCK_CALLS = true; * Execute a block. * @param {!Sequencer} sequencer Which sequencer is executing. * @param {!Thread} thread Thread which to read and execute. - * @param {string=} opt_waitingInputName If evaluating an input, its name. * @return {?Any} Reported value, if available immediately. */ -var execute = function (sequencer, thread, opt_waitingInputName) { +var execute = function (sequencer, thread) { var runtime = sequencer.runtime; // Current block to execute is the one on the top of the stack. @@ -48,14 +47,18 @@ var execute = function (sequencer, thread, opt_waitingInputName) { if (DEBUG_BLOCK_CALLS) { console.time('Yielding reporter evaluation'); } - var result = execute(sequencer, thread, inputName); + runtime.glowBlock(inputBlockId, true); + var result = execute(sequencer, thread); // Did the reporter yield? if (thread.status === Thread.STATUS_YIELD) { // Reporter yielded; don't pop stack and wait for it to unyield. // The value will be populated once the reporter unyields, // and passed up to the currentStackFrame on next execution. + // Save name of this input to be filled by child `util.report`. + currentStackFrame.waitingReporter = inputName; return; } + runtime.glowBlock(inputBlockId, false); thread.popStack(); argValues[inputName] = result; } @@ -84,12 +87,11 @@ var execute = function (sequencer, thread, opt_waitingInputName) { sequencer.proceedThread(thread); }, report: function(reportedValue) { - thread.pushReportedValue(opt_waitingInputName, reportedValue); if (DEBUG_BLOCK_CALLS) { - console.log('Reported: ', reportedValue, - ' for ', opt_waitingInputName); + console.log('Reported: ', reportedValue); console.timeEnd('Yielding reporter evaluation'); } + thread.pushReportedValue(reportedValue); sequencer.proceedThread(thread); }, timeout: thread.addTimeout.bind(thread), diff --git a/src/engine/thread.js b/src/engine/thread.js index 65b251648..bbb23f678 100644 --- a/src/engine/thread.js +++ b/src/engine/thread.js @@ -71,6 +71,7 @@ Thread.prototype.pushStack = function (blockId) { if (this.stack.length > this.stackFrames.length) { this.stackFrames.push({ reported: {}, // Collects reported input values. + waitingReporter: null, // Name of waiting reporter. executionContext: {} // A context passed to block implementations. }); } @@ -104,13 +105,14 @@ Thread.prototype.peekStackFrame = function () { /** * Push a reported value to the parent of the current stack frame. - * @param {!string} inputName Name of input reported. * @param {!Any} value Reported value to push. */ -Thread.prototype.pushReportedValue = function (inputName, value) { +Thread.prototype.pushReportedValue = function (value) { var parentStackFrame = this.stackFrames[this.stackFrames.length - 2]; if (parentStackFrame) { - parentStackFrame.reported[inputName] = value; + var waitingReporter = parentStackFrame.waitingReporter; + parentStackFrame.reported[waitingReporter] = value; + parentStackFrame.waitingReporter = null; } }; From bed3e28c02dd1e05d7c6288e29e3a78c6e98d9f0 Mon Sep 17 00:00:00 2001 From: Tim Mickel <tim.mickel@gmail.com> Date: Fri, 17 Jun 2016 17:18:44 -0400 Subject: [PATCH 06/27] Simplifications of `execute` ordering and always cache returned reporter values in currentStackFrame.reported. --- src/engine/execute.js | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/engine/execute.js b/src/engine/execute.js index 6657a3e15..2112da0f0 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -21,6 +21,17 @@ var execute = function (sequencer, thread) { var opcode = runtime.blocks.getOpcode(currentBlockId); + if (!opcode) { + console.warn('Could not get opcode for block: ' + currentBlockId); + return; + } + + var blockFunction = runtime.getOpcodeFunction(opcode); + if (!blockFunction) { + console.warn('Could not get implementation for opcode: ' + opcode); + return; + } + // Generate values for arguments (inputs). var argValues = {}; @@ -36,11 +47,7 @@ var execute = function (sequencer, thread) { var input = inputs[inputName]; var inputBlockId = input.block; // Is there a value for this input waiting in the stack frame? - if (currentStackFrame.reported && - currentStackFrame.reported[inputName]) { - // Use that value. - argValues[inputName] = currentStackFrame.reported[inputName]; - } else { + if (!currentStackFrame.reported[inputName]) { // Otherwise, we need to evaluate the block. // Push to the stack to evaluate this input. thread.pushStack(inputBlockId); @@ -50,29 +57,20 @@ var execute = function (sequencer, thread) { runtime.glowBlock(inputBlockId, true); var result = execute(sequencer, thread); // Did the reporter yield? + currentStackFrame.waitingReporter = inputName; if (thread.status === Thread.STATUS_YIELD) { // Reporter yielded; don't pop stack and wait for it to unyield. // The value will be populated once the reporter unyields, // and passed up to the currentStackFrame on next execution. // Save name of this input to be filled by child `util.report`. - currentStackFrame.waitingReporter = inputName; return; } runtime.glowBlock(inputBlockId, false); - thread.popStack(); + thread.pushReportedValue(result); argValues[inputName] = result; + thread.popStack(); } - } - - if (!opcode) { - console.warn('Could not get opcode for block: ' + currentBlockId); - return; - } - - var blockFunction = runtime.getOpcodeFunction(opcode); - if (!blockFunction) { - console.warn('Could not get implementation for opcode: ' + opcode); - return; + argValues[inputName] = currentStackFrame.reported[inputName]; } if (DEBUG_BLOCK_CALLS) { From 6181bcd5cba666cea2a49ca974f7a326e8b63e12 Mon Sep 17 00:00:00 2001 From: Tim Mickel <tim.mickel@gmail.com> Date: Mon, 20 Jun 2016 14:10:19 -0400 Subject: [PATCH 07/27] Refactor Thread.peekParentStackFrame --- src/engine/thread.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/engine/thread.js b/src/engine/thread.js index bbb23f678..746d7dcc6 100644 --- a/src/engine/thread.js +++ b/src/engine/thread.js @@ -103,12 +103,20 @@ Thread.prototype.peekStackFrame = function () { return this.stackFrames[this.stackFrames.length - 1]; }; +/** + * Get stack frame above the current top. + * @return {?Object} Second to last stack frame stored on this thread. + */ +Thread.prototype.peekParentStackFrame = function () { + return this.stackFrames[this.stackFrames.length - 2]; +}; + /** * Push a reported value to the parent of the current stack frame. * @param {!Any} value Reported value to push. */ Thread.prototype.pushReportedValue = function (value) { - var parentStackFrame = this.stackFrames[this.stackFrames.length - 2]; + var parentStackFrame = this.peekParentStackFrame(); if (parentStackFrame) { var waitingReporter = parentStackFrame.waitingReporter; parentStackFrame.reported[waitingReporter] = value; From 173f0615d3e99246318ec133bf54ff244aa9edbf Mon Sep 17 00:00:00 2001 From: Tim Mickel <tim.mickel@gmail.com> Date: Mon, 20 Jun 2016 14:11:21 -0400 Subject: [PATCH 08/27] Refactor: always push reports to the stack frame --- src/engine/execute.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/engine/execute.js b/src/engine/execute.js index 2112da0f0..dae207a9c 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -10,7 +10,6 @@ var DEBUG_BLOCK_CALLS = true; * Execute a block. * @param {!Sequencer} sequencer Which sequencer is executing. * @param {!Thread} thread Thread which to read and execute. - * @return {?Any} Reported value, if available immediately. */ var execute = function (sequencer, thread) { var runtime = sequencer.runtime; @@ -47,7 +46,7 @@ var execute = function (sequencer, thread) { var input = inputs[inputName]; var inputBlockId = input.block; // Is there a value for this input waiting in the stack frame? - if (!currentStackFrame.reported[inputName]) { + if (typeof currentStackFrame.reported[inputName] === 'undefined') { // Otherwise, we need to evaluate the block. // Push to the stack to evaluate this input. thread.pushStack(inputBlockId); @@ -55,9 +54,8 @@ var execute = function (sequencer, thread) { console.time('Yielding reporter evaluation'); } runtime.glowBlock(inputBlockId, true); - var result = execute(sequencer, thread); - // Did the reporter yield? currentStackFrame.waitingReporter = inputName; + execute(sequencer, thread); if (thread.status === Thread.STATUS_YIELD) { // Reporter yielded; don't pop stack and wait for it to unyield. // The value will be populated once the reporter unyields, @@ -66,8 +64,6 @@ var execute = function (sequencer, thread) { return; } runtime.glowBlock(inputBlockId, false); - thread.pushReportedValue(result); - argValues[inputName] = result; thread.popStack(); } argValues[inputName] = currentStackFrame.reported[inputName]; @@ -98,12 +94,16 @@ var execute = function (sequencer, thread) { sequencer.stepToSubstack(thread, substackNum); } }); + if (thread.status === Thread.STATUS_RUNNING) { + if (DEBUG_BLOCK_CALLS) { + console.log('reporting value: ', primitiveReturnValue); + } + thread.pushReportedValue(primitiveReturnValue); + } if (DEBUG_BLOCK_CALLS) { console.log('ending stack frame: ', currentStackFrame); - console.log('returned immediately: ', primitiveReturnValue); console.groupEnd(); } - return primitiveReturnValue; }; module.exports = execute; From b21c9edf046807bbdb926cb5afd6813b22475b1a Mon Sep 17 00:00:00 2001 From: Tim Mickel <tim.mickel@gmail.com> Date: Mon, 20 Jun 2016 14:24:00 -0400 Subject: [PATCH 09/27] Commenting improvements --- src/engine/execute.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/engine/execute.js b/src/engine/execute.js index dae207a9c..64e69ac77 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -45,22 +45,22 @@ var execute = function (sequencer, thread) { for (var inputName in inputs) { var input = inputs[inputName]; var inputBlockId = input.block; - // Is there a value for this input waiting in the stack frame? + // Is there no value for this input waiting in the stack frame? if (typeof currentStackFrame.reported[inputName] === 'undefined') { - // Otherwise, we need to evaluate the block. + // If there's not, we need to evaluate the block. // Push to the stack to evaluate this input. thread.pushStack(inputBlockId); if (DEBUG_BLOCK_CALLS) { - console.time('Yielding reporter evaluation'); + console.time('Reporter evaluation'); } runtime.glowBlock(inputBlockId, true); + // Save name of input for `Thread.pushReportedValue`. currentStackFrame.waitingReporter = inputName; execute(sequencer, thread); if (thread.status === Thread.STATUS_YIELD) { // Reporter yielded; don't pop stack and wait for it to unyield. // The value will be populated once the reporter unyields, // and passed up to the currentStackFrame on next execution. - // Save name of this input to be filled by child `util.report`. return; } runtime.glowBlock(inputBlockId, false); From 9d9749681bf21bb23e592d6e0ed367fbf82d197d Mon Sep 17 00:00:00 2001 From: Tim Mickel <tim.mickel@gmail.com> Date: Mon, 20 Jun 2016 14:28:12 -0400 Subject: [PATCH 10/27] Comment and `else` for reporter finishes right away --- src/engine/execute.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/engine/execute.js b/src/engine/execute.js index 64e69ac77..6dfd4e331 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -62,9 +62,11 @@ var execute = function (sequencer, thread) { // The value will be populated once the reporter unyields, // and passed up to the currentStackFrame on next execution. return; + } else { + // Reporter finished right away; pop the stack. + runtime.glowBlock(inputBlockId, false); + thread.popStack(); } - runtime.glowBlock(inputBlockId, false); - thread.popStack(); } argValues[inputName] = currentStackFrame.reported[inputName]; } From e83cfa6049f59ca1bfa5c3c704867ed7288518d0 Mon Sep 17 00:00:00 2001 From: Tim Mickel <tim.mickel@gmail.com> Date: Mon, 20 Jun 2016 14:29:47 -0400 Subject: [PATCH 11/27] Add comment and clear currentStackFrame.reported --- src/engine/execute.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/engine/execute.js b/src/engine/execute.js index 6dfd4e331..007739b0b 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -71,6 +71,12 @@ var execute = function (sequencer, thread) { argValues[inputName] = currentStackFrame.reported[inputName]; } + // 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 + // (e.g., on return from a substack) gets fresh inputs. + currentStackFrame.reported = {}; + if (DEBUG_BLOCK_CALLS) { console.groupCollapsed('Executing: ' + opcode); console.log('with arguments: ', argValues); From e56c6e69803c983c424059788dc1f339f6267c3f Mon Sep 17 00:00:00 2001 From: Tim Mickel <tim.mickel@gmail.com> Date: Mon, 20 Jun 2016 14:31:48 -0400 Subject: [PATCH 12/27] Rename `primitiveReturnValue` -> `primitiveReportedValue` --- src/engine/execute.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/engine/execute.js b/src/engine/execute.js index 007739b0b..2cee0a1c9 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -82,8 +82,8 @@ var execute = function (sequencer, thread) { console.log('with arguments: ', argValues); console.log('and stack frame: ', currentStackFrame); } - var primitiveReturnValue = null; - primitiveReturnValue = blockFunction(argValues, { + var primitiveReportedValue = null; + primitiveReportedValue = blockFunction(argValues, { yield: thread.yield.bind(thread), done: function() { sequencer.proceedThread(thread); @@ -104,9 +104,9 @@ var execute = function (sequencer, thread) { }); if (thread.status === Thread.STATUS_RUNNING) { if (DEBUG_BLOCK_CALLS) { - console.log('reporting value: ', primitiveReturnValue); + console.log('reporting value: ', primitiveReportedValue); } - thread.pushReportedValue(primitiveReturnValue); + thread.pushReportedValue(primitiveReportedValue); } if (DEBUG_BLOCK_CALLS) { console.log('ending stack frame: ', currentStackFrame); From f210c12d4d37ae401f029de5e280bf411719d4d6 Mon Sep 17 00:00:00 2001 From: Tim Mickel <tim.mickel@gmail.com> Date: Mon, 20 Jun 2016 14:42:06 -0400 Subject: [PATCH 13/27] Add more operators for testing --- src/blocks/scratch3_operators.js | 42 ++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/blocks/scratch3_operators.js b/src/blocks/scratch3_operators.js index 0b4471c35..03fe3e352 100644 --- a/src/blocks/scratch3_operators.js +++ b/src/blocks/scratch3_operators.js @@ -15,7 +15,14 @@ Scratch3OperatorsBlocks.prototype.getPrimitives = function() { 'math_number': this.number, 'text': this.text, 'operator_add': this.add, + 'operator_subtract': this.subtract, + 'operator_multiply': this.multiply, + 'operator_divide': this.divide, + 'operator_lt': this.lt, 'operator_equals': this.equals, + 'operator_gt': this.gt, + 'operator_and': this.and, + 'operator_or': this.or, 'operator_random': this.random }; }; @@ -32,10 +39,45 @@ Scratch3OperatorsBlocks.prototype.add = function (args) { return args.NUM1 + args.NUM2; }; +Scratch3OperatorsBlocks.prototype.subtract = function (args) { + return args.NUM1 - args.NUM2; +}; + +Scratch3OperatorsBlocks.prototype.multiply = function (args) { + return args.NUM1 * args.NUM2; +}; + +Scratch3OperatorsBlocks.prototype.divide = function (args) { + return args.NUM1 / args.NUM2; +}; + +Scratch3OperatorsBlocks.prototype.lt = function (args) { + return args.OPERAND1 < args.OPERAND2; +}; + Scratch3OperatorsBlocks.prototype.equals = function (args) { return args.OPERAND1 == args.OPERAND2; }; +Scratch3OperatorsBlocks.prototype.gt = function (args) { + return args.OPERAND1 > args.OPERAND2; +}; + +Scratch3OperatorsBlocks.prototype.and = function (args) { + if (!args.OPERAND1 || !args.OPERAND2) { + return false; + } + return true; +}; + +Scratch3OperatorsBlocks.prototype.or = function (args) { + return args.OPERAND1 || args.OPERAND2; +}; + +Scratch3OperatorsBlocks.prototype.not = function (args) { + return !args.OPERAND; +}; + Scratch3OperatorsBlocks.prototype.random = function (args, util) { // As a demo, this implementation of random returns after 1 second of yield. // @todo Match Scratch 2.0 implementation with int-truncation. From f802faa461ae102cea802f0781b9e80defa4974e Mon Sep 17 00:00:00 2001 From: Tim Mickel <tim.mickel@gmail.com> Date: Mon, 20 Jun 2016 14:42:56 -0400 Subject: [PATCH 14/27] operator_not in primitive table --- src/blocks/scratch3_operators.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/blocks/scratch3_operators.js b/src/blocks/scratch3_operators.js index 03fe3e352..55b3b1129 100644 --- a/src/blocks/scratch3_operators.js +++ b/src/blocks/scratch3_operators.js @@ -23,6 +23,7 @@ Scratch3OperatorsBlocks.prototype.getPrimitives = function() { 'operator_gt': this.gt, 'operator_and': this.and, 'operator_or': this.or, + 'operator_not': this.not, 'operator_random': this.random }; }; From c63747e61bce51c0ad78ddc6f35f84d90230f60d Mon Sep 17 00:00:00 2001 From: Tim Mickel <tim.mickel@gmail.com> Date: Mon, 20 Jun 2016 15:04:28 -0400 Subject: [PATCH 15/27] Move stepping logic for reporters to sequencer --- src/engine/execute.js | 23 ++++++----------------- src/engine/sequencer.js | 27 +++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/engine/execute.js b/src/engine/execute.js index 2cee0a1c9..6e1962808 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -48,24 +48,13 @@ var execute = function (sequencer, thread) { // Is there no value for this input waiting in the stack frame? if (typeof currentStackFrame.reported[inputName] === 'undefined') { // If there's not, we need to evaluate the block. - // Push to the stack to evaluate this input. - thread.pushStack(inputBlockId); - if (DEBUG_BLOCK_CALLS) { - console.time('Reporter evaluation'); - } - runtime.glowBlock(inputBlockId, true); - // Save name of input for `Thread.pushReportedValue`. - currentStackFrame.waitingReporter = inputName; - execute(sequencer, thread); - if (thread.status === Thread.STATUS_YIELD) { - // Reporter yielded; don't pop stack and wait for it to unyield. - // The value will be populated once the reporter unyields, - // and passed up to the currentStackFrame on next execution. + var reporterYielded = ( + sequencer.stepToReporter(thread, inputBlockId, inputName) + ); + // If the reporter yielded, return immediately; + // it needs time to finish and report its value. + if (reporterYielded) { return; - } else { - // Reporter finished right away; pop the stack. - runtime.glowBlock(inputBlockId, false); - thread.popStack(); } } argValues[inputName] = currentStackFrame.reported[inputName]; diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js index e82d4054c..27e3b9996 100644 --- a/src/engine/sequencer.js +++ b/src/engine/sequencer.js @@ -127,6 +127,33 @@ Sequencer.prototype.stepToSubstack = function (thread, substackNum) { } }; +/** + * Step a thread into an input reporter, and manage its status appropriately. + * @param {!Thread} thread Thread object to step to reporter. + * @param {!string} blockId ID of reporter block. + * @param {!string} inputName Name of input on parent block. + * @return {boolean} True if yielded, false if it finished immediately. + */ +Sequencer.prototype.stepToReporter = function (thread, blockId, inputName) { + var currentStackFrame = thread.peekStackFrame(); + // Push to the stack to evaluate the reporter block. + thread.pushStack(blockId); + // Save name of input for `Thread.pushReportedValue`. + currentStackFrame.waitingReporter = inputName; + // Actually execute the block. + this.startThread(thread); + if (thread.status === Thread.STATUS_YIELD) { + // Reporter yielded; caller must wait for it to unyield. + // The value will be populated once the reporter unyields, + // and passed up to the currentStackFrame on next execution. + return true; + } else if (thread.status === Thread.STATUS_DONE) { + // Reporter finished, mark the thread as running. + thread.status = Thread.STATUS_RUNNING; + return false; + } +}; + /** * Finish stepping a thread and proceed it to the next block. * @param {!Thread} thread Thread object to proceed. From d44b806b4f4b883393306dd30aad90647592d210 Mon Sep 17 00:00:00 2001 From: Tim Mickel <tim.mickel@gmail.com> Date: Mon, 20 Jun 2016 16:44:53 -0400 Subject: [PATCH 16/27] Add blocking yield mode --- src/blocks/scratch3_operators.js | 2 +- src/engine/execute.js | 1 + src/engine/sequencer.js | 26 ++++++++++++++++++++++++++ src/engine/thread.js | 17 ++++++++++++++++- 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/blocks/scratch3_operators.js b/src/blocks/scratch3_operators.js index 55b3b1129..fe194da45 100644 --- a/src/blocks/scratch3_operators.js +++ b/src/blocks/scratch3_operators.js @@ -83,7 +83,7 @@ Scratch3OperatorsBlocks.prototype.random = function (args, util) { // As a demo, this implementation of random returns after 1 second of yield. // @todo Match Scratch 2.0 implementation with int-truncation. // See: http://bit.ly/1Qc0GzC - util.yield(); + util.yieldAndBlock(); setTimeout(function() { var randomValue = (Math.random() * (args.TO - args.FROM)) + args.FROM; util.report(randomValue); diff --git a/src/engine/execute.js b/src/engine/execute.js index 6e1962808..008305182 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -74,6 +74,7 @@ var execute = function (sequencer, thread) { var primitiveReportedValue = null; primitiveReportedValue = blockFunction(argValues, { yield: thread.yield.bind(thread), + yieldAndBlock: thread.yieldAndBlock.bind(thread), done: function() { sequencer.proceedThread(thread); }, diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js index 27e3b9996..480267fdc 100644 --- a/src/engine/sequencer.js +++ b/src/engine/sequencer.js @@ -32,6 +32,13 @@ Sequencer.WORK_TIME = 10; Sequencer.prototype.stepThreads = function (threads) { // Start counting toward WORK_TIME this.timer.start(); + // Check for a blocking thread. + var blockingThread = this.getBlockingThread_(threads); + if (blockingThread) { + // Attempt to resolve any timeouts, but otherwise stop stepping. + blockingThread.resolveTimeouts(); + return []; + } // List of threads which have been killed by this step. var inactiveThreads = []; // If all of the threads are yielding, we should yield. @@ -63,6 +70,10 @@ Sequencer.prototype.stepThreads = function (threads) { activeThread.status = Thread.STATUS_RUNNING; // @todo Deal with the return value } + // Has the thread gone into "blocking" mode? If so, stop stepping. + if (activeThread.status === Thread.STATUS_YIELD_BLOCK) { + return inactiveThreads; + } if (activeThread.stack.length === 0 && activeThread.status === Thread.STATUS_DONE) { // Finished with this thread - tell runtime to clean it up. @@ -78,6 +89,21 @@ Sequencer.prototype.stepThreads = function (threads) { return inactiveThreads; }; +/** + * Return the thread blocking all other threads, if one exists. + * If not, return false. + * @param {Array.<Thread>} threads List of which threads to check. + * @return {?Thread} The blocking thread if one exists. + */ +Sequencer.prototype.getBlockingThread_ = function (threads) { + for (var i = 0; i < threads.length; i++) { + if (threads[i].status === Thread.STATUS_YIELD_BLOCK) { + return threads[i]; + } + } + return false; +}; + /** * Step the requested thread * @param {!Thread} thread Thread object to step diff --git a/src/engine/thread.js b/src/engine/thread.js index 746d7dcc6..781197110 100644 --- a/src/engine/thread.js +++ b/src/engine/thread.js @@ -52,13 +52,21 @@ Thread.STATUS_RUNNING = 0; */ Thread.STATUS_YIELD = 1; +/** + * Thread status for a yielded thread that should block all other threads. + * This is desirable when an asynchronous capability should appear to take + * a synchronous amount of time (e.g., color touching color). + * @const + */ +Thread.STATUS_YIELD_BLOCK = 2; + /** * Thread status for a finished/done thread. * Thread is moved to this state when the interpreter * can proceed with execution. * @const */ -Thread.STATUS_DONE = 2; +Thread.STATUS_DONE = 3; /** * Push stack and update stack frames appropriately. @@ -131,6 +139,13 @@ Thread.prototype.yield = function () { this.status = Thread.STATUS_YIELD; }; +/** + * Yields the thread and blocks other threads until unyielded. + */ +Thread.prototype.yieldAndBlock = function () { + this.status = Thread.STATUS_YIELD_BLOCK; +}; + /** * Add an execution-synced timeouts for this thread. * See also: util/yieldtimers.js:timeout From 405ad1044e2669563b44d73274187356b6f79f1b Mon Sep 17 00:00:00 2001 From: Tim Mickel <tim.mickel@gmail.com> Date: Mon, 20 Jun 2016 16:46:45 -0400 Subject: [PATCH 17/27] getBlockingThread_ returns null when none available. --- src/engine/sequencer.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js index 480267fdc..12d86b41b 100644 --- a/src/engine/sequencer.js +++ b/src/engine/sequencer.js @@ -91,7 +91,6 @@ Sequencer.prototype.stepThreads = function (threads) { /** * Return the thread blocking all other threads, if one exists. - * If not, return false. * @param {Array.<Thread>} threads List of which threads to check. * @return {?Thread} The blocking thread if one exists. */ @@ -101,7 +100,7 @@ Sequencer.prototype.getBlockingThread_ = function (threads) { return threads[i]; } } - return false; + return null; }; /** From 09b9c506a9236831d8f641452619244af28eda9f Mon Sep 17 00:00:00 2001 From: Tim Mickel <tim.mickel@gmail.com> Date: Mon, 20 Jun 2016 16:47:42 -0400 Subject: [PATCH 18/27] Check for blocking case in stepToReporter --- src/engine/sequencer.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js index 12d86b41b..10657d4d0 100644 --- a/src/engine/sequencer.js +++ b/src/engine/sequencer.js @@ -167,7 +167,8 @@ Sequencer.prototype.stepToReporter = function (thread, blockId, inputName) { currentStackFrame.waitingReporter = inputName; // Actually execute the block. this.startThread(thread); - if (thread.status === Thread.STATUS_YIELD) { + if (thread.status === Thread.STATUS_YIELD || + thread.status === Thread.STATUS_YIELD_BLOCK) { // Reporter yielded; caller must wait for it to unyield. // The value will be populated once the reporter unyields, // and passed up to the currentStackFrame on next execution. From 8f6a88c0955c45870a1ba40ac89c6ced3e14453e Mon Sep 17 00:00:00 2001 From: Tim Mickel <tim.mickel@gmail.com> Date: Fri, 24 Jun 2016 10:52:49 -0400 Subject: [PATCH 19/27] Ensure predicates always return booleans --- src/blocks/scratch3_operators.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/blocks/scratch3_operators.js b/src/blocks/scratch3_operators.js index fe194da45..837411262 100644 --- a/src/blocks/scratch3_operators.js +++ b/src/blocks/scratch3_operators.js @@ -53,30 +53,27 @@ Scratch3OperatorsBlocks.prototype.divide = function (args) { }; Scratch3OperatorsBlocks.prototype.lt = function (args) { - return args.OPERAND1 < args.OPERAND2; + return Boolean(args.OPERAND1 < args.OPERAND2); }; Scratch3OperatorsBlocks.prototype.equals = function (args) { - return args.OPERAND1 == args.OPERAND2; + return Boolean(args.OPERAND1 == args.OPERAND2); }; Scratch3OperatorsBlocks.prototype.gt = function (args) { - return args.OPERAND1 > args.OPERAND2; + return Boolean(args.OPERAND1 > args.OPERAND2); }; Scratch3OperatorsBlocks.prototype.and = function (args) { - if (!args.OPERAND1 || !args.OPERAND2) { - return false; - } - return true; + return Boolean(args.OPERAND1 && args.OPERAND2); }; Scratch3OperatorsBlocks.prototype.or = function (args) { - return args.OPERAND1 || args.OPERAND2; + return Boolean(args.OPERAND1 || args.OPERAND2); }; Scratch3OperatorsBlocks.prototype.not = function (args) { - return !args.OPERAND; + return Boolean(!args.OPERAND); }; Scratch3OperatorsBlocks.prototype.random = function (args, util) { From d72cc55c119502121db5b1036a7a2821bd03484a Mon Sep 17 00:00:00 2001 From: Tim Mickel <tim.mickel@gmail.com> Date: Fri, 24 Jun 2016 11:14:22 -0400 Subject: [PATCH 20/27] Example that uses promises instead of `util.report` --- package.json | 3 ++- src/blocks/scratch3_operators.js | 13 +++++++++---- src/engine/execute.js | 27 ++++++++++++++++++--------- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index e55a497e1..2d4d54773 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ }, "dependencies": { "htmlparser2": "3.9.0", - "memoizee": "0.3.10" + "memoizee": "0.3.10", + "promise": "7.1.1" }, "devDependencies": { "eslint": "2.7.0", diff --git a/src/blocks/scratch3_operators.js b/src/blocks/scratch3_operators.js index 837411262..4237f47da 100644 --- a/src/blocks/scratch3_operators.js +++ b/src/blocks/scratch3_operators.js @@ -1,3 +1,5 @@ +var Promise = require('promise'); + function Scratch3OperatorsBlocks(runtime) { /** * The runtime instantiating this block package. @@ -81,10 +83,13 @@ Scratch3OperatorsBlocks.prototype.random = function (args, util) { // @todo Match Scratch 2.0 implementation with int-truncation. // See: http://bit.ly/1Qc0GzC util.yieldAndBlock(); - setTimeout(function() { - var randomValue = (Math.random() * (args.TO - args.FROM)) + args.FROM; - util.report(randomValue); - }, 1000); + var examplePromise = new Promise(function(resolve) { + setTimeout(function() { + var res = (Math.random() * (args.TO - args.FROM)) + args.FROM; + resolve(res); + }, 1000); + }); + return examplePromise; }; module.exports = Scratch3OperatorsBlocks; diff --git a/src/engine/execute.js b/src/engine/execute.js index 008305182..e491cf778 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -1,3 +1,4 @@ +var Promise = require('promise'); var Thread = require('./thread'); /** @@ -78,21 +79,29 @@ var execute = function (sequencer, thread) { done: function() { sequencer.proceedThread(thread); }, - report: function(reportedValue) { - if (DEBUG_BLOCK_CALLS) { - console.log('Reported: ', reportedValue); - console.timeEnd('Yielding reporter evaluation'); - } - thread.pushReportedValue(reportedValue); - sequencer.proceedThread(thread); - }, timeout: thread.addTimeout.bind(thread), stackFrame: currentStackFrame.executionContext, startSubstack: function (substackNum) { sequencer.stepToSubstack(thread, substackNum); } }); - if (thread.status === Thread.STATUS_RUNNING) { + + // Deal with any reported value. + // If it's a promise, wait until promise resolves. + var isPromise = ( + primitiveReportedValue && + primitiveReportedValue.then && + typeof primitiveReportedValue.then === 'function' + ); + if (isPromise) { + primitiveReportedValue.then(function(resolvedValue) { + if (DEBUG_BLOCK_CALLS) { + console.log('reporting value: ', resolvedValue); + } + thread.pushReportedValue(resolvedValue); + sequencer.proceedThread(thread); + }); + } else if (thread.status === Thread.STATUS_RUNNING) { if (DEBUG_BLOCK_CALLS) { console.log('reporting value: ', primitiveReportedValue); } From 57057bfffcebc7e1c5a994500957349bc1fc1a80 Mon Sep 17 00:00:00 2001 From: Tim Mickel <tim.mickel@gmail.com> Date: Fri, 24 Jun 2016 11:16:56 -0400 Subject: [PATCH 21/27] Remove unused require to fix build --- src/engine/execute.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/engine/execute.js b/src/engine/execute.js index e491cf778..097af742b 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -1,4 +1,3 @@ -var Promise = require('promise'); var Thread = require('./thread'); /** From 9881ee76b93e2740726298e49e758a784d9c96f4 Mon Sep 17 00:00:00 2001 From: Tim Mickel <tim.mickel@gmail.com> Date: Fri, 24 Jun 2016 11:19:38 -0400 Subject: [PATCH 22/27] Deal with promise rejection also. --- src/engine/execute.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/engine/execute.js b/src/engine/execute.js index 097af742b..528a3bb93 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -94,12 +94,21 @@ var execute = function (sequencer, thread) { ); if (isPromise) { primitiveReportedValue.then(function(resolvedValue) { + // Promise resolved: the primitive reported a value. if (DEBUG_BLOCK_CALLS) { console.log('reporting value: ', resolvedValue); } thread.pushReportedValue(resolvedValue); sequencer.proceedThread(thread); - }); + }, function(rejectionReason) { + // Promise rejected: the primitive had some error. + // Log it and proceed. + if (DEBUG_BLOCK_CALLS) { + console.warn('primitive rejected promise: ', rejectionReason); + } + sequencer.proceedThread(thread); + } + ); } else if (thread.status === Thread.STATUS_RUNNING) { if (DEBUG_BLOCK_CALLS) { console.log('reporting value: ', primitiveReportedValue); From 9a7ab57f6f5d4a6875901c899cdc546c1a5fc13a Mon Sep 17 00:00:00 2001 From: Tim Mickel <tim.mickel@gmail.com> Date: Tue, 28 Jun 2016 13:39:44 -0400 Subject: [PATCH 23/27] Always yield thread when a promise is returned. --- src/engine/execute.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/engine/execute.js b/src/engine/execute.js index 528a3bb93..632b29c82 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -93,6 +93,11 @@ var execute = function (sequencer, thread) { typeof primitiveReportedValue.then === 'function' ); if (isPromise) { + if (thread.status === Thread.STATUS_RUNNING) { + // Primitive returned a promise; automatically yield thread. + thread.status = Thread.STATUS_YIELD; + } + // Promise handlers primitiveReportedValue.then(function(resolvedValue) { // Promise resolved: the primitive reported a value. if (DEBUG_BLOCK_CALLS) { @@ -107,8 +112,7 @@ var execute = function (sequencer, thread) { console.warn('primitive rejected promise: ', rejectionReason); } sequencer.proceedThread(thread); - } - ); + }); } else if (thread.status === Thread.STATUS_RUNNING) { if (DEBUG_BLOCK_CALLS) { console.log('reporting value: ', primitiveReportedValue); From 6daee9a70e5f5ad55e8f45e6fb9edcf7021c1cae Mon Sep 17 00:00:00 2001 From: Tim Mickel <tim.mickel@gmail.com> Date: Thu, 30 Jun 2016 16:57:12 -0400 Subject: [PATCH 24/27] Remove VM-locking yield mode per discussion --- src/blocks/scratch3_operators.js | 3 +-- src/engine/execute.js | 1 - src/engine/sequencer.js | 25 ------------------------- src/engine/thread.js | 17 +---------------- 4 files changed, 2 insertions(+), 44 deletions(-) diff --git a/src/blocks/scratch3_operators.js b/src/blocks/scratch3_operators.js index 4237f47da..75077061c 100644 --- a/src/blocks/scratch3_operators.js +++ b/src/blocks/scratch3_operators.js @@ -78,11 +78,10 @@ Scratch3OperatorsBlocks.prototype.not = function (args) { return Boolean(!args.OPERAND); }; -Scratch3OperatorsBlocks.prototype.random = function (args, util) { +Scratch3OperatorsBlocks.prototype.random = function (args) { // As a demo, this implementation of random returns after 1 second of yield. // @todo Match Scratch 2.0 implementation with int-truncation. // See: http://bit.ly/1Qc0GzC - util.yieldAndBlock(); var examplePromise = new Promise(function(resolve) { setTimeout(function() { var res = (Math.random() * (args.TO - args.FROM)) + args.FROM; diff --git a/src/engine/execute.js b/src/engine/execute.js index 632b29c82..8fbb73dfc 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -74,7 +74,6 @@ var execute = function (sequencer, thread) { var primitiveReportedValue = null; primitiveReportedValue = blockFunction(argValues, { yield: thread.yield.bind(thread), - yieldAndBlock: thread.yieldAndBlock.bind(thread), done: function() { sequencer.proceedThread(thread); }, diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js index 10657d4d0..81813a141 100644 --- a/src/engine/sequencer.js +++ b/src/engine/sequencer.js @@ -32,13 +32,6 @@ Sequencer.WORK_TIME = 10; Sequencer.prototype.stepThreads = function (threads) { // Start counting toward WORK_TIME this.timer.start(); - // Check for a blocking thread. - var blockingThread = this.getBlockingThread_(threads); - if (blockingThread) { - // Attempt to resolve any timeouts, but otherwise stop stepping. - blockingThread.resolveTimeouts(); - return []; - } // List of threads which have been killed by this step. var inactiveThreads = []; // If all of the threads are yielding, we should yield. @@ -70,10 +63,6 @@ Sequencer.prototype.stepThreads = function (threads) { activeThread.status = Thread.STATUS_RUNNING; // @todo Deal with the return value } - // Has the thread gone into "blocking" mode? If so, stop stepping. - if (activeThread.status === Thread.STATUS_YIELD_BLOCK) { - return inactiveThreads; - } if (activeThread.stack.length === 0 && activeThread.status === Thread.STATUS_DONE) { // Finished with this thread - tell runtime to clean it up. @@ -89,20 +78,6 @@ Sequencer.prototype.stepThreads = function (threads) { return inactiveThreads; }; -/** - * Return the thread blocking all other threads, if one exists. - * @param {Array.<Thread>} threads List of which threads to check. - * @return {?Thread} The blocking thread if one exists. - */ -Sequencer.prototype.getBlockingThread_ = function (threads) { - for (var i = 0; i < threads.length; i++) { - if (threads[i].status === Thread.STATUS_YIELD_BLOCK) { - return threads[i]; - } - } - return null; -}; - /** * Step the requested thread * @param {!Thread} thread Thread object to step diff --git a/src/engine/thread.js b/src/engine/thread.js index 781197110..746d7dcc6 100644 --- a/src/engine/thread.js +++ b/src/engine/thread.js @@ -52,21 +52,13 @@ Thread.STATUS_RUNNING = 0; */ Thread.STATUS_YIELD = 1; -/** - * Thread status for a yielded thread that should block all other threads. - * This is desirable when an asynchronous capability should appear to take - * a synchronous amount of time (e.g., color touching color). - * @const - */ -Thread.STATUS_YIELD_BLOCK = 2; - /** * Thread status for a finished/done thread. * Thread is moved to this state when the interpreter * can proceed with execution. * @const */ -Thread.STATUS_DONE = 3; +Thread.STATUS_DONE = 2; /** * Push stack and update stack frames appropriately. @@ -139,13 +131,6 @@ Thread.prototype.yield = function () { this.status = Thread.STATUS_YIELD; }; -/** - * Yields the thread and blocks other threads until unyielded. - */ -Thread.prototype.yieldAndBlock = function () { - this.status = Thread.STATUS_YIELD_BLOCK; -}; - /** * Add an execution-synced timeouts for this thread. * See also: util/yieldtimers.js:timeout From ab6e0d383963e852f454dcfb1d141f914631f485 Mon Sep 17 00:00:00 2001 From: Tim Mickel <tim.mickel@gmail.com> Date: Thu, 30 Jun 2016 17:06:50 -0400 Subject: [PATCH 25/27] Remove YieldTimers, unused WeDo blocks --- playground/index.html | 6 -- src/blocks/scratch3_control.js | 13 +-- src/blocks/wedo2.js | 152 --------------------------------- src/engine/execute.js | 1 - src/engine/runtime.js | 3 +- src/engine/sequencer.js | 12 +-- src/engine/thread.js | 33 ------- src/util/yieldtimers.js | 71 --------------- 8 files changed, 11 insertions(+), 280 deletions(-) delete mode 100644 src/blocks/wedo2.js delete mode 100644 src/util/yieldtimers.js diff --git a/playground/index.html b/playground/index.html index 0d4903d3d..82535807b 100644 --- a/playground/index.html +++ b/playground/index.html @@ -79,12 +79,6 @@ </block> <block type="control_delete_this_clone"></block> </category> - <category name="Wedo"> - <block type="wedo_setcolor"></block> - <block type="wedo_motorspeed"></block> - <block type="wedo_whentilt"></block> - <block type="wedo_whendistanceclose"></block> - </category> <category name="Operators"> <block type="operator_add"> <value name="NUM1"> diff --git a/src/blocks/scratch3_control.js b/src/blocks/scratch3_control.js index 1d05a8c7a..e13edefd9 100644 --- a/src/blocks/scratch3_control.js +++ b/src/blocks/scratch3_control.js @@ -1,3 +1,5 @@ +var Promise = require('promise'); + function Scratch3ControlBlocks(runtime) { /** * The runtime instantiating this block package. @@ -38,11 +40,12 @@ Scratch3ControlBlocks.prototype.forever = function(args, util) { util.startSubstack(); }; -Scratch3ControlBlocks.prototype.wait = function(args, util) { - util.yield(); - util.timeout(function() { - util.done(); - }, 1000 * args.DURATION); +Scratch3ControlBlocks.prototype.wait = function(args) { + return new Promise(function(resolve) { + setTimeout(function() { + resolve(); + }, 1000 * args.DURATION); + }); }; Scratch3ControlBlocks.prototype.if = function(args, util) { diff --git a/src/blocks/wedo2.js b/src/blocks/wedo2.js deleted file mode 100644 index d7516bc18..000000000 --- a/src/blocks/wedo2.js +++ /dev/null @@ -1,152 +0,0 @@ - -var YieldTimers = require('../util/yieldtimers.js'); - -function WeDo2Blocks(runtime) { - /** - * The runtime instantiating this block package. - * @type {Runtime} - */ - this.runtime = runtime; - - /** - * Current motor speed, as a percentage (100 = full speed). - * @type {number} - * @private - */ - this._motorSpeed = 100; - - /** - * The timeout ID for a pending motor action. - * @type {?int} - * @private - */ - this._motorTimeout = null; -} - -/** - * Retrieve the block primitives implemented by this package. - * @return {Object.<string, Function>} Mapping of opcode to Function. - */ -WeDo2Blocks.prototype.getPrimitives = function() { - return { - 'wedo_motorclockwise': this.motorClockwise, - 'wedo_motorcounterclockwise': this.motorCounterClockwise, - 'wedo_motorspeed': this.motorSpeed, - 'wedo_setcolor': this.setColor, - 'wedo_whendistanceclose': this.whenDistanceClose, - 'wedo_whentilt': this.whenTilt - }; -}; - -/** - * Clamp a value between a minimum and maximum value. - * @todo move this to a common utility class. - * @param {number} val The value to clamp. - * @param {number} min The minimum return value. - * @param {number} max The maximum return value. - * @returns {number} The clamped value. - * @private - */ -WeDo2Blocks.prototype._clamp = function(val, min, max) { - return Math.max(min, Math.min(val, max)); -}; - -/** - * Common implementation for motor blocks. - * @param {string} direction The direction to turn ('left' or 'right'). - * @param {number} durationSeconds The number of seconds to run. - * @param {Object} util The util instance to use for yielding and finishing. - * @private - */ -WeDo2Blocks.prototype._motorOnFor = function(direction, durationSeconds, util) { - if (this._motorTimeout > 0) { - // @todo maybe this should go through util - YieldTimers.resolve(this._motorTimeout); - this._motorTimeout = null; - } - if (typeof window !== 'undefined' && window.native) { - window.native.motorRun(direction, this._motorSpeed); - } - - var instance = this; - var myTimeout = this._motorTimeout = util.timeout(function() { - if (instance._motorTimeout == myTimeout) { - instance._motorTimeout = null; - } - if (typeof window !== 'undefined' && window.native) { - window.native.motorStop(); - } - util.done(); - }, 1000 * durationSeconds); - - util.yield(); -}; - -WeDo2Blocks.prototype.motorClockwise = function(argValues, util) { - this._motorOnFor('right', parseFloat(argValues[0]), util); -}; - -WeDo2Blocks.prototype.motorCounterClockwise = function(argValues, util) { - this._motorOnFor('left', parseFloat(argValues[0]), util); -}; - -WeDo2Blocks.prototype.motorSpeed = function(argValues) { - var speed = argValues[0]; - switch (speed) { - case 'slow': - this._motorSpeed = 20; - break; - case 'medium': - this._motorSpeed = 50; - break; - case 'fast': - this._motorSpeed = 100; - break; - } -}; - -/** - * Convert a color name to a WeDo color index. - * Supports 'mystery' for a random hue. - * @param {string} colorName The color to retrieve. - * @returns {number} The WeDo color index. - * @private - */ -WeDo2Blocks.prototype._getColor = function(colorName) { - var colors = { - 'yellow': 7, - 'orange': 8, - 'coral': 9, - 'magenta': 1, - 'purple': 2, - 'blue': 3, - 'green': 6, - 'white': 10 - }; - - if (colorName == 'mystery') { - return Math.floor((Math.random() * 10) + 1); - } - - return colors[colorName]; -}; - -WeDo2Blocks.prototype.setColor = function(argValues, util) { - if (typeof window !== 'undefined' && window.native) { - var colorIndex = this._getColor(argValues[0]); - window.native.setLedColor(colorIndex); - } - // Pause for quarter second - util.yield(); - util.timeout(function() { - util.done(); - }, 250); -}; - -WeDo2Blocks.prototype.whenDistanceClose = function() { -}; - -WeDo2Blocks.prototype.whenTilt = function() { -}; - -module.exports = WeDo2Blocks; diff --git a/src/engine/execute.js b/src/engine/execute.js index 8fbb73dfc..acff32585 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -77,7 +77,6 @@ var execute = function (sequencer, thread) { done: function() { sequencer.proceedThread(thread); }, - timeout: thread.addTimeout.bind(thread), stackFrame: currentStackFrame.executionContext, startSubstack: function (substackNum) { sequencer.stepToSubstack(thread, substackNum); diff --git a/src/engine/runtime.js b/src/engine/runtime.js index f6ad79f0b..8c4c862e1 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -6,8 +6,7 @@ var util = require('util'); var defaultBlockPackages = { 'scratch3_control': require('../blocks/scratch3_control'), 'scratch3_event': require('../blocks/scratch3_event'), - 'scratch3_operators': require('../blocks/scratch3_operators'), - 'wedo2': require('../blocks/wedo2') + 'scratch3_operators': require('../blocks/scratch3_operators') }; /** diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js index 81813a141..5eca9c202 100644 --- a/src/engine/sequencer.js +++ b/src/engine/sequencer.js @@ -52,16 +52,8 @@ Sequencer.prototype.stepThreads = function (threads) { // Normal-mode thread: step. this.startThread(activeThread); } else if (activeThread.status === Thread.STATUS_YIELD) { - // Yield-mode thread: resolve timers. - activeThread.resolveTimeouts(); - if (activeThread.status === Thread.STATUS_YIELD) { - // Still yielding. - numYieldingThreads++; - } - } else if (activeThread.status === Thread.STATUS_DONE) { - // Moved to a done state - finish up - activeThread.status = Thread.STATUS_RUNNING; - // @todo Deal with the return value + // Yielding thread: do nothing for this step. + continue; } if (activeThread.stack.length === 0 && activeThread.status === Thread.STATUS_DONE) { diff --git a/src/engine/thread.js b/src/engine/thread.js index 746d7dcc6..d14d83f90 100644 --- a/src/engine/thread.js +++ b/src/engine/thread.js @@ -1,5 +1,3 @@ -var YieldTimers = require('../util/yieldtimers.js'); - /** * A thread is a running stack context and all the metadata needed. * @param {?string} firstBlock First block to execute in the thread. @@ -30,12 +28,6 @@ function Thread (firstBlock) { * @type {number} */ this.status = 0; /* Thread.STATUS_RUNNING */ - - /** - * Execution-synced timeouts. - * @type {number} - */ - this.timeoutIds = []; } /** @@ -131,29 +123,4 @@ Thread.prototype.yield = function () { this.status = Thread.STATUS_YIELD; }; -/** - * Add an execution-synced timeouts for this thread. - * See also: util/yieldtimers.js:timeout - * @param {!Function} callback To be called when the timer is done. - * @param {number} timeDelta Time to wait, in ms. - */ -Thread.prototype.addTimeout = function (callback, timeDelta) { - var timeoutId = YieldTimers.timeout(callback, timeDelta); - this.timeoutIds.push(timeoutId); -}; - -/** - * Attempt to resolve all execution-synced timeouts on this thread. - */ -Thread.prototype.resolveTimeouts = function () { - var newTimeouts = []; - for (var i = 0; i < this.timeoutIds.length; i++) { - var resolved = YieldTimers.resolve(this.timeoutIds[i]); - if (!resolved) { - newTimeouts.push(this.timeoutIds[i]); - } - } - this.timeoutIds = newTimeouts; -}; - module.exports = Thread; diff --git a/src/util/yieldtimers.js b/src/util/yieldtimers.js deleted file mode 100644 index 84c6d3259..000000000 --- a/src/util/yieldtimers.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * @fileoverview Timers that are synchronized with the Scratch sequencer. - */ -var Timer = require('./timer'); - -function YieldTimers () {} - -/** - * Shared collection of timers. - * Each timer is a [Function, number] with the callback - * and absolute time for it to run. - * @type {Object.<number,Array>} - */ -YieldTimers.timers = {}; - -/** - * Monotonically increasing timer ID. - * @type {number} - */ -YieldTimers.timerId = 0; - -/** - * Utility for measuring time. - * @type {!Timer} - */ -YieldTimers.globalTimer = new Timer(); - -/** - * The timeout function is passed to primitives and is intended - * as a convenient replacement for window.setTimeout. - * The sequencer will attempt to resolve the timer every time - * the yielded thread would have been stepped. - * @param {!Function} callback To be called when the timer is done. - * @param {number} timeDelta Time to wait, in ms. - * @return {number} Timer ID to be used with other methods. - */ -YieldTimers.timeout = function (callback, timeDelta) { - var id = ++YieldTimers.timerId; - YieldTimers.timers[id] = [ - callback, - YieldTimers.globalTimer.time() + timeDelta - ]; - return id; -}; - -/** - * Attempt to resolve a timeout. - * If the time has passed, call the callback. - * Otherwise, do nothing. - * @param {number} id Timer ID to resolve. - * @return {boolean} True if the timer has resolved. - */ -YieldTimers.resolve = function (id) { - var timer = YieldTimers.timers[id]; - if (!timer) { - // No such timer. - return false; - } - var callback = timer[0]; - var time = timer[1]; - if (YieldTimers.globalTimer.time() < time) { - // Not done yet. - return false; - } - // Execute the callback and remove the timer. - callback(); - delete YieldTimers.timers[id]; - return true; -}; - -module.exports = YieldTimers; From ec4567aa8ad99b264673a909543186a50009de8c Mon Sep 17 00:00:00 2001 From: Tim Mickel <tim.mickel@gmail.com> Date: Thu, 30 Jun 2016 17:12:16 -0400 Subject: [PATCH 26/27] Simplify logic for Thread status --- src/engine/sequencer.js | 22 ++++++++++------------ src/engine/thread.js | 9 +++++---- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js index 5eca9c202..1498d6320 100644 --- a/src/engine/sequencer.js +++ b/src/engine/sequencer.js @@ -134,17 +134,10 @@ Sequencer.prototype.stepToReporter = function (thread, blockId, inputName) { currentStackFrame.waitingReporter = inputName; // Actually execute the block. this.startThread(thread); - if (thread.status === Thread.STATUS_YIELD || - thread.status === Thread.STATUS_YIELD_BLOCK) { - // Reporter yielded; caller must wait for it to unyield. - // The value will be populated once the reporter unyields, - // and passed up to the currentStackFrame on next execution. - return true; - } else if (thread.status === Thread.STATUS_DONE) { - // Reporter finished, mark the thread as running. - thread.status = Thread.STATUS_RUNNING; - return false; - } + // If a reporter yielded, caller must wait for it to unyield. + // The value will be populated once the reporter unyields, + // and passed up to the currentStackFrame on next execution. + return thread.status === Thread.STATUS_YIELD; }; /** @@ -155,7 +148,8 @@ Sequencer.prototype.proceedThread = function (thread) { var currentBlockId = thread.peekStack(); // Mark the status as done and proceed to the next block. this.runtime.glowBlock(currentBlockId, false); - thread.status = Thread.STATUS_DONE; + // If the block was yielding, move back to running state. + thread.status = Thread.STATUS_RUNNING; // Pop from the stack - finished this level of execution. thread.popStack(); // Push next connected block, if there is one. @@ -167,6 +161,10 @@ Sequencer.prototype.proceedThread = function (thread) { while (thread.peekStack() === null && thread.stack.length > 0) { thread.popStack(); } + // If we still can't find a next block to run, mark the thread as done. + if (thread.peekStack() === null) { + thread.status = Thread.STATUS_DONE; + } }; module.exports = Sequencer; diff --git a/src/engine/thread.js b/src/engine/thread.js index d14d83f90..e5c4df2e4 100644 --- a/src/engine/thread.js +++ b/src/engine/thread.js @@ -32,22 +32,23 @@ function Thread (firstBlock) { /** * Thread status for initialized or running thread. - * Threads are in this state when the primitive is called for the first time. + * This is the default state for a thread - execution should run normally, + * stepping from block to block. * @const */ Thread.STATUS_RUNNING = 0; /** * Thread status for a yielded thread. - * Threads are in this state when a primitive has yielded. + * Threads are in this state when a primitive has yielded; execution is paused + * until the relevant primitive unyields. * @const */ Thread.STATUS_YIELD = 1; /** * Thread status for a finished/done thread. - * Thread is moved to this state when the interpreter - * can proceed with execution. + * Thread is in this state when there are no more blocks to execute. * @const */ Thread.STATUS_DONE = 2; From 1c24770f8ca6dba227c4e1033fd43a48ccccbc7c Mon Sep 17 00:00:00 2001 From: Tim Mickel <tim.mickel@gmail.com> Date: Thu, 30 Jun 2016 17:13:43 -0400 Subject: [PATCH 27/27] Remove debug calls from `execute` --- src/engine/execute.js | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/src/engine/execute.js b/src/engine/execute.js index acff32585..188f0bf52 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -1,11 +1,5 @@ var Thread = require('./thread'); -/** - * If set, block calls, args, and return values will be logged to the console. - * @const {boolean} - */ -var DEBUG_BLOCK_CALLS = true; - /** * Execute a block. * @param {!Sequencer} sequencer Which sequencer is executing. @@ -66,11 +60,6 @@ var execute = function (sequencer, thread) { // (e.g., on return from a substack) gets fresh inputs. currentStackFrame.reported = {}; - if (DEBUG_BLOCK_CALLS) { - console.groupCollapsed('Executing: ' + opcode); - console.log('with arguments: ', argValues); - console.log('and stack frame: ', currentStackFrame); - } var primitiveReportedValue = null; primitiveReportedValue = blockFunction(argValues, { yield: thread.yield.bind(thread), @@ -98,29 +87,17 @@ var execute = function (sequencer, thread) { // Promise handlers primitiveReportedValue.then(function(resolvedValue) { // Promise resolved: the primitive reported a value. - if (DEBUG_BLOCK_CALLS) { - console.log('reporting value: ', resolvedValue); - } thread.pushReportedValue(resolvedValue); sequencer.proceedThread(thread); }, function(rejectionReason) { // Promise rejected: the primitive had some error. // Log it and proceed. - if (DEBUG_BLOCK_CALLS) { - console.warn('primitive rejected promise: ', rejectionReason); - } + console.warn('Primitive rejected promise: ', rejectionReason); sequencer.proceedThread(thread); }); } else if (thread.status === Thread.STATUS_RUNNING) { - if (DEBUG_BLOCK_CALLS) { - console.log('reporting value: ', primitiveReportedValue); - } thread.pushReportedValue(primitiveReportedValue); } - if (DEBUG_BLOCK_CALLS) { - console.log('ending stack frame: ', currentStackFrame); - console.groupEnd(); - } }; module.exports = execute;