← Back

Deep Dive: How Scratch Blocks are Compiled to JavaScript

1. Scratch Block Format (project.json)

Scratch blocks are stored as a linked list of JSON objects in project.json. Each block has:

Example: "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" }
    }
  }
}

2. Interpreter vs Compiler Approach

Scratch (Interpreter)

When running each block, Scratch's interpreter must:

TurboWarp (Compiler)

TurboWarp compiles blocks directly to JavaScript functions at load time, eliminating all that overhead.

3. Compilation Examples

Example 1: Simple Loop

Scratch blocks:

when green flag clicked forever move (my variable) steps

Compiled JavaScript:

javascript
const 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:

Example 2: Bubble Sort Algorithm

Scratch blocks:

define sort set [length] to (length of [list]) repeat until <(length) = [0]> set [newLength] to [0] set [i] to [1] repeat ((length) - (1)) change [i] by (1) if <(item ((i) - (1)) of [list]) > (item (i) of [list])> then set [temp] to (item (i) of [list]) replace item (i) of [list] with (item ((i) - (1)) of [list]) replace item ((i) - (1)) of [list] with (temp) set [newLength] to (i) end end set [length] to (newLength) end

Compiled JavaScript:

javascript
const 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;
  }
};

4. Runtime Helper Functions

The compiled code uses runtime helpers to handle Scratch's quirky semantics:

compareEqual — Scratch's unusual equality rules

javascript
const 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 indices

javascript
const 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];
};

5. Compilation Pipeline

┌─────────────────────────────────────────────────────────────────────┐ │ TurboWarp Compiler Pipeline │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ 1. PARSE │ │ project.json → Block AST (linked list of block objects) │ │ │ │ 2. IR GENERATION (irgen.js) │ │ Block AST → Intermediate Representation │ │ - Resolves block references │ │ - Identifies loops, conditions, procedures │ │ - Determines which scripts need generators (yield points) │ │ │ │ 3. JS GENERATION (jsgen.js) │ │ IR → JavaScript source string │ │ - Generates variable declarations │ │ - Converts each block opcode to JS equivalent │ │ - Inlines runtime helpers where possible │ │ │ │ 4. FUNCTION CREATION │ │ JavaScript string → Executable function │ │ - Uses `new Function()` to create the function │ │ - Binds to runtime context (stage, target, runtime) │ │ │ │ 5. EXECUTION │ │ Hat blocks trigger compiled functions │ │ - Green flag → run all "when flag clicked" functions │ │ - Broadcasts → run matching "when I receive" functions │ │ - Generator functions yield to allow interleaving │ │ │ └─────────────────────────────────────────────────────────────────────┘

6. Block Opcode to JavaScript Mapping

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)

7. Why the Generated Code Looks Strange

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] ?? ""

8. Performance Comparison

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.