Extension Structure
A compiled extension follows a specific structure that integrates with MistWarp's compiler architecture. Understanding this structure is essential for creating effective compiled extensions.
Basic Extension Template
Every compiled extension starts with this fundamental structure:
Extension Declaration
The extension begins by checking for the unsandboxed environment, which is required for accessing compiler internals:
(function(Scratch) {
'use strict';
if (!Scratch.extensions.unsandboxed) {
throw new Error("Extension needs to be run unsandboxed.");
}
Accessing Compiler Internals
Next, the extension obtains references to MistWarp's compiler components:
const { vm, BlockType, ArgumentType } = Scratch;
const { runtime } = vm;
// Access the compiler internals
const iwnafhwtb = vm.exports.i_will_not_ask_for_help_when_these_break();
const { JSGenerator, IRGenerator, ScriptTreeGenerator } = iwnafhwtb;
The i_will_not_ask_for_help_when_these_break() function provides access to internal APIs that may change between versions. The name serves as a warning that these APIs are not stable.
Compiler Type System
Compiled extensions work with MistWarp's type system for optimization:
const {
TYPE_NUMBER,
TYPE_STRING,
TYPE_BOOLEAN,
TYPE_UNKNOWN,
TYPE_NUMBER_NAN,
TypedInput,
ConstantInput,
VariableInput,
Frame,
sanitize
} = JSGenerator.unstable_exports;
Patching System
Compiled extensions use a patching system to modify compiler behavior. The patching function ensures multiple extensions can coexist:
Patch Function
const PATCHES_ID = 'extensionname_patches';
const cst_patch = (obj, functions) => {
if (obj[PATCHES_ID]) return; // Prevent double-patching
obj[PATCHES_ID] = {};
for (const name in functions) {
const original = obj[name];
obj[PATCHES_ID][name] = obj[name];
if (original) {
obj[name] = function(...args) {
const callOriginal = (...args) => original.call(this, ...args);
return functions[name].call(this, callOriginal, ...args);
};
} else {
obj[name] = function(...args) {
return functions[name].call(this, () => {}, ...args);
};
}
}
};
This system allows extensions to override compiler methods while preserving the ability to call the original implementation.
Compiler Phase Patches
JavaScript Generation Patch
The JSGenerator patch handles converting blocks into final JavaScript code:
cst_patch(JSGenerator.prototype, {
descendStackedBlock(fn, node, ...args) {
let b = node.block;
switch (node.kind) {
case 'myextension.myblock':
const input1 = this.descendInput(node.INPUT1).asNumber();
const input2 = this.descendInput(node.INPUT2).asString();
this.source += `vm.runtime.visualReport("${b.id}", Math.pow(${input1}, 2))\n`;
return;
default:
return fn(node, ...args);
}
},
descendInput(fn, node, ...args) {
switch (node.kind) {
case 'myextension.myblock':
const input1 = this.descendInput(node.INPUT1).asNumber();
return new TypedInput(`Math.pow(${input1}, 2)`, TYPE_NUMBER);
default:
return fn(node, ...args);
}
}
});
Script Tree Generation Patch
The ScriptTreeGenerator patch identifies extension blocks and prepares them for compilation:
cst_patch(ScriptTreeGenerator.prototype, {
descendStackedBlock(fn, block, ...args) {
switch (block.opcode) {
case 'myextension_myblock':
return {
block,
kind: 'myextension.myblock',
INPUT1: this.descendInputOfBlock(block, 'INPUT1'),
INPUT2: this.descendInputOfBlock(block, 'INPUT2'),
};
default:
return fn(block, ...args);
}
},
descendInput(fn, block, ...args) {
// Similar structure for reporter blocks
switch (block.opcode) {
case 'myextension_myblock':
return {
block,
kind: 'myextension.myblock',
INPUT1: this.descendInputOfBlock(block, 'INPUT1'),
INPUT2: this.descendInputOfBlock(block, 'INPUT2'),
};
default:
return fn(block, ...args);
}
}
});
Extension Class Definition
After setting up the patches, define the extension class:
Basic Class Structure
class MyExtension {
getInfo() {
return {
id: 'myextension',
name: 'My Extension',
color1: '#2DA4A0',
version: 1.0,
blocks: [
{
opcode: 'myblock',
text: 'calculate [INPUT1] squared',
blockType: Scratch.BlockType.REPORTER,
arguments: {
INPUT1: {
type: Scratch.ArgumentType.NUMBER,
defaultValue: 5
}
},
func: 'fallbackFunction'
}
]
};
}
fallbackFunction(args) {
// Fallback implementation for non-compiled environments
return Math.pow(args.INPUT1, 2);
}
}
Registration
Scratch.extensions.register(new MyExtension());
Input Type Handling
Compiled extensions must properly handle different input types:
Type Conversion Methods
asNumber(): Converts input to number with Scratch semanticsasString(): Converts input to stringasBoolean(): Converts input to booleanasRaw(): Uses input without type conversionasSafe(): Safely handles input with error checking
Type-Specific Outputs
When generating code, specify the output type:
// For number outputs
return new TypedInput(`Math.pow(${input}, 2)`, TYPE_NUMBER);
// For string outputs
return new TypedInput(`"Result: " + ${input}`, TYPE_STRING);
// For boolean outputs
return new TypedInput(`${input} > 0`, TYPE_BOOLEAN);
Error Handling
Compiled extensions should include proper error handling:
Compilation Errors
try {
const input = this.descendInput(node.INPUT).asNumber();
return new TypedInput(`Math.sqrt(${input})`, TYPE_NUMBER);
} catch (error) {
// Fall back to safe default
return new TypedInput(`0`, TYPE_NUMBER);
}
Runtime Validation
// Generate code with runtime validation
this.source += `vm.runtime.visualReport("${b.id}",
${input} >= 0 ? Math.sqrt(${input}) : 0)\n`;
Best Practices
Naming Conventions
- Use consistent naming for block opcodes (
extension_blockname) - Use descriptive kind names (
extension.blockname) - Include extension name in patch IDs to avoid conflicts
Performance Considerations
- Generate minimal, efficient JavaScript code
- Avoid unnecessary type conversions
- Use appropriate input methods (
asRaw()when type doesn't matter) - Cache expensive computations when possible
Compatibility
- Always provide fallback functions for non-compiled environments
- Test with different input types and edge cases
- Handle undefined or invalid inputs gracefully
This structure provides the foundation for creating powerful compiled extensions that integrate seamlessly with MistWarp's compilation system while maintaining compatibility and performance.