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:
- Parse – TypeScript source is parsed and type-checked
- Codegen – Rust codegen emits LLVM IR from the typed AST
- Link – LLVM produces a native binary linked against
libperry_stdlib.aand 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
f64at 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
StringHeaderpointers
No Dynamic Dispatch
All types are resolved at compile time. Perry does not support:
- Dynamic property access (
obj[variable]– useif/else ifchains) - Optional chaining (
?.– use explicit null checks) - Nullish coalescing (
??– use explicitif (x !== undefined)) - Regular expressions (
/regex/.test()– useindexOfor 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
StringHeaderpointers. Rust receives*const u8and usesstr_from_header()to decode. - Use
f64for numeric FFI parameters. - Use
i64for string/pointer FFI parameters. - Do not use
i32– it causes verifier errors.
Perry-Specific Patterns to Avoid
| Pattern | Use Instead |
|---|---|
obj[variable] dynamic key access | if/else if per key |
?. optional chaining | Explicit null checks |
?? nullish coalescing | if (x !== undefined) |
/regex/.test() | indexOf or char checks |
{ key } ES6 shorthand | { key: key } |
array.map(fn) on class fields | for loop |
for...of on arrays | for (let i = 0; i < arr.length; i++) |
c >= 'a' && c <= 'z' char ranges | ALPHA_STR.indexOf(c) >= 0 |
Closures capturing this methods | Module-level functions + module-level vars |
requestAnimationFrame | setInterval |
setTimeout self-recursion | setInterval |
| String-returning functions in async | Inline string operations |
new Date() in async | Date.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.