Fix compiler's hat implementation

Now supports edge-activated and predicate-based hats
This commit is contained in:
Muffin committed 2023-08-13 16:53:25 -05:00
1 parent 843b4c3839
commit ff41d1dcf8
9 files changed
+189 -28

No files matched your search

+2 -1
View File
@@ -29,7 +29,8 @@ const compile = thread => {
return {
startingFunction: entry,
procedures
procedures,
executableHat: ir.entry.executableHat
};
};
+6
View File
@@ -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
View File
@@ -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;
}
}
+10
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}; })
@@ -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;
}; })