Skip to main content

Extension Structure

A compiled extension follows a specific structure that integrates with RemixWarp'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 RemixWarp'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 RemixWarp'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 semantics
  • asString(): Converts input to string
  • asBoolean(): Converts input to boolean
  • asRaw(): Uses input without type conversion
  • asSafe(): 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 RemixWarp's compilation system while maintaining compatibility and performance.