Patching the Compiler
The core mechanism of compiled extensions is patching RemixWarp's compiler to inject custom code generation logic. This process involves modifying three key compiler phases to handle extension blocks.
Understanding the Compilation Pipeline
RemixWarp's compiler transforms Scratch blocks through several phases:
- Block Parsing: Raw block data is processed
- Script Tree Generation: Blocks are organized into a tree structure
- IR Generation: The tree is converted to intermediate representation
- JavaScript Generation: Final JavaScript code is produced
Compiled extensions hook into phases 2 and 4, skipping the IR phase for direct optimization.
Patch Implementation Strategy
Safe Patching Pattern
The patching system ensures multiple extensions can coexist without conflicts:
const PATCHES_ID = 'myextension_patches';
const cst_patch = (obj, functions) => {
// Prevent double-patching
if (obj[PATCHES_ID]) return;
obj[PATCHES_ID] = {};
for (const name in functions) {
const original = obj[name];
obj[PATCHES_ID][name] = obj[name];
if (original) {
// Wrap existing function
obj[name] = function(...args) {
const callOriginal = (...args) => original.call(this, ...args);
return functions[name].call(this, callOriginal, ...args);
};
} else {
// Create new function
obj[name] = function(...args) {
return functions[name].call(this, () => {}, ...args);
};
}
}
};
This pattern preserves the original functionality while adding extension-specific behavior.
Script Tree Generation Patching
The Script Tree Generator identifies blocks and converts them into a structured format for compilation.
Handling Different Block Types
cst_patch(ScriptTreeGenerator.prototype, {
descendStackedBlock(fn, block, ...args) {
switch (block.opcode) {
// Command blocks (hat/stack blocks)
case 'myextension_command':
return {
block,
kind: 'myextension.command',
INPUT1: this.descendInputOfBlock(block, 'INPUT1'),
INPUT2: this.descendInputOfBlock(block, 'INPUT2'),
};
// Boolean blocks
case 'myextension_comparison':
return {
block,
kind: 'myextension.comparison',
LEFT: this.descendInputOfBlock(block, 'LEFT'),
RIGHT: this.descendInputOfBlock(block, 'RIGHT'),
};
default:
return fn(block, ...args);
}
},
descendInput(fn, block, ...args) {
switch (block.opcode) {
// Reporter blocks (return values)
case 'myextension_reporter':
return {
block,
kind: 'myextension.reporter',
VALUE: this.descendInputOfBlock(block, 'VALUE'),
};
// Boolean reporters
case 'myextension_predicate':
return {
block,
kind: 'myextension.predicate',
TEST: this.descendInputOfBlock(block, 'TEST'),
};
default:
return fn(block, ...args);
}
}
});
Input Processing
The descendInputOfBlock method processes block inputs and handles different connection types:
- Direct values: Numbers, strings, booleans entered directly
- Block connections: Outputs from other blocks
- Variable references: References to variables or lists
- Dropdown selections: Menu choices