Merge pull request from tmickel/feature/execute-with-args

Refactor for block execution
This commit is contained in:
Tim Mickel 2016-06-13 11:05:00 -04:00 committed by GitHub
commit 07354cddbc
8 changed files with 322 additions and 206 deletions

View file

@ -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() {

View file

@ -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;

View file

@ -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;

View file

@ -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;
};
// ---------------------------------------------------------------------
/**

94
src/engine/execute.js Normal file
View file

@ -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;

View file

@ -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 {

View file

@ -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;

View file

@ -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;