Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Compilation Model

Hone compiles TypeScript to native binaries using the Perry AOT compiler – a TypeScript-to-native ahead-of-time compiler written in Rust. There is no V8, no JIT, and no JavaScript runtime at execution time.

How Perry Compiles TypeScript

Perry performs whole-program compilation:

  1. Parse – TypeScript source is parsed and type-checked
  2. Codegen – Rust codegen emits LLVM IR from the typed AST
  3. Link – LLVM produces a native binary linked against libperry_stdlib.a and platform UI libraries
# Compile the IDE for macOS
cd hone-ide && perry compile src/app.ts --output hone-ide

# Compile for iOS simulator
cd hone-ide && perry compile src/app.ts --target ios-simulator --output Hone

# Compile for web (WASM)
cd hone-ide && perry compile src/app.ts --target web --output hone-ide.html

NaN-Boxing

All values at runtime are 64-bit doubles. Pointers, strings, objects, and other non-numeric types are encoded inside the NaN payload bits of IEEE 754 doubles. This means:

  • Every variable is a single f64 at the machine level
  • Type checks are bitwise operations on the NaN payload
  • No heap tagging or boxed value types
  • String parameters across FFI are NaN-boxed StringHeader pointers

No Dynamic Dispatch

All types are resolved at compile time. Perry does not support:

  • Dynamic property access (obj[variable] – use if/else if chains)
  • Optional chaining (?. – use explicit null checks)
  • Nullish coalescing (?? – use explicit if (x !== undefined))
  • Regular expressions (/regex/.test() – use indexOf or character checks)

Closure Semantics

Perry captures closure variables by value, not by reference. This is the most important semantic difference from standard JavaScript/TypeScript:

// WRONG -- Perry captures count by value at closure creation time
let count = 0;
function increment() {
  count++; // Always increments a stale copy
}

// CORRECT -- Use module-level variables and module-level functions
let count = 0;
function increment() {
  count++; // Module-level function reads the live module-level variable
}

Store mutable state in module-level let variables and access them through module-level named functions.

package.json Perry Section

Each package declares its Perry configuration in package.json:

{
  "perry": {
    "nativeLibrary": {
      "functions": [
        "createWindow",
        "drawText",
        "handleInput"
      ]
    },
    "targets": ["macos", "ios", "windows", "linux", "web"]
  }
}

FFI functions listed here are resolved at link time. Perry generates __wrapper_<function_name> symbols (double underscore prefix) for each declared function.

FFI Conventions

  • String parameters are NaN-boxed StringHeader pointers. Rust receives *const u8 and uses str_from_header() to decode.
  • Use f64 for numeric FFI parameters.
  • Use i64 for string/pointer FFI parameters.
  • Do not use i32 – it causes verifier errors.

Perry-Specific Patterns to Avoid

PatternUse Instead
obj[variable] dynamic key accessif/else if per key
?. optional chainingExplicit null checks
?? nullish coalescingif (x !== undefined)
/regex/.test()indexOf or char checks
{ key } ES6 shorthand{ key: key }
array.map(fn) on class fieldsfor loop
for...of on arraysfor (let i = 0; i < arr.length; i++)
c >= 'a' && c <= 'z' char rangesALPHA_STR.indexOf(c) >= 0
Closures capturing this methodsModule-level functions + module-level vars
requestAnimationFramesetInterval
setTimeout self-recursionsetInterval
String-returning functions in asyncInline string operations
new Date() in asyncDate.now()

Building Perry Itself

When modifying Perry’s Rust source (located at ../perry/):

# Build the compiler
cd ../perry && CARGO_PROFILE_RELEASE_LTO=off cargo build --release -p perry

# Build the macOS UI library
cd ../perry && CARGO_PROFILE_RELEASE_LTO=off cargo build --release -p perry-ui-macos

# Rebuild stdlib after changing perry-runtime source
cd ../perry && cargo clean -p perry-runtime --release && \
  CARGO_PROFILE_RELEASE_LTO=off cargo build --release -p perry-stdlib -p perry-ui-macos -p perry

Always use CARGO_PROFILE_RELEASE_LTO=off – thin LTO produces bitcode that the macOS clang linker cannot read.