elevate stack block id and frame info into thread

- Add pointer member to thread. This is the current executing block.
- Add stackFrame member to thread. This is the current frame
  information like procedure parameters.

This is a step potentially towards stack-less threads. With further
modifications we could have stack and stackFrames be null if a script
does not call a procedure.
This commit is contained in:
Michael "Z" Goddard 2019-04-25 10:40:08 -04:00
parent ffcd0e6518
commit e1254bd8c7
No known key found for this signature in database
GPG key ID: 762CD40DD5349872
8 changed files with 197 additions and 156 deletions

View file

@ -24,7 +24,6 @@ class Scratch3ProcedureBlocks {
} }
call (args, util) { call (args, util) {
if (!util.stackFrame.executed) {
const procedureCode = args.mutation.proccode; const procedureCode = args.mutation.proccode;
const paramNamesIdsAndDefaults = util.getProcedureParamNamesIdsAndDefaults(procedureCode); const paramNamesIdsAndDefaults = util.getProcedureParamNamesIdsAndDefaults(procedureCode);
@ -37,6 +36,8 @@ class Scratch3ProcedureBlocks {
const [paramNames, paramIds, paramDefaults] = paramNamesIdsAndDefaults; const [paramNames, paramIds, paramDefaults] = paramNamesIdsAndDefaults;
util.startProcedure(procedureCode);
// Initialize params for the current stackFrame to {}, even if the procedure does // Initialize params for the current stackFrame to {}, even if the procedure does
// not take any arguments. This is so that `getParam` down the line does not look // not take any arguments. This is so that `getParam` down the line does not look
// at earlier stack frames for the values of a given parameter (#1729) // at earlier stack frames for the values of a given parameter (#1729)
@ -49,9 +50,6 @@ class Scratch3ProcedureBlocks {
} }
} }
util.stackFrame.executed = true;
util.startProcedure(procedureCode);
}
} }
argumentReporterStringNumber (args, util) { argumentReporterStringNumber (args, util) {

View file

@ -60,11 +60,7 @@ class BlockUtility {
* @type {object} * @type {object}
*/ */
get stackFrame () { get stackFrame () {
const frame = this.thread.peekStackFrame(); return this.thread.getExecutionContext();
if (frame.executionContext === null) {
frame.executionContext = {};
}
return frame.executionContext;
} }
/** /**

View file

@ -385,7 +385,6 @@ const execute = function (sequencer, thread) {
// Current block to execute is the one on the top of the stack. // Current block to execute is the one on the top of the stack.
const currentBlockId = thread.peekStack(); const currentBlockId = thread.peekStack();
const currentStackFrame = thread.peekStackFrame();
let blockContainer = thread.blockContainer; let blockContainer = thread.blockContainer;
let blockCached = BlocksExecuteCache.getCached(blockContainer, currentBlockId, BlockCached); let blockCached = BlocksExecuteCache.getCached(blockContainer, currentBlockId, BlockCached);
@ -404,8 +403,8 @@ const execute = function (sequencer, thread) {
const length = ops.length; const length = ops.length;
let i = 0; let i = 0;
if (currentStackFrame.reported !== null) { if (thread.reported !== null) {
const reported = currentStackFrame.reported; const reported = thread.reported;
// Reinstate all the previous values. // Reinstate all the previous values.
for (; i < reported.length; i++) { for (; i < reported.length; i++) {
const {opCached: oldOpCached, inputValue} = reported[i]; const {opCached: oldOpCached, inputValue} = reported[i];
@ -441,7 +440,7 @@ const execute = function (sequencer, thread) {
} }
// The reporting block must exist and must be the next one in the sequence of operations. // The reporting block must exist and must be the next one in the sequence of operations.
if (thread.justReported !== null && ops[i] && ops[i].id === currentStackFrame.reporting) { if (thread.justReported !== null && ops[i] && ops[i].id === thread.reportingBlockId) {
const opCached = ops[i]; const opCached = ops[i];
const inputValue = thread.justReported; const inputValue = thread.justReported;
@ -462,8 +461,8 @@ const execute = function (sequencer, thread) {
i += 1; i += 1;
} }
currentStackFrame.reporting = null; thread.reportingBlockId = null;
currentStackFrame.reported = null; thread.reported = null;
} }
for (; i < length; i++) { for (; i < length; i++) {
@ -518,8 +517,8 @@ const execute = function (sequencer, thread) {
// operation if it is promise waiting will set its parent value at // operation if it is promise waiting will set its parent value at
// that time. // that time.
thread.justReported = null; thread.justReported = null;
currentStackFrame.reporting = ops[i].id; thread.reportingBlockId = ops[i].id;
currentStackFrame.reported = ops.slice(0, i).map(reportedCached => { thread.reported = ops.slice(0, i).map(reportedCached => {
const inputName = reportedCached._parentKey; const inputName = reportedCached._parentKey;
const reportedValues = reportedCached._parentValues; const reportedValues = reportedCached._parentValues;

View file

@ -1490,7 +1490,7 @@ class Runtime extends EventEmitter {
isActiveThread (thread) { isActiveThread (thread) {
return ( return (
( (
thread.stack.length > 0 && thread.stackFrame !== null &&
thread.status !== Thread.STATUS_DONE) && thread.status !== Thread.STATUS_DONE) &&
this.threads.indexOf(thread) > -1); this.threads.indexOf(thread) > -1);
} }
@ -1667,11 +1667,20 @@ class Runtime extends EventEmitter {
// Start the thread with this top block. // Start the thread with this top block.
newThreads.push(this._pushThread(topBlockId, target)); newThreads.push(this._pushThread(topBlockId, target));
}, optTarget); }, optTarget);
// For compatibility with Scratch 2, edge triggered hats need to be processed before // For compatibility with Scratch 2, edge triggered hats need to be
// threads are stepped. See ScratchRuntime.as for original implementation // processed before threads are stepped. See ScratchRuntime.as for
// original implementation.
//
// TODO: Move the execute call to sequencer. Maybe in a method call
// stepHat or stepOne.
newThreads.forEach(thread => { newThreads.forEach(thread => {
execute(this.sequencer, thread); execute(this.sequencer, thread);
if (thread.status !== Thread.STATUS_DONE) {
thread.goToNextBlock(); thread.goToNextBlock();
if (thread.stackFrame === null) {
this.sequencer.retireThread(thread);
}
}
}); });
return newThreads; return newThreads;
} }

View file

@ -103,7 +103,7 @@ class Sequencer {
for (let i = 0; i < threads.length; i++) { for (let i = 0; i < threads.length; i++) {
const activeThread = this.activeThread = threads[i]; const activeThread = this.activeThread = threads[i];
// Check if the thread is done so it is not executed. // Check if the thread is done so it is not executed.
if (activeThread.stack.length === 0 || if (activeThread.stackFrame === null ||
activeThread.status === Thread.STATUS_DONE) { activeThread.status === Thread.STATUS_DONE) {
// Finished with this thread. // Finished with this thread.
stoppedThread = true; stoppedThread = true;
@ -137,7 +137,7 @@ class Sequencer {
} }
// Check if the thread completed while it just stepped to make // Check if the thread completed while it just stepped to make
// sure we remove it before the next iteration of all threads. // sure we remove it before the next iteration of all threads.
if (activeThread.stack.length === 0 || if (activeThread.stackFrame === null ||
activeThread.status === Thread.STATUS_DONE) { activeThread.status === Thread.STATUS_DONE) {
// Finished with this thread. // Finished with this thread.
stoppedThread = true; stoppedThread = true;
@ -156,7 +156,7 @@ class Sequencer {
let nextActiveThread = 0; let nextActiveThread = 0;
for (let i = 0; i < this.runtime.threads.length; i++) { for (let i = 0; i < this.runtime.threads.length; i++) {
const thread = this.runtime.threads[i]; const thread = this.runtime.threads[i];
if (thread.stack.length !== 0 && if (thread.stackFrame !== null &&
thread.status !== Thread.STATUS_DONE) { thread.status !== Thread.STATUS_DONE) {
this.runtime.threads[nextActiveThread] = thread; this.runtime.threads[nextActiveThread] = thread;
nextActiveThread++; nextActiveThread++;
@ -184,7 +184,7 @@ class Sequencer {
thread.popStack(); thread.popStack();
// Did the null follow a hat block? // Did the null follow a hat block?
if (thread.stack.length === 0) { if (thread.peekStackFrame() === null) {
thread.status = Thread.STATUS_DONE; thread.status = Thread.STATUS_DONE;
return; return;
} }
@ -248,7 +248,7 @@ class Sequencer {
while (!thread.peekStack()) { while (!thread.peekStack()) {
thread.popStack(); thread.popStack();
if (thread.stack.length === 0) { if (thread.stackFrame === null) {
// No more stack to run! // No more stack to run!
thread.status = Thread.STATUS_DONE; thread.status = Thread.STATUS_DONE;
return; return;
@ -299,7 +299,11 @@ class Sequencer {
currentBlockId, currentBlockId,
branchNum branchNum
); );
thread.peekStackFrame().isLoop = isLoop; if (isLoop) {
const stackFrame = thread.peekStackFrame();
stackFrame.needsReset = true;
stackFrame.isLoop = true;
}
if (branchId) { if (branchId) {
// Push branch ID to the thread's stack. // Push branch ID to the thread's stack.
thread.pushStack(branchId); thread.pushStack(branchId);
@ -361,7 +365,9 @@ class Sequencer {
*/ */
retireThread (thread) { retireThread (thread) {
thread.stack = []; thread.stack = [];
thread.stackFrame = []; thread.pointer = null;
thread.stackFrames = [];
thread.stackFrame = null;
thread.requestScriptGlowInFrame = false; thread.requestScriptGlowInFrame = false;
thread.status = Thread.STATUS_DONE; thread.status = Thread.STATUS_DONE;
} }

View file

@ -5,14 +5,23 @@
const _stackFrameFreeList = []; const _stackFrameFreeList = [];
/** /**
* A frame used for each level of the stack. A general purpose * Default params object for stack frames outside of a procedure.
* place to store a bunch of execution context and parameters *
* @param {boolean} warpMode Whether this level of the stack is warping * StackFrame.params uses a null prototype object. It does not have Object
* methods like hasOwnProperty. With a null prototype
* `typeof params[key] !== 'undefined'` has similar behaviour to hasOwnProperty.
* @type {object}
*/
const defaultParams = Object.create(null);
/**
* A frame used for each level of the stack. A general purpose place to store a
* bunch of execution contexts and parameters.
* @constructor * @constructor
* @private * @private
*/ */
class _StackFrame { class _StackFrame {
constructor (warpMode) { constructor () {
/** /**
* Whether this level of the stack is a loop. * Whether this level of the stack is a loop.
* @type {boolean} * @type {boolean}
@ -20,103 +29,85 @@ class _StackFrame {
this.isLoop = false; this.isLoop = false;
/** /**
* Whether this level is in warp mode. Is set by some legacy blocks and * Whether this level is in warp mode. Set to true by the sequencer for
* "turbo mode" * some procedures.
*
* After being set to true at the beginning of a procedure a thread
* will be in warpMode until it pops a stack frame to reveal one that
* is not in warpMode. Either this value is always false for a stack
* frame or always true after a procedure sets it.
* @type {boolean} * @type {boolean}
*/ */
this.warpMode = warpMode; this.warpMode = false;
/**
* Reported value from just executed block.
* @type {Any}
*/
this.justReported = null;
/**
* The active block that is waiting on a promise.
* @type {string}
*/
this.reporting = '';
/**
* Persists reported inputs during async block.
* @type {Object}
*/
this.reported = null;
/**
* Name of waiting reporter.
* @type {string}
*/
this.waitingReporter = null;
/** /**
* Procedure parameters. * Procedure parameters.
*
* After being set by a procedure these values do not change and they
* will be copied to deeper stack frames.
* @type {Object} * @type {Object}
*/ */
this.params = null; this.params = defaultParams;
/** /**
* A context passed to block implementations. * A context passed to block implementations.
* @type {Object} * @type {Object}
*/ */
this.executionContext = null; this.executionContext = {};
/**
* Has this frame changed and need a reset?
* @type {boolean}
*/
this.needsReset = false;
} }
/** /**
* Reset all properties of the frame to pristine null and false states. * Reset some properties of the frame to default values. Used to recycle.
* Used to recycle.
* @return {_StackFrame} this * @return {_StackFrame} this
*/ */
reset () { reset () {
this.isLoop = false; this.isLoop = false;
this.warpMode = false; this.executionContext = {};
this.justReported = null; this.needsReset = false;
this.reported = null;
this.waitingReporter = null;
this.params = null;
this.executionContext = null;
return this; return this;
} }
/**
* Reuse an active stack frame in the stack.
* @param {?boolean} warpMode defaults to current warpMode
* @returns {_StackFrame} this
*/
reuse (warpMode = this.warpMode) {
this.reset();
this.warpMode = Boolean(warpMode);
return this;
}
/** /**
* Create or recycle a stack frame object. * Create or recycle a stack frame object.
* @param {boolean} warpMode Enable warpMode on this frame. * @param {_StackFrame} parent Parent frame to copy "immutable" values.
* @returns {_StackFrame} The clean stack frame with correct warpMode setting. * @returns {_StackFrame} The clean stack frame with correct warpMode
* setting.
*/ */
static create (warpMode) { static create (parent) {
const stackFrame = _stackFrameFreeList.pop(); const stackFrame = _stackFrameFreeList.pop() || new _StackFrame();
if (typeof stackFrame !== 'undefined') { stackFrame.warpMode = parent.warpMode;
stackFrame.warpMode = Boolean(warpMode); stackFrame.params = parent.params;
return stackFrame; return stackFrame;
} }
return new _StackFrame(warpMode);
}
/** /**
* Put a stack frame object into the recycle bin for reuse. * Put a stack frame object into the recycle bin for reuse.
* @param {_StackFrame} stackFrame The frame to reset and recycle. * @param {_StackFrame} stackFrame The frame to reset and recycle.
*/ */
static release (stackFrame) { static release (stackFrame) {
if (typeof stackFrame !== 'undefined') { if (stackFrame !== null) {
_stackFrameFreeList.push(stackFrame.reset()); _stackFrameFreeList.push(
stackFrame.needsReset ? stackFrame.reset() : stackFrame
);
} }
} }
} }
/**
* The initial stack frame for all threads. A call to pushStack will create the
* first to be used frame for a thread. That first frame will use the initial
* values from initialStackFrame.
* @type {_StackFrame}
*/
const initialStackFrame = new _StackFrame();
/** /**
* A thread is a running stack context and all the metadata needed. * A thread is a running stack context and all the metadata needed.
* @param {?string} firstBlock First block to execute in the thread. * @param {?string} firstBlock First block to execute in the thread.
@ -137,12 +128,25 @@ class Thread {
*/ */
this.stack = []; this.stack = [];
/**
* The "instruction" pointer the thread is currently at. This
* determines what block is executed.
* @type {string}
*/
this.pointer = null;
/** /**
* Stack frames for the thread. Store metadata for the executing blocks. * Stack frames for the thread. Store metadata for the executing blocks.
* @type {Array.<_StackFrame>} * @type {Array.<_StackFrame>}
*/ */
this.stackFrames = []; this.stackFrames = [];
/**
* The current stack frame that goes along with the pointer.
* @type {_StackFrame}
*/
this.stackFrame = null;
/** /**
* Status of the thread, one of three states (below) * Status of the thread, one of three states (below)
* @type {number} * @type {number}
@ -186,7 +190,25 @@ class Thread {
*/ */
this.warpTimer = null; this.warpTimer = null;
/**
* The value just reported by a promise.
* @type {*}
*/
this.justReported = null; this.justReported = null;
/**
* The id of the block that we will report the promise resolved value
* for.
* @type {string}
*/
this.reportingBlockId = null;
/**
* The already reported values in a sequence of blocks to restore when
* the awaited promise resolves.
* @type {Array.<*>}
*/
this.reported = null;
} }
/** /**
@ -239,12 +261,16 @@ class Thread {
* @param {string} blockId Block ID to push to stack. * @param {string} blockId Block ID to push to stack.
*/ */
pushStack (blockId) { pushStack (blockId) {
this.stack.push(blockId); if (this.stackFrame === null) {
// Push an empty stack frame, if we need one. this.pointer = blockId;
// Might not, if we just popped the stack. this.stackFrame = _StackFrame.create(initialStackFrame);
if (this.stack.length > this.stackFrames.length) { } else {
const parent = this.stackFrames[this.stackFrames.length - 1]; this.stack.push(this.pointer);
this.stackFrames.push(_StackFrame.create(typeof parent !== 'undefined' && parent.warpMode)); this.pointer = blockId;
const parent = this.stackFrame;
this.stackFrames.push(parent);
this.stackFrame = _StackFrame.create(parent);
} }
} }
@ -254,21 +280,27 @@ class Thread {
* @param {string} blockId Block ID to push to stack. * @param {string} blockId Block ID to push to stack.
*/ */
reuseStackForNextBlock (blockId) { reuseStackForNextBlock (blockId) {
this.stack[this.stack.length - 1] = blockId; this.pointer = blockId;
this.stackFrames[this.stackFrames.length - 1].reuse(); if (this.stackFrame.needsReset) this.stackFrame.reset();
} }
/** /**
* Pop last block on the stack and its stack frame. * Move the instruction pointer to the last value before this stack of
* @return {string} Block ID popped from the stack. * blocks was pushed and executed.
* @return {?string} Block ID popped from the stack.
*/ */
popStack () { popStack () {
_StackFrame.release(this.stackFrames.pop()); const lastPointer = this.pointer;
return this.stack.pop(); this.pointer = this.stack.pop() || null;
_StackFrame.release(this.stackFrame);
this.stackFrame = this.stackFrames.pop() || null;
return lastPointer;
} }
/** /**
* Pop back down the stack frame until we hit a procedure call or the stack frame is emptied * Move the instruction pointer to the last procedure call block and resume
* execution there or to the end of this thread and stop executing this
* thread.
*/ */
stopThisScript () { stopThisScript () {
let blockID = this.peekStack(); let blockID = this.peekStack();
@ -277,11 +309,10 @@ class Thread {
if (typeof block !== 'undefined' && block.opcode === 'procedures_call') { if (typeof block !== 'undefined' && block.opcode === 'procedures_call') {
break; break;
} }
this.popStack(); blockID = this.popStack();
blockID = this.peekStack();
} }
if (this.stack.length === 0) { if (this.stackFrame === null) {
// Clean up! // Clean up!
this.requestScriptGlowInFrame = false; this.requestScriptGlowInFrame = false;
this.status = Thread.STATUS_DONE; this.status = Thread.STATUS_DONE;
@ -293,7 +324,7 @@ class Thread {
* @return {?string} Block ID on top of stack. * @return {?string} Block ID on top of stack.
*/ */
peekStack () { peekStack () {
return this.stack.length > 0 ? this.stack[this.stack.length - 1] : null; return this.pointer;
} }
@ -302,7 +333,7 @@ class Thread {
* @return {?object} Last stack frame stored on this thread. * @return {?object} Last stack frame stored on this thread.
*/ */
peekStackFrame () { peekStackFrame () {
return this.stackFrames.length > 0 ? this.stackFrames[this.stackFrames.length - 1] : null; return this.stackFrame;
} }
/** /**
@ -310,7 +341,7 @@ class Thread {
* @return {?object} Second to last stack frame stored on this thread. * @return {?object} Second to last stack frame stored on this thread.
*/ */
peekParentStackFrame () { peekParentStackFrame () {
return this.stackFrames.length > 1 ? this.stackFrames[this.stackFrames.length - 2] : null; return this.stackFrames.length > 0 ? this.stackFrames[this.stackFrames.length - 1] : null;
} }
/** /**
@ -321,14 +352,22 @@ class Thread {
this.justReported = typeof value === 'undefined' ? null : value; this.justReported = typeof value === 'undefined' ? null : value;
} }
/**
* Return an execution context for a block to use.
* @returns {object} the execution context
*/
getExecutionContext () {
const frame = this.stackFrame;
frame.needsReset = true;
return frame.executionContext;
}
/** /**
* Initialize procedure parameters on this stack frame. * Initialize procedure parameters on this stack frame.
*/ */
initParams () { initParams () {
const stackFrame = this.peekStackFrame(); const stackFrame = this.stackFrame;
if (stackFrame.params === null) { stackFrame.params = Object.create(null);
stackFrame.params = {};
}
} }
/** /**
@ -338,7 +377,7 @@ class Thread {
* @param {*} value Value to set for parameter. * @param {*} value Value to set for parameter.
*/ */
pushParam (paramName, value) { pushParam (paramName, value) {
const stackFrame = this.peekStackFrame(); const stackFrame = this.stackFrame;
stackFrame.params[paramName] = value; stackFrame.params[paramName] = value;
} }
@ -348,15 +387,9 @@ class Thread {
* @return {*} value Value for parameter. * @return {*} value Value for parameter.
*/ */
getParam (paramName) { getParam (paramName) {
for (let i = this.stackFrames.length - 1; i >= 0; i--) { const stackFrame = this.stackFrame;
const frame = this.stackFrames[i]; if (typeof stackFrame.params[paramName] !== 'undefined') {
if (frame.params === null) { return stackFrame.params[paramName];
continue;
}
if (frame.params.hasOwnProperty(paramName)) {
return frame.params[paramName];
}
return null;
} }
return null; return null;
} }
@ -376,26 +409,26 @@ class Thread {
* where execution proceeds from one block to the next. * where execution proceeds from one block to the next.
*/ */
goToNextBlock () { goToNextBlock () {
const nextBlockId = this.target.blocks.getNextBlock(this.peekStack()); const nextBlockId = this.target.blocks.getNextBlock(this.pointer);
this.reuseStackForNextBlock(nextBlockId); this.reuseStackForNextBlock(nextBlockId);
} }
/** /**
* Attempt to determine whether a procedure call is recursive, * Attempt to determine whether a procedure call is recursive, by examining
* by examining the stack. * the stack.
* @param {!string} procedureCode Procedure code of procedure being called. * @param {!string} procedureCode Procedure code of procedure being called.
* @return {boolean} True if the call appears recursive. * @return {boolean} True if the call appears recursive.
*/ */
isRecursiveCall (procedureCode) { isRecursiveCall (procedureCode) {
let callCount = 5; // Max number of enclosing procedure calls to examine. const stackHeight = this.stack.length;
const sp = this.stack.length - 1; // Limit the number of stack levels that are examined for procedures.
for (let i = sp - 1; i >= 0; i--) { const stackBottom = Math.max(stackHeight - 5, 0);
for (let i = stackHeight - 1; i >= stackBottom; i--) {
const block = this.target.blocks.getBlock(this.stack[i]); const block = this.target.blocks.getBlock(this.stack[i]);
if (block.opcode === 'procedures_call' && if (block.opcode === 'procedures_call' &&
block.mutation.proccode === procedureCode) { block.mutation.proccode === procedureCode) {
return true; return true;
} }
if (--callCount < 0) return false;
} }
return false; return false;
} }

View file

@ -137,7 +137,7 @@ test('retireThread', t => {
const r = new Runtime(); const r = new Runtime();
const s = new Sequencer(r); const s = new Sequencer(r);
const th = generateThread(r); const th = generateThread(r);
t.strictEquals(th.stack.length, 12); t.strictEquals(th.stack.length, 11);
s.retireThread(th); s.retireThread(th);
t.strictEquals(th.stack.length, 0); t.strictEquals(th.stack.length, 0);
t.strictEquals(th.status, Thread.STATUS_DONE); t.strictEquals(th.status, Thread.STATUS_DONE);

View file

@ -40,7 +40,7 @@ test('popStack', t => {
const th = new Thread('arbitraryString'); const th = new Thread('arbitraryString');
th.pushStack('arbitraryString'); th.pushStack('arbitraryString');
t.strictEquals(th.popStack(), 'arbitraryString'); t.strictEquals(th.popStack(), 'arbitraryString');
t.strictEquals(th.popStack(), undefined); t.strictEquals(th.popStack(), null);
t.end(); t.end();
}); });