mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-06-03 00:54:57 -04:00
Merge pull request #83 from tmickel/feature/yielding-reporters
Yielding reporters, blocking yields
This commit is contained in:
commit
1a48e75341
11 changed files with 193 additions and 328 deletions
|
@ -8,7 +8,7 @@
|
||||||
"max-len": [2, 80, 4],
|
"max-len": [2, 80, 4],
|
||||||
"semi": [2, "always"],
|
"semi": [2, "always"],
|
||||||
"strict": [2, "never"],
|
"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}]
|
"valid-jsdoc": ["error", {"requireReturn": false}]
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
|
|
|
@ -15,7 +15,8 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"htmlparser2": "3.9.0",
|
"htmlparser2": "3.9.0",
|
||||||
"memoizee": "0.3.10"
|
"memoizee": "0.3.10",
|
||||||
|
"promise": "7.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "2.7.0",
|
"eslint": "2.7.0",
|
||||||
|
|
|
@ -79,12 +79,6 @@
|
||||||
</block>
|
</block>
|
||||||
<block type="control_delete_this_clone"></block>
|
<block type="control_delete_this_clone"></block>
|
||||||
</category>
|
</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">
|
<category name="Operators">
|
||||||
<block type="operator_add">
|
<block type="operator_add">
|
||||||
<value name="NUM1">
|
<value name="NUM1">
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
var Promise = require('promise');
|
||||||
|
|
||||||
function Scratch3ControlBlocks(runtime) {
|
function Scratch3ControlBlocks(runtime) {
|
||||||
/**
|
/**
|
||||||
* The runtime instantiating this block package.
|
* The runtime instantiating this block package.
|
||||||
|
@ -38,11 +40,12 @@ Scratch3ControlBlocks.prototype.forever = function(args, util) {
|
||||||
util.startSubstack();
|
util.startSubstack();
|
||||||
};
|
};
|
||||||
|
|
||||||
Scratch3ControlBlocks.prototype.wait = function(args, util) {
|
Scratch3ControlBlocks.prototype.wait = function(args) {
|
||||||
util.yield();
|
return new Promise(function(resolve) {
|
||||||
util.timeout(function() {
|
setTimeout(function() {
|
||||||
util.done();
|
resolve();
|
||||||
}, 1000 * args.DURATION);
|
}, 1000 * args.DURATION);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
Scratch3ControlBlocks.prototype.if = function(args, util) {
|
Scratch3ControlBlocks.prototype.if = function(args, util) {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
var Promise = require('promise');
|
||||||
|
|
||||||
function Scratch3OperatorsBlocks(runtime) {
|
function Scratch3OperatorsBlocks(runtime) {
|
||||||
/**
|
/**
|
||||||
* The runtime instantiating this block package.
|
* The runtime instantiating this block package.
|
||||||
|
@ -15,7 +17,16 @@ Scratch3OperatorsBlocks.prototype.getPrimitives = function() {
|
||||||
'math_number': this.number,
|
'math_number': this.number,
|
||||||
'text': this.text,
|
'text': this.text,
|
||||||
'operator_add': this.add,
|
'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;
|
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) {
|
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;
|
module.exports = Scratch3OperatorsBlocks;
|
||||||
|
|
|
@ -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;
|
|
|
@ -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.
|
* Execute a block.
|
||||||
* @const {boolean}
|
* @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 execute = function (sequencer, thread) {
|
||||||
var runtime = sequencer.runtime;
|
var runtime = sequencer.runtime;
|
||||||
|
|
||||||
|
@ -13,13 +12,19 @@ var execute = function (sequencer, thread) {
|
||||||
var currentBlockId = thread.peekStack();
|
var currentBlockId = thread.peekStack();
|
||||||
var currentStackFrame = thread.peekStackFrame();
|
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);
|
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).
|
// Generate values for arguments (inputs).
|
||||||
var argValues = {};
|
var argValues = {};
|
||||||
|
|
||||||
|
@ -34,53 +39,65 @@ var execute = function (sequencer, thread) {
|
||||||
for (var inputName in inputs) {
|
for (var inputName in inputs) {
|
||||||
var input = inputs[inputName];
|
var input = inputs[inputName];
|
||||||
var inputBlockId = input.block;
|
var inputBlockId = input.block;
|
||||||
// Push to the stack to evaluate this input.
|
// Is there no value for this input waiting in the stack frame?
|
||||||
thread.pushStack(inputBlockId);
|
if (typeof currentStackFrame.reported[inputName] === 'undefined') {
|
||||||
var result = execute(sequencer, thread);
|
// If there's not, we need to evaluate the block.
|
||||||
thread.popStack();
|
var reporterYielded = (
|
||||||
argValues[input.name] = result;
|
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) {
|
// If we've gotten this far, all of the input blocks are evaluated,
|
||||||
console.warn('Could not get opcode for block: ' + currentBlockId);
|
// and `argValues` is fully populated. So, execute the block primitive.
|
||||||
return;
|
// 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);
|
var primitiveReportedValue = null;
|
||||||
if (!blockFunction) {
|
primitiveReportedValue = blockFunction(argValues, {
|
||||||
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, {
|
|
||||||
yield: thread.yield.bind(thread),
|
yield: thread.yield.bind(thread),
|
||||||
done: function() {
|
done: function() {
|
||||||
sequencer.proceedThread(thread);
|
sequencer.proceedThread(thread);
|
||||||
},
|
},
|
||||||
timeout: YieldTimers.timeout,
|
stackFrame: currentStackFrame.executionContext,
|
||||||
stackFrame: currentStackFrame,
|
|
||||||
startSubstack: function (substackNum) {
|
startSubstack: function (substackNum) {
|
||||||
sequencer.stepToSubstack(thread, substackNum);
|
sequencer.stepToSubstack(thread, substackNum);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Update if the thread has set a yield timer ID
|
|
||||||
// @todo hack
|
// Deal with any reported value.
|
||||||
if (YieldTimers.timerId > oldYieldTimerId) {
|
// If it's a promise, wait until promise resolves.
|
||||||
thread.yieldTimerId = YieldTimers.timerId;
|
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;
|
module.exports = execute;
|
||||||
|
|
|
@ -6,8 +6,7 @@ var util = require('util');
|
||||||
var defaultBlockPackages = {
|
var defaultBlockPackages = {
|
||||||
'scratch3_control': require('../blocks/scratch3_control'),
|
'scratch3_control': require('../blocks/scratch3_control'),
|
||||||
'scratch3_event': require('../blocks/scratch3_event'),
|
'scratch3_event': require('../blocks/scratch3_event'),
|
||||||
'scratch3_operators': require('../blocks/scratch3_operators'),
|
'scratch3_operators': require('../blocks/scratch3_operators')
|
||||||
'wedo2': require('../blocks/wedo2')
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
var Timer = require('../util/timer');
|
var Timer = require('../util/timer');
|
||||||
var Thread = require('./thread');
|
var Thread = require('./thread');
|
||||||
var YieldTimers = require('../util/yieldtimers.js');
|
|
||||||
var execute = require('./execute.js');
|
var execute = require('./execute.js');
|
||||||
|
|
||||||
function Sequencer (runtime) {
|
function Sequencer (runtime) {
|
||||||
|
@ -53,16 +52,8 @@ Sequencer.prototype.stepThreads = function (threads) {
|
||||||
// Normal-mode thread: step.
|
// Normal-mode thread: step.
|
||||||
this.startThread(activeThread);
|
this.startThread(activeThread);
|
||||||
} else if (activeThread.status === Thread.STATUS_YIELD) {
|
} else if (activeThread.status === Thread.STATUS_YIELD) {
|
||||||
// Yield-mode thread: check if the time has passed.
|
// Yielding thread: do nothing for this step.
|
||||||
if (!YieldTimers.resolve(activeThread.yieldTimerId)) {
|
continue;
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
if (activeThread.stack.length === 0 &&
|
if (activeThread.stack.length === 0 &&
|
||||||
activeThread.status === Thread.STATUS_DONE) {
|
activeThread.status === Thread.STATUS_DONE) {
|
||||||
|
@ -101,7 +92,7 @@ Sequencer.prototype.startThread = function (thread) {
|
||||||
// move to done.
|
// move to done.
|
||||||
if (thread.status === Thread.STATUS_RUNNING &&
|
if (thread.status === Thread.STATUS_RUNNING &&
|
||||||
thread.peekStack() === currentBlockId) {
|
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.
|
* Finish stepping a thread and proceed it to the next block.
|
||||||
* @param {!Thread} thread Thread object to proceed.
|
* @param {!Thread} thread Thread object to proceed.
|
||||||
|
@ -136,7 +148,8 @@ Sequencer.prototype.proceedThread = function (thread) {
|
||||||
var currentBlockId = thread.peekStack();
|
var currentBlockId = thread.peekStack();
|
||||||
// Mark the status as done and proceed to the next block.
|
// Mark the status as done and proceed to the next block.
|
||||||
this.runtime.glowBlock(currentBlockId, false);
|
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.
|
// Pop from the stack - finished this level of execution.
|
||||||
thread.popStack();
|
thread.popStack();
|
||||||
// Push next connected block, if there is one.
|
// 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) {
|
while (thread.peekStack() === null && thread.stack.length > 0) {
|
||||||
thread.popStack();
|
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;
|
module.exports = Sequencer;
|
||||||
|
|
|
@ -28,32 +28,27 @@ function Thread (firstBlock) {
|
||||||
* @type {number}
|
* @type {number}
|
||||||
*/
|
*/
|
||||||
this.status = 0; /* Thread.STATUS_RUNNING */
|
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.
|
* 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
|
* @const
|
||||||
*/
|
*/
|
||||||
Thread.STATUS_RUNNING = 0;
|
Thread.STATUS_RUNNING = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Thread status for a yielded thread.
|
* 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
|
* @const
|
||||||
*/
|
*/
|
||||||
Thread.STATUS_YIELD = 1;
|
Thread.STATUS_YIELD = 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Thread status for a finished/done thread.
|
* Thread status for a finished/done thread.
|
||||||
* Thread is moved to this state when the interpreter
|
* Thread is in this state when there are no more blocks to execute.
|
||||||
* can proceed with execution.
|
|
||||||
* @const
|
* @const
|
||||||
*/
|
*/
|
||||||
Thread.STATUS_DONE = 2;
|
Thread.STATUS_DONE = 2;
|
||||||
|
@ -67,7 +62,11 @@ Thread.prototype.pushStack = function (blockId) {
|
||||||
// Push an empty stack frame, if we need one.
|
// Push an empty stack frame, if we need one.
|
||||||
// Might not, if we just popped the stack.
|
// Might not, if we just popped the stack.
|
||||||
if (this.stack.length > this.stackFrames.length) {
|
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];
|
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.
|
* Yields the thread.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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;
|
|
Loading…
Add table
Add a link
Reference in a new issue