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": { 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/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/scratch3_operators.js b/src/blocks/scratch3_operators.js index 94329b5a3..75077061c 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. @@ -15,7 +17,16 @@ Scratch3OperatorsBlocks.prototype.getPrimitives = function() { 'math_number': this.number, 'text': this.text, 'operator_add': this.add, - 'operator_equals': this.equals + '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_not': this.not, + 'operator_random': this.random }; }; @@ -31,8 +42,53 @@ 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 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 Boolean(args.OPERAND1 > args.OPERAND2); +}; + +Scratch3OperatorsBlocks.prototype.and = function (args) { + return Boolean(args.OPERAND1 && args.OPERAND2); +}; + +Scratch3OperatorsBlocks.prototype.or = function (args) { + return Boolean(args.OPERAND1 || args.OPERAND2); +}; + +Scratch3OperatorsBlocks.prototype.not = function (args) { + return Boolean(!args.OPERAND); +}; + +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 + 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/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 0d1dbc3cc..188f0bf52 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -1,11 +1,10 @@ -var YieldTimers = require('../util/yieldtimers.js'); +var Thread = require('./thread'); /** - * If set, block calls, args, and return values will be logged to the console. - * @const {boolean} + * Execute a block. + * @param {!Sequencer} sequencer Which sequencer is executing. + * @param {!Thread} thread Thread which to read and execute. */ -var DEBUG_BLOCK_CALLS = true; - var execute = function (sequencer, thread) { var runtime = sequencer.runtime; @@ -13,13 +12,19 @@ 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); + 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 = {}; @@ -34,53 +39,65 @@ 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 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. + 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; + } + } + argValues[inputName] = currentStackFrame.reported[inputName]; } - if (!opcode) { - console.warn('Could not get opcode for block: ' + currentBlockId); - return; - } + // 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 = {}; - var blockFunction = runtime.getOpcodeFunction(opcode); - if (!blockFunction) { - console.warn('Could not get implementation for opcode: ' + opcode); - return; - } - - if (DEBUG_BLOCK_CALLS) { - console.groupCollapsed('Executing: ' + opcode); - console.log('with arguments: ', argValues); - console.log('and stack frame: ', currentStackFrame); - } - var primitiveReturnValue = null; - // @todo deal with the return value - primitiveReturnValue = blockFunction(argValues, { + var primitiveReportedValue = null; + primitiveReportedValue = blockFunction(argValues, { yield: thread.yield.bind(thread), done: function() { sequencer.proceedThread(thread); }, - timeout: YieldTimers.timeout, - stackFrame: currentStackFrame, + stackFrame: currentStackFrame.executionContext, 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; + + // 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) { + 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. + thread.pushReportedValue(resolvedValue); + sequencer.proceedThread(thread); + }, function(rejectionReason) { + // Promise rejected: the primitive had some error. + // Log it and proceed. + console.warn('Primitive rejected promise: ', rejectionReason); + sequencer.proceedThread(thread); + }); + } else if (thread.status === Thread.STATUS_RUNNING) { + thread.pushReportedValue(primitiveReportedValue); } - if (DEBUG_BLOCK_CALLS) { - console.log('ending stack frame: ', currentStackFrame); - console.log('returned: ', primitiveReturnValue); - console.groupEnd(); - } - return primitiveReturnValue; }; module.exports = execute; 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 deaa15266..1498d6320 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) { @@ -53,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: check if the time has passed. - if (!YieldTimers.resolve(activeThread.yieldTimerId)) { - // Thread is still yielding - // if YieldTimers.resolve returns false. - 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) { @@ -101,7 +92,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); } }; @@ -128,6 +119,27 @@ 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 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; +}; + /** * Finish stepping a thread and proceed it to the next block. * @param {!Thread} thread Thread object to proceed. @@ -136,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. @@ -148,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 c98efab48..e5c4df2e4 100644 --- a/src/engine/thread.js +++ b/src/engine/thread.js @@ -28,32 +28,27 @@ function Thread (firstBlock) { * @type {number} */ this.status = 0; /* Thread.STATUS_RUNNING */ - - /** - * Yield timer ID (for checking when the thread should unyield). - * @type {number} - */ - this.yieldTimerId = -1; } /** * 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; @@ -67,7 +62,11 @@ 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. + waitingReporter: null, // Name of waiting reporter. + executionContext: {} // A context passed to block implementations. + }); } }; @@ -97,6 +96,27 @@ 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.peekParentStackFrame(); + if (parentStackFrame) { + var waitingReporter = parentStackFrame.waitingReporter; + parentStackFrame.reported[waitingReporter] = value; + parentStackFrame.waitingReporter = null; + } +}; + /** * Yields the thread. */ diff --git a/src/util/yieldtimers.js b/src/util/yieldtimers.js deleted file mode 100644 index 45e244eaf..000000000 --- a/src/util/yieldtimers.js +++ /dev/null @@ -1,90 +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; -}; - -/** - * 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;