Native code generation (experimental)
The harn-codegen crate is an experimental, opt-in ahead-of-time / JIT
compiler that lowers a subset of Harn to native machine code via
Cranelift. It is a side project, not a shipped
feature.
Not part of the distributed binary.
harn-codegenispublish = falseand is not a dependency ofharn-cliorharn-vm. The crates.ioharnbinary never links Cranelift. The crate builds and tests undercargo … --workspace(so CI keeps it honest), but you opt in explicitly with-p harn-codegen.
Why (and why not)
Harn is an orchestration language: real programs are dominated by LLM calls, tool dispatch, and other I/O, where native code generation buys nothing — the bottleneck is the network, not interpreter dispatch. So this is not a general "make harnesses faster" feature, and it is deliberately kept out of the default build.
The honest, narrow niche it targets is the opposite end of the spectrum: small, hot, side-effect-free numeric kernels — scoring functions, parsers, bit-twiddling, geometry, fixed-point math — that run in tight loops. For those, compiling the bytecode to machine code removes interpreter overhead entirely. If you have such a kernel and have measured it as a real cost, this is a tool worth reaching for.
The scalar subset
The compiler handles Harn's three unboxed scalar types — int (i64),
float (f64), and bool — and the operations over them:
- arithmetic
+ - * /and integer%(trap-checked divide-by-zero;i64::MIN / -1wraps like the VM rather than trapping); - comparisons
== != < > <= >=and logical!,&&,||; if/else,whileloops, ternaries, short-circuit operators;let/varlocals and reassignment.
Anything else — strings, lists, dicts, nil as a value, closures,
harness.*/host calls, await, ** (its result type is value-dependent),
float % — is reported as CodegenError::Unsupported. Callers treat that as
"this function stays on the interpreter"; it is the expected outcome for most
functions, not an error.
Parameters must carry a scalar type annotation:
fn score(hits: int, misses: int) -> int {
var total = 0
var i = 0
while i < hits {
total = total + 10
i = i + 1
}
return total - misses
}
Pipeline
Harn source ──(harn-vm front-end)──▶ Chunk / bytecode
│
├─ decode reachable scalar instructions
├─ verify typed control-flow graph (ScalarFunction)
├─ lower Cranelift IR
└─ backend
├─ jit in-process machine code ▶ NativeFunction
└─ aot relocatable object file ▶ ObjectArtifact
The front end is the real Harn compiler (harn_vm::compile_source) — the
native compiler never re-implements lexing, parsing, or bytecode emission, so
it always tracks the language the installed VM actually speaks. It then:
- decodes only reachable bytecode (control-flow walk), so the unreachable
Nil; Returnepilogue the compiler appends after an explicitreturnnever disqualifies an otherwise-scalar function; - verifies with a monotone type-inference fixpoint that assigns one static
type to every operand-stack position and local slot, rejecting anything not
provably monomorphic (an
int-on-one-path/float-on-another slot, a mismatched control-flow merge); - lowers to Cranelift IR and hands off to a backend.
A pure-Rust reference interpreter (harn_codegen::evaluate) mirrors the VM's
semantics exactly (wrapping integer arithmetic, IEEE-754 floats with NaN
ordering). It is the differential-test oracle and a dependency-free fallback.
Calling convention
Every compiled function is emitted with one uniform C signature, regardless of its Harn arity or types:
extern "C" fn(args: *const u64, ret: *mut u64, trap: *mut u8)
Arguments and the result are raw 64-bit slots (int keeps its bits, float
uses IEEE-754 bits, bool is 0/1). This keeps the Rust ↔ native boundary a
single monomorphic function pointer and gives the AOT object one stable,
easily-linked symbol (harn_scalar_<name>). Integer divide-by-zero sets
*trap = 1 and returns, surfacing as a NativeTrap instead of a hardware trap
that would abort the host.
Using it
Library:
use harn_codegen::{compile_named, ScalarValue};
let f = compile_named("fn add(a: int, b: int) -> int { return a + b }", "add")?;
let sum = f.call(&[ScalarValue::Int(2), ScalarValue::Int(3)])?; // Int(5)
CLI (harn-nativec):
# Report whether a function is scalar-eligible and JIT it
harn-nativec kernel.harn score
# Emit a relocatable object you can link with any system linker
harn-nativec kernel.harn score --object score.o
# JIT and run with arguments (cross-checked against the reference interpreter)
harn-nativec kernel.harn score --run 12 3
Tests
All tests are in-process and deterministic — no wall clock, no threads, no
external toolchain. tests/native.rs is a differential suite that compiles
real Harn source and asserts the JIT agrees with the reference interpreter
across input grids (including overflow, divide-by-zero traps, i64::MIN / -1,
and NaN comparisons); tests/aot.rs checks object emission without invoking a
linker.