Skip to content

Commit

Permalink
Add proper hat support to the compiler
Browse files Browse the repository at this point in the history
Edge activated hats and predicate hats now work
  • Loading branch information
GarboMuffin committed Aug 9, 2023
1 parent c549569 commit 780e7f1
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 24 deletions.
81 changes: 66 additions & 15 deletions src/compiler/irgen.js
Original file line number Diff line number Diff line change
Expand Up @@ -1453,6 +1453,55 @@ class ScriptTreeGenerator {
}
}

/**
* @param {Block} hatBlock
*/
walkHat (hatBlock) {
const nextBlock = hatBlock.next;
if (this.thread.stackClick) {
// TODO: Scratch parity - we need to execute this
return this.walkStack(nextBlock);
}

// startHats automatically runs the first block in each thread it starts, which makes
// this quite special.

this.script.yields = true;

const opcode = hatBlock.opcode;
const hatInfo = this.runtime._hats[opcode];
let hatNode;
if (hatInfo.edgeActivated) {
// Edge activated HAT
hatNode = {
// TODO: run all of our extra edge hat tests on this
kind: 'hat.edge',
id: hatBlock.id,
condition: this.descendCompatLayer(hatBlock)
};
} else {
const opcodeFunction = this.runtime.getOpcodeFunction(opcode);
if (opcodeFunction) {
// Non-edge-activated HAT
hatNode = {
// TODO: this needs tests!
kind: 'hat.predicate',
condition: this.descendCompatLayer(hatBlock)
};
} else {
// Probably an EVENT block.
hatNode = {
kind: 'hat.noop'
};
}
}

return [
hatNode,
...this.walkStack(nextBlock)
];
}

/**
* @param {string} topBlockId The ID of the top block of the script.
* @returns {IntermediateScript}
Expand All @@ -1475,24 +1524,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;
}

if (!entryBlock) {
// This is an empty script.
return this.script;
// 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);
}
}

this.script.stack = this.walkStack(entryBlock);

return this.script;
}
}
Expand Down
10 changes: 10 additions & 0 deletions src/compiler/jsexecute.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -606,5 +614,7 @@ const scopedEval = source => {

execute.scopedEval = scopedEval;
execute.runtimeFunctions = runtimeFunctions;
execute.saveGlobalState = saveGlobalState;
execute.restoreGlobalState = restoreGlobalState;

module.exports = execute;
24 changes: 24 additions & 0 deletions src/compiler/jsgen.js
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,30 @@ class JSGenerator {
this.source += `}\n`;
break;

case 'hat.edge':
this.source += '{\n';
// For exact Scratch parity, evaluate the input before checking old edge state.
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';
break;
case 'hat.noop':
this.source += 'yield; /* hat noop */\n';
break;
case 'hat.predicate':
this.source += `if (!${this.descendInput(node.condition).asBoolean()}) {\n`;
this.retire();
this.source += '}\n';
this.source += 'yield;\n';
break;

case 'event.broadcast':
this.source += `startHats("event_whenbroadcastreceived", { BROADCAST_OPTION: ${this.descendInput(node.broadcast).asString()} });\n`;
this.resetVariableInputs();
Expand Down
10 changes: 8 additions & 2 deletions src/engine/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -2116,8 +2117,13 @@ 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) {
// 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();
}
Expand Down
15 changes: 12 additions & 3 deletions src/engine/thread.js
Original file line number Diff line number Diff line change
Expand Up @@ -462,10 +462,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;
Expand All @@ -477,10 +482,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;
}
Expand Down
5 changes: 1 addition & 4 deletions test/integration/execute.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,7 @@ fs.readdirSync(executeDir)
// TW: Script compilation errors should fail.
if (enableCompiler) {
vm.on('COMPILE_ERROR', (target, error) => {
// Edge-activated hats are a known error.
if (!`${error}`.includes('edge-activated hat')) {
throw new Error(`Could not compile script in ${target.getName()}: ${error}`);
}
throw new Error(`Could not compile script in ${target.getName()}: ${error}`);
});
}

Expand Down

0 comments on commit 780e7f1

Please sign in to comment.