Architecture
Rayzor is a native code generator, not a source-to-source backend: every backend emits machine code, WASM bytecode, or C used as an assembler. What it adds over the official compiler is a tiered runtime and a single optimization pipeline shared by every backend — code starts executing immediately, and only what proves hot pays compilation cost.
Language transpilation is an explicit non-goal. There is no JavaScript target and none planned.
The pipeline
HIR still knows what the user wrote, so diagnostics and ownership analysis read naturally there. MIR is flat and typed, so one optimization pipeline serves every backend — that is the whole reason for the split. Type checking is not a separate phase; it is folded into AST lowering.
MIR
SSA over basic blocks with real phi nodes, type metadata carried down rather than erased. Every collection is a BTreeMap — explicitly so iteration order is deterministic and codegen is reproducible.
MIR prints in an LLVM-like textual form — registers $N, blocks bbN — sorted by id so dumps diff cleanly. RAYZOR_DUMP_MIR=1 fires before the passes; use rayzor dump --diff for what the backend actually sees.
Optimization levels
The default is O2. InsertFree is added before the level match at every level — it is a correctness pass, not an optimization.
O0 is not "no optimization." It still forces inlining, because Haxe inline is a language guarantee rather than a hint — and because inlining small constructors is what exposes the Alloc+GEP shape SRA needs. Without it, per-iteration constructor allocations are never scalarised and loops leak.
Ordering is load-bearing: inlining exposes Alloc+GEP for SRA, GlobalLoadCaching dedups metadata loads for BCE, BCE emits the invariant load LICM hoists, and LICM's clean loop bodies are what LoopUnrolling and LoopVectorization need.
Tiered execution
The interpreter is tier 0, the next three rungs are Cranelift, and Maximum is LLVM. Promotion compares tier ordinals, so a counter clearing several thresholds at once skips rungs.
The promotion barrier is the safety-critical part. Function pointers cannot be swapped while JIT code is running, so a safepoint gates the swap: the promoter requests promotion, waits for the in-flight execution counter to drain to zero, swaps under a write lock, and returns to idle. Installs are monotonic — a pointer for a function already at a higher tier is dropped.
SIMD
Vectors are language-level types, not intrinsics you reach for through externs. The rayzor.SIMD* family are @:coreType abstracts whose representation follows from their identity — each lowers straight to a MIR vector type, in 128- and 256-bit widths, across float and integer lanes.
The 256-bit types are not portable. SIMD8i32 and SIMD32i8 are LLVM-only — wasm's v128 and Cranelift have no 256-bit vector type and refuse them rather than narrowing. Gate behind #if llvm with a 128-bit fallback. The TCC linker path on Linux also lacks SIMD in its final tier.
The rayzor package
Beyond the Haxe standard library, Rayzor ships a systems layer: memory primitives that map to real machine types, a native vector monomorphized per element type, and a concurrency surface the compiler validates through Send and Sync.
Pointer-sized abstracts that carry machine addresses and are never truncated.
Vec<T> is monomorphized per element type — VecI32, VecF64, packed VecBool, VecPtr — so primitives are contiguous and unboxed.
Message passing and shared state, validated at compile time through Send and Sync.
Windowing, compile-time feature detection, and a built-in spec runner.
Parallelism adapts to the machine. On multi-NUMA-node systems the pool pins one worker per node so allocations land first-touch on that node's controller; on UMA hardware and wasm it runs inline on the calling thread unless you force fanout.
Backends
AOT defaults to LLVM; without the llvm-backend feature rayzor aot errors rather than silently degrading.
Memory & object layout
Cleanup is decided at compile time. The HIR-level drop-point analyzer computes last use per variable, tracking loop position, reassignment, block depth, and two escape sets — general escapes and lambda captures, since a captured variable is owned by the closure.
There is no vtable pointer in the header — dispatch resolves the vtable from the class id in slot 0, and interface values are fat pointers wrapped at the new site. Allocation size takes the maximum over the whole extends chain at the allocation site, because an imported parent's fields may not have been visible when the subclass was registered.
On-disk formats
One MIR module plus metadata and cached maps.
Pre-resolved stdlib symbols, for fast startup.
All modules, module table, entry point, build info.
Cache invalidation uses three independent keys: a source content hash, the compiler semver, and a content-derived compiler cache ABI id — the last exists because parser or MIR-shape changes do not bump the semver. Cached maps are keyed by name, never by symbol or type id, because ids are reassigned per compilation.
Pass internals, the runtime ABI, closure conventions and the format specs.