Merge pull request from tmickel/feature/yielding-reporters

Yielding reporters, blocking yields
This commit is contained in:
Tim Mickel 2016-06-30 18:58:03 -04:00 committed by GitHub
commit 1a48e75341
11 changed files with 193 additions and 328 deletions

View file

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

View file

@ -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",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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.
*/

View file

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