diff --git a/src/blocks/scratch3_control.js b/src/blocks/scratch3_control.js index 50e7af645..1d05a8c7a 100644 --- a/src/blocks/scratch3_control.js +++ b/src/blocks/scratch3_control.js @@ -15,14 +15,16 @@ Scratch3ControlBlocks.prototype.getPrimitives = function() { 'control_repeat': this.repeat, 'control_forever': this.forever, 'control_wait': this.wait, + 'control_if': this.if, + 'control_if_else': this.ifElse, 'control_stop': this.stop }; }; -Scratch3ControlBlocks.prototype.repeat = function(argValues, util) { +Scratch3ControlBlocks.prototype.repeat = function(args, util) { // Initialize loop if (util.stackFrame.loopCounter === undefined) { - util.stackFrame.loopCounter = parseInt(argValues[0]); // @todo arg + util.stackFrame.loopCounter = parseInt(args.TIMES); } // Decrease counter util.stackFrame.loopCounter--; @@ -32,15 +34,39 @@ Scratch3ControlBlocks.prototype.repeat = function(argValues, util) { } }; -Scratch3ControlBlocks.prototype.forever = function(argValues, util) { +Scratch3ControlBlocks.prototype.forever = function(args, util) { util.startSubstack(); }; -Scratch3ControlBlocks.prototype.wait = function(argValues, util) { +Scratch3ControlBlocks.prototype.wait = function(args, util) { util.yield(); util.timeout(function() { util.done(); - }, 1000 * parseFloat(argValues[0])); + }, 1000 * args.DURATION); +}; + +Scratch3ControlBlocks.prototype.if = function(args, util) { + // Only execute one time. `if` will be returned to + // when the substack finishes, but it shouldn't execute again. + if (util.stackFrame.executed === undefined) { + util.stackFrame.executed = true; + if (args.CONDITION) { + util.startSubstack(); + } + } +}; + +Scratch3ControlBlocks.prototype.ifElse = function(args, util) { + // Only execute one time. `ifElse` will be returned to + // when the substack finishes, but it shouldn't execute again. + if (util.stackFrame.executed === undefined) { + util.stackFrame.executed = true; + if (args.CONDITION) { + util.startSubstack(1); + } else { + util.startSubstack(2); + } + } }; Scratch3ControlBlocks.prototype.stop = function() { diff --git a/src/blocks/scratch3_event.js b/src/blocks/scratch3_event.js index ff15416df..18a5ab621 100644 --- a/src/blocks/scratch3_event.js +++ b/src/blocks/scratch3_event.js @@ -27,17 +27,8 @@ Scratch3EventBlocks.prototype.whenBroadcastReceived = function() { // No-op }; -Scratch3EventBlocks.prototype.broadcast = function(argValues, util) { - util.startHats(function(hat) { - if (hat.opcode === 'event_whenbroadcastreceived') { - var shadows = hat.fields.CHOICE.blocks; - for (var sb in shadows) { - var shadowblock = shadows[sb]; - return shadowblock.fields.CHOICE.value === argValues[0]; - } - } - return false; - }); +Scratch3EventBlocks.prototype.broadcast = function() { + // @todo }; module.exports = Scratch3EventBlocks; diff --git a/src/blocks/scratch3_operators.js b/src/blocks/scratch3_operators.js new file mode 100644 index 000000000..eff1fe18d --- /dev/null +++ b/src/blocks/scratch3_operators.js @@ -0,0 +1,38 @@ +function Scratch3OperatorsBlocks(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; +} + +/** + * Retrieve the block primitives implemented by this package. + * @return {Object.<string, Function>} Mapping of opcode to Function. + */ +Scratch3OperatorsBlocks.prototype.getPrimitives = function() { + return { + 'math_number': this.number, + 'text': this.text, + 'math_add': this.add, + 'logic_equals': this.equals + }; +}; + +Scratch3OperatorsBlocks.prototype.number = function (args) { + return Number(args.NUM); +}; + +Scratch3OperatorsBlocks.prototype.text = function (args) { + return String(args.TEXT); +}; + +Scratch3OperatorsBlocks.prototype.add = function (args) { + return args.NUM1 + args.NUM2; +}; + +Scratch3OperatorsBlocks.prototype.equals = function (args) { + return args.VALUE1 == args.VALUE2; +}; + +module.exports = Scratch3OperatorsBlocks; diff --git a/src/engine/blocks.js b/src/engine/blocks.js index 752cbb4d3..5ee0a7e10 100644 --- a/src/engine/blocks.js +++ b/src/engine/blocks.js @@ -22,6 +22,13 @@ function Blocks () { this._stacks = []; } +/** + * Blockly inputs that represent statements/substacks + * are prefixed with this string. + * @const{string} + */ +Blocks.SUBSTACK_INPUT_PREFIX = 'SUBSTACK'; + /** * Provide an object with metadata for the requested block ID. * @param {!string} blockId ID of block we have stored. @@ -60,7 +67,7 @@ Blocks.prototype.getSubstack = function (id, substackNum) { if (typeof block === 'undefined') return null; if (!substackNum) substackNum = 1; - var inputName = 'SUBSTACK'; + var inputName = Blocks.SUBSTACK_INPUT_PREFIX; if (substackNum > 1) { inputName += substackNum; } @@ -80,6 +87,34 @@ Blocks.prototype.getOpcode = function (id) { return this._blocks[id].opcode; }; +/** + * Get all fields and their values for a block. + * @param {?string} id ID of block to query. + * @return {!Object} All fields and their values. + */ +Blocks.prototype.getFields = function (id) { + if (typeof this._blocks[id] === 'undefined') return null; + return this._blocks[id].fields; +}; + +/** + * Get all non-substack inputs for a block. + * @param {?string} id ID of block to query. + * @return {!Object} All non-substack inputs and their associated blocks. + */ +Blocks.prototype.getInputs = function (id) { + if (typeof this._blocks[id] === 'undefined') return null; + var inputs = {}; + for (var input in this._blocks[id].inputs) { + // Ignore blocks prefixed with substack prefix. + if (input.substring(0, Blocks.SUBSTACK_INPUT_PREFIX.length) + != Blocks.SUBSTACK_INPUT_PREFIX) { + inputs[input] = this._blocks[id].inputs[input]; + } + } + return inputs; +}; + // --------------------------------------------------------------------- /** diff --git a/src/engine/execute.js b/src/engine/execute.js new file mode 100644 index 000000000..d2f8381f0 --- /dev/null +++ b/src/engine/execute.js @@ -0,0 +1,94 @@ +var YieldTimers = require('../util/yieldtimers.js'); + +/** + * 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) { + var runtime = sequencer.runtime; + + // Current block to execute is the one on the top of the stack. + 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). + var argValues = {}; + + // Add all fields on this block to the argValues. + var fields = runtime.blocks.getFields(currentBlockId); + for (var fieldName in fields) { + argValues[fieldName] = fields[fieldName].value; + } + + // Recursively evaluate input blocks. + var inputs = runtime.blocks.getInputs(currentBlockId); + 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; + } + + 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; + } + + if (DEBUG_BLOCK_CALLS) { + console.groupCollapsed('Executing: ' + opcode); + console.log('with arguments: ', argValues); + console.log('and stack frame: ', currentStackFrame); + } + var primitiveReturnValue = null; + try { + // @todo deal with the return value + primitiveReturnValue = blockFunction(argValues, { + yield: thread.yield.bind(thread), + done: function() { + sequencer.proceedThread(thread); + }, + timeout: YieldTimers.timeout, + stackFrame: currentStackFrame, + startSubstack: function (substackNum) { + sequencer.stepToSubstack(thread, substackNum); + } + }); + } + catch(e) { + console.error( + 'Exception calling block function for opcode: ' + + opcode + '\n' + e); + } finally { + // 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); + console.groupEnd(); + } + return primitiveReturnValue; + } +}; + +module.exports = execute; diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 5b4fab48d..aff2a3aec 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -6,6 +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') }; @@ -121,6 +122,7 @@ Runtime.prototype.getOpcodeFunction = function (opcode) { Runtime.prototype._pushThread = function (id) { this.emit(Runtime.STACK_GLOW_ON, id); var thread = new Thread(id); + thread.pushStack(id); this.threads.push(thread); }; @@ -231,6 +233,9 @@ Runtime.prototype._step = function () { * @param {boolean} isGlowing True to turn on glow; false to turn off. */ Runtime.prototype.glowBlock = function (blockId, isGlowing) { + if (!this.blocks.getBlock(blockId)) { + return; + } if (isGlowing) { this.emit(Runtime.BLOCK_GLOW_ON, blockId); } else { diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js index c31081798..deaa15266 100644 --- a/src/engine/sequencer.js +++ b/src/engine/sequencer.js @@ -1,6 +1,7 @@ var Timer = require('../util/timer'); var Thread = require('./thread'); var YieldTimers = require('../util/yieldtimers.js'); +var execute = require('./execute.js'); function Sequencer (runtime) { /** @@ -24,12 +25,6 @@ function Sequencer (runtime) { */ Sequencer.WORK_TIME = 10; -/** - * If set, block calls, args, and return values will be logged to the console. - * @const {boolean} - */ -Sequencer.DEBUG_BLOCK_CALLS = true; - /** * Step through all threads in `this.threads`, running them in order. * @param {Array.<Thread>} threads List of which threads to step. @@ -49,33 +44,27 @@ Sequencer.prototype.stepThreads = function (threads) { this.timer.timeElapsed() < Sequencer.WORK_TIME) { // New threads at the end of the iteration. var newThreads = []; + // Reset yielding thread count. + numYieldingThreads = 0; // Attempt to run each thread one time for (var i = 0; i < threads.length; i++) { var activeThread = threads[i]; if (activeThread.status === Thread.STATUS_RUNNING) { // Normal-mode thread: step. - this.stepThread(activeThread); + this.startThread(activeThread); } else if (activeThread.status === Thread.STATUS_YIELD) { // Yield-mode thread: check if the time has passed. - YieldTimers.resolve(activeThread.yieldTimerId); - numYieldingThreads++; + 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 } - // First attempt to pop from the stack - if (activeThread.stack.length > 0 && - activeThread.nextBlock === null && - activeThread.status === Thread.STATUS_DONE) { - activeThread.nextBlock = activeThread.stack.pop(); - // Don't pop stack frame - we need the data. - // A new one won't be created when we execute. - if (activeThread.nextBlock !== null) { - activeThread.status === Thread.STATUS_RUNNING; - } - } - if (activeThread.nextBlock === null && + if (activeThread.stack.length === 0 && activeThread.status === Thread.STATUS_DONE) { // Finished with this thread - tell runtime to clean it up. inactiveThreads.push(activeThread); @@ -94,175 +83,71 @@ Sequencer.prototype.stepThreads = function (threads) { * Step the requested thread * @param {!Thread} thread Thread object to step */ -Sequencer.prototype.stepThread = function (thread) { - // 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; - - // Save the current block and set the nextBlock. - // If the primitive would like to do control flow, - // it can overwrite nextBlock. - var currentBlock = thread.nextBlock; - if (!currentBlock || !this.runtime.blocks.getBlock(currentBlock)) { +Sequencer.prototype.startThread = function (thread) { + var currentBlockId = thread.peekStack(); + if (!currentBlockId) { + // A "null block" - empty substack. Pop the stack. + thread.popStack(); thread.status = Thread.STATUS_DONE; return; } - thread.nextBlock = this.runtime.blocks.getNextBlock(currentBlock); - - var opcode = this.runtime.blocks.getOpcode(currentBlock); - - // Push the current block to the stack - thread.stack.push(currentBlock); - // Push an empty stack frame, if we need one. - // Might not, if we just popped the stack. - if (thread.stack.length > thread.stackFrames.length) { - thread.stackFrames.push({}); - } - var currentStackFrame = thread.stackFrames[thread.stackFrames.length - 1]; - - /** - * A callback for the primitive to indicate its thread should yield. - * @type {Function} - */ - var threadYieldCallback = function () { - thread.status = Thread.STATUS_YIELD; - }; - - /** - * A callback for the primitive to indicate its thread is finished - * @type {Function} - */ - var instance = this; - var threadDoneCallback = function () { - thread.status = Thread.STATUS_DONE; - // Refresh nextBlock in case it has changed during a yield. - thread.nextBlock = instance.runtime.blocks.getNextBlock(currentBlock); - // Pop the stack and stack frame - thread.stack.pop(); - thread.stackFrames.pop(); - // Stop showing run feedback in the editor. - instance.runtime.glowBlock(currentBlock, false); - }; - - /** - * A callback for the primitive to start hats. - * @todo very hacked... - * Provide a callback that is passed in a block and returns true - * if it is a hat that should be triggered. - * @param {Function} callback Provided callback. - */ - var startHats = function(callback) { - var stacks = instance.runtime.blocks.getStacks(); - for (var i = 0; i < stacks.length; i++) { - var stack = stacks[i]; - var stackBlock = instance.runtime.blocks.getBlock(stack); - var result = callback(stackBlock); - if (result) { - // Check if the stack is already running - var stackRunning = false; - - for (var j = 0; j < instance.runtime.threads.length; j++) { - if (instance.runtime.threads[j].topBlock == stack) { - stackRunning = true; - break; - } - } - if (!stackRunning) { - instance.runtime._pushThread(stack); - } - } - } - }; - - /** - * Record whether we have switched stack, - * to avoid proceeding the thread automatically. - * @type {boolean} - */ - var switchedStack = false; - /** - * A callback for a primitive to start a substack. - * @type {Function} - */ - var threadStartSubstack = function () { - // Set nextBlock to the start of the substack - var substack = instance.runtime.blocks.getSubstack(currentBlock); - if (substack && substack.value) { - thread.nextBlock = substack.value; - } else { - thread.nextBlock = null; - } - switchedStack = true; - }; - - // @todo extreme hack to get the single argument value for prototype - var argValues = []; - var blockInputs = this.runtime.blocks.getBlock(currentBlock).fields; - for (var bi in blockInputs) { - var outer = blockInputs[bi]; - for (var b in outer.blocks) { - var block = outer.blocks[b]; - var fields = block.fields; - for (var f in fields) { - var field = fields[f]; - argValues.push(field.value); - } - } - } - // Start showing run feedback in the editor. - this.runtime.glowBlock(currentBlock, true); + this.runtime.glowBlock(currentBlockId, true); - if (!opcode) { - console.warn('Could not get opcode for block: ' + currentBlock); - } - else { - var blockFunction = this.runtime.getOpcodeFunction(opcode); - if (!blockFunction) { - console.warn('Could not get implementation for opcode: ' + opcode); - } - else { - if (Sequencer.DEBUG_BLOCK_CALLS) { - console.groupCollapsed('Executing: ' + opcode); - console.log('with arguments: ', argValues); - console.log('and stack frame: ', currentStackFrame); - } - var blockFunctionReturnValue = null; - try { - // @todo deal with the return value - blockFunctionReturnValue = blockFunction(argValues, { - yield: threadYieldCallback, - done: threadDoneCallback, - timeout: YieldTimers.timeout, - stackFrame: currentStackFrame, - startSubstack: threadStartSubstack, - startHats: startHats - }); - } - catch(e) { - console.error( - 'Exception calling block function for opcode: ' + - opcode + '\n' + e); - } finally { - // Update if the thread has set a yield timer ID - // @todo hack - if (YieldTimers.timerId > oldYieldTimerId) { - thread.yieldTimerId = YieldTimers.timerId; - } - if (thread.status === Thread.STATUS_RUNNING && !switchedStack) { - // Thread executed without yielding - move to done - threadDoneCallback(); - } - if (Sequencer.DEBUG_BLOCK_CALLS) { - console.log('ending stack frame: ', currentStackFrame); - console.log('returned: ', blockFunctionReturnValue); - console.groupEnd(); - } - } - } - } + // Execute the current block + execute(this, thread); + // If the block executed without yielding and without doing control flow, + // move to done. + if (thread.status === Thread.STATUS_RUNNING && + thread.peekStack() === currentBlockId) { + this.proceedThread(thread, currentBlockId); + } +}; + +/** + * Step a thread into a block's substack. + * @param {!Thread} thread Thread object to step to substack. + * @param {Number} substackNum Which substack to step to (i.e., 1, 2). + */ +Sequencer.prototype.stepToSubstack = function (thread, substackNum) { + if (!substackNum) { + substackNum = 1; + } + var currentBlockId = thread.peekStack(); + var substackId = this.runtime.blocks.getSubstack( + currentBlockId, + substackNum + ); + if (substackId) { + // Push substack ID to the thread's stack. + thread.pushStack(substackId); + } else { + // Push null, so we come back to the current block. + thread.pushStack(null); + } +}; + +/** + * Finish stepping a thread and proceed it to the next block. + * @param {!Thread} thread Thread object to proceed. + */ +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; + // Pop from the stack - finished this level of execution. + thread.popStack(); + // Push next connected block, if there is one. + var nextBlockId = this.runtime.blocks.getNextBlock(currentBlockId); + if (nextBlockId) { + thread.pushStack(nextBlockId); + } + // Pop from the stack until we have a next block. + while (thread.peekStack() === null && thread.stack.length > 0) { + thread.popStack(); + } }; module.exports = Sequencer; diff --git a/src/engine/thread.js b/src/engine/thread.js index 07ceaca35..c98efab48 100644 --- a/src/engine/thread.js +++ b/src/engine/thread.js @@ -9,11 +9,7 @@ function Thread (firstBlock) { * @type {!string} */ this.topBlock = firstBlock; - /** - * ID of next block that the thread will execute, or null if none. - * @type {?string} - */ - this.nextBlock = firstBlock; + /** * Stack for the thread. When the sequencer enters a control structure, * the block is pushed onto the stack so we know where to exit. @@ -62,4 +58,50 @@ Thread.STATUS_YIELD = 1; */ Thread.STATUS_DONE = 2; +/** + * Push stack and update stack frames appropriately. + * @param {string} blockId Block ID to push to stack. + */ +Thread.prototype.pushStack = function (blockId) { + this.stack.push(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({}); + } +}; + +/** + * Pop last block on the stack and its stack frame. + * @return {string} Block ID popped from the stack. + */ +Thread.prototype.popStack = function () { + this.stackFrames.pop(); + return this.stack.pop(); +}; + +/** + * Get top stack item. + * @return {?string} Block ID on top of stack. + */ +Thread.prototype.peekStack = function () { + return this.stack[this.stack.length - 1]; +}; + + +/** + * Get top stack frame. + * @return {?Object} Last stack frame stored on this thread. + */ +Thread.prototype.peekStackFrame = function () { + return this.stackFrames[this.stackFrames.length - 1]; +}; + +/** + * Yields the thread. + */ +Thread.prototype.yield = function () { + this.status = Thread.STATUS_YIELD; +}; + module.exports = Thread;