This is progress

- if compilation errors, fallback to interpreter
- retire compiled threads once done
- add control_repeat
- add math_whole_number
This commit is contained in:
Thomas Weber committed 2020-06-09 20:00:17 -05:00
1 parent 4a93dab4fa
commit 92da52e4fc
16 files changed
+446 -47

No files matched your search

+11 -10
View File
@@ -1,17 +1,18 @@
module.exports.getStatements = () => {
return {
}
return {
};
};
module.exports.getInputs = () => {
return {
math_number: number,
};
return {
math_number: number,
math_whole_number: number,
};
};
const number = (util) => {
const NUM = util.getFieldUnsafe('NUM');
const number = Number(NUM);
return number;
};
const NUM = util.getFieldUnsafe('NUM');
const number = Number(NUM);
return number;
};
@@ -1,19 +1,32 @@
module.exports.getStatements = () => {
return {
control_forever: forever,
}
return {
control_repeat: repeat,
control_forever: forever,
};
};
module.exports.getInputs = () => {
return {
return {
};
};
};
const repeat = (util) => {
const TIMES = util.getInput('TIMES');
const SUBSTACK = util.compileSubstack('SUBSTACK');
const times = util.nextVariable();
util.writeLn(`var ${times} = ${TIMES};`);
util.writeLn(`while (${times} >= 0.5) {`);
util.writeLn(`${times}--;`);
util.write(SUBSTACK);
util.yieldLoop();
util.writeLn(`}`);
};
const forever = (util) => {
const SUBSTACK = util.compileSubstack('SUBSTACK');
util.writeLn('while (true) {');
util.write(SUBSTACK);
util.yieldLoop();
util.writeLn('}');
const SUBSTACK = util.compileSubstack('SUBSTACK');
util.writeLn('while (true) {');
util.write(SUBSTACK);
util.yieldLoop();
util.writeLn('}');
};
@@ -0,0 +1,11 @@
module.exports.getStatements = () => {
return {
};
};
module.exports.getInputs = () => {
return {
};
};
@@ -0,0 +1,11 @@
module.exports.getStatements = () => {
return {
};
};
module.exports.getInputs = () => {
return {
};
};
@@ -1,16 +1,16 @@
module.exports.getStatements = () => {
return {
looks_changeeffectby: changeEffect,
}
return {
looks_changeeffectby: changeEffect,
};
};
module.exports.getInputs = () => {
return {
};
return {
};
};
const changeEffect = (util) => {
const CHANGE = util.getInput('CHANGE');
util.writeLn(`target.setEffect("color", target.effects.color + ${CHANGE});`);
const CHANGE = util.getInput('CHANGE');
util.writeLn(`target.setEffect("color", target.effects.color + ${CHANGE});`);
};
@@ -0,0 +1,11 @@
module.exports.getStatements = () => {
return {
};
};
module.exports.getInputs = () => {
return {
};
};
@@ -0,0 +1,11 @@
module.exports.getStatements = () => {
return {
};
};
module.exports.getInputs = () => {
return {
};
};
@@ -0,0 +1,11 @@
module.exports.getStatements = () => {
return {
};
};
module.exports.getInputs = () => {
return {
};
};
@@ -0,0 +1,11 @@
module.exports.getStatements = () => {
return {
};
};
module.exports.getInputs = () => {
return {
};
};
@@ -0,0 +1,11 @@
module.exports.getStatements = () => {
return {
};
};
module.exports.getInputs = () => {
return {
};
};
+23 -8
View File
@@ -6,8 +6,15 @@ const inputs = {};
const defaultExtensions = [
require('./blocks/compiler_natives'),
require('./blocks/compiler_scratch3_control'),
require('./blocks/compiler_scratch3_motion'),
require('./blocks/compiler_scratch3_looks'),
require('./blocks/compiler_scratch3_sounds'),
require('./blocks/compiler_scratch3_events'),
require('./blocks/compiler_scratch3_control'),
require('./blocks/compiler_scratch3_sensing'),
require('./blocks/compiler_scratch3_operators'),
require('./blocks/compiler_scratch3_data'),
require('./blocks/compiler_scratch3_procedures'),
];
defaultExtensions.forEach((ext) => {
@@ -26,7 +33,7 @@ defaultExtensions.forEach((ext) => {
}
});
class BLockUtil {
class BlockUtil {
constructor(compiler, block) {
this.compiler = compiler;
this.block = block;
@@ -41,13 +48,13 @@ class BLockUtil {
}
}
class InputUtil extends BLockUtil {
class InputUtil extends BlockUtil {
constructor(compiler, block) {
super(compiler, block);
}
}
class StatementUtil extends BLockUtil {
class StatementUtil extends BlockUtil {
constructor(compiler, block) {
super(compiler, block);
this.source = '';
@@ -65,20 +72,26 @@ class StatementUtil extends BLockUtil {
this.source += s;
}
nextVariable() {
this.compiler.variables++;
return 'var' + this.compiler.variables;
}
compileSubstack(inputName) {
const inputValue = this.block.inputs[inputName];
if (!inputValue) {
// empty substack
return '';
}
const substack = inputValue.block;
return this.compiler.compileStack(substack);
}
}
class Compiler {
/**
*
* @param {Thread} thread
*/
constructor(thread) {
this.thread = thread;
this.variables = 0;
}
compileInput(parentBlock, inputName) {
@@ -110,6 +123,7 @@ class Compiler {
const util = new StatementUtil(this, block);
let compiler = statements[block.opcode];
if (!compiler) {
log.error('unknown opcode', block);
throw new Error('unknown opcode: ' + block.opcode);
}
@@ -126,6 +140,7 @@ class Compiler {
const topBlockId = this.thread.topBlock;
const topBlock = this.thread.target.blocks.getBlock(topBlockId);
if (!topBlock) throw new Error('not a hat');
const script = this.compileStack(topBlock.next);
+9
View File
@@ -0,0 +1,9 @@
const execute = function (sequencer, thread) {
const result = thread.generator.next();
if (result.done) {
sequencer.retireThread(thread);
}
};
module.exports = execute;
+184 -1
View File
@@ -4,6 +4,7 @@ const log = require('../util/log');
const Thread = require('./thread');
const {Map} = require('immutable');
const cast = require('../util/cast');
const compilerExecute = require('../compiler/executor');
/**
* Single BlockUtility instance reused by execute for every pritimive ran.
@@ -397,7 +398,189 @@ const _prepareBlockProfiling = function (profiler, blockCached) {
* @param {!Thread} thread Thread which to read and execute.
*/
const execute = function (sequencer, thread) {
thread.generator.next();
if (thread.isCompiled) {
compilerExecute(sequencer, thread);
return;
}
const runtime = sequencer.runtime;
// store sequencer and thread so block functions can access them through
// convenience methods.
blockUtility.sequencer = sequencer;
blockUtility.thread = thread;
// Current block to execute is the one on the top of the stack.
const currentBlockId = thread.peekStack();
const currentStackFrame = thread.peekStackFrame();
let blockContainer = thread.blockContainer;
let blockCached = BlocksExecuteCache.getCached(blockContainer, currentBlockId, BlockCached);
if (blockCached === null) {
blockContainer = runtime.flyoutBlocks;
blockCached = BlocksExecuteCache.getCached(blockContainer, currentBlockId, BlockCached);
// Stop if block or target no longer exists.
if (blockCached === null) {
// No block found: stop the thread; script no longer exists.
sequencer.retireThread(thread);
return;
}
}
const ops = blockCached._ops;
const length = ops.length;
let i = 0;
if (currentStackFrame.reported !== null) {
const reported = currentStackFrame.reported;
// Reinstate all the previous values.
for (; i < reported.length; i++) {
const {opCached: oldOpCached, inputValue} = reported[i];
const opCached = ops.find(op => op.id === oldOpCached);
if (opCached) {
const inputName = opCached._parentKey;
const argValues = opCached._parentValues;
if (inputName === 'BROADCAST_INPUT') {
// Something is plugged into the broadcast input.
// Cast it to a string. We don't need an id here.
argValues.BROADCAST_OPTION.id = null;
argValues.BROADCAST_OPTION.name = cast.toString(inputValue);
} else {
argValues[inputName] = inputValue;
}
}
}
// Find the last reported block that is still in the set of operations.
// This way if the last operation was removed, we'll find the next
// candidate. If an earlier block that was performed was removed then
// we'll find the index where the last operation is now.
if (reported.length > 0) {
const lastExisting = reported.reverse().find(report => ops.find(op => op.id === report.opCached));
if (lastExisting) {
i = ops.findIndex(opCached => opCached.id === lastExisting.opCached) + 1;
} else {
i = 0;
}
}
// 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) {
const opCached = ops[i];
const inputValue = thread.justReported;
thread.justReported = null;
const inputName = opCached._parentKey;
const argValues = opCached._parentValues;
if (inputName === 'BROADCAST_INPUT') {
// Something is plugged into the broadcast input.
// Cast it to a string. We don't need an id here.
argValues.BROADCAST_OPTION.id = null;
argValues.BROADCAST_OPTION.name = cast.toString(inputValue);
} else {
argValues[inputName] = inputValue;
}
i += 1;
}
currentStackFrame.reporting = null;
currentStackFrame.reported = null;
}
const start = i;
for (; i < length; i++) {
const lastOperation = i === length - 1;
const opCached = ops[i];
const blockFunction = opCached._blockFunction;
// Update values for arguments (inputs).
const argValues = opCached._argValues;
// Fields are set during opCached initialization.
// Blocks should glow when a script is starting,
// not after it has finished (see #1404).
// Only blocks in blockContainers that don't forceNoGlow
// should request a glow.
if (!blockContainer.forceNoGlow) {
thread.requestScriptGlowInFrame = true;
}
// Inputs are set during previous steps in the loop.
const primitiveReportedValue = blockFunction(argValues, blockUtility);
// If it's a promise, wait until promise resolves.
if (isPromise(primitiveReportedValue)) {
handlePromise(primitiveReportedValue, sequencer, thread, opCached, lastOperation);
// Store the already reported values. They will be thawed into the
// future versions of the same operations by block id. The reporting
// operation if it is promise waiting will set its parent value at
// that time.
thread.justReported = null;
currentStackFrame.reporting = ops[i].id;
currentStackFrame.reported = ops.slice(0, i).map(reportedCached => {
const inputName = reportedCached._parentKey;
const reportedValues = reportedCached._parentValues;
if (inputName === 'BROADCAST_INPUT') {
return {
opCached: reportedCached.id,
inputValue: reportedValues[inputName].BROADCAST_OPTION.name
};
}
return {
opCached: reportedCached.id,
inputValue: reportedValues[inputName]
};
});
// We are waiting for a promise. Stop running this set of operations
// and continue them later after thawing the reported values.
break;
} else if (thread.status === Thread.STATUS_RUNNING) {
if (lastOperation) {
handleReport(primitiveReportedValue, sequencer, thread, opCached, lastOperation);
} else {
// By definition a block that is not last in the list has a
// parent.
const inputName = opCached._parentKey;
const parentValues = opCached._parentValues;
if (inputName === 'BROADCAST_INPUT') {
// Something is plugged into the broadcast input.
// Cast it to a string. We don't need an id here.
parentValues.BROADCAST_OPTION.id = null;
parentValues.BROADCAST_OPTION.name = cast.toString(primitiveReportedValue);
} else {
parentValues[inputName] = primitiveReportedValue;
}
}
}
}
if (runtime.profiler !== null) {
if (blockCached._profiler !== runtime.profiler) {
_prepareBlockProfiling(runtime.profiler, blockCached);
}
// Determine the index that is after the last executed block. `i` is
// currently the block that was just executed. `i + 1` will be the block
// after that. `length` with the min call makes sure we don't try to
// reference an operation outside of the set of operations.
const end = Math.min(i + 1, length);
for (let p = start; p < end; p++) {
ops[p]._profilerFrame.count += 1;
}
}
};
module.exports = execute;
+9 -1
View File
@@ -1591,7 +1591,15 @@ class Runtime extends EventEmitter {
thread.pushStack(id);
this.threads.push(thread);
thread.compile();
// Attempt to compile the script
// If compilation fails, fallback to the standard scratch-vm interpreter
try {
thread.compile();
thread.isCompiled = true;
} catch (e) {
log.error('Cannot compile thread', e);
thread.isCompiled = false;
}
return thread;
}
+100 -9
View File
@@ -1,6 +1,7 @@
const Timer = require('../util/timer');
const Thread = require('./thread');
const execute = require('./execute.js');
const compilerExecute = require('../compiler/executor');
/**
* Profiler frame name for stepping a single thread.
@@ -177,17 +178,107 @@ class Sequencer {
* @param {!Thread} thread Thread object to step.
*/
stepThread (thread) {
if (this.runtime.profiler !== null) {
if (executeProfilerId === -1) {
executeProfilerId = this.runtime.profiler.idByName(executeProfilerFrame);
}
// Increment the number of times execute is called.
this.runtime.profiler.increment(executeProfilerId);
if (thread.isCompiled) {
compilerExecute(this, thread);
return;
}
execute(this, thread);
// TODO: all of the control flow for turbo, warp, ...
let currentBlockId = thread.peekStack();
if (!currentBlockId) {
// A "null block" - empty branch.
thread.popStack();
// Did the null follow a hat block?
if (thread.stack.length === 0) {
thread.status = Thread.STATUS_DONE;
return;
}
}
// Save the current block ID to notice if we did control flow.
while ((currentBlockId = thread.peekStack())) {
let isWarpMode = thread.peekStackFrame().warpMode;
if (isWarpMode && !thread.warpTimer) {
// Initialize warp-mode timer if it hasn't been already.
// This will start counting the thread toward `Sequencer.WARP_TIME`.
thread.warpTimer = new Timer();
thread.warpTimer.start();
}
// Execute the current block.
if (this.runtime.profiler !== null) {
if (executeProfilerId === -1) {
executeProfilerId = this.runtime.profiler.idByName(executeProfilerFrame);
}
// Increment the number of times execute is called.
this.runtime.profiler.increment(executeProfilerId);
}
if (thread.target === null) {
this.retireThread(thread);
} else {
execute(this, thread);
}
thread.blockGlowInFrame = currentBlockId;
// If the thread has yielded or is waiting, yield to other threads.
if (thread.status === Thread.STATUS_YIELD) {
// Mark as running for next iteration.
thread.status = Thread.STATUS_RUNNING;
// In warp mode, yielded blocks are re-executed immediately.
if (isWarpMode &&
thread.warpTimer.timeElapsed() <= Sequencer.WARP_TIME) {
continue;
}
return;
} else if (thread.status === Thread.STATUS_PROMISE_WAIT) {
// A promise was returned by the primitive. Yield the thread
// until the promise resolves. Promise resolution should reset
// thread.status to Thread.STATUS_RUNNING.
return;
} else if (thread.status === Thread.STATUS_YIELD_TICK) {
// stepThreads will reset the thread to Thread.STATUS_RUNNING
return;
}
// If no control flow has happened, switch to next block.
if (thread.peekStack() === currentBlockId) {
thread.goToNextBlock();
}
// If no next block has been found at this point, look on the stack.
while (!thread.peekStack()) {
thread.popStack();
if (thread.stack.length === 0) {
// No more stack to run!
thread.status = Thread.STATUS_DONE;
return;
}
const stackFrame = thread.peekStackFrame();
isWarpMode = stackFrame.warpMode;
if (stackFrame.isLoop) {
// The current level of the stack is marked as a loop.
// Return to yield for the frame/tick in general.
// Unless we're in warp mode - then only return if the
// warp timer is up.
if (!isWarpMode ||
thread.warpTimer.timeElapsed() > Sequencer.WARP_TIME) {
// Don't do anything to the stack, since loops need
// to be re-executed.
return;
}
// Don't go to the next block for this level of the stack,
// since loops need to be re-executed.
continue;
} else if (stackFrame.waitingReporter) {
// This level of the stack was waiting for a value.
// This means a reporter has just returned - so don't go
// to the next block for this level of the stack.
return;
}
// Get next block of existing block on the stack.
thread.goToNextBlock();
}
}
}
/**
+2
View File
@@ -189,6 +189,8 @@ class Thread {
this.warpTimer = null;
this.justReported = null;
this.isCompiled = false;
}
/**