Fix compiler's hat implementation
Now supports edge-activated and predicate-based hats
This commit is contained in:
1 parent
843b4c3839
commit
ff41d1dcf8
9 files changed
+189
-28
No files matched your search
@@ -29,7 +29,8 @@ const compile = thread => {
|
||||
|
||||
return {
|
||||
startingFunction: entry,
|
||||
procedures
|
||||
procedures,
|
||||
executableHat: ir.entry.executableHat
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -69,6 +69,12 @@ class IntermediateScript {
|
||||
* @type {Function|null}
|
||||
*/
|
||||
this.cachedCompileResult = null;
|
||||
|
||||
/**
|
||||
* Whether the top block of this script is an executable hat.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.executableHat = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+69
-15
@@ -1453,6 +1453,58 @@ class ScriptTreeGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Block} hatBlock
|
||||
*/
|
||||
walkHat (hatBlock) {
|
||||
const nextBlock = hatBlock.next;
|
||||
const opcode = hatBlock.opcode;
|
||||
const hatInfo = this.runtime._hats[opcode];
|
||||
|
||||
if (this.thread.stackClick) {
|
||||
// We still need to treat the hat as a normal block (so executableHat should be false) for
|
||||
// interpreter parity, but the reuslt is ignored.
|
||||
const opcodeFunction = this.runtime.getOpcodeFunction(opcode);
|
||||
if (opcodeFunction) {
|
||||
return [
|
||||
this.descendCompatLayer(hatBlock),
|
||||
...this.walkStack(nextBlock)
|
||||
];
|
||||
}
|
||||
return this.walkStack(nextBlock);
|
||||
}
|
||||
|
||||
if (hatInfo.edgeActivated) {
|
||||
// Edge-activated HAT
|
||||
this.script.yields = true;
|
||||
this.script.executableHat = true;
|
||||
return [
|
||||
{
|
||||
kind: 'hat.edge',
|
||||
id: hatBlock.id,
|
||||
condition: this.descendCompatLayer(hatBlock)
|
||||
},
|
||||
...this.walkStack(nextBlock)
|
||||
];
|
||||
}
|
||||
|
||||
const opcodeFunction = this.runtime.getOpcodeFunction(opcode);
|
||||
if (opcodeFunction) {
|
||||
// Predicate-based HAT
|
||||
this.script.yields = true;
|
||||
this.script.executableHat = true;
|
||||
return [
|
||||
{
|
||||
kind: 'hat.predicate',
|
||||
condition: this.descendCompatLayer(hatBlock)
|
||||
},
|
||||
...this.walkStack(nextBlock)
|
||||
];
|
||||
}
|
||||
|
||||
return this.walkStack(nextBlock);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} topBlockId The ID of the top block of the script.
|
||||
* @returns {IntermediateScript}
|
||||
@@ -1475,24 +1527,26 @@ class ScriptTreeGenerator {
|
||||
this.readTopBlockComment(topBlock.comment);
|
||||
}
|
||||
|
||||
// If the top block is a hat, advance to its child.
|
||||
let entryBlock;
|
||||
if (this.runtime.getIsHat(topBlock.opcode) || topBlock.opcode === 'procedures_definition') {
|
||||
if (this.runtime.getIsEdgeActivatedHat(topBlock.opcode)) {
|
||||
throw new Error(`Not compiling an edge-activated hat: ${topBlock.opcode}`);
|
||||
}
|
||||
entryBlock = topBlock.next;
|
||||
// We do need to evaluate empty hats
|
||||
const hatInfo = this.runtime._hats[topBlock.opcode];
|
||||
const isHat = !!hatInfo;
|
||||
if (isHat) {
|
||||
this.script.stack = this.walkHat(topBlock);
|
||||
} else {
|
||||
entryBlock = topBlockId;
|
||||
// We don't evaluate the procedures_definition top block as it never does anything
|
||||
// We also don't want it to be treated like a hat block
|
||||
let entryBlock;
|
||||
if (topBlock.opcode === 'procedures_definition') {
|
||||
entryBlock = topBlock.next;
|
||||
} else {
|
||||
entryBlock = topBlockId;
|
||||
}
|
||||
|
||||
if (entryBlock) {
|
||||
this.script.stack = this.walkStack(entryBlock);
|
||||
}
|
||||
}
|
||||
|
||||
if (!entryBlock) {
|
||||
// This is an empty script.
|
||||
return this.script;
|
||||
}
|
||||
|
||||
this.script.stack = this.walkStack(entryBlock);
|
||||
|
||||
return this.script;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -578,6 +578,14 @@ const execute = thread => {
|
||||
thread.generator.next();
|
||||
};
|
||||
|
||||
const threadStack = [];
|
||||
const saveGlobalState = () => {
|
||||
threadStack.push(globalState.thread);
|
||||
};
|
||||
const restoreGlobalState = () => {
|
||||
globalState.thread = threadStack.pop();
|
||||
};
|
||||
|
||||
const insertRuntime = source => {
|
||||
let result = baseRuntime;
|
||||
for (const functionName of Object.keys(runtimeFunctions)) {
|
||||
@@ -606,5 +614,7 @@ const scopedEval = source => {
|
||||
|
||||
execute.scopedEval = scopedEval;
|
||||
execute.runtimeFunctions = runtimeFunctions;
|
||||
execute.saveGlobalState = saveGlobalState;
|
||||
execute.restoreGlobalState = restoreGlobalState;
|
||||
|
||||
module.exports = execute;
|
||||
+29
-1
@@ -385,6 +385,7 @@ class JSGenerator {
|
||||
this._setupVariables = {};
|
||||
|
||||
this.descendedIntoModulo = false;
|
||||
this.isInHat = false;
|
||||
|
||||
this.debug = this.target.runtime.debug;
|
||||
}
|
||||
@@ -650,7 +651,8 @@ class JSGenerator {
|
||||
const joinedArgs = args.join(',');
|
||||
|
||||
const yieldForRecursion = !this.isWarp && procedureCode === this.script.procedureCode;
|
||||
if (yieldForRecursion) {
|
||||
const yieldForHat = this.isInHat;
|
||||
if (yieldForRecursion || yieldForHat) {
|
||||
const runtimeFunction = procedureData.yields ? 'yieldThenCallGenerator' : 'yieldThenCall';
|
||||
return new TypedInput(`(yield* ${runtimeFunction}(${procedureReference}, ${joinedArgs}))`, TYPE_UNKNOWN);
|
||||
}
|
||||
@@ -848,6 +850,32 @@ class JSGenerator {
|
||||
this.source += `}\n`;
|
||||
break;
|
||||
|
||||
case 'hat.edge':
|
||||
this.isInHat = true;
|
||||
this.source += '{\n';
|
||||
// For exact Scratch parity, evaluate the input before checking old edge state.
|
||||
// Can matter if the input is not instantly evaluated.
|
||||
this.source += `const resolvedValue = ${this.descendInput(node.condition).asBoolean()};\n`;
|
||||
this.source += `const id = "${sanitize(node.id)}";\n`;
|
||||
this.source += 'const hasOldEdgeValue = target.hasEdgeActivatedValue(id);\n';
|
||||
this.source += `const oldEdgeValue = target.updateEdgeActivatedValue(id, resolvedValue);\n`;
|
||||
this.source += `const edgeWasActivated = hasOldEdgeValue ? (!oldEdgeValue && resolvedValue) : resolvedValue;\n`;
|
||||
this.source += `if (!edgeWasActivated) {\n`;
|
||||
this.retire();
|
||||
this.source += '}\n';
|
||||
this.source += 'yield;\n';
|
||||
this.source += '}\n';
|
||||
this.isInHat = false;
|
||||
break;
|
||||
case 'hat.predicate':
|
||||
this.isInHat = true;
|
||||
this.source += `if (!${this.descendInput(node.condition).asBoolean()}) {\n`;
|
||||
this.retire();
|
||||
this.source += '}\n';
|
||||
this.source += 'yield;\n';
|
||||
this.isInHat = false;
|
||||
break;
|
||||
|
||||
case 'event.broadcast':
|
||||
this.source += `startHats("event_whenbroadcastreceived", { BROADCAST_OPTION: ${this.descendInput(node.broadcast).asString()} });\n`;
|
||||
this.resetVariableInputs();
|
||||
|
||||
+10
-2
@@ -9,6 +9,7 @@ const BlockType = require('../extension-support/block-type');
|
||||
const Profiler = require('./profiler');
|
||||
const Sequencer = require('./sequencer');
|
||||
const execute = require('./execute.js');
|
||||
const compilerExecute = require('../compiler/jsexecute');
|
||||
const ScratchBlocksConstants = require('./scratch-blocks-constants');
|
||||
const TargetType = require('../extension-support/target-type');
|
||||
const Thread = require('./thread');
|
||||
@@ -2116,8 +2117,15 @@ class Runtime extends EventEmitter {
|
||||
// For compatibility with Scratch 2, edge triggered hats need to be processed before
|
||||
// threads are stepped. See ScratchRuntime.as for original implementation
|
||||
newThreads.forEach(thread => {
|
||||
// tw: do not step compiled threads, the hat block can't be executed
|
||||
if (!thread.isCompiled) {
|
||||
if (thread.isCompiled) {
|
||||
if (thread.executableHat) {
|
||||
// It is quite likely that we are currently executing a block, so make sure
|
||||
// that we leave the compiler's state intact at the end.
|
||||
compilerExecute.saveGlobalState();
|
||||
compilerExecute(thread);
|
||||
compilerExecute.restoreGlobalState();
|
||||
}
|
||||
} else {
|
||||
execute(this.sequencer, thread);
|
||||
thread.goToNextBlock();
|
||||
}
|
||||
|
||||
+15
-3
@@ -214,6 +214,7 @@ class Thread {
|
||||
* @type {Object.<string, import('../compiler/compile').CompiledScript>}
|
||||
*/
|
||||
this.procedures = null;
|
||||
this.executableHat = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -462,10 +463,15 @@ class Thread {
|
||||
|
||||
this.triedToCompile = true;
|
||||
|
||||
// stackClick === true disables hat block generation
|
||||
// It would be great to cache these separately, but for now it's easiest to just disable them to avoid
|
||||
// cached versions of scripts breaking projects.
|
||||
const canCache = !this.stackClick;
|
||||
|
||||
const topBlock = this.topBlock;
|
||||
// Flyout blocks are stored in a special block container.
|
||||
const blocks = this.blockContainer.getBlock(topBlock) ? this.blockContainer : this.target.runtime.flyoutBlocks;
|
||||
const cachedResult = blocks.getCachedCompileResult(topBlock);
|
||||
const cachedResult = canCache && blocks.getCachedCompileResult(topBlock);
|
||||
// If there is a cached error, do not attempt to recompile.
|
||||
if (cachedResult && !cachedResult.success) {
|
||||
return;
|
||||
@@ -477,10 +483,14 @@ class Thread {
|
||||
} else {
|
||||
try {
|
||||
result = compile(this);
|
||||
blocks.cacheCompileResult(topBlock, result);
|
||||
if (canCache) {
|
||||
blocks.cacheCompileResult(topBlock, result);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('cannot compile script', this.target.getName(), error);
|
||||
blocks.cacheCompileError(topBlock, error);
|
||||
if (canCache) {
|
||||
blocks.cacheCompileError(topBlock, error);
|
||||
}
|
||||
this.target.runtime.emitCompileError(this.target, error);
|
||||
return;
|
||||
}
|
||||
@@ -493,6 +503,8 @@ class Thread {
|
||||
|
||||
this.generator = result.startingFunction(this)();
|
||||
|
||||
this.executableHat = result.executableHat;
|
||||
|
||||
if (!this.blockContainer.forceNoGlow) {
|
||||
this.blockGlowInFrame = this.topBlock;
|
||||
this.requestScriptGlowInFrame = true;
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
// TW Snapshot
|
||||
// Input SHA-256: 19e0b3f9dcac763041039a8ffe87b6732eb744f9ebb136e8d61a53ccf19118e3
|
||||
|
||||
// Errors:
|
||||
// Sprite1: Error: Not compiling an edge-activated hat: event_whengreaterthan
|
||||
|
||||
// Sprite1 script
|
||||
(function factoryXYZ(thread) { const target = thread.target; const runtime = target.runtime; const stage = runtime.getTargetForStage();
|
||||
const b0 = stage.variables["PsY$$vp$IVH;dDAr[q2h"];
|
||||
@@ -28,3 +25,27 @@ b0.value = 1;
|
||||
yield* executeInCompatibilityLayer({"MESSAGE":"end",}, b1, false, false, null);
|
||||
retire(); return;
|
||||
}; })
|
||||
|
||||
// Sprite1 script
|
||||
(function factoryXYZ(thread) { const target = thread.target; const runtime = target.runtime; const stage = runtime.getTargetForStage();
|
||||
const b0 = stage.variables["l^q!%fq]Bv;72dlGf}^Z"];
|
||||
const b1 = runtime.getOpcodeFunction("event_whengreaterthan");
|
||||
const b2 = stage.variables["PsY$$vp$IVH;dDAr[q2h"];
|
||||
const b3 = runtime.getOpcodeFunction("looks_say");
|
||||
return function* genXYZ () {
|
||||
{
|
||||
const resolvedValue = toBoolean((yield* executeInCompatibilityLayer({"VALUE":b0.value,"WHENGREATERTHANMENU":"TIMER",}, b1, false, false, null)));
|
||||
const id = "iNmua~6veGey6O-_UB9.";
|
||||
const hasOldEdgeValue = target.hasEdgeActivatedValue(id);
|
||||
const oldEdgeValue = target.updateEdgeActivatedValue(id, resolvedValue);
|
||||
const edgeWasActivated = hasOldEdgeValue ? (!oldEdgeValue && resolvedValue) : resolvedValue;
|
||||
if (!edgeWasActivated) {
|
||||
retire(); return;
|
||||
}
|
||||
yield;
|
||||
}
|
||||
if (compareEqual(b2.value, 0)) {
|
||||
yield* executeInCompatibilityLayer({"MESSAGE":"fail",}, b3, false, false, null);
|
||||
}
|
||||
retire(); return;
|
||||
}; })
|
||||
+24
-3
@@ -1,9 +1,6 @@
|
||||
// TW Snapshot
|
||||
// Input SHA-256: 19e0b3f9dcac763041039a8ffe87b6732eb744f9ebb136e8d61a53ccf19118e3
|
||||
|
||||
// Errors:
|
||||
// Sprite1: Error: Not compiling an edge-activated hat: event_whengreaterthan
|
||||
|
||||
// Sprite1 script
|
||||
(function factoryXYZ(thread) { const target = thread.target; const runtime = target.runtime; const stage = runtime.getTargetForStage();
|
||||
const b0 = stage.variables["PsY$$vp$IVH;dDAr[q2h"];
|
||||
@@ -28,3 +25,27 @@ b0.value = 1;
|
||||
yield* executeInCompatibilityLayer({"MESSAGE":"end",}, b1, false, false, null);
|
||||
retire(); return;
|
||||
}; })
|
||||
|
||||
// Sprite1 script
|
||||
(function factoryXYZ(thread) { const target = thread.target; const runtime = target.runtime; const stage = runtime.getTargetForStage();
|
||||
const b0 = stage.variables["l^q!%fq]Bv;72dlGf}^Z"];
|
||||
const b1 = runtime.getOpcodeFunction("event_whengreaterthan");
|
||||
const b2 = stage.variables["PsY$$vp$IVH;dDAr[q2h"];
|
||||
const b3 = runtime.getOpcodeFunction("looks_say");
|
||||
return function* genXYZ () {
|
||||
{
|
||||
const resolvedValue = toBoolean((yield* executeInCompatibilityLayer({"VALUE":b0.value,"WHENGREATERTHANMENU":"TIMER",}, b1, false, false, null)));
|
||||
const id = "iNmua~6veGey6O-_UB9.";
|
||||
const hasOldEdgeValue = target.hasEdgeActivatedValue(id);
|
||||
const oldEdgeValue = target.updateEdgeActivatedValue(id, resolvedValue);
|
||||
const edgeWasActivated = hasOldEdgeValue ? (!oldEdgeValue && resolvedValue) : resolvedValue;
|
||||
if (!edgeWasActivated) {
|
||||
retire(); return;
|
||||
}
|
||||
yield;
|
||||
}
|
||||
if (compareEqual(b2.value, 0)) {
|
||||
yield* executeInCompatibilityLayer({"MESSAGE":"fail",}, b3, false, false, null);
|
||||
}
|
||||
retire(); return;
|
||||
}; })
|
||||
Reference in new issue
Block a user