Creating Your First Extension
This tutorial walks through creating a simple compiled extension from scratch, demonstrating the key concepts and patterns you'll use in more complex extensions.
Project Setup
Create a new JavaScript file for your extension. We'll build a "Math Utils" extension that provides optimized mathematical operations.
Extension Header
Start with the basic extension structure:
/**!
* Math Utils Extension
* @author Your Name
* @version 1.0
* @copyright MIT License
*/
(function(Scratch) {
'use strict';
// Verify unsandboxed environment
if (!Scratch.extensions.unsandboxed) {
throw new Error("Math Utils extension needs to be run unsandboxed.");
}
Accessing Compiler APIs
Get references to the necessary MistWarp components:
const { vm, BlockType, ArgumentType } = Scratch;
const { runtime } = vm;
// Access compiler internals
const compilerAPI = vm.exports.i_will_not_ask_for_help_when_these_break();
const { JSGenerator, IRGenerator, ScriptTreeGenerator } = compilerAPI;
// Import type system
const {
TYPE_NUMBER,
TYPE_STRING,
TYPE_BOOLEAN,
TypedInput,
ConstantInput
} = JSGenerator.unstable_exports;
Setting Up the Patch System
Implement the patching mechanism:
const PATCHES_ID = 'mathutils_patches';
const applyPatch = (obj, functions) => {
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) {
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);
};
}
}
};
Creating Your First Block
Let's create a simple "square" block that calculates the square of a number.
Script Tree Generation
First, handle the block identification phase:
applyPatch(ScriptTreeGenerator.prototype, {
descendStackedBlock(fn, block, ...args) {
switch (block.opcode) {
case 'mathutils_square':
return {
block,
kind: 'mathutils.square',
NUMBER: this.descendInputOfBlock(block, 'NUMBER'),
};
default:
return fn(block, ...args);
}
},
descendInput(fn, block, ...args) {
switch (block.opcode) {
case 'mathutils_square':
return {
block,
kind: 'mathutils.square',
NUMBER: this.descendInputOfBlock(block, 'NUMBER'),
};
default:
return fn(block, ...args);
}
}
});
JavaScript Generation
Next, implement the code generation:
applyPatch(JSGenerator.prototype, {
descendStackedBlock(fn, node, ...args) {
let b = node.block;
switch (node.kind) {
case 'mathutils.square':
const number = this.descendInput(node.NUMBER).asNumber();
this.source += `vm.runtime.visualReport("${b.id}", (${number} * ${number}));\n`;
return;
default:
return fn(node, ...args);
}
},
descendInput(fn, node, ...args) {
switch (node.kind) {
case 'mathutils.square':
const number = this.descendInput(node.NUMBER).asNumber();
return new TypedInput(`(${number} * ${number})`, TYPE_NUMBER);
default:
return fn(node, ...args);
}
}
});
Extension Class Definition
Define the extension class with block definitions:
class MathUtilsExtension {
getInfo() {
return {
id: 'mathutils',
name: 'Math Utils',
color1: '#4C97FF',
color2: '#4280D7',
version: 1.0,
blocks: [
{
opcode: 'square',
text: 'square of [NUMBER]',
blockType: BlockType.REPORTER,
arguments: {
NUMBER: {
type: ArgumentType.NUMBER,
defaultValue: 5
}
},
func: 'squareFallback'
}
]
};
}
// Fallback function for non-compiled environments
squareFallback(args) {
const number = Number(args.NUMBER) || 0;
return number * number;
}
}
Testing Your Extension
Basic Testing
Add some debugging to verify your extension is working:
console.log('Math Utils Extension loaded!');
// Optional: Add debug info to blocks
applyPatch(JSGenerator.prototype, {
descendInput(fn, node, ...args) {
switch (node.kind) {
case 'mathutils.square':
const number = this.descendInput(node.NUMBER).asNumber();
console.log('Compiling square block with input:', number);
return new TypedInput(`(${number} * ${number})`, TYPE_NUMBER);
default:
return fn(node, ...args);
}
}
});
Testing Procedure
- Load your extension in MistWarp
- Create a simple project using your "square" block
- Try different input types:
- Direct numbers:
square of (5) - Variables:
square of (my variable) - Complex expressions:
square of ((3 + 2))
- Direct numbers:
- Check the compiled output for correctness
Adding More Blocks
Let's add a more complex block with multiple inputs:
Power Block
// In ScriptTreeGenerator patch:
case 'mathutils_power':
return {
block,
kind: 'mathutils.power',
BASE: this.descendInputOfBlock(block, 'BASE'),
EXPONENT: this.descendInputOfBlock(block, 'EXPONENT'),
};
// In JSGenerator patch:
case 'mathutils.power':
const base = this.descendInput(node.BASE).asNumber();
const exponent = this.descendInput(node.EXPONENT).asNumber();
return new TypedInput(`Math.pow(${base}, ${exponent})`, TYPE_NUMBER);
// In block definition:
{
opcode: 'power',
text: '[BASE] to the power of [EXPONENT]',
blockType: BlockType.REPORTER,
arguments: {
BASE: {
type: ArgumentType.NUMBER,
defaultValue: 2
},
EXPONENT: {
type: ArgumentType.NUMBER,
defaultValue: 3
}
},
func: 'powerFallback'
}
Optimization Techniques
Constant Folding
Optimize when inputs are constants:
case 'mathutils.power':
const base = this.descendInput(node.BASE);
const exponent = this.descendInput(node.EXPONENT);
// Check if both inputs are constants
if (base instanceof ConstantInput && exponent instanceof ConstantInput) {
const result = Math.pow(base.constantValue, exponent.constantValue);
return new TypedInput(`${result}`, TYPE_NUMBER);
}
// Handle special cases
if (exponent instanceof ConstantInput) {
switch (exponent.constantValue) {
case 2:
const baseCode = base.asNumber();
return new TypedInput(`(${baseCode} * ${baseCode})`, TYPE_NUMBER);
case 0:
return new TypedInput(`1`, TYPE_NUMBER);
case 1:
return new TypedInput(`${base.asNumber()}`, TYPE_NUMBER);
}
}
// General case
return new TypedInput(`Math.pow(${base.asNumber()}, ${exponent.asNumber()})`, TYPE_NUMBER);
Complete Extension Example
Here's the complete, functional extension:
/**!
* Math Utils Extension
* @author Your Name
* @version 1.0
*/
(function(Scratch) {
'use strict';
if (!Scratch.extensions.unsandboxed) {
throw new Error("Math Utils extension needs to be run unsandboxed.");
}
const { vm, BlockType, ArgumentType } = Scratch;
const compilerAPI = vm.exports.i_will_not_ask_for_help_when_these_break();
const { JSGenerator, ScriptTreeGenerator } = compilerAPI;
const { TYPE_NUMBER, TypedInput, ConstantInput } = JSGenerator.unstable_exports;
const PATCHES_ID = 'mathutils_patches';
const applyPatch = (obj, functions) => {
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) {
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);
};
}
}
};
// Script Tree Generation
applyPatch(ScriptTreeGenerator.prototype, {
descendStackedBlock(fn, block, ...args) {
switch (block.opcode) {
case 'mathutils_square':
return {
block,
kind: 'mathutils.square',
NUMBER: this.descendInputOfBlock(block, 'NUMBER'),
};
default:
return fn(block, ...args);
}
},
descendInput(fn, block, ...args) {
switch (block.opcode) {
case 'mathutils_square':
return {
block,
kind: 'mathutils.square',
NUMBER: this.descendInputOfBlock(block, 'NUMBER'),
};
default:
return fn(block, ...args);
}
}
});
// JavaScript Generation
applyPatch(JSGenerator.prototype, {
descendStackedBlock(fn, node, ...args) {
let b = node.block;
switch (node.kind) {
case 'mathutils.square':
const number = this.descendInput(node.NUMBER).asNumber();
this.source += `vm.runtime.visualReport("${b.id}", (${number} * ${number}));\n`;
return;
default:
return fn(node, ...args);
}
},
descendInput(fn, node, ...args) {
switch (node.kind) {
case 'mathutils.square':
const number = this.descendInput(node.NUMBER);
// Optimize constants
if (number instanceof ConstantInput) {
const result = number.constantValue * number.constantValue;
return new TypedInput(`${result}`, TYPE_NUMBER);
}
// General case
const numberCode = number.asNumber();
return new TypedInput(`(${numberCode} * ${numberCode})`, TYPE_NUMBER);
default:
return fn(node, ...args);
}
}
});
class MathUtilsExtension {
getInfo() {
return {
id: 'mathutils',
name: 'Math Utils',
color1: '#4C97FF',
blocks: [
{
opcode: 'square',
text: 'square of [NUMBER]',
blockType: BlockType.REPORTER,
arguments: {
NUMBER: {
type: ArgumentType.NUMBER,
defaultValue: 5
}
},
func: 'squareFallback'
}
]
};
}
squareFallback(args) {
const number = Number(args.NUMBER) || 0;
return number * number;
}
constructor() {
console.log('Math Utils Extension loaded!');
}
}
Scratch.extensions.register(new MathUtilsExtension());
})(Scratch);
Next Steps
Now that you have a working compiled extension:
- Add more mathematical operations (cube root, factorial, etc.)
- Implement string manipulation functions
- Create boolean logic operations
- Add conditional optimizations for common patterns
- Implement error handling for edge cases
Each addition will deepen your understanding of the compilation system and help you create more sophisticated extensions.