# shen $ ./zincdec globals.csexp reverse --asm

Architecture

The compile pipeline

// Shen → kmacros → normalize → debruijn → zinc-c → csexp → C VM

Shen source is compiled through a fully in-repo pipeline. shen->kl (our own full-arity Shen→KLambda compiler) feeds kmacros, normalize-term (KLambda expansion, A-normal form, debruijn indices), zinc-c (KLambda → ZINC bytecode), and compile-zinc (→ canonical s-expressions), landing in nat->csexp and finally the C VM.

Shen source → kmacros → normalize-term → debruijn → zinc-c → compile-zinc → nat->csexp → C VM

Two global namespaces

// the #1 source of 'why can't the C VM see my closure?' bugs

There are two separate global tables. The C VM native global_table[] (populated by parse_bundle from the bundle and by init_globals) is what raw C bytecode [global X] reaches. The metacircular interp's Shen global-table (an assoc list in interp.shen) is how the interp resolves [global G] via lookup-global.

A runtime-loaded closure (shen.foo) lives in the interp's Shen global-table (namespace 2) — NOT its own C global_table[] entry. To call it, drive it through the metacircular interp: eval-kl, toplevel-interp, or a bundled closure that resolves names via lookup-global.

Design intent

// static sites skip safe wrappers; dynamic sites need them

Call sites split into two kinds. Static call sites — code produced by the compiler, type-safe by construction — need no runtime type check and no safe wrapper; zinc-c special-cases primitive? heads to emit [prim F], a direct primitive dispatch that bypasses the global table. Dynamic call sites — boundaries, higher-order use, untyped input — route through the Shen safe wrappers (safe.X) so a type error becomes a catchable simple-error.

The C primitives have no runtime type guards. Type validation is owned entirely by the Shen safe-wrapper layer. This is safe only for a type-safe bundle — the canonical globals.csexp is the reduced self-contained interpreter, which never passes bad types.

A custom moving generational GC

// nursery + old-gen · precise roots · write barrier

A 2 MB nursery (pages marked space==3) is the allocation fast lane; the full-copy collect() is the (rare) old-gen collector and compacts old gen. Typed headers drive a tag-dispatch scavenger; roots are precise-only via the shadow stack + typed walkers — no conservative C-stack scan. A write barrier at address-> vector writes keeps old-gen→nursery references correct.

The same collector runs under WebAssembly in the playground — the wasm build swaps the 4 GB mmap reservation for an aligned_alloc heap via a #ifdef __wasm__ shim, and a 5000-eval GC stress probe stays clean.