Scratch blocks are stored as a linked list of JSON objects in project.json. Each block has:
opcode — The block type (e.g., motion_movesteps, control_forever)inputs — References to other blocks (arguments)fields — Static values (dropdown selections)next — Reference to the next block in the stackparent — Reference to the parent blockExample: "Forever" loop with "move 10 steps":
json{
"va[U{Cbi_NZpSOSx_kVA": {
"opcode": "event_whenflagclicked",
"inputs": {},
"fields": {},
"next": "tzXnZ{8G!xK|t^WAWF{m",
"topLevel": true
},
"tzXnZ{8G!xK|t^WAWF{m": {
"opcode": "control_forever",
"inputs": {
"SUBSTACK": { "block": "$xf$bq|xl(}RhT-K,taS" }
},
"next": null
},
"$xf$bq|xl(}RhT-K,taS": {
"opcode": "motion_movesteps",
"inputs": {
"STEPS": { "block": "cw__.I:g}Y~`:5KmO00q" }
},
"next": null
},
"cw__.I:g}Y~`:5KmO00q": {
"opcode": "data_variable",
"fields": {
"VARIABLE": { "id": "`jEk@4|i[#Fk?(8x)AV.-my variable" }
}
}
}
When running each block, Scratch's interpreter must:
TurboWarp compiles blocks directly to JavaScript functions at load time, eliminating all that overhead.
Scratch blocks:
Compiled JavaScript:
javascriptconst myVariable = stage.variables["`jEk@4|i[#Fk?(8x)AV.-my variable"];
return function* () {
while (true) {
runtime.ext_scratch3_motion._moveSteps((+myVariable.value || 0), target);
yield; // Allow other scripts to run
}
};
Key observations:
myVariable is resolved once at compile timefunction* (generator) — Used for cooperative multitasking; yield pauses execution(+myVariable.value || 0) — Ensures numeric conversion (Scratch quirk)_moveSteps instead of opcode lookupScratch blocks:
Compiled JavaScript:
javascriptconst length = stage.variables["O;aH~(njYNn}Bl@}!%pS-length-"];
const list = stage.variables["O;aH~(njYNn}Bl@}!%pS-list-list"];
const newLength = stage.variables["O;aH~(njYNn}Bl@}!%pS-new-"];
const i = stage.variables["O;aH~(njYNn}Bl@}!%pS-i-"];
const temp = stage.variables["O;aH~(njYNn}Bl@}!%pS-tmp-"];
return function fun1_sort() {
length.value = list.value.length;
// repeat until length = 0
while (!compareEqual(length.value, 0)) {
newLength.value = 0;
i.value = 1;
// repeat length - 1 times
for (var counter = ((+length.value || 0) - 1) || 0; counter >= 0.5; counter--) {
// change i by 1
i.value = ((+i.value || 0) + 1);
// if item i - 1 of list is greater than item i of list
if (compareGreaterThan(
list.value[((((i.value || 0) - 1) || 0) | 0) - 1] ?? "",
list.value[((i.value || 0) | 0) - 1] ?? ""
)) {
// swap item i and i - 1 of list
temp.value = listGet(list.value, i.value);
listReplace(list, i.value, list.value[((((+i.value || 0) - 1) || 0) | 0) - 1] ?? "");
listReplace(list, (+i.value || 0) - 1, temp.value);
newLength.value = i.value;
}
}
length.value = newLength.value;
}
};
The compiled code uses runtime helpers to handle Scratch's quirky semantics:
compareEqual — Scratch's unusual equality rulesconst compareEqual = (v1, v2) => {
// Fast path: both numbers
if (typeof v1 === 'number' && typeof v2 === 'number' && !isNaN(v1) && !isNaN(v2)) {
return v1 === v2;
}
// Slow path: case-insensitive string comparison or mixed types
return compareEqualSlow(v1, v2);
};
const compareEqualSlow = (v1, v2) => {
const n1 = +v1;
if (isNaN(n1) || (n1 === 0 && isNotActuallyZero(v1))) {
return ('' + v1).toLowerCase() === ('' + v2).toLowerCase();
}
const n2 = +v2;
if (isNaN(n2) || (n2 === 0 && isNotActuallyZero(v2))) {
return ('' + v1).toLowerCase() === ('' + v2).toLowerCase();
}
return n1 === n2;
};
listGet — Scratch's 1-indexed lists and special indicesconst listIndex = (index, length) => {
if (typeof index !== 'number') {
return listIndexSlow(index, length);
}
index = index | 0;
return index < 1 || index > length ? -1 : index - 1;
};
const listIndexSlow = (index, length) => {
if (index === 'last') {
return length - 1;
} else if (index === 'random' || index === 'any') {
return length > 0 ? (Math.random() * length) | 0 : -1;
}
index = (+index || 0) | 0;
return index < 1 || index > length ? -1 : index - 1;
};
const listGet = (list, idx) => {
const index = listIndex(idx, list.length);
return index === -1 ? '' : list[index];
};
| Scratch Block | Opcode | Compiled JavaScript |
|---|---|---|
| move (10) steps | motion_movesteps |
runtime.ext_scratch3_motion._moveSteps(10, target) |
| set [var] to (5) | data_setvariableto |
var.value = 5 |
| change [var] by (1) | data_changevariableby |
var.value = (+var.value || 0) + 1 |
| if <> then | control_if |
if (condition) { ... } |
| repeat (10) | control_repeat |
for (var i = 10; i >= 0.5; i--) { ... } |
| forever | control_forever |
while (true) { ... yield; } |
| wait (1) secs | control_wait |
yield* waitSeconds(1) |
| say [Hello] | looks_say |
runtime.ext_scratch3_looks._say("Hello", target) |
| (x position) | motion_xposition |
target.x |
| ((a) + (b)) | operator_add |
(+a || 0) + (+b || 0) |
| <(a) = (b)> | operator_equals |
compareEqual(a, b) |
The generated JavaScript prioritizes correctness over readability:
javascript// Why ((+value || 0) + 1) instead of (value + 1)?
// Because in Scratch, "hello" + 1 = 1, not "hello1" or NaN
i.value = ((+i.value || 0) + 1);
// Why (index | 0) - 1 instead of index - 1?
// Because Scratch list indices must be integers
list.value[((i.value || 0) | 0) - 1]
// Why ?? "" instead of just accessing?
// Because accessing out-of-bounds in Scratch returns empty string, not undefined
list.value[index] ?? ""
| Operation | Scratch Interpreter | TurboWarp Compiler |
|---|---|---|
| Variable read | Hash lookup + object property | Direct property access |
| Block execution | Opcode lookup + function call | Inlined JS |
| Loop iteration | Stack manipulation + yield check | Native for/while |
| Type coercion | Runtime type checking | Compile-time optimization |
| 10–100× speedup | ||
Key Insight
TurboWarp doesn't convert Scratch to "readable" JavaScript — it generates optimized machine-specific code that matches Scratch's exact semantics while removing interpreter overhead. The code is generated at runtime via new Function(), making it a true JIT compiler.