vextoly/cryn

Fix native x86-64 backend: REX.W, SSE prefix ordering, setcc, lock_xchg

Commit b065a7b · niklas · 2026-10-22T19:26:15+02:00

Changeset
b065a7b767411e162800da396e5d37d83ea75ad2
Parents
efe20b63e551e6be58a79a72e9a5c426da3db6ee

View source at this commit

Fix native x86-64 backend: REX.W, SSE prefix ordering, setcc, lock_xchg

- Add REX.W to movsx_al/movsx_ax and wrap_wv for signed 8/16-bit ext
- Fix conv_u: pxor instead of mov_ri64, JP instead of JNP, movzx for unsigned
- Fix float->bool and float I_CMP: JP instead of JNP for NaN detection
- Fix setcc_r: correct SETNE opcode (0x05 not 0x85)
- Fix lock_xchg: use 0x86 for 8-bit exchanges
- Fix SSE prefix ordering: emit prefix before REX
- Inline SSE fixes in out_f_body
- Add tests 31-35 and expected outputs
- Update README for make test-native

Comments

No comments yet.

Log in to comment

Diff

diff --git a/Makefile b/Makefile
index 6999493..67c3039 100644
--- a/Makefile
+++ b/Makefile
@@ -8,9 +8,9 @@ CC      ?= cc
 CFLAGS  ?= -O2 -std=c11 -Wall -Wextra -Wno-unused-parameter
 BIN     = cryn
 
-SRCS = src/main.c src/util.c src/diag.c src/lex.c src/parse.c src/types.c \
+SRCS = src/main.c src/util.c src/diag.c src/lex.c src/parse.c src/types.c src/be_x64.c \
        src/check.c src/opt.c src/be_c.c
-HDRS = src/util.h src/diag.h src/lex.h src/ast.h src/parse.h src/types.h \
+HDRS = src/util.h src/diag.h src/lex.h src/ast.h src/parse.h src/types.h src/be_x64.h \
        src/ir.h src/check.h src/opt.h src/be_c.h src/rt_src.h
 
 all: $(BIN)
@@ -27,7 +27,13 @@ $(BIN): $(SRCS) $(HDRS)
 test: $(BIN)
 	sh tests/run.sh
 
+test-release: $(BIN)
+	CRYN_ARGS=--release sh tests/run.sh
+
+test-native: $(BIN)
+	CRYN_ARGS=--native sh tests/run.sh
+
 clean:
 	rm -f $(BIN) tools/embed src/rt_src.h
 
-.PHONY: all test clean
+.PHONY: all test test-release test-native clean
diff --git a/README.md b/README.md
index a84872f..a5fd208 100644
--- a/README.md
+++ b/README.md
@@ -17,13 +17,21 @@ define main
 end
 ```
 
-This compiles to a native executable through portable generated C:
+This compiles to a native executable through either of two backends:
 
 ```text
 Cryn source -> lexer -> parser -> AST -> semantic analysis -> Cryn IR
-           -> optimizer -> C backend -> host C compiler -> executable
+           -> optimizer -> C backend    -> host C compiler -> executable
+                       -> native backend -> ELF (x86-64)  -> executable
 ```
 
+The C backend emits a self-contained C11 translation unit and hands it to
+the host C compiler (the portable reference path). The native backend
+lowers the IR directly to x86-64 machine code and writes an ELF executable
+itself — no C compiler, linker, or CRT objects are involved at program
+build time; the host C library is linked dynamically as the system-call
+interface.
+
 The bootstrap compiler (`cryn`) is written in portable ISO C11 and has no
 dependencies beyond a C compiler, pthreads, and libm (`fmod`, used by
 float `remainder`).
@@ -34,7 +42,7 @@ float `remainder`).
 
 > C-level power through a radically small set of composable primitives.
 
-* **37 commands total.** No `if`, `while`, `for`, `switch`, `array`,
+* **38 commands total.** No `if`, `while`, `for`, `switch`, `array`,
   `struct`, `class`, or `pointer` keywords. Control flow is explicit
   jumps; arrays are indexed connections; structures are blocks of memory;
   pointers are address values.
@@ -80,6 +88,9 @@ src/
     ir.h         Cryn IR instruction definitions
     opt.[ch]     IR cleanup: unreachable code and dead label removal
     be_c.[ch]    C backend: emits a self-contained C11 translation unit
+    be_x64.[ch]  native x86-64 backend: lowers the IR to machine code and
+                 writes an ELF executable directly (host libc via a
+                 hand-built GOT; musl and glibc `_start` conventions)
     rt/
         cryn_rt.h    minimal runtime interface (embedded into output)
         cryn_rt.c    runtime implementation
@@ -107,8 +118,10 @@ semantic layer is tied to C generation.
 ## Building
 
 ```sh
-make            # builds ./cryn
-make test       # runs the test suite
+make              # builds ./cryn
+make test         # runs the test suite (C backend)
+make test-release # runs the test suite in release mode (no bounds checks)
+make test-native  # runs the test suite through the native x86-64 backend
 make clean
 ```
 
@@ -128,10 +141,16 @@ cryn help
 cryn version
 ```
 
+Both `build` and `run` accept `--release` (no runtime bounds checks;
+statically provable checks are always eliminated at compile time) and
+`--native` (emit an x86-64 ELF executable directly instead of generated C;
+Linux, System V ABI, dynamic host libc).
+
 `cryn` invokes the host C compiler (`$CC`, default `cc`) with
 `-O2 -std=c11 -pthread` to turn generated C into an executable. Generated
 programs are normal executables and need no Cryn runtime at runtime; the
-runtime is compiled into every generated translation unit.
+runtime is compiled into every generated translation unit. With `--native`
+no host C compiler is needed at all.
 
 ## How the compiler works
 
@@ -160,6 +179,16 @@ runtime is compiled into every generated translation unit.
    stack and focus history live in a per-thread context struct. The
    runtime sources are embedded and emitted into every translation unit,
    making the output a single self-contained `.c` file.
+7. **Native backend** (`be_x64.c`): the same IR lowered straight to
+   x86-64. Each transformation gets a frame of callee-saved machine state
+   (`r12` comparison, `r13` focus, `r14` context, `r15` working value);
+   the runtime helpers (value stack, focus, blocks with headers, streams,
+   threads, float formatting) are emitted as machine code, and the writer
+   produces a static ELF with a hand-built GOT and SysV hash table that
+   links against the host C library for syscalls (stdio, malloc, pthreads,
+   `fmod`, and `snprintf`/`strtod` so float output is byte-identical to
+   the C backend). File handles, contexts, and block headers match the C
+   runtime's semantics.
 
 ## Runtime dependencies (complete list)
 
@@ -170,7 +199,7 @@ The runtime (`src/rt/cryn_rt.*`) provides only:
 | `malloc`/`free` | `make <size> of`, anonymous `make`, `free` | 16-byte header for sizes plus a mutex-protected registry of live blocks; validation of stale addresses is defined, never undefined reads |
 | value stack     | `push`, `pop`, `swap`, `call`    | growable array, per thread     |
 | focus history   | `move` (relative), `back`, `home`| fixed 1024 entries per context (shared across active frames) |
-| FILE* streams   | `input`, `output`, `open`, `use`, `close`, `flush`| stdin/stdout by default; `use` routes the context's streams through a handle |
+| FILE* streams   | `input`, `output`, `open`, `use`, `close`, `flush`, `seek`| stdin/stdout by default; `use` routes the context's streams through a handle; `seek` positions handle streams at a byte offset |
 | `system()`      | `system`                         | shell escape                   |
 | pthreads        | `spawn`, `wait`                  | join is an ordering guarantee  |
 | formatting      | `output`                         | shortest round-trip float printing |
@@ -183,27 +212,35 @@ library beyond the above.
 ## Testing
 
 ```sh
-make test
+make test           # C backend
+make test-release   # release mode: bounds checks removed
+make test-native    # native x86-64 backend
 ```
 
-46 checks: arithmetic (including overflow wrap and division edge cases),
-unsigned arithmetic, floats, all six comparison conditions, branches,
-loops, nested loops, functions, recursion, mutual recursion, the value
-stack, cells and focus navigation, arrays (sieve of Eratosthenes),
-strings, manual memory, pointer arithmetic, linked lists, callbacks
-through transform references, file I/O with first-class handles
-(two files interleaved, append mode, flush), threads (`spawn`/`wait`),
-a spinlock built from `exchange`, a word
-counter, bubble sort, a free-list allocator, a limits stress test
-(301 cells, 300 labels, 302-deep focus, 1100-deep value stack), and
-sixteen programs that must fail to compile.
+48 checks each: arithmetic (including overflow wrap and division edge
+cases), unsigned arithmetic, floats (including `remainder`/fmod and
+NaN), all six comparison conditions, branches, loops, nested loops,
+functions, recursion, mutual recursion, the value stack, cells and focus
+navigation, arrays (sieve of Eratosthenes), strings, manual memory,
+pointer arithmetic, linked lists, callbacks through transform references,
+file I/O with first-class handles (two files interleaved, append mode,
+flush), `seek` repositioning an `open both` stream (interleaved reads and
+writes on one file), threads (`spawn`/`wait`), a spinlock built from
+`exchange`, compare-and-swap built from `exchange` and `wait` (the
+evaluation that closed further atomic primitives), a word counter, bubble
+sort, a free-list allocator, a limits stress test (301 cells, 300 labels,
+302-deep focus, 1100-deep value stack), and sixteen programs that must
+fail to compile. Every run test passes under all three modes.
 
 ## Current limitations
 
 These are honest boundaries of the first implementation, not pretenses:
 
-* **Backend is C.** A native x86-64 backend is planned; the IR is ready
-  for it.
+* **Two backends.** The C backend is the portable reference and needs a
+  host C compiler at build time. The native backend emits x86-64 ELF
+  directly (Linux, System V ABI, musl or glibc host libc; the interpreter
+  path and the libc SONAME are detected at compile time, override with
+  `CRYN_INTERP`). Other targets stay on the C backend.
 * **Concurrency is minimal.** `spawn` starts a thread running a
   zero-parameter transformation; `wait` joins all spawned contexts of the
   current one. Shared world cells may be accessed concurrently; a race
@@ -225,8 +262,10 @@ These are honest boundaries of the first implementation, not pretenses:
   not statically known. The callee's result is discarded and the caller's
   working value (the reference) is preserved; results flow through world
   cells or addresses.
-* **Bounds checks abort** with a diagnostic. A release mode that removes
-  them is future work.
+* **Bounds checks abort** with a diagnostic in the default mode. Release
+  mode (`--release`) removes them; statically provable checks (literal
+  indices into static arrays, and literal indices provably within a
+  block's size) are eliminated at compile time in every mode.
 * **Fixed limits:** 1024 focus history entries, 4096 labels, 4096 cells
   per transformation, 4096 provable value stack depth, 256 concurrent
   spawns per context, 256 open files per context. A single
@@ -235,8 +274,8 @@ These are honest boundaries of the first implementation, not pretenses:
   by memory.
 * **No separate compilation** yet: one `.cryn` file per program.
 * **The compiler itself is not yet written in Cryn.** Self-hosting is the
-  long-term goal; the architecture (small passes, plain data structures,
-  no frameworks) was chosen for it.
+  remaining long-term goal; the architecture (small passes, plain data
+  structures, no frameworks) was chosen for it.
 
 ## Contributing
 
@@ -250,16 +289,21 @@ These are honest boundaries of the first implementation, not pretenses:
   and `wiki/TUTORIAL.md`, published as
   [Learning-Cryn](https://git.disroot.org/Vextoly/Cryn/wiki/Learning-Cryn)),
   and a note in this README if it affects the runtime surface.
-* Run `make test` before committing.
+* Run `make test`, `make test-release`, and `make test-native` before
+  committing.
 
 ## Future work
 
-1. Native x86-64 backend (direct IR lowering; the C backend remains the
-   portable reference).
-2. Self-hosting: rewrite the compiler in Cryn.
-3. Release mode without bounds checks; compile-time elimination of
-   statically provable checks.
-4. File positioning (`seek`) for `open both` streams, if a use case
-   cannot be met with the sequential byte-stream model plus `flush`.
-5. Further atomic primitives only when a use case cannot be met with
-   `exchange` (which builds locks) and `wait`.
\ No newline at end of file
+1. Self-hosting: rewrite the compiler in Cryn (the C backend bootstraps
+   the port; the native backend then keeps the toolchain C-free).
+2. Extend the native backend beyond Linux/x86-64 (static PIE, more
+   libc targets) as demand appears; the C backend covers everything
+   else.
+
+Done from the original list: the native x86-64 backend (`--native`),
+release mode with compile-time elimination of statically provable bounds
+checks (`--release`), file positioning (`seek`) for `open both` streams,
+and the atomic-primitives evaluation — compare-and-swap and atomic
+arithmetic build from `exchange` and `wait` alone
+([tests/32_cas.cryn](tests/32_cas.cryn) is the proof), so no new
+primitive was added.
\ No newline at end of file
diff --git a/src/ast.h b/src/ast.h
index 48fd681..a4553a5 100644
--- a/src/ast.h
+++ b/src/ast.h
@@ -55,6 +55,7 @@ typedef enum {
     A_RET, A_STOP,
     A_INPUT, A_OUTPUT, A_OUTPUT_TEXT,
     A_OPEN, A_CLOSE, A_SYSTEM, A_USE, A_FLUSH,
+    A_SEEK,
     A_SPAWN, A_WAIT,
     A_XCHG,
     A_MARK
diff --git a/src/be_c.c b/src/be_c.c
index c9246ae..04a3ec2 100644
--- a/src/be_c.c
+++ b/src/be_c.c
@@ -184,7 +184,7 @@ static void emit_store(Buf* o, Insn* in) {
 /* function emission                                                  */
 /* ------------------------------------------------------------------ */
 
-static void emit_fn(Buf* o, ProgramIR* p, int fi) {
+static void emit_fn(Buf* o, ProgramIR* p, int fi, int release) {
     FnIR* fn = &p->fns[fi];
     emit_fn_sig(o, p, fi);
     buf_puts(o, " {\n");
@@ -252,17 +252,16 @@ static void emit_fn(Buf* o, ProgramIR* p, int fi) {
             const char* ct = ctype(in->t);
             uint64_t esz = type_size(in->t);
             if (in->op == I_MOVE_LIT) {
-                if (in->b > 0)
-                    buf_printf(o, "    crn_check_range((uint64_t)%" PRIu64 ", %d);\n",
-                               in->imm.u, in->b);
-                else if (in->b < 0)
+                /* a literal index against a static bound (b > 0) was
+                   proven in range by the checker: no check is needed */
+                if (in->b < 0 && !release)
                     buf_printf(o, "    crn_check_block(F, (uint64_t)%" PRIu64 ", %" PRIu64 ");\n",
                                in->imm.u, esz);
                 buf_printf(o, "    crn_focus_push(F); F = (%s*)F + %" PRIu64 ";\n", ct, in->imm.u);
             } else {
-                if (in->b > 0)
+                if (in->b > 0 && !release)
                     buf_printf(o, "    crn_check_range(wv.u, %d);\n", in->b);
-                else if (in->b < 0)
+                else if (in->b < 0 && !release)
                     buf_printf(o, "    crn_check_block(F, wv.u, %" PRIu64 ");\n", esz);
                 buf_printf(o, "    crn_focus_push(F); F = (%s*)F + wv.u;\n", ct);
             }
@@ -716,6 +715,13 @@ static void emit_fn(Buf* o, ProgramIR* p, int fi) {
         case I_FLUSH:
             buf_puts(o, "    crn_flush();\n");
             break;
+        case I_SEEK:
+            if (in->b == 1)
+                buf_printf(o, "    wv.i = crn_seek((int64_t)%" PRIu64 "ULL);\n", in->imm.u);
+            else
+                buf_puts(o, "    wv.i = crn_seek((int64_t)wv.u);\n");
+            cur = ty_int(64);
+            break;
         case I_SYSTEM:
             buf_puts(o, "    wv.i = crn_system((uint8_t*)(uintptr_t)wv.u);\n");
             cur = ty_int(64);
@@ -753,7 +759,7 @@ static void emit_fn(Buf* o, ProgramIR* p, int fi) {
 /* program emission                                                   */
 /* ------------------------------------------------------------------ */
 
-void be_c_emit(ProgramIR* p, Buf* out) {
+void be_c_emit(ProgramIR* p, Buf* out, int release) {
     buf_puts(out, "/* generated by the Cryn compiler - do not edit */\n");
     buf_puts(out, "/* Cryn source compiled to portable C11 */\n\n");
     buf_puts(out, cr_rt_h_src);
@@ -820,7 +826,7 @@ void be_c_emit(ProgramIR* p, Buf* out) {
 
     /* functions */
     for (int i = 0; i < p->nfns; i++)
-        emit_fn(out, p, i);
+        emit_fn(out, p, i, release);
 
     /* entry point */
     buf_puts(out, "int main(void) {\n    crn_init();\n    crn_init_world();\n");
@@ -879,6 +885,7 @@ static const char* irop_name(IROp op) {
     case I_CLOSE: return "close";
     case I_USE: return "use";
     case I_FLUSH: return "flush";
+    case I_SEEK: return "seek";
     case I_SYSTEM: return "system";
     case I_SPAWN: return "spawn";
     case I_WAIT: return "wait";
diff --git a/src/be_c.h b/src/be_c.h
index 4506f56..4d17a8d 100644
--- a/src/be_c.h
+++ b/src/be_c.h
@@ -3,7 +3,9 @@
 
 #include "ir.h"
 
-/* Emit a complete, self-contained C translation unit for the program. */
-void be_c_emit(ProgramIR* p, Buf* out);
+/* Emit a complete, self-contained C translation unit for the program.
+   release != 0 removes runtime bounds checks (statically provable
+   checks are always eliminated, in every mode). */
+void be_c_emit(ProgramIR* p, Buf* out, int release);
 
 #endif
diff --git a/src/be_x64.c b/src/be_x64.c
new file mode 100644
index 0000000..7d475b3
--- /dev/null
+++ b/src/be_x64.c
@@ -0,0 +1,2463 @@
+/* be_x64.c - native x86-64 backend.
+ *
+ * Lowers the Cryn IR directly to x86-64 machine code and emits a static
+ * ELF64 executable. The only external dependency is the host C library,
+ * used as the system-call interface (stdio, malloc, pthreads, fmod,
+ * snprintf/strtod so float formatting is byte-identical to the C
+ * backend); it is linked dynamically through a hand-built GOT. No C
+ * compiler, no linker, and no CRT objects are involved at program build
+ * time.
+ *
+ * Internal calling convention (Val = 8-byte union carried in GP regs):
+ *   rdi, rsi, rdx, rcx, r8, r9  arguments (then stack)
+ *   rax                         result
+ *   r12d                        comparison state (-1/0/1)
+ *   r13 (F)                     focus pointer
+ *   r14 (ctx)                   per-thread context
+ *   r15 (wv)                    working value (floats as double bits)
+ *   rbx                         scratch; all six are callee-saved by us
+ * The value stack is empty and the focus history is cut back at every
+ * call boundary (the checker guarantees it), so a per-thread context
+ * reached through r14 is exact.
+ *
+ * Stack discipline: generated function bodies keep rsp 16-aligned; a
+ * call therefore enters a helper with rsp == 8 (mod 16). Helpers that
+ * call libc use the frame  push rbp; mov rbp,rsp; sub rsp,32  which
+ * restores 16-byte alignment. Frame-less helpers must only tail-jump
+ * to a die path from a state where rsp == 8 (mod 16).
+ */
+
+#include "be_x64.h"
+#include "util.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+#include <unistd.h>
+
+/* ------------------------------------------------------------------ */
+/* emitter state                                                       */
+/* ------------------------------------------------------------------ */
+
+enum { SEG_RO, SEG_GOT, SEG_DATA };
+#define SEG_LBL 100 /* fix kind 4: rip-relative to a text label */
+
+typedef struct Fix {
+    int kind; /* 0: rel32 in text -> label
+                 1: disp32 in text, rip-relative to (seg, segoff)
+                 2: abs64 in data buffer at segoff -> label
+                 4: disp32 in text, rip-relative to label */
+    size_t at;
+    int label;
+    int seg;
+    size_t segoff;
+} Fix;
+
+typedef struct XE {
+    Buf text, ro, data;
+    size_t bss_size;                 /* data segment zero tail */
+    Fix* fixes;
+    int nfix, fixcap;
+    int* labels;
+    int nlabels, labelcap;
+    struct { const char* name; int is_data; } * imps;
+    int nimps, impcap;
+    int style_musl;
+    /* layout results */
+    uint64_t base, text_va, ro_va, got_va, dyn_va, data_va;
+    size_t start_off;
+    size_t interp_off, rx_filesz, rw_off, rw_filesz;
+} XE;
+
+static void fix_add(XE* e, int kind, size_t at, int label, int seg, size_t segoff) {
+    if (e->nfix == e->fixcap) {
+        e->fixcap = e->fixcap ? e->fixcap * 2 : 256;
+        e->fixes = xrealloc(e->fixes, (size_t)e->fixcap * sizeof(Fix));
+    }
+    Fix* f = &e->fixes[e->nfix++];
+    f->kind = kind; f->at = at; f->label = label; f->seg = seg; f->segoff = segoff;
+}
+
+static int lbl_new(XE* e) {
+    if (e->nlabels == e->labelcap) {
+        e->labelcap = e->labelcap ? e->labelcap * 2 : 512;
+        e->labels = xrealloc(e->labels, (size_t)e->labelcap * sizeof(int));
+    }
+    e->labels[e->nlabels] = -1;
+    return e->nlabels++;
+}
+static void lbl_here(XE* e, int l) { e->labels[l] = (int)e->text.len; }
+
+/* ------------------------------------------------------------------ */
+/* x86-64 encoding kit                                                 */
+/* ------------------------------------------------------------------ */
+
+/* regs: 0 ax,1 cx,2 dx,3 bx,4 sp,5 bp,6 si,7 di,8-15 r8-r15 */
+
+static void tc(XE* e, unsigned char b) { buf_putc(&e->text, (char)b); }
+static void tc2(XE* e, unsigned char a, unsigned char b) { tc(e, a); tc(e, b); }
+static void tc3(XE* e, unsigned char a, unsigned char b, unsigned char c) { tc(e, a); tc(e, b); tc(e, c); }
+static void tc4u(XE* e, uint32_t v) {
+    tc(e, (unsigned char)v); tc(e, (unsigned char)(v >> 8));
+    tc(e, (unsigned char)(v >> 16)); tc(e, (unsigned char)(v >> 24));
+}
+static void tc8u(XE* e, uint64_t v) { tc4u(e, (uint32_t)v); tc4u(e, (uint32_t)(v >> 32)); }
+static void tc4u_seq(XE* e, const unsigned char* p) { tc(e, p[0]); tc(e, p[1]); tc(e, p[2]); tc(e, p[3]); }
+static void tc5u_seq(XE* e, const unsigned char* p) { tc4u_seq(e, p); tc(e, p[4]); }
+
+static void R(XE* e, int w, int r, int b) {
+    unsigned char v = 0x40;
+    if (w) v |= 8;
+    if (r >= 8) v |= 4;
+    if (b >= 8) v |= 1;
+    if (v != 0x40) tc(e, v);
+}
+static void MR(XE* e, int mod, int reg, int rm) {
+    tc(e, (unsigned char)((mod << 6) | ((reg & 7) << 3) | (rm & 7)));
+}
+
+static void mov_rr(XE* e, int d, int s) { R(e, 1, s, d); tc(e, 0x89); MR(e, 3, s, d); }
+static void mov_ri64(XE* e, int r, uint64_t v) { R(e, 1, 0, r); tc(e, (unsigned char)(0xB8 + (r & 7))); tc8u(e, v); }
+static void mov_ri32(XE* e, int r, int32_t v) { R(e, 0, 0, r); tc(e, (unsigned char)(0xB8 + (r & 7))); tc4u(e, (uint32_t)v); }
+static void ld_d(XE* e, int reg, int base, int32_t d) { R(e, 1, reg, base); tc(e, 0x8B); MR(e, 2, reg, base); tc4u(e, (uint32_t)d); }
+static void st_d(XE* e, int base, int32_t d, int reg) { R(e, 1, reg, base); tc(e, 0x89); MR(e, 2, reg, base); tc4u(e, (uint32_t)d); }
+static void lea_rd(XE* e, int reg, int base, int32_t d) { R(e, 1, reg, base); tc(e, 0x8D); MR(e, 2, reg, base); tc4u(e, (uint32_t)d); }
+
+/* [base + idx*8 + d] */
+static void ld_s8(XE* e, int reg, int base, int idx, int32_t d) {
+    R(e, 1, reg, base); tc(e, 0x8B); MR(e, 2, reg, 4);
+    tc(e, (unsigned char)(0xC0 | ((idx & 7) << 3) | (base & 7)));
+    tc4u(e, (uint32_t)d);
+}
+static void st_s8(XE* e, int base, int idx, int32_t d, int reg) {
+    R(e, 1, reg, base); tc(e, 0x89); MR(e, 2, reg, 4);
+    tc(e, (unsigned char)(0xC0 | ((idx & 7) << 3) | (base & 7)));
+    tc4u(e, (uint32_t)d);
+}
+static void lea_s8(XE* e, int reg, int base, int idx, int32_t d) {
+    R(e, 1, reg, base); tc(e, 0x8D); MR(e, 2, reg, 4);
+    tc(e, (unsigned char)(0xC0 | ((idx & 7) << 3) | (base & 7)));
+    tc4u(e, (uint32_t)d);
+}
+static void lea_s4(XE* e, int reg, int base, int idx, int32_t d) {
+    R(e, 1, reg, base); tc(e, 0x8D); MR(e, 2, reg, 4);
+    tc(e, (unsigned char)(0x80 | ((idx & 7) << 3) | (base & 7)));
+    tc4u(e, (uint32_t)d);
+}
+
+static void lea_rip(XE* e, int reg, int seg, size_t off) {
+    R(e, 1, reg, 5); tc(e, 0x8D); MR(e, 0, reg, 5);
+    fix_add(e, 1, e->text.len, 0, seg, off); tc4u(e, 0);
+}
+static void lea_rip_lbl(XE* e, int reg, int label) {
+    R(e, 1, reg, 5); tc(e, 0x8D); MR(e, 0, reg, 5);
+    fix_add(e, 4, e->text.len, label, 0, 0); tc4u(e, 0);
+}
+static void mov_rip(XE* e, int reg, int seg, size_t off) {
+    R(e, 1, reg, 5); tc(e, 0x8B); MR(e, 0, reg, 5);
+    fix_add(e, 1, e->text.len, 0, seg, off); tc4u(e, 0);
+}
+static void call_rip(XE* e, int seg, size_t off) {
+    tc2(e, 0xFF, 0x15);
+    fix_add(e, 1, e->text.len, 0, seg, off); tc4u(e, 0);
+}
+static void jmp_l(XE* e, int l) { tc(e, 0xE9); fix_add(e, 0, e->text.len, l, 0, 0); tc4u(e, 0); }
+static void jcc_l(XE* e, int cc, int l) { tc2(e, 0x0F, (unsigned char)cc); fix_add(e, 0, e->text.len, l, 0, 0); tc4u(e, 0); }
+static void call_l(XE* e, int l) { tc(e, 0xE8); fix_add(e, 0, e->text.len, l, 0, 0); tc4u(e, 0); }
+
+/* ALU: ext 0=add 1=or 4=and 5=sub 6=xor 7=cmp */
+static void alu_rr(XE* e, int ext, int d, int s) { R(e, 1, s, d); tc(e, (unsigned char)(0x01 + 8 * ext)); MR(e, 3, s, d); }
+static void alu_ri(XE* e, int ext, int r, int32_t v) {
+    R(e, 1, 0, r);
+    if (v == (int8_t)v) { tc(e, 0x83); MR(e, 3, ext, r); tc(e, (unsigned char)v); }
+    else { tc(e, 0x81); MR(e, 3, ext, r); tc4u(e, (uint32_t)v); }
+}
+static void imul_r(XE* e, int d, int s) { R(e, 1, d, s); tc2(e, 0x0F, 0xAF); MR(e, 3, d, s); }
+static void imul_ri(XE* e, int d, int s, int32_t v) { R(e, 1, d, s); tc(e, 0x69); MR(e, 3, d, s); tc4u(e, (uint32_t)v); }
+static void push_r(XE* e, int r) { if (r >= 8) tc(e, 0x41); tc(e, (unsigned char)(0x50 + (r & 7))); }
+static void pop_r(XE* e, int r) { if (r >= 8) tc(e, 0x41); tc(e, (unsigned char)(0x58 + (r & 7))); }
+static void cqo(XE* e) { tc2(e, 0x48, 0x99); }
+static void idiv_r(XE* e, int r) { R(e, 1, 0, r); tc(e, 0xF7); MR(e, 3, 7, r); }
+static void neg_r(XE* e, int r) { R(e, 1, 0, r); tc(e, 0xF7); MR(e, 3, 3, r); }
+static void shl_ri(XE* e, int r, int n) { R(e, 1, 0, r); tc(e, 0xC1); MR(e, 3, 4, r); tc(e, (unsigned char)n); }
+static void test_rr(XE* e, int a, int b) { R(e, 1, b, a); tc(e, 0x85); MR(e, 3, b, a); }
+static void leave_(XE* e) { tc(e, 0xC9); }
+static void ret(XE* e) { tc(e, 0xC3); }
+static void sub_rsp(XE* e, int n) {
+    if (n == (int8_t)n) { tc3(e, 0x48, 0x83, 0xEC); tc(e, (unsigned char)n); }
+    else { tc3(e, 0x48, 0x81, 0xEC); tc4u(e, (uint32_t)n); }
+}
+static void add_rsp(XE* e, int n) {
+    if (n == (int8_t)n) { tc2(e, 0x48, 0x83); MR(e, 3, 0, 4); tc(e, (unsigned char)n); }
+    else { tc2(e, 0x48, 0x81); MR(e, 3, 0, 4); tc4u(e, (uint32_t)n); }
+}
+static void movsxd_rr(XE* e, int d, int s) { R(e, 1, d, s); tc(e, 0x63); MR(e, 3, d, s); }
+static void movsx_al(XE* e, int r) { R(e, 1, r, 0); tc2(e, 0x0F, 0xBE); MR(e, 3, r, 0); }
+static void movsx_ax(XE* e, int r) { R(e, 1, r, 0); tc2(e, 0x0F, 0xBF); MR(e, 3, r, 0); }
+static void movsx8(XE* e, int reg, int base, int32_t d) { R(e, 0, reg, base); tc2(e, 0x0F, 0xBE); MR(e, 2, reg, base); tc4u(e, (uint32_t)d); }
+static void movzx8(XE* e, int reg, int base, int32_t d) { R(e, 0, reg, base); tc2(e, 0x0F, 0xB6); MR(e, 2, reg, base); tc4u(e, (uint32_t)d); }
+static void movsx16(XE* e, int reg, int base, int32_t d) { R(e, 0, reg, base); tc2(e, 0x0F, 0xBF); MR(e, 2, reg, base); tc4u(e, (uint32_t)d); }
+static void movzx16(XE* e, int reg, int base, int32_t d) { R(e, 0, reg, base); tc2(e, 0x0F, 0xB7); MR(e, 2, reg, base); tc4u(e, (uint32_t)d); }
+static void movsx32m(XE* e, int reg, int base, int32_t d) { R(e, 1, reg, base); tc(e, 0x63); MR(e, 2, reg, base); tc4u(e, (uint32_t)d); }
+static void mov32m(XE* e, int reg, int base, int32_t d) { R(e, 0, reg, base); tc(e, 0x8B); MR(e, 2, reg, base); tc4u(e, (uint32_t)d); }
+static void st8(XE* e, int base, int32_t d, int reg) { R(e, 0, reg, base); tc(e, 0x88); MR(e, 2, reg, base); tc4u(e, (uint32_t)d); }
+static void st16(XE* e, int base, int32_t d, int reg) { tc(e, 0x66); R(e, 0, reg, base); tc(e, 0x89); MR(e, 2, reg, base); tc4u(e, (uint32_t)d); }
+static void st32(XE* e, int base, int32_t d, int reg) { R(e, 0, reg, base); tc(e, 0x89); MR(e, 2, reg, base); tc4u(e, (uint32_t)d); }
+static void st_zero64(XE* e, int base, int32_t d) {
+    R(e, 1, 0, base); tc(e, 0xC7); MR(e, 2, 0, base); tc4u(e, (uint32_t)d); tc4u(e, 0);
+}
+static void setcc_r(XE* e, int cc, int reg) {
+    R(e, 0, 0, reg); tc2(e, 0x0F, (unsigned char)(0x90 + cc)); MR(e, 3, 0, reg);
+    R(e, 0, reg, reg); tc2(e, 0x0F, 0xB6); MR(e, 3, reg, reg);
+}
+
+/* SSE */
+static void movq_xr(XE* e, int xmm, int r) { tc(e, 0x66); R(e, 1, xmm, r); tc2(e, 0x0F, 0x6E); MR(e, 3, xmm, r); }
+static void movq_rx(XE* e, int r, int xmm) { tc(e, 0x66); R(e, 1, xmm, r); tc2(e, 0x0F, 0x7E); MR(e, 3, xmm, r); }
+static void sse_op(XE* e, int prefix, int op, int d, int s) { tc(e, prefix); R(e, 0, d, s); tc(e, 0x0F); tc(e, op); MR(e, 3, d, s); }
+static void addsd(XE* e, int d, int s) { sse_op(e, 0xF2, 0x58, d, s); }
+static void subsd(XE* e, int d, int s) { sse_op(e, 0xF2, 0x5C, d, s); }
+static void mulsd(XE* e, int d, int s) { sse_op(e, 0xF2, 0x59, d, s); }
+static void divsd(XE* e, int d, int s) { sse_op(e, 0xF2, 0x5E, d, s); }
+static void cvtsi2sd(XE* e, int x, int r) { tc(e, 0xF2); R(e, 1, x, r); tc2(e, 0x0F, 0x2A); MR(e, 3, x, r); }
+static void cvttsd2si(XE* e, int r, int x) { tc(e, 0xF2); R(e, 1, x, r); tc2(e, 0x0F, 0x2C); MR(e, 3, x, r); }
+static void cvtsd2ss(XE* e, int d, int s) { sse_op(e, 0xF2, 0x5A, d, s); }
+static void cvtss2sd(XE* e, int d, int s) { sse_op(e, 0xF3, 0x5A, d, s); }
+static void ucomisd(XE* e, int a, int b) { sse_op(e, 0x66, 0x2E, a, b); }
+static void movsd_rip(XE* e, int xmm, int seg, size_t off) {
+    tc(e, 0xF2); R(e, 0, xmm, 5); tc2(e, 0x0F, 0x10); MR(e, 0, xmm, 5);
+    fix_add(e, 1, e->text.len, 0, seg, off); tc4u(e, 0);
+}
+static void pxor(XE* e, int a, int b) { tc(e, 0x66); R(e, 0, b, a); tc2(e, 0x0F, 0xEF); MR(e, 3, b, a); }
+static void xor_rr(XE* e, int d, int s) { R(e, 1, s, d); tc(e, 0x31); MR(e, 3, s, d); }
+static void movd_eax_xmm0(XE* e) { tc3(e, 0x66, 0x0F, 0x7E); tc(e, 0xC0); }
+static void movd_xmm0_eax(XE* e) { tc3(e, 0x66, 0x0F, 0x6E); tc(e, 0xC0); }
+static void movzx_al(XE* e, int r) { R(e, 0, r, 0); tc2(e, 0x0F, 0xB6); MR(e, 3, r, 0); }
+static void movzx_ax(XE* e, int r) { R(e, 0, r, 0); tc2(e, 0x0F, 0xB7); MR(e, 3, r, 0); }
+
+/* lock xchg r, m with size in bytes (1,2,4,8) */
+static void lock_xchg(XE* e, int base, int32_t d, int reg, int size) {
+    tc(e, 0xF0);
+    if (size == 2) tc(e, 0x66);
+    R(e, size == 8, reg, base);
+    tc(e, size == 1 ? 0x86 : 0x87);
+    MR(e, 2, reg, base);
+    tc4u(e, (uint32_t)d);
+}
+
+/* ------------------------------------------------------------------ */
+/* context layout (per thread, reached through r14)                    */
+/* ------------------------------------------------------------------ */
+
+#define CTX_STK     0
+#define CTX_SP      8
+#define CTX_CAP     16
+#define CTX_FH      24
+#define CTX_FHSP    8216
+#define CTX_IN      8224
+#define CTX_OUT     8232
+#define CTX_FILES   8240
+#define CTX_FMODES  10288
+#define CTX_SPAWNED 10544
+#define CTX_NSPAWN  12592
+#define CTX_SIZE    12596
+
+#define R_CMP 12
+#define R_F   13
+#define R_CTX 14
+#define R_WV  15
+
+/* ------------------------------------------------------------------ */
+/* imports, rodata, data objects                                       */
+/* ------------------------------------------------------------------ */
+
+static int imp_idx(XE* e, const char* name, int is_data) {
+    for (int i = 0; i < e->nimps; i++)
+        if (strcmp(e->imps[i].name, name) == 0) return i;
+    if (e->nimps == e->impcap) {
+        e->impcap = e->impcap ? e->impcap * 2 : 32;
+        e->imps = xrealloc(e->imps, (size_t)e->impcap * sizeof(*e->imps));
+    }
+    e->imps[e->nimps].name = name;
+    e->imps[e->nimps].is_data = is_data;
+    return e->nimps++;
+}
+static void call_c(XE* e, const char* name) { call_rip(e, SEG_GOT, (size_t)(3 + imp_idx(e, name, 0)) * 8); }
+static void libc_data(XE* e, int reg, const char* name) { mov_rip(e, reg, SEG_GOT, (size_t)(3 + imp_idx(e, name, 1)) * 8); }
+
+static size_t ro_put(XE* e, const void* p, size_t n) {
+    size_t off = e->ro.len;
+    buf_put(&e->ro, p, n);
+    return off;
+}
+static size_t ro_str(XE* e, const char* s) { return ro_put(e, s, strlen(s) + 1); }
+static size_t ro_f64(XE* e, double d) { size_t o = e->ro.len; buf_put(&e->ro, &d, 8); return o; }
+static size_t data_put(XE* e, const void* p, size_t n) {
+    size_t off = e->data.len;
+    buf_put(&e->data, p, n);
+    return off;
+}
+
+/* ------------------------------------------------------------------ */
+/* helper runtime                                                      */
+/* ------------------------------------------------------------------ */
+
+typedef struct H {
+    int die, die_range;
+    int die_oom, die_div0, die_flushfail, die_focus, die_toolarge;
+    int die_notblock, die_free_null, die_usebad, die_closebad;
+    int die_indcall, die_spawnpar, die_spawnmax, die_threadfail, die_seekstreams;
+    int push, pop, fpush, fpop;
+    int alloc, free_, blockn, chk_block, chk_range;
+    int d2i64, d2u64, d2i8, d2i16, d2i32, d2u8, d2u16, d2u32;
+    int out_i64, out_u64, out_f64, out_f32, out_bool, out_char, out_addr, out_text;
+    int input, open, close, use, flush, seek, system;
+    int spawn, thread_main, wait, stop;
+    size_t fmt_err, fmt_range, fmt_lld, fmt_llu, fmt_hex, fmt_g;
+    size_t s_nan, s_true, s_false;
+    size_t modetab;   /* "rb\0\0" "wb\0\0" "w+b\0" "ab\0\0" */
+    size_t k_2p63, k_2n63, k_2p64;
+    size_t fbuf;      /* 64 writable bytes in .data */
+    size_t done_ctr;  /* completed-thread counter in .data */
+} H;
+
+#define FRAME32(e)  do { push_r(e, 5); mov_rr(e, 5, 4); sub_rsp(e, 32); } while (0)
+
+/* fprintf(stderr, "cryn: runtime error: %s\n", rdi); exit(1) */
+static void h_die(XE* e, H* h) {
+    lbl_here(e, h->die);
+    FRAME32(e);
+    st_d(e, 5, -16, 7);
+    libc_data(e, 0, "stderr");
+    ld_d(e, 7, 0, 0);
+    lea_rip(e, 6, SEG_RO, h->fmt_err);
+    ld_d(e, 2, 5, -16);
+    tc2(e, 0x31, 0xC0);
+    call_c(e, "fprintf");
+    mov_ri32(e, 7, 1);
+    call_c(e, "exit");
+}
+
+static void h_die_range(XE* e, H* h) {
+    lbl_here(e, h->die_range);
+    FRAME32(e);
+    st_d(e, 5, -16, 7);              /* i */
+    st_d(e, 5, -24, 6);              /* n */
+    libc_data(e, 0, "stderr");
+    ld_d(e, 7, 0, 0);
+    lea_rip(e, 6, SEG_RO, h->fmt_range);
+    ld_d(e, 2, 5, -16);              /* i */
+    ld_d(e, 8, 5, -24);              /* n */
+    tc2(e, 0x31, 0xC0);
+    call_c(e, "fprintf");
+    mov_ri32(e, 7, 1);
+    call_c(e, "exit");
+}
+
+static void h_msg_dies(XE* e, H* h) {
+    struct { int l; const char* m; } ds[] = {
+        {h->die_oom, "out of memory"},
+        {h->die_div0, "division by zero"},
+        {h->die_flushfail, "flush failed"},
+        {h->die_focus, "focus history underflow"},
+        {h->die_toolarge, "allocation too large"},
+        {h->die_notblock, "index into memory that is not a heap block (only the base address of a live block made with 'make <size> of <type>' is bounds checked)"},
+        {h->die_free_null, "free of the null address"},
+        {h->die_usebad, "use of a handle that is not open in this context (handles come from 'open'; 0 means the standard streams)"},
+        {h->die_closebad, "close of a handle that is not open in this context (handles come from 'open' and are closed once)"},
+        {h->die_indcall, "indirect call of a transformation that takes parameters"},
+        {h->die_spawnpar, "spawn of a transformation that takes parameters"},
+        {h->die_spawnmax, "too many outstanding spawned contexts"},
+        {h->die_threadfail, "thread creation failed"},
+        {h->die_seekstreams, "seek requires a stream routed through an open handle ('use' a handle from 'open' first)"},
+    };
+    for (size_t k = 0; k < sizeof ds / sizeof ds[0]; k++) {
+        lbl_here(e, ds[k].l);
+        lea_rip(e, 7, SEG_RO, ro_str(e, ds[k].m));
+        jmp_l(e, h->die);             /* entered at rsp 8 */
+    }
+}
+
+static void h_stack(XE* e, H* h) {
+    /* push(rdi) */
+    lbl_here(e, h->push);
+    ld_d(e, 0, R_CTX, CTX_SP);
+    int grow = lbl_new(e), ok = lbl_new(e);
+    R(e, 1, 0, R_CTX); tc(e, 0x39); MR(e, 2, 0, R_CTX); tc4u(e, CTX_CAP);
+    jcc_l(e, 0x86, grow);             /* jbe: cap <= sp -> grow */
+    jmp_l(e, ok);
+    lbl_here(e, grow);
+    push_r(e, 7);                     /* v */
+    push_r(e, 5); mov_rr(e, 5, 4);
+    sub_rsp(e, 8);
+    ld_d(e, 7, R_CTX, CTX_CAP);
+    test_rr(e, 7, 7);
+    int nz = lbl_new(e), set = lbl_new(e);
+    jcc_l(e, 0x85, nz);               /* jnz */
+    mov_ri32(e, 7, 64);
+    jmp_l(e, set);
+    lbl_here(e, nz);
+    shl_ri(e, 7, 1);
+    lbl_here(e, set);
+    st_d(e, R_CTX, CTX_CAP, 7);
+    shl_ri(e, 7, 3);                  /* rdi = bytes */
+    mov_rr(e, 6, 7);                  /* rsi = bytes */
+    ld_d(e, 7, R_CTX, CTX_STK);       /* rdi = old */
+    call_c(e, "realloc");
+    test_rr(e, 0, 0);
+    int oom = lbl_new(e);
+    jcc_l(e, 0x85, oom);              /* jnz */
+    call_l(e, h->die_oom);
+    lbl_here(e, oom);
+    st_d(e, R_CTX, CTX_STK, 0);
+    ld_d(e, 0, R_CTX, CTX_SP);
+    add_rsp(e, 8);
+    pop_r(e, 5);
+    pop_r(e, 7);
+    lbl_here(e, ok);
+    ld_d(e, 1, R_CTX, CTX_STK);
+    st_s8(e, 1, 0, 0, 7);
+    R(e, 1, 0, R_CTX); tc(e, 0x83); MR(e, 2, 0, R_CTX); tc4u(e, CTX_SP); tc(e, 1);
+    ret(e);
+
+    /* pop() -> rax */
+    lbl_here(e, h->pop);
+    R(e, 1, 0, R_CTX); tc(e, 0x83); MR(e, 2, 5, R_CTX); tc4u(e, CTX_SP); tc(e, 1);
+    ld_d(e, 0, R_CTX, CTX_SP);
+    ld_d(e, 1, R_CTX, CTX_STK);
+    ld_s8(e, 0, 1, 0, 0);
+    ret(e);
+}
+
+static void h_focus(XE* e, H* h) {
+    lbl_here(e, h->fpush);
+    ld_d(e, 0, R_CTX, CTX_FHSP);
+    mov_ri32(e, 1, 1024);
+    alu_rr(e, 7, 0, 1);               /* cmp rax, 1024 */
+    jcc_l(e, 0x83, h->die_focus);     /* jae, tail jmp at rsp 8 */
+    st_s8(e, R_CTX, 0, CTX_FH, 7);
+    R(e, 1, 0, R_CTX); tc(e, 0x83); MR(e, 2, 0, R_CTX); tc4u(e, CTX_FHSP); tc(e, 1);
+    ret(e);
+    lbl_here(e, h->fpop);
+    R(e, 1, 0, R_CTX); tc(e, 0x83); MR(e, 2, 5, R_CTX); tc4u(e, CTX_FHSP); tc(e, 1);
+    ld_d(e, 0, R_CTX, CTX_FHSP);
+    ld_s8(e, 0, R_CTX, 0, CTX_FH);
+    ret(e);
+}
+
+static void h_heap(XE* e, H* h) {
+    /* alloc(rdi = bytes) -> rax = data */
+    lbl_here(e, h->alloc);
+    FRAME32(e);
+    st_d(e, 5, -16, 7);
+    mov_ri64(e, 1, (uint64_t)-32);
+    alu_rr(e, 7, 7, 1);
+    int ok1 = lbl_new(e);
+    jcc_l(e, 0x82, ok1);              /* jb */
+    call_l(e, h->die_toolarge);
+    lbl_here(e, ok1);
+    ld_d(e, 7, 5, -16);
+    alu_ri(e, 0, 7, 16);
+    call_c(e, "malloc");
+    test_rr(e, 0, 0);
+    int ok2 = lbl_new(e);
+    jcc_l(e, 0x85, ok2);              /* jnz */
+    call_l(e, h->die_oom);
+    lbl_here(e, ok2);
+    mov_ri64(e, 1, 0x4352594E424C4BULL);
+    st_d(e, 0, 0, 1);
+    ld_d(e, 2, 5, -16);
+    st_d(e, 0, 8, 2);
+    st_d(e, 5, -24, 0);
+    lea_rd(e, 7, 0, 16);
+    ld_d(e, 2, 5, -16);
+    tc2(e, 0x31, 0xF6);
+    call_c(e, "memset");
+    leave_(e);
+    ret(e);
+
+    /* free(rdi = ptr) */
+    lbl_here(e, h->free_);
+    FRAME32(e);
+    test_rr(e, 7, 7);
+    int ok3 = lbl_new(e);
+    jcc_l(e, 0x85, ok3);
+    call_l(e, h->die_free_null);
+    lbl_here(e, ok3);
+    mov_rr(e, 0, 7);
+    alu_ri(e, 5, 0, 16);              /* rax = ptr - 16 (header) */
+    mov_ri64(e, 1, 0x4352594E424C4BULL);
+    R(e, 1, 1, 0); tc(e, 0x39); MR(e, 1, 1, 0); tc(e, 0);
+    int ok4 = lbl_new(e);
+    jcc_l(e, 0x84, ok4);              /* je */
+    call_l(e, h->die_notblock);
+    lbl_here(e, ok4);
+    st_zero64(e, 0, 0);
+    mov_rr(e, 7, 0);
+    call_c(e, "free");
+    leave_(e);
+    ret(e);
+
+    /* blockn(rdi = data, rsi = elemsz) -> rax (frame-less) */
+    lbl_here(e, h->blockn);
+    test_rr(e, 7, 7);
+    jcc_l(e, 0x84, h->die_notblock);
+    ld_d(e, 0, 7, -16);
+    mov_ri64(e, 1, 0x4352594E424C4BULL);
+    alu_rr(e, 7, 0, 1);
+    jcc_l(e, 0x85, h->die_notblock);
+    ld_d(e, 0, 7, -8);
+    test_rr(e, 6, 6);
+    int nz = lbl_new(e);
+    jcc_l(e, 0x85, nz);
+    mov_ri32(e, 6, 1);
+    lbl_here(e, nz);
+    mov_rr(e, 1, 6);
+    tc2(e, 0x31, 0xD2);
+    idiv_r(e, 1);
+    ret(e);
+
+    /* chk_block(rdi = data, rsi = i, rdx = elemsz) */
+    lbl_here(e, h->chk_block);
+    mov_rr(e, 8, 6);
+    mov_rr(e, 6, 2);
+    call_l(e, h->blockn);
+    alu_rr(e, 7, 8, 0);
+    int ok = lbl_new(e);
+    jcc_l(e, 0x82, ok);               /* jb */
+    mov_rr(e, 7, 8);
+    mov_rr(e, 6, 0);
+    jmp_l(e, h->die_range);           /* rsp 8 */
+    lbl_here(e, ok);
+    ret(e);
+
+    /* chk_range(rdi = i, rsi = n) */
+    lbl_here(e, h->chk_range);
+    alu_rr(e, 7, 7, 6);
+    int okr = lbl_new(e);
+    jcc_l(e, 0x82, okr);
+    jmp_l(e, h->die_range);
+    lbl_here(e, okr);
+    ret(e);
+}
+
+/* signed conversion: xmm0 -> rax */
+static void conv_s(XE* e, int id, size_t ro_hi, uint64_t v_hi, size_t ro_lo, uint64_t v_lo, int widen) {
+    lbl_here(e, id);
+    int lhi = lbl_new(e), llo = lbl_new(e), lcvt = lbl_new(e);
+    movsd_rip(e, 1, SEG_RO, ro_hi);
+    ucomisd(e, 0, 1);
+    jcc_l(e, 0x83, lhi);              /* jae hi */
+    movsd_rip(e, 1, SEG_RO, ro_lo);
+    ucomisd(e, 0, 1);
+    jcc_l(e, 0x86, llo);              /* jbe lo */
+    jmp_l(e, lcvt);
+    lbl_here(e, lhi);
+    mov_ri64(e, 0, v_hi);
+    ret(e);
+    lbl_here(e, llo);
+    mov_ri64(e, 0, v_lo);
+    ret(e);
+    lbl_here(e, lcvt);
+    cvttsd2si(e, 0, 0);
+    if (widen == 1) movsx_al(e, 0);
+    else if (widen == 2) movsx_ax(e, 0);
+    else if (widen == 3) movsxd_rr(e, 0, 0);
+    ret(e);
+}
+
+/* unsigned conversion: NaN falls through to cvt (matches C on x86) */
+static void conv_u(XE* e, int id, size_t ro_lim, uint64_t v_hi, int widen) {
+    lbl_here(e, id);
+    int lhi = lbl_new(e), llo = lbl_new(e), lcvt = lbl_new(e);
+    movsd_rip(e, 1, SEG_RO, ro_lim);
+    ucomisd(e, 0, 1);
+    jcc_l(e, 0x83, lhi);              /* jae hi */
+    pxor(e, 1, 1);
+    ucomisd(e, 0, 1);                 /* vs 0.0 */
+    jcc_l(e, 0x8A, lcvt);             /* jp: NaN -> cvt */
+    jcc_l(e, 0x86, llo);              /* jbe 0 */
+    jmp_l(e, lcvt);
+    lbl_here(e, lhi);
+    mov_ri64(e, 0, v_hi);
+    ret(e);
+    lbl_here(e, llo);
+    mov_ri64(e, 0, 0);
+    ret(e);
+    lbl_here(e, lcvt);
+    cvttsd2si(e, 0, 0);
+    if (widen == 1) movzx_al(e, 0);
+    else if (widen == 2) movzx_ax(e, 0);
+    else if (widen == 3) { R(e, 0, 0, 0); tc(e, 0x89); MR(e, 3, 0, 0); }
+    ret(e);
+}
+
+static void h_conv(XE* e, H* h) {
+    size_t c127 = ro_f64(e, 127.0), cm128 = ro_f64(e, -128.0);
+    size_t c32767 = ro_f64(e, 32767.0), cm32768 = ro_f64(e, -32768.0);
+    size_t c2p31 = ro_f64(e, 2147483648.0), cm2p31m1 = ro_f64(e, -2147483649.0);
+    size_t c255 = ro_f64(e, 255.0), c65535 = ro_f64(e, 65535.0);
+    size_t c2p32 = ro_f64(e, 4294967296.0);
+    conv_s(e, h->d2i64, h->k_2p63, 0x7FFFFFFFFFFFFFFFULL, h->k_2n63, 0x8000000000000000ULL, 0);
+    conv_s(e, h->d2i8, c127, 127, cm128, (uint64_t)(uint64_t)-128, 1);
+    conv_s(e, h->d2i16, c32767, 32767, cm32768, (uint64_t)-32768, 2);
+    conv_s(e, h->d2i32, c2p31, 2147483647, cm2p31m1, 0xFFFFFFFF80000000ULL, 3);
+    conv_u(e, h->d2u64, h->k_2p64, 0xFFFFFFFFFFFFFFFFULL, 0);
+    conv_u(e, h->d2u8, c255, 255, 1);
+    conv_u(e, h->d2u16, c65535, 65535, 2);
+    conv_u(e, h->d2u32, c2p32, 4294967295ULL, 3);
+}
+
+static void h_out(XE* e, H* h) {
+    /* out_i64(rdi = v) */
+    lbl_here(e, h->out_i64);
+    FRAME32(e);
+    st_d(e, 5, -16, 7);
+    ld_d(e, 7, R_CTX, CTX_OUT);
+    lea_rip(e, 6, SEG_RO, h->fmt_lld);
+    ld_d(e, 2, 5, -16);
+    tc2(e, 0x31, 0xC0);
+    call_c(e, "fprintf");
+    leave_(e);
+    ret(e);
+    /* out_u64 */
+    lbl_here(e, h->out_u64);
+    FRAME32(e);
+    st_d(e, 5, -16, 7);
+    ld_d(e, 7, R_CTX, CTX_OUT);
+    lea_rip(e, 6, SEG_RO, h->fmt_llu);
+    ld_d(e, 2, 5, -16);
+    tc2(e, 0x31, 0xC0);
+    call_c(e, "fprintf");
+    leave_(e);
+    ret(e);
+    /* out_addr */
+    lbl_here(e, h->out_addr);
+    FRAME32(e);
+    st_d(e, 5, -16, 7);
+    ld_d(e, 7, R_CTX, CTX_OUT);
+    lea_rip(e, 6, SEG_RO, h->fmt_hex);
+    ld_d(e, 2, 5, -16);
+    tc2(e, 0x31, 0xC0);
+    call_c(e, "fprintf");
+    leave_(e);
+    ret(e);
+    /* out_bool(rdi) */
+    lbl_here(e, h->out_bool);
+    FRAME32(e);
+    test_rr(e, 7, 7);
+    int t = lbl_new(e), go_ = lbl_new(e);
+    jcc_l(e, 0x85, t);                /* jnz true */
+    lea_rip(e, 7, SEG_RO, h->s_false);
+    jmp_l(e, go_);
+    lbl_here(e, t);
+    lea_rip(e, 7, SEG_RO, h->s_true);
+    lbl_here(e, go_);
+    ld_d(e, 6, R_CTX, CTX_OUT);
+    call_c(e, "fputs");
+    leave_(e);
+    ret(e);
+    /* out_char(rdi = code) */
+    lbl_here(e, h->out_char);
+    FRAME32(e);
+    mov_ri32(e, 6, 0xFF);
+    alu_rr(e, 4, 7, 6);               /* and */
+    ld_d(e, 6, R_CTX, CTX_OUT);
+    call_c(e, "fputc");
+    leave_(e);
+    ret(e);
+    /* out_text(rdi = bytes) */
+    lbl_here(e, h->out_text);
+    push_r(e, 5); mov_rr(e, 5, 4);
+    push_r(e, 3); push_r(e, 7);
+    sub_rsp(e, 8);
+    mov_rr(e, 3, 7);                  /* rbx = p */
+    int loop = lbl_new(e), done = lbl_new(e);
+    lbl_here(e, loop);
+    movzx8(e, 7, 3, 0);
+    test_rr(e, 7, 7);
+    jcc_l(e, 0x84, done);             /* jz */
+    ld_d(e, 6, R_CTX, CTX_OUT);
+    call_c(e, "fputc");
+    alu_ri(e, 0, 3, 1);
+    jmp_l(e, loop);
+    lbl_here(e, done);
+    add_rsp(e, 8);
+    pop_r(e, 7); pop_r(e, 3); pop_r(e, 5);
+    ret(e);
+}
+
+/* out_f64(xmm0) and out_f32(xmm0, already widened to double) */
+static void out_f_body(XE* e, H* h, int id, int maxprec, int narrow) {
+    int done_lbl = lbl_new(e);
+    int exit_lbl = lbl_new(e);
+    lbl_here(e, id);
+    push_r(e, 5); mov_rr(e, 5, 4);
+    push_r(e, 3);
+    sub_rsp(e, 24);
+    /* movsd [rbp-24],xmm0 */
+    tc(e, 0xF2); R(e, 0, 0, 5); tc2(e, 0x0F, 0x11); MR(e, 2, 0, 5); tc4u(e, (uint32_t)-24);
+    ucomisd(e, 0, 0);
+    int notnan = lbl_new(e);
+    jcc_l(e, 0x8B, notnan);           /* jp */
+    lea_rip(e, 7, SEG_RO, h->s_nan);
+    ld_d(e, 6, R_CTX, CTX_OUT);
+    call_c(e, "fputs");
+    jmp_l(e, exit_lbl);
+    lbl_here(e, notnan);
+    mov_ri32(e, 3, 1);                /* rbx = prec */
+    int loop = lbl_new(e);
+    lbl_here(e, loop);
+    lea_rip(e, 7, SEG_DATA, h->fbuf);
+    mov_ri32(e, 6, 64);
+    lea_rip(e, 2, SEG_RO, h->fmt_g);
+    mov_rr(e, 1, 3);
+    tc(e, 0xF2); R(e, 0, 0, 5); tc2(e, 0x0F, 0x10); MR(e, 2, 0, 5); tc4u(e, (uint32_t)-24);  // load xmm0
+    mov_ri32(e, 0, 1);                /* al = 1 vector arg */
+    call_c(e, "snprintf");
+    lea_rip(e, 7, SEG_DATA, h->fbuf);
+    tc2(e, 0x31, 0xF6);
+    call_c(e, "strtod");
+    if (narrow) {
+        cvtsd2ss(e, 0, 0);
+        cvtss2sd(e, 0, 0);
+    }
+    tc(e, 0xF2); R(e, 0, 1, 5); tc2(e, 0x0F, 0x10); MR(e, 2, 1, 5); tc4u(e, (uint32_t)-24);
+    ucomisd(e, 0, 1);
+    jcc_l(e, 0x84, done_lbl);         /* je done */
+    alu_ri(e, 0, 3, 1);
+    mov_ri32(e, 1, maxprec);
+    alu_rr(e, 7, 3, 1);               /* cmp prec, max */
+    jcc_l(e, 0x8E, loop);             /* jle */
+    lbl_here(e, done_lbl);
+    lea_rip(e, 7, SEG_DATA, h->fbuf);
+    ld_d(e, 6, R_CTX, CTX_OUT);
+    call_c(e, "fputs");
+    lbl_here(e, exit_lbl);
+    add_rsp(e, 24);
+    pop_r(e, 3);
+    pop_r(e, 5);
+    ret(e);
+}
+
+static void h_io(XE* e, H* h) {
+    /* input() -> rax */
+    lbl_here(e, h->input);
+    FRAME32(e);
+    ld_d(e, 7, R_CTX, CTX_IN);
+    call_c(e, "fgetc");
+    movsxd_rr(e, 0, 0);
+    leave_(e);
+    ret(e);
+    /* open(rdi = path, esi = mode) -> rax handle */
+    lbl_here(e, h->open);
+    FRAME32(e);
+    st_d(e, 5, -16, 7);               /* path */
+    st32(e, 5, -28, 6);               /* mode (clear of the f slot) */
+    lea_rip(e, 0, SEG_RO, h->modetab);
+    movsx32m(e, 1, 5, -28);           /* rcx = (i64)mode */
+    lea_s4(e, 6, 0, 1, 0);            /* rsi = &modes[mode] */
+    ld_d(e, 7, 5, -16);
+    call_c(e, "fopen");
+    test_rr(e, 0, 0);
+    int fail = lbl_new(e);
+    jcc_l(e, 0x84, fail);             /* jz */
+    st_d(e, 5, -24, 0);               /* f */
+    tc2(e, 0x31, 0xC9);
+    int scan = lbl_new(e), found = lbl_new(e), full = lbl_new(e);
+    lbl_here(e, scan);
+    ld_s8(e, 2, R_CTX, 1, CTX_FILES);
+    test_rr(e, 2, 2);
+    jcc_l(e, 0x84, found);            /* jz -> free slot */
+    alu_ri(e, 0, 1, 1);
+    mov_ri32(e, 2, 256);
+    alu_rr(e, 7, 1, 2);               /* cmp rcx, 256 */
+    jcc_l(e, 0x82, scan);             /* jb */
+    jmp_l(e, full);
+    lbl_here(e, full);
+    ld_d(e, 7, 5, -24);
+    call_c(e, "fclose");
+    mov_ri64(e, 0, (uint64_t)-1);
+    leave_(e);
+    ret(e);
+    lbl_here(e, found);
+    ld_d(e, 2, 5, -24);
+    st_s8(e, R_CTX, 1, CTX_FILES, 2);
+    ld_d(e, 2, 5, -24);               /* reload f (paranoia) */
+    movzx8(e, 2, 5, -28);             /* mode byte from dword slot */
+    /* fmodes[rcx] = mode: st8 with index needs base+idx+disp byte store */
+    R(e, 0, 2, R_CTX); tc(e, 0x88); MR(e, 2, 2, 4);
+    tc(e, (unsigned char)((1 & 7) << 3) | (R_CTX & 7));
+    tc4u(e, CTX_FMODES);
+    lea_rd(e, 0, 1, 1);               /* rax = rcx+1 */
+    leave_(e);
+    ret(e);
+    /* close(rdi = h) -> rax 0/-1 */
+    lbl_here(e, h->close);
+    FRAME32(e);
+    st_d(e, 5, -16, 7);
+    test_rr(e, 7, 7);
+    int d1 = lbl_new(e);
+    jcc_l(e, 0x85, d1);
+    call_l(e, h->die_closebad);
+    lbl_here(e, d1);
+    mov_ri32(e, 1, 1);
+    alu_rr(e, 7, 7, 1);               /* cmp h,1 */
+    int d2 = lbl_new(e);
+    jcc_l(e, 0x8D, d2);               /* jge */
+    call_l(e, h->die_closebad);
+    lbl_here(e, d2);
+    mov_ri32(e, 1, 256);
+    alu_rr(e, 7, 7, 1);
+    int d3 = lbl_new(e);
+    jcc_l(e, 0x8E, d3);               /* jle */
+    call_l(e, h->die_closebad);
+    lbl_here(e, d3);
+    lea_rd(e, 1, 7, -1);              /* rcx = h-1 */
+    ld_s8(e, 0, R_CTX, 1, CTX_FILES);
+    test_rr(e, 0, 0);
+    int d4 = lbl_new(e);
+    jcc_l(e, 0x85, d4);
+    call_l(e, h->die_closebad);
+    lbl_here(e, d4);
+    st_d(e, 5, -24, 0);               /* f */
+    tc2(e, 0x31, 0xD2);
+    st_s8(e, R_CTX, 1, CTX_FILES, 2);
+    /* if (ctx->in == f) in = stdin */
+    ld_d(e, 2, R_CTX, CTX_IN);
+    ld_d(e, 0, 5, -24);
+    alu_rr(e, 7, 2, 0);
+    int s1 = lbl_new(e);
+    jcc_l(e, 0x85, s1);
+    libc_data(e, 0, "stdin");
+    ld_d(e, 2, 0, 0);
+    st_d(e, R_CTX, CTX_IN, 2);
+    lbl_here(e, s1);
+    ld_d(e, 2, R_CTX, CTX_OUT);
+    ld_d(e, 0, 5, -24);
+    alu_rr(e, 7, 2, 0);
+    int s2 = lbl_new(e);
+    jcc_l(e, 0x85, s2);
+    libc_data(e, 0, "stdout");
+    ld_d(e, 2, 0, 0);
+    st_d(e, R_CTX, CTX_OUT, 2);
+    lbl_here(e, s2);
+    ld_d(e, 7, 5, -24);
+    call_c(e, "fclose");
+    test_rr(e, 0, 0);
+    int cok = lbl_new(e);
+    jcc_l(e, 0x84, cok);              /* jz ok */
+    mov_ri64(e, 0, (uint64_t)-1);
+    leave_(e);
+    ret(e);
+    lbl_here(e, cok);
+    tc2(e, 0x31, 0xC0);
+    leave_(e);
+    ret(e);
+    /* use(rdi = h) */
+    lbl_here(e, h->use);
+    FRAME32(e);
+    st_d(e, 5, -16, 7);
+    test_rr(e, 7, 7);
+    int u0 = lbl_new(e);
+    jcc_l(e, 0x85, u0);               /* jnz */
+    libc_data(e, 0, "stdin");
+    ld_d(e, 2, 0, 0);
+    st_d(e, R_CTX, CTX_IN, 2);
+    libc_data(e, 0, "stdout");
+    ld_d(e, 2, 0, 0);
+    st_d(e, R_CTX, CTX_OUT, 2);
+    leave_(e);
+    ret(e);
+    lbl_here(e, u0);
+    mov_ri32(e, 1, 1);
+    alu_rr(e, 7, 7, 1);
+    int u1 = lbl_new(e);
+    jcc_l(e, 0x8D, u1);               /* jge */
+    call_l(e, h->die_usebad);
+    lbl_here(e, u1);
+    mov_ri32(e, 1, 256);
+    alu_rr(e, 7, 7, 1);
+    int u2 = lbl_new(e);
+    jcc_l(e, 0x8E, u2);
+    call_l(e, h->die_usebad);
+    lbl_here(e, u2);
+    lea_rd(e, 1, 7, -1);
+    ld_s8(e, 2, R_CTX, 1, CTX_FILES);
+    test_rr(e, 2, 2);
+    int u3 = lbl_new(e);
+    jcc_l(e, 0x85, u3);
+    call_l(e, h->die_usebad);
+    lbl_here(e, u3);
+    st_d(e, 5, -24, 2);               /* f */
+    /* r8 = fmodes[h-1] */
+    R(e, 0, 0, R_CTX); tc2(e, 0x0F, 0xB6); MR(e, 2, 0, 4);
+    tc(e, (unsigned char)((1 & 7) << 3) | (R_CTX & 7));
+    tc4u(e, CTX_FMODES);
+    movsxd_rr(e, 8, 0);
+    /* mode 2: in = f, out = f */
+    mov_ri32(e, 1, 2);
+    alu_rr(e, 7, 8, 1);
+    int m2 = lbl_new(e);
+    jcc_l(e, 0x85, m2);               /* jne */
+    ld_d(e, 2, 5, -24);
+    st_d(e, R_CTX, CTX_IN, 2);
+    ld_d(e, 2, 5, -24);
+    st_d(e, R_CTX, CTX_OUT, 2);
+    leave_(e);
+    ret(e);
+    lbl_here(e, m2);
+    mov_ri32(e, 1, 0);
+    alu_rr(e, 7, 8, 1);
+    int m0 = lbl_new(e);
+    jcc_l(e, 0x85, m0);               /* jne */
+    ld_d(e, 2, 5, -24);
+    st_d(e, R_CTX, CTX_IN, 2);
+    leave_(e);
+    ret(e);
+    lbl_here(e, m0);
+    ld_d(e, 2, 5, -24);
+    st_d(e, R_CTX, CTX_OUT, 2);
+    leave_(e);
+    ret(e);
+    /* flush() */
+    lbl_here(e, h->flush);
+    FRAME32(e);
+    ld_d(e, 7, R_CTX, CTX_OUT);
+    call_c(e, "fflush");
+    test_rr(e, 0, 0);
+    int fl = lbl_new(e);
+    jcc_l(e, 0x84, fl);
+    call_l(e, h->die_flushfail);
+    lbl_here(e, fl);
+    leave_(e);
+    ret(e);
+    /* seek(rdi = off) -> rax */
+    lbl_here(e, h->seek);
+    FRAME32(e);
+    st_d(e, 5, -16, 7);               /* off */
+    st_zero64(e, 5, -24);             /* any = 0 */
+    st_zero64(e, 5, -32);             /* bad = 0 */
+    /* in stream */
+    ld_d(e, 7, R_CTX, CTX_IN);
+    libc_data(e, 1, "stdin");
+    ld_d(e, 2, 1, 0);
+    alu_rr(e, 7, 7, 2);
+    int k1 = lbl_new(e);
+    jcc_l(e, 0x84, k1);               /* je skip */
+    ld_d(e, 7, R_CTX, CTX_IN);
+    ld_d(e, 6, 5, -16);
+    tc2(e, 0x31, 0xD2);
+    call_c(e, "fseek");
+    test_rr(e, 0, 0);
+    int kb1 = lbl_new(e);
+    jcc_l(e, 0x84, kb1);
+    mov_ri64(e, 0, 1);
+    st_d(e, 5, -32, 0);
+    lbl_here(e, kb1);
+    mov_ri64(e, 0, 1);
+    st_d(e, 5, -24, 0);
+    lbl_here(e, k1);
+    /* out stream */
+    ld_d(e, 7, R_CTX, CTX_OUT);
+    ld_d(e, 2, R_CTX, CTX_IN);
+    alu_rr(e, 7, 7, 2);
+    int k2 = lbl_new(e);
+    jcc_l(e, 0x84, k2);               /* je (same file) */
+    libc_data(e, 1, "stdout");
+    ld_d(e, 2, 1, 0);
+    mov_rr(e, 7, 0);                  /* save out */ /* fix below */
+    ld_d(e, 7, R_CTX, CTX_OUT);
+    alu_rr(e, 7, 7, 2);
+    int k3 = lbl_new(e);
+    jcc_l(e, 0x84, k3);               /* je stdout */
+    ld_d(e, 7, R_CTX, CTX_OUT);
+    ld_d(e, 6, 5, -16);
+    tc2(e, 0x31, 0xD2);
+    call_c(e, "fseek");
+    test_rr(e, 0, 0);
+    int kb2 = lbl_new(e);
+    jcc_l(e, 0x84, kb2);
+    mov_ri64(e, 0, 1);
+    st_d(e, 5, -32, 0);
+    lbl_here(e, kb2);
+    mov_ri64(e, 0, 1);
+    st_d(e, 5, -24, 0);
+    lbl_here(e, k3);
+    lbl_here(e, k2);
+    /* any? */
+    ld_d(e, 0, 5, -24);
+    test_rr(e, 0, 0);
+    int kany = lbl_new(e);
+    jcc_l(e, 0x85, kany);
+    call_l(e, h->die_seekstreams);
+    lbl_here(e, kany);
+    ld_d(e, 0, 5, -32);
+    test_rr(e, 0, 0);
+    int kbad = lbl_new(e);
+    jcc_l(e, 0x85, kbad);              /* jnz -> bad */
+    tc2(e, 0x31, 0xC0);
+    leave_(e);
+    ret(e);
+    lbl_here(e, kbad);
+    mov_ri64(e, 0, (uint64_t)-1);
+    leave_(e);
+    ret(e);
+    /* system(rdi = cmd) -> rax */
+    lbl_here(e, h->system);
+    FRAME32(e);
+    call_c(e, "system");
+    movsxd_rr(e, 0, 0);
+    leave_(e);
+    ret(e);
+    /* stop(rdi = code) */
+    lbl_here(e, h->stop);
+    FRAME32(e);
+    mov_ri32(e, 6, 0xFF);
+    alu_rr(e, 4, 7, 6);
+    call_c(e, "exit");
+    /* spawn(rdi = CrFnRef*) */
+    lbl_here(e, h->spawn);
+    FRAME32(e);
+    st_d(e, 5, -16, 7);
+    mov32m(e, 1, 7, 8);               /* ecx = np */
+    test_rr(e, 1, 1);
+    int sp1 = lbl_new(e);
+    jcc_l(e, 0x84, sp1);
+    call_l(e, h->die_spawnpar);
+    lbl_here(e, sp1);
+    movsx32m(e, 0, R_CTX, CTX_NSPAWN);
+    mov_ri32(e, 1, 256);
+    alu_rr(e, 7, 0, 1);
+    int sp2 = lbl_new(e);
+    jcc_l(e, 0x82, sp2);              /* jb */
+    call_l(e, h->die_spawnmax);
+    lbl_here(e, sp2);
+    lea_s8(e, 7, R_CTX, 0, CTX_SPAWNED);
+    tc2(e, 0x31, 0xF6);
+    lea_rip_lbl(e, 2, h->thread_main);
+    ld_d(e, 1, 5, -16);
+    call_c(e, "pthread_create");
+    test_rr(e, 0, 0);
+    int sp3 = lbl_new(e);
+    jcc_l(e, 0x84, sp3);
+    call_l(e, h->die_threadfail);
+    lbl_here(e, sp3);
+    /* nspawn++ */
+    R(e, 0, 0, R_CTX); tc(e, 0xFF); MR(e, 2, 0, R_CTX); tc4u(e, CTX_NSPAWN);
+    leave_(e);
+    ret(e);
+    /* thread_main(rdi = ref): new thread root */
+    lbl_here(e, h->thread_main);
+    push_r(e, 5); mov_rr(e, 5, 4);
+    push_r(e, 3);
+    sub_rsp(e, 40);
+    mov_rr(e, 3, 7);                  /* rbx = ref */
+    mov_ri32(e, 7, CTX_SIZE);
+    call_c(e, "malloc");
+    test_rr(e, 0, 0);
+    int tm1 = lbl_new(e);
+    jcc_l(e, 0x85, tm1);
+    call_l(e, h->die_oom);
+    lbl_here(e, tm1);
+    mov_rr(e, R_CTX, 0);
+    mov_rr(e, 7, 0);
+    tc2(e, 0x31, 0xF6);
+    mov_ri32(e, 2, CTX_SIZE);
+    movsxd_rr(e, 2, 2);
+    call_c(e, "memset");
+    libc_data(e, 0, "stdin");
+    ld_d(e, 1, 0, 0);
+    st_d(e, R_CTX, CTX_IN, 1);
+    libc_data(e, 0, "stdout");
+    ld_d(e, 1, 0, 0);
+    st_d(e, R_CTX, CTX_OUT, 1);
+    ld_d(e, 0, 3, 0);                  /* rax = fn from ref */
+    R(e, 1, 0, 0); tc(e, 0xFF); MR(e, 3, 2, 0); /* call *rax */
+    /* lock incq done_ctr */
+    tc(e, 0xF0); R(e, 1, 0, 5); tc(e, 0xFF); MR(e, 0, 0, 5);
+    fix_add(e, 1, e->text.len, 0, SEG_DATA, h->done_ctr);
+    tc4u(e, 0);
+    mov_rr(e, 7, R_CTX);
+    call_c(e, "free");
+    tc2(e, 0x31, 0xC0);
+    add_rsp(e, 40);
+    pop_r(e, 3);
+    pop_r(e, 5);
+    ret(e);
+    /* wait() -> rax */
+    lbl_here(e, h->wait);
+    FRAME32(e);
+    int wspin = lbl_new(e);
+    lbl_here(e, wspin);
+    mov_rip(e, 0, SEG_DATA, h->done_ctr);
+    movsx32m(e, 2, R_CTX, CTX_NSPAWN);/* rdx = nspawn */
+    alu_rr(e, 7, 0, 2);               /* cmp done, nspawn */
+    int wjoins = lbl_new(e);
+    jcc_l(e, 0x8D, wjoins);           /* jge -> all done */
+    call_c(e, "sched_yield");
+    jmp_l(e, wspin);
+    lbl_here(e, wjoins);
+    tc2(e, 0x31, 0xC9);               /* ecx = i */
+    int wloop = lbl_new(e), wdone = lbl_new(e);
+    lbl_here(e, wloop);
+    movsx32m(e, 0, R_CTX, CTX_NSPAWN);/* rax = nspawn */
+    mov_rr(e, 2, 0);                  /* rdx = nspawn */
+    alu_rr(e, 7, 1, 2);               /* cmp rcx, rdx */
+    jcc_l(e, 0x8D, wdone);            /* jge done */
+    mov_rr(e, 0, 1);
+    movsxd_rr(e, 0, 0);
+    ld_s8(e, 7, R_CTX, 0, CTX_SPAWNED);
+    tc2(e, 0x31, 0xF6);
+    call_c(e, "pthread_join");
+    R(e, 0, 0, 1); tc(e, 0xFF); MR(e, 3, 0, 1);     /* inc ecx */
+    jmp_l(e, wloop);
+    lbl_here(e, wdone);
+    st32(e, R_CTX, CTX_NSPAWN, 0);
+    /* done_ctr = 0 */
+    tc2(e, 0x31, 0xC0);
+    R(e, 1, 0, 5); tc(e, 0x89); MR(e, 0, 0, 5);
+    fix_add(e, 1, e->text.len, 0, SEG_DATA, h->done_ctr);
+    tc4u(e, 0);
+    leave_(e);
+    ret(e);
+}
+
+static void h_all(XE* e, H* h) {
+    h_die(e, h);
+    h_die_range(e, h);
+    h_msg_dies(e, h);
+    h_stack(e, h);
+    h_focus(e, h);
+    h_heap(e, h);
+    h_conv(e, h);
+    h_out(e, h);
+    out_f_body(e, h, h->out_f64, 17, 0);
+    out_f_body(e, h, h->out_f32, 9, 1);
+    h_io(e, h);
+}
+
+/* ------------------------------------------------------------------ */
+/* codegen                                                             */
+/* ------------------------------------------------------------------ */
+
+typedef struct FnGen {
+    XE* e;
+    H* h;
+    ProgramIR* p;
+    FnIR* fn;
+    int release;
+    int* slot;      /* local idx -> rbp offset (negative) */
+    int fh0off;
+    int epilogue;
+    Type* cur;      /* static type of wv, tracked like be_c */
+    int lblbase;    /* IR label ids are offset per function */
+    int fi;         /* program fn index */
+    int* flabels;   /* program fn idx -> text label */
+    size_t* glob_off; /* global idx -> .data/.bss offset (bss via data tail) */
+    size_t* str_off;  /* string idx -> .data offset */
+    size_t* ref_off;  /* fn idx -> fnref .data offset */
+    size_t ro_2p63;
+} FnGen;
+
+static uint64_t addr_elem_size(const Type* addr) {
+    const Type* pt = addr->inner;
+    if (pt->kind == TY_ARR) return type_size(pt->inner);
+    return type_size(pt);
+}
+
+static int ty_width(const Type* t) { return ty_int_bits(t); }
+
+/* truncate rax (64-bit) to width w with sign extension if signed */
+static void trunc_rax_w(FnGen* g, int w, int signed_) {
+    XE* e = g->e;
+    (void)signed_;
+    if (w >= 64) return;
+    if (w == 8) { R(e, 0, 0, 0); tc2(e, 0x0F, 0xBE); MR(e, 3, 0, 0); }
+    else if (w == 16) { R(e, 0, 0, 0); tc2(e, 0x0F, 0xBF); MR(e, 3, 0, 0); }
+    else if (w == 32) { R(e, 1, 0, 0); tc(e, 0x63); MR(e, 3, 0, 0); }
+    else { /* 1..7 bytes: mask */ }
+}
+static void truncu_rax_w(FnGen* g, int w) {
+    XE* e = g->e;
+    if (w >= 64) return;
+    if (w == 8) { R(e, 0, 0, 0); tc2(e, 0x0F, 0xB6); MR(e, 3, 0, 0); }
+    else if (w == 16) { R(e, 0, 0, 0); tc2(e, 0x0F, 0xB7); MR(e, 3, 0, 0); }
+    else if (w == 32) { R(e, 0, 0, 0); tc(e, 0x89); MR(e, 3, 0, 0); }
+    else { /* 1..7 bytes: mask */ }
+}
+
+/* rax (raw Val) -> typed store at [base+off] */
+static void store_typed(FnGen* g, int base, int32_t off, Type* t) {
+    XE* e = g->e;
+    switch (t->kind) {
+    case TY_BOOL: case TY_CHAR: case TY_INT8: case TY_UINT8:
+        st8(e, base, off, 0); break;
+    case TY_INT16: case TY_UINT16:
+        st16(e, base, off, 0); break;
+    case TY_INT32: case TY_UINT32:
+        st32(e, base, off, 0); break;
+    case TY_FLOAT32:
+        /* rax holds double bits; narrow */
+        movq_xr(e, 0, 0);
+        cvtsd2ss(e, 0, 0);
+        tc(e, 0xF3); R(e, 0, 0, base); tc2(e, 0x0F, 0x11); MR(e, 2, 0, base); tc4u(e, (uint32_t)off);
+        break;
+    case TY_FLOAT64:
+        movq_xr(e, 0, 0);
+        tc(e, 0xF2); R(e, 0, 0, base); tc2(e, 0x0F, 0x11); MR(e, 2, 0, base); tc4u(e, (uint32_t)off);
+        break;
+    default:
+        st_d(e, base, off, 0); break; /* 64-bit ints, addr, func */
+    }
+}
+
+/* typed load [base+off] -> r15 (floats widened to double bits) */
+static void load_typed(FnGen* g, int base, int32_t off, Type* t) {
+    XE* e = g->e;
+    switch (t->kind) {
+    case TY_INT8: movsx8(e, R_WV, base, off); break;
+    case TY_INT16: movsx16(e, R_WV, base, off); break;
+    case TY_INT32: movsx32m(e, R_WV, base, off); break;
+    case TY_BOOL: case TY_CHAR: case TY_UINT8: movzx8(e, R_WV, base, off); break;
+    case TY_UINT16: movzx16(e, R_WV, base, off); break;
+    case TY_UINT32: mov32m(e, R_WV, base, off); break;
+    case TY_FLOAT32:
+        tc(e, 0xF3); R(e, 0, R_WV, base); tc2(e, 0x0F, 0x10); MR(e, 2, R_WV, base); tc4u(e, (uint32_t)off);
+        cvtss2sd(e, R_WV, R_WV);
+        movq_rx(e, R_WV, R_WV);
+        break;
+    case TY_FLOAT64:
+        tc(e, 0xF2); R(e, 0, R_WV, base); tc2(e, 0x0F, 0x10); MR(e, 2, R_WV, base); tc4u(e, (uint32_t)off);
+        movq_rx(e, R_WV, R_WV);
+        break;
+    default:
+        ld_d(e, R_WV, base, off); break;
+    }
+}
+
+/* truncate r15 in place to integer type t (wrap), like (T) in be_c */
+static void wrap_wv(FnGen* g, Type* t) {
+    XE* e = g->e;
+    int w = ty_int_bits(t);
+    if (w == 64) return;
+    int signed_ = ty_is_signed_int(t);
+    if (w == 8) {
+        if (signed_) { R(e, 1, R_WV, R_WV); tc2(e, 0x0F, 0xBE); MR(e, 3, R_WV, R_WV); }
+        else { R(e, 0, R_WV, R_WV); tc2(e, 0x0F, 0xB6); MR(e, 3, R_WV, R_WV); }
+    } else if (w == 16) {
+        if (signed_) { R(e, 1, R_WV, R_WV); tc2(e, 0x0F, 0xBF); MR(e, 3, R_WV, R_WV); }
+        else { R(e, 0, R_WV, R_WV); tc2(e, 0x0F, 0xB7); MR(e, 3, R_WV, R_WV); }
+    } else if (w == 32) {
+        if (signed_) { R(e, 1, R_WV, R_WV); tc(e, 0x63); MR(e, 3, R_WV, R_WV); }
+        else { R(e, 0, R_WV, R_WV); tc(e, 0x89); MR(e, 3, R_WV, R_WV); }
+    }
+}
+
+static void load_b_reg(FnGen* g, Insn* in, int isfloat) {
+    /* operand b: imm (IV_*) or popped; lands in rbx */
+    XE* e = g->e;
+    if (in->b == 1) {
+        uint64_t bits;
+        if (in->imm.kind == IV_FLOAT) {
+            double d = in->imm.d;
+            memcpy(&bits, &d, 8);
+        } else if (isfloat) {
+            double d = in->imm.kind == IV_INT ? (double)(int64_t)in->imm.u : (double)in->imm.u;
+            memcpy(&bits, &d, 8);
+        } else {
+            bits = in->imm.u;
+        }
+        if (bits <= 0xFFFFFFFFULL) mov_ri32(e, 3, (int32_t)bits);
+        else mov_ri64(e, 3, bits);
+    } else {
+        call_l(e, g->h->pop);
+        mov_rr(e, 3, 0);
+    }
+}
+
+/* comparison state -> r12d from flags of cmp r15,rax (signed) etc. */
+static void cmp_combine_signed(FnGen* g) {
+    XE* e = g->e;
+    setcc_r(e, 0x0C, 1);               /* setl cl  (wv < b) */
+    setcc_r(e, 0x0F, 2);               /* setg dl  (wv > b) */
+    mov_rr(e, R_CMP, 2);
+    R(e, 0, 0, R_CMP); tc(e, 0x29); MR(e, 3, 1, R_CMP); /* sub r12d, ecx */
+}
+static void cmp_combine_unsigned(FnGen* g) {
+    XE* e = g->e;
+    setcc_r(e, 0x02, 1);               /* setb cl */
+    setcc_r(e, 0x07, 2);               /* seta dl */
+    mov_rr(e, R_CMP, 2);
+    R(e, 0, 0, R_CMP); tc(e, 0x29); MR(e, 3, 1, R_CMP);
+}
+
+static double imm_f64(ImmV* imm) {
+    if (imm->kind == IV_FLOAT) return imm->d;
+    if (imm->kind == IV_INT) return (double)(int64_t)imm->u;
+    return (double)imm->u;
+}
+
+/* ------------------------------------------------------------------ */
+/* per-instruction codegen                                             */
+/* ------------------------------------------------------------------ */
+
+static void gen_insn(FnGen* g, Insn* in) {
+    XE* e = g->e;
+    H* h = g->h;
+    FnIR* fn = g->fn;
+    switch (in->op) {
+    case I_LBL:
+        lbl_here(e, in->a + g->lblbase);
+        return;
+    case I_JMP:
+        jmp_l(e, in->a + g->lblbase);
+        return;
+    case I_JMPCOND: {
+        int cc = in->b == CMP_LESS ? 0x8C : in->b == CMP_EQUAL ? 0x84 :
+                 in->b == CMP_GREATER ? 0x8F : in->b == CMP_DIFFERENT ? 0x85 :
+                 in->b == CMP_LESS_OR_EQUAL ? 0x8E : 0x8D;
+        /* test r12d, r12d; jcc */
+        tc3(e, 0x45, 0x85, 0xE4);
+        jcc_l(e, cc, in->a + g->lblbase);
+        return;
+    }
+    case I_MOVE_LOCAL:
+        ld_d(e, 0, 5, g->fh0off);
+        st_d(e, R_CTX, CTX_FHSP, 0);
+        lea_rd(e, R_F, 5, g->slot[in->a]);
+        return;
+    case I_MOVE_GLOBAL:
+        ld_d(e, 0, 5, g->fh0off);
+        st_d(e, R_CTX, CTX_FHSP, 0);
+        lea_rip(e, R_F, SEG_DATA, g->glob_off[in->a]);
+        return;
+    case I_MOVE_LIT: {
+        uint64_t esz = type_size(in->t);
+        if (in->b < 0 && !g->release) {
+            mov_rr(e, 7, R_F);
+            mov_ri64(e, 6, in->imm.u);
+            mov_ri32(e, 2, (int32_t)esz);
+            call_l(e, h->chk_block);
+        }
+        mov_rr(e, 7, R_F);
+        call_l(e, h->fpush);
+        lea_rd(e, R_F, R_F, (int32_t)(in->imm.u * esz));
+        return;
+    }
+    case I_MOVE_INDEX: {
+        uint64_t esz = type_size(in->t);
+        if (in->b > 0 && !g->release) {
+            mov_rr(e, 7, R_WV);
+            mov_ri32(e, 6, in->b);
+            call_l(e, h->chk_range);
+        } else if (in->b < 0 && !g->release) {
+            mov_rr(e, 7, R_F);
+            mov_rr(e, 6, R_WV);
+            mov_ri32(e, 2, (int32_t)esz);
+            call_l(e, h->chk_block);
+        }
+        mov_rr(e, 7, R_F);
+        call_l(e, h->fpush);
+        imul_ri(e, 1, R_WV, (int32_t)esz);
+        R(e, 1, 0, R_F); tc(e, 0x01); MR(e, 3, 1, R_F); /* add r13, rcx */
+        return;
+    }
+    case I_MOVE_AT:
+        mov_rr(e, 7, R_F);
+        call_l(e, h->fpush);
+        mov_rr(e, R_F, R_WV);
+        return;
+    case I_BACK: {
+        ld_d(e, 0, R_CTX, CTX_FHSP);
+        ld_d(e, 1, 5, g->fh0off);
+        alu_rr(e, 7, 0, 1);           /* cmp fh_sp, fh0 */
+        int noback = lbl_new(e), done = lbl_new(e);
+        jcc_l(e, 0x86, noback);       /* jbe */
+        call_l(e, h->fpop);
+        mov_rr(e, R_F, 0);
+        jmp_l(e, done);
+        lbl_here(e, noback);
+        xor_rr(e, R_F, R_F);
+        lbl_here(e, done);
+        return;
+    }
+    case I_HOME:
+        ld_d(e, 0, 5, g->fh0off);
+        st_d(e, R_CTX, CTX_FHSP, 0);
+        xor_rr(e, R_F, R_F);
+        return;
+    case I_ADDR_HERE:
+        mov_rr(e, R_WV, R_F);
+        g->cur = in->t;
+        return;
+    case I_ADDR_LOCAL:
+        lea_rd(e, R_WV, 5, g->slot[in->a]);
+        g->cur = in->t;
+        return;
+    case I_ADDR_GLOBAL:
+        lea_rip(e, R_WV, SEG_DATA, g->glob_off[in->a]);
+        g->cur = in->t;
+        return;
+    case I_ADDR_FN:
+        lea_rip(e, R_WV, SEG_DATA, g->ref_off[in->a]);
+        g->cur = ty_func();
+        return;
+    case I_MAKE_LOCAL: {
+        Type* t = fn->locals[in->a].type;
+        int off = g->slot[in->a];
+        if (t->kind == TY_ARR) {
+            lea_rd(e, 7, 5, off);
+            tc2(e, 0x31, 0xF6);
+            mov_ri32(e, 2, (int32_t)type_size(t));
+            movsxd_rr(e, 2, 2);
+            call_c(e, "memset");
+        } else {
+            st_zero64(e, 5, off);
+        }
+        return;
+    }
+    case I_MAKE_ANON:
+        mov_ri32(e, 7, (int32_t)type_size(in->t));
+        movsxd_rr(e, 7, 7);
+        call_l(e, h->alloc);
+        mov_rr(e, R_WV, 0);
+        g->cur = ty_addr_to(in->t);
+        return;
+    case I_MAKE_DYN: {
+        uint64_t esz = type_size(in->t);
+        uint64_t maxn = ((uint64_t)1 << 44) / (esz ? esz : 1);
+        if (in->b == 1) {
+            uint64_t n = in->imm.u;
+            if (n > maxn) { /* compile-time provable */ }
+            mov_ri32(e, 7, (int32_t)n);
+            movsxd_rr(e, 7, 7);
+        } else if (in->b == 0) {
+            Type* st = fn->locals[in->a].type;
+            load_typed(g, 5, g->slot[in->a], st);
+            mov_rr(e, 7, R_WV);
+            movsxd_rr(e, 7, 7);
+        } else {
+            Type* gt = g->p->globs[in->a].type;
+            lea_rip(e, 0, SEG_DATA, g->glob_off[in->a]);
+            load_typed(g, 0, 0, gt);
+            mov_rr(e, 7, R_WV);
+        }
+        mov_ri64(e, 6, maxn);
+        alu_rr(e, 7, 7, 6);           /* cmp n, maxn */
+        int dynok = lbl_new(e);
+        jcc_l(e, 0x86, dynok);        /* jbe */
+        call_l(e, h->die_toolarge);
+        lbl_here(e, dynok);
+        imul_ri(e, 7, 7, (int32_t)esz);
+        call_l(e, h->alloc);
+        mov_rr(e, R_WV, 0);
+        g->cur = ty_addr_to(ty_array(in->t, 0));
+        return;
+    }
+    case I_MAKE_STRLOCAL: {
+        LocalIR* l = &fn->locals[in->a];
+        size_t sl = l->str_initlen ? l->str_initlen : 1;
+        size_t soff = ro_put(e, l->str_init ? l->str_init : "", sl);
+        lea_rd(e, 7, 5, g->slot[in->a]);
+        lea_rip(e, 6, SEG_RO, soff);
+        mov_ri32(e, 2, (int32_t)sl);
+        movsxd_rr(e, 2, 2);
+        call_c(e, "memcpy");
+        return;
+    }
+    case I_MAKE_STRANON:
+        lea_rip(e, R_WV, SEG_DATA, g->str_off[in->a]);
+        g->cur = in->t;
+        return;
+    case I_BIND_LOCAL:
+        st_d(e, 5, g->slot[in->a], R_WV);
+        return;
+    case I_FREE:
+        mov_rr(e, 7, R_WV);
+        call_l(e, h->free_);
+        return;
+    case I_LOAD:
+        load_typed(g, R_F, 0, in->t);
+        g->cur = in->t;
+        return;
+    case I_STORE: {
+        Type* t = in->t;
+        switch (in->b) {
+        case SC_EXACT:
+            if (t->kind == TY_FLOAT32) {
+                movq_xr(e, 0, R_WV);
+                cvtsd2ss(e, 0, 0);
+                tc(e, 0xF3); R(e, 0, 0, R_F); tc2(e, 0x0F, 0x11); MR(e, 2, 0, R_F); tc4u(e, 0);
+            } else if (t->kind == TY_FLOAT64) {
+                movq_xr(e, 0, R_WV);
+                tc(e, 0xF2); R(e, 0, 0, R_F); tc2(e, 0x0F, 0x11); MR(e, 2, 0, R_F); tc4u(e, 0);
+            } else {
+                mov_rr(e, 0, R_WV);
+                store_typed(g, R_F, 0, t);
+            }
+            break;
+        case SC_INTLIT:
+            mov_rr(e, 0, R_WV);
+            store_typed(g, R_F, 0, t);
+            break;
+        case SC_INTLIT_BOOL:
+            st8(e, R_F, 0, R_WV);
+            break;
+        case SC_INTLIT_FLOAT:
+            movq_xr(e, 0, R_WV);
+            if (t->kind == TY_FLOAT32) {
+                cvtsd2ss(e, 0, 0);
+                tc(e, 0xF3); R(e, 0, 0, R_F); tc2(e, 0x0F, 0x11); MR(e, 2, 0, R_F); tc4u(e, 0);
+            } else {
+                tc(e, 0xF2); R(e, 0, 0, R_F); tc2(e, 0x0F, 0x11); MR(e, 2, 0, R_F); tc4u(e, 0);
+            }
+            break;
+        case SC_F32:
+            movq_xr(e, 0, R_WV);
+            cvtsd2ss(e, 0, 0);
+            tc(e, 0xF3); R(e, 0, 0, R_F); tc2(e, 0x0F, 0x11); MR(e, 2, 0, R_F); tc4u(e, 0);
+            break;
+        }
+        return;
+    }
+    case I_SET: {
+        if (in->imm.kind == IV_FLOAT) {
+            double d = in->imm.d;
+            uint64_t bits;
+            memcpy(&bits, &d, 8);
+            mov_ri64(e, R_WV, bits);
+        } else if (in->imm.u <= 0xFFFFFFFFULL) {
+            mov_ri32(e, R_WV, (int32_t)in->imm.u);
+        } else {
+            mov_ri64(e, R_WV, in->imm.u);
+        }
+        g->cur = in->t;
+        return;
+    }
+    case I_ADD: case I_SUB: case I_MUL: case I_DIV: case I_MOD: {
+        Type* t = in->t;
+        int isfloat = ty_is_float(t);
+        int opext = in->op == I_ADD ? 0 : in->op == I_SUB ? 5 :
+                    in->op == I_MUL ? -1 : 7; /* mul/div/mod special */
+        if (isfloat) {
+            load_b_reg(g, in, 1);        /* rbx = b bits */
+            movq_xr(e, 0, R_WV);
+            movq_xr(e, 1, 3);
+            if (in->op == I_ADD) addsd(e, 0, 1);
+            else if (in->op == I_SUB) subsd(e, 0, 1);
+            else if (in->op == I_MUL) mulsd(e, 0, 1);
+            else if (in->op == I_DIV) divsd(e, 0, 1);
+            else {
+                /* fmod(x, y) */
+                call_c(e, "fmod");
+            }
+            movq_rx(e, R_WV, 0);
+            if (t->kind == TY_FLOAT32) {
+                movq_xr(e, 0, R_WV);
+                cvtsd2ss(e, 0, 0);
+                cvtss2sd(e, 0, 0);
+                movq_rx(e, R_WV, 0);
+            }
+            g->cur = t;
+            return;
+        }
+        if (t->kind == TY_ADDR) {
+            uint64_t esz = addr_elem_size(t);
+            load_b_reg(g, in, 0);
+            imul_ri(e, 1, 3, (int32_t)esz);
+            if (in->op == I_ADD) R(e, 1, 0, R_WV), tc(e, 0x01), MR(e, 3, 1, R_WV);
+            else R(e, 1, 0, R_WV), tc(e, 0x29), MR(e, 3, 1, R_WV);
+            g->cur = t;
+            return;
+        }
+        if (in->a == 1 && in->op == I_SUB) {
+            /* pointer difference */
+            call_l(e, h->pop);
+            R(e, 1, 0, R_WV); tc(e, 0x29); MR(e, 3, 1, R_WV); /* sub wv, popped */
+            mov_ri32(e, 1, (int32_t)addr_elem_size(g->cur));
+            cqo(e);
+            idiv_r(e, 1);
+            mov_rr(e, R_WV, 0);
+            g->cur = ty_int(64);
+            return;
+        }
+        load_b_reg(g, in, 0);
+        if (in->op == I_DIV || in->op == I_MOD) {
+            test_rr(e, 3, 3);
+            int nz = lbl_new(e);
+            jcc_l(e, 0x85, nz);
+            call_l(e, h->die_div0);
+            lbl_here(e, nz);
+            int w = ty_int_bits(t);
+            if (ty_is_signed_int(t) || t->kind == TY_INTLIT) {
+                (void)w;
+                mov_ri64(e, 1, (uint64_t)-1);
+                alu_rr(e, 7, 3, 1);   /* cmp rbx, -1 */
+                int norm = lbl_new(e), spec = lbl_new(e), done = lbl_new(e);
+                jcc_l(e, 0x85, norm);
+                lbl_here(e, spec);
+                if (in->op == I_DIV) { mov_rr(e, 0, R_WV); neg_r(e, 0); }
+                else tc2(e, 0x31, 0xC0);
+                jmp_l(e, done);
+                lbl_here(e, norm);
+                mov_rr(e, 0, R_WV);
+                cqo(e);
+                idiv_r(e, 3);
+                if (in->op == I_MOD) { R(e, 1, 0, 2); tc(e, 0x89); MR(e, 3, 2, 0); } /* rax = rdx */
+                lbl_here(e, done);
+            } else {
+                mov_rr(e, 0, R_WV);
+                tc2(e, 0x31, 0xD2);
+                idiv_r(e, 3);
+                if (in->op == I_MOD) { R(e, 1, 0, 2); tc(e, 0x89); MR(e, 3, 2, 0); }
+            }
+            mov_rr(e, R_WV, 0);
+            wrap_wv(g, t);
+            g->cur = t;
+            return;
+        }
+        /* add/sub/mul */
+        mov_rr(e, 0, R_WV);
+        if (in->op == I_MUL) imul_r(e, 0, 3);
+        else if (in->op == I_ADD) R(e, 1, 3, 0), tc(e, 0x01), MR(e, 3, 3, 0);
+        else R(e, 1, 3, 0), tc(e, 0x29), MR(e, 3, 3, 0);
+        mov_rr(e, R_WV, 0);
+        wrap_wv(g, t);
+        g->cur = t;
+        (void)opext;
+        return;
+    }
+    case I_NEG: {
+        Type* t = in->t;
+        if (ty_is_float(t)) {
+            mov_ri64(e, 0, 0x8000000000000000ULL);
+            alu_rr(e, 6, R_WV, 0);
+        } else {
+            mov_rr(e, 0, R_WV);
+            neg_r(e, 0);
+            mov_rr(e, R_WV, 0);
+            wrap_wv(g, t);
+        }
+        return;
+    }
+    case I_PUSH:
+        mov_rr(e, 7, R_WV);
+        call_l(e, h->push);
+        return;
+    case I_POP:
+        call_l(e, h->pop);
+        mov_rr(e, R_WV, 0);
+        g->cur = in->t;
+        return;
+    case I_SWAP:
+        call_l(e, h->pop);
+        mov_rr(e, 3, 0);
+        mov_rr(e, 7, R_WV);
+        call_l(e, h->push);
+        mov_rr(e, R_WV, 3);
+        return;
+    case I_CMP: {
+        Type* t = in->t;
+        if (ty_is_float(t)) {
+            load_b_reg(g, in, 1);
+            movq_xr(e, 0, R_WV);
+            movq_xr(e, 1, 3);
+            tc2(e, 0x31, 0xC9);            /* zero before flags use */
+            tc2(e, 0x31, 0xD2);
+            ucomisd(e, 0, 1);
+            int is_nan = lbl_new(e), fin = lbl_new(e);
+            jcc_l(e, 0x8A, is_nan);   /* jp: NaN */
+            setcc_r(e, 0x02, 1);      /* setb cl: a < b */
+            setcc_r(e, 0x07, 2);      /* seta dl: a > b */
+            mov_rr(e, R_CMP, 2);
+            R(e, 0, 0, R_CMP); tc(e, 0x29); MR(e, 3, 1, R_CMP);
+            jmp_l(e, fin);
+            lbl_here(e, is_nan);
+            R(e, 0, 0, R_CMP); tc(e, 0x31); MR(e, 3, R_CMP, R_CMP);
+            lbl_here(e, fin);
+        } else if (ty_is_signed_int(t) || t->kind == TY_INTLIT) {
+            load_b_reg(g, in, 0);
+            mov_rr(e, 0, 3);
+            tc2(e, 0x31, 0xC9);            /* xor ecx,ecx */
+            tc2(e, 0x31, 0xD2);            /* xor edx,edx */
+            alu_rr(e, 7, R_WV, 0);         /* cmp wv, b */
+            cmp_combine_signed(g);
+        } else {
+            load_b_reg(g, in, 0);
+            mov_rr(e, 0, 3);
+            tc2(e, 0x31, 0xC9);
+            tc2(e, 0x31, 0xD2);
+            alu_rr(e, 7, R_WV, 0);
+            cmp_combine_unsigned(g);
+        }
+        return;
+    }
+    case I_CONV: {
+        Type* from = in->u ? in->u : g->cur;
+        Type* to = in->t;
+        if (ty_is_float(from)) {
+            if (to->kind == TY_FLOAT64) {
+                /* nothing */
+            } else if (to->kind == TY_FLOAT32) {
+                movq_xr(e, 0, R_WV);
+                cvtsd2ss(e, 0, 0);
+                cvtss2sd(e, 0, 0);
+                movq_rx(e, R_WV, 0);
+            } else if (to->kind == TY_BOOL) {
+                movq_xr(e, 0, R_WV);
+                pxor(e, 1, 1);
+                ucomisd(e, 0, 1);
+                int isnan2 = lbl_new(e), fin2 = lbl_new(e);
+                jcc_l(e, 0x8A, isnan2);
+                setcc_r(e, 0x05, R_WV); /* setne r15d */
+                jmp_l(e, fin2);
+                lbl_here(e, isnan2);
+                mov_ri32(e, R_WV, 1);
+                lbl_here(e, fin2);
+            } else {
+                int hid = to->kind == TY_INT8 ? h->d2i8 : to->kind == TY_INT16 ? h->d2i16 :
+                          to->kind == TY_INT32 ? h->d2i32 : to->kind == TY_UINT8 ? h->d2u8 :
+                          to->kind == TY_UINT16 ? h->d2u16 : to->kind == TY_UINT32 ? h->d2u32 :
+                          to->kind == TY_UINT64 ? h->d2u64 : h->d2i64;
+                movq_xr(e, 0, R_WV);
+                call_l(e, hid);
+                mov_rr(e, R_WV, 0);
+            }
+        } else if (to->kind == TY_FLOAT64 || to->kind == TY_FLOAT32) {
+            int was_signed = (from->kind == TY_INTLIT) || ty_is_signed_int(from);
+            if (was_signed) {
+                cvtsi2sd(e, 0, R_WV);
+            } else {
+                /* unsigned u64 -> double */
+                mov_rr(e, 0, R_WV);
+                R(e, 1, 0, 0); tc(e, 0x85); MR(e, 3, 0, 0); /* test rax,rax */
+                int negf = lbl_new(e), posf = lbl_new(e);
+                jcc_l(e, 0x89, negf);  /* jns */
+                cvtsi2sd(e, 0, 0);
+                jmp_l(e, posf);
+                lbl_here(e, negf);
+                mov_rr(e, 0, R_WV);
+                cvtsi2sd(e, 0, 0);
+                movsd_rip(e, 1, SEG_RO, g->ro_2p63);
+                addsd(e, 0, 1);
+                lbl_here(e, posf);
+            }
+            if (to->kind == TY_FLOAT32) {
+                cvtsd2ss(e, 0, 0);
+                cvtss2sd(e, 0, 0);
+            }
+            movq_rx(e, R_WV, 0);
+        } else if (to->kind == TY_BOOL) {
+            test_rr(e, R_WV, R_WV);
+            setcc_r(e, 0x05, R_WV);
+        } else {
+            /* int -> int: truncate and extend per target */
+            if (to->kind == TY_UINT64 || to->kind == TY_ADDR) {
+                /* raw */
+            } else {
+                wrap_wv(g, to);
+            }
+        }
+        g->cur = to;
+        return;
+    }
+    case I_CALL: {
+        FnIR* callee = &g->p->fns[in->a];
+        int np = callee->nparams;
+        static const int argregs[6] = {7, 6, 2, 1, 8, 9};
+        if (np > 0) {
+            /* r11 = stk + (sp - np) * 8 */
+            ld_d(e, 1, R_CTX, CTX_STK);       /* r11 = stk */
+            ld_d(e, 0, R_CTX, CTX_SP);        /* rax = sp */
+            mov_ri32(e, 2, np);
+            movsxd_rr(e, 2, 2);               /* rdx = np */
+            R(e, 1, 2, 0); tc(e, 0x29); MR(e, 3, 2, 0); /* sub rax, rdx */
+            lea_s8(e, 1, 1, 0, 0);            /* r11 = r11 + rax*8 */
+            for (int k = 0; k < np && k < 6; k++)
+                ld_d(e, argregs[k], 1, 8 * k);
+            for (int k = 6; k < np; k++) {
+                ld_d(e, 0, 1, 8 * k);
+                R(e, 1, 0, 4); tc(e, 0x89); MR(e, 2, 0, 4);
+                tc(e, 0x24); tc4u(e, (uint32_t)(8 * (k - 6)));
+            }
+            mov_ri32(e, 0, np);
+            movsxd_rr(e, 0, 0);
+            R(e, 1, 0, R_CTX); tc(e, 0x29); MR(e, 2, 0, R_CTX); tc4u(e, CTX_SP);
+        }
+        call_l(e, g->flabels[in->a]);
+        if (callee->result->kind != TY_NOTHING) {
+            mov_rr(e, R_WV, 0);
+            g->cur = callee->result;
+        }
+        return;
+    }
+    case I_CALLIND:
+        mov_rr(e, 3, R_WV);
+        mov32m(e, 1, 3, 8);
+        test_rr(e, 1, 1);
+        int okind = lbl_new(e);
+        jcc_l(e, 0x84, okind);
+        call_l(e, h->die_indcall);
+        lbl_here(e, okind);
+        ld_d(e, 0, 3, 0);               /* rax = fn pointer */
+        R(e, 1, 0, 0); tc(e, 0xFF); MR(e, 3, 2, 0); /* call *rax */
+        return;
+    case I_RET:
+        jmp_l(e, g->epilogue);
+        return;
+    case I_STOP: {
+        if (ty_is_float(g->cur)) {
+            movq_xr(e, 0, R_WV);
+            call_l(e, h->d2i64);
+            mov_rr(e, 7, 0);
+        } else if (g->cur->kind == TY_NOTHING || g->cur->kind == TY_FUNC) {
+            tc2(e, 0x31, 0xFF);
+        } else {
+            mov_rr(e, 7, R_WV);
+        }
+        call_l(e, h->stop);
+        return;
+    }
+    case I_INPUT:
+        call_l(e, h->input);
+        mov_rr(e, R_WV, 0);
+        g->cur = ty_int(64);
+        return;
+    case I_OUTPUT: {
+        Type* t = in->t;
+        if (ty_is_float(t)) {
+            movq_xr(e, 0, R_WV);
+            call_l(e, t->kind == TY_FLOAT32 ? h->out_f32 : h->out_f64);
+        } else if (t->kind == TY_CHAR || t->kind == TY_UINT8) {
+            mov_rr(e, 7, R_WV);
+            call_l(e, h->out_char);
+        } else if (t->kind == TY_BOOL) {
+            mov_rr(e, 7, R_WV);
+            call_l(e, h->out_bool);
+        } else if (t->kind == TY_ADDR) {
+            mov_rr(e, 7, R_WV);
+            call_l(e, h->out_addr);
+        } else if (t->kind == TY_INTLIT) {
+            if (!t->litneg && t->litval > (uint64_t)INT64_MAX) {
+                mov_rr(e, 7, R_WV);
+                call_l(e, h->out_u64);
+            } else {
+                mov_rr(e, 7, R_WV);
+                call_l(e, h->out_i64);
+            }
+        } else if (ty_is_signed_int(t)) {
+            mov_rr(e, 7, R_WV);
+            call_l(e, h->out_i64);
+        } else {
+            mov_rr(e, 7, R_WV);
+            call_l(e, h->out_u64);
+        }
+        return;
+    }
+    case I_OUTTEXT:
+        mov_rr(e, 7, R_WV);
+        call_l(e, h->out_text);
+        return;
+    case I_OPEN:
+        mov_rr(e, 7, R_WV);
+        mov_ri32(e, 6, in->b);
+        call_l(e, h->open);
+        mov_rr(e, R_WV, 0);
+        g->cur = ty_int(64);
+        return;
+    case I_CLOSE:
+        mov_rr(e, 7, R_WV);
+        call_l(e, h->close);
+        mov_rr(e, R_WV, 0);
+        g->cur = ty_int(64);
+        return;
+    case I_USE:
+        if (in->b == 1) mov_ri32(e, 7, (int32_t)in->imm.u);
+        else mov_rr(e, 7, R_WV);
+        call_l(e, h->use);
+        return;
+    case I_FLUSH:
+        call_l(e, h->flush);
+        return;
+    case I_SEEK:
+        if (in->b == 1) mov_ri32(e, 7, (int32_t)in->imm.u);
+        else mov_rr(e, 7, R_WV);
+        call_l(e, h->seek);
+        mov_rr(e, R_WV, 0);
+        g->cur = ty_int(64);
+        return;
+    case I_SYSTEM:
+        mov_rr(e, 7, R_WV);
+        call_l(e, h->system);
+        mov_rr(e, R_WV, 0);
+        g->cur = ty_int(64);
+        return;
+    case I_SPAWN:
+        lea_rip(e, 7, SEG_DATA, g->ref_off[in->a]);
+        call_l(e, h->spawn);
+        return;
+    case I_WAIT:
+        call_l(e, h->wait);
+        mov_rr(e, R_WV, 0);
+        g->cur = ty_int(64);
+        return;
+    case I_XCHG: {
+        Type* t = in->t;
+        if (t->kind == TY_FLOAT64) {
+            mov_rr(e, 0, R_WV);
+            lock_xchg(e, R_F, 0, 0, 8);
+            mov_rr(e, R_WV, 0);
+        } else if (t->kind == TY_FLOAT32) {
+            movq_xr(e, 0, R_WV);
+            cvtsd2ss(e, 0, 0);
+            movd_eax_xmm0(e);
+            lock_xchg(e, R_F, 0, 0, 4);
+            movd_xmm0_eax(e);
+            cvtss2sd(e, 0, 0);
+            movq_rx(e, R_WV, 0);
+        } else {
+            int w = ty_int_bits(t);
+            int signed_ = ty_is_signed_int(t);
+            if (w == 64) {
+                mov_rr(e, 0, R_WV);
+                lock_xchg(e, R_F, 0, 0, 8);
+                mov_rr(e, R_WV, 0);
+            } else if (w == 32) {
+                mov_rr(e, 0, R_WV);
+                lock_xchg(e, R_F, 0, 0, 4);
+                if (signed_) movsxd_rr(e, R_WV, 0);
+                else { R(e, 0, 0, 0); tc(e, 0x89); MR(e, 3, 0, R_WV); mov_rr(e, R_WV, 0); }
+            } else if (w == 16) {
+                mov_rr(e, 0, R_WV);
+                lock_xchg(e, R_F, 0, 0, 2);
+                if (signed_) movsx_ax(e, 0);
+                else movzx_ax(e, 0);
+                mov_rr(e, R_WV, 0);
+            } else {
+                mov_rr(e, 0, R_WV);
+                lock_xchg(e, R_F, 0, 0, 1);
+                if (signed_) movsx_al(e, 0);
+                else movzx_al(e, 0);
+                mov_rr(e, R_WV, 0);
+            }
+        }
+        g->cur = in->t;
+        return;
+    }
+    default:
+        return;
+    }
+}
+
+/* ------------------------------------------------------------------ */
+/* function generation                                                 */
+/* ------------------------------------------------------------------ */
+
+static void gen_fn(FnGen* g) {
+    XE* e = g->e;
+    FnIR* fn = g->fn;
+    static const int argregs[6] = {7, 6, 2, 1, 8, 9};
+
+    /* frame layout: [rbp-8..-40] saved r12,r13,r14,r15,rbx
+       locals below that, then fh0 slot; framesize ≡ 8 (mod 16) */
+    int cur = 48;
+    g->slot = xmalloc((size_t)(fn->nlocals ? fn->nlocals : 1) * sizeof(int));
+    int maxout = 0;
+    for (int i = 0; i < fn->nins; i++)
+        if (fn->ins[i].op == I_CALL) {
+            int np = g->p->fns[fn->ins[i].a].nparams;
+            if (np - 6 > maxout) maxout = np - 6;
+        }
+    int outgoing = maxout > 0 ? 8 * maxout : 0;
+    for (int i = 0; i < fn->nlocals; i++) {
+        Type* t = fn->locals[i].type;
+        uint64_t sz = t->kind == TY_ARR ? type_size(t) : 8;
+        sz = (sz + 7) & ~(uint64_t)7;   /* 8-aligned slots */
+        cur += (int)sz;
+        g->slot[i] = -cur;
+    }
+    cur += 8;
+    g->fh0off = -cur;
+    int frame = cur + outgoing;
+    while (frame % 16 != 8) frame += 8;
+
+    lbl_here(e, g->flabels[g->fi]);
+    push_r(e, 5); mov_rr(e, 5, 4);
+    push_r(e, R_CMP); push_r(e, R_F); push_r(e, R_CTX); push_r(e, R_WV); push_r(e, 3);
+    sub_rsp(e, frame);
+
+    /* spill params (typed truncation like the C backend) */
+    for (int k = 0; k < fn->nparams; k++) {
+        Type* t = fn->locals[k].type;
+        if (k < 6) {
+            mov_rr(e, 0, argregs[k]);
+            store_typed(g, 5, g->slot[k], t);
+        } else {
+            ld_d(e, 0, 5, 16 + 8 * (k - 6));
+            store_typed(g, 5, g->slot[k], t);
+        }
+    }
+    /* fh0 = ctx->fh_sp */
+    ld_d(e, 0, R_CTX, CTX_FHSP);
+    st_d(e, 5, g->fh0off, 0);
+    xor_rr(e, R_F, R_F);
+
+    g->cur = ty_nothing();
+    g->epilogue = lbl_new(e);
+    for (int i = 0; i < fn->nins; i++)
+        gen_insn(g, &fn->ins[i]);
+
+    lbl_here(e, g->epilogue);
+    mov_rr(e, 0, R_WV);
+    ld_d(e, 1, 5, g->fh0off);
+    st_d(e, R_CTX, CTX_FHSP, 1);
+    add_rsp(e, frame);
+    pop_r(e, 3); pop_r(e, R_WV); pop_r(e, R_CTX); pop_r(e, R_F); pop_r(e, R_CMP);
+    pop_r(e, 5);
+    ret(e);
+    free(g->slot);
+}
+
+/* ------------------------------------------------------------------ */
+/* program assembly + entry                                            */
+/* ------------------------------------------------------------------ */
+
+static void emit_entry(XE* e, H* h, int main_lbl) {
+    /* entry_main: libc main(argc, argv) */
+    int entry = lbl_new(e);
+    lbl_here(e, entry);
+    push_r(e, 5); mov_rr(e, 5, 4);
+    push_r(e, R_CMP); push_r(e, R_F); push_r(e, R_CTX); push_r(e, R_WV); push_r(e, 3);
+    sub_rsp(e, 56);
+    mov_ri32(e, 7, CTX_SIZE);
+    movsxd_rr(e, 7, 7);
+    call_c(e, "malloc");
+    test_rr(e, 0, 0);
+    int okm = lbl_new(e);
+    jcc_l(e, 0x85, okm);
+    call_l(e, h->die_oom);
+    lbl_here(e, okm);
+    mov_rr(e, R_CTX, 0);
+    mov_rr(e, 7, 0);
+    tc2(e, 0x31, 0xF6);
+    mov_ri32(e, 2, CTX_SIZE);
+    movsxd_rr(e, 2, 2);
+    call_c(e, "memset");
+    libc_data(e, 0, "stdin");
+    ld_d(e, 1, 0, 0);
+    st_d(e, R_CTX, CTX_IN, 1);
+    libc_data(e, 0, "stdout");
+    ld_d(e, 1, 0, 0);
+    st_d(e, R_CTX, CTX_OUT, 1);
+    call_l(e, main_lbl);
+    tc2(e, 0x31, 0xFF);
+    call_c(e, "fflush");
+    tc2(e, 0x31, 0xC0);
+    add_rsp(e, 56);
+    pop_r(e, 3); pop_r(e, R_WV); pop_r(e, R_CTX); pop_r(e, R_F); pop_r(e, R_CMP);
+    pop_r(e, 5);
+    ret(e);
+
+    /* _start: hand-rolled crt */
+    e->start_off = e->text.len;
+    tc2(e, 0x31, 0xED);                /* xor ebp,ebp */
+    tc4u_seq(e, (const unsigned char*)"\x48\x8B\x34\x24");          /* mov rsi,[rsp] */
+    tc5u_seq(e, (const unsigned char*)"\x48\x8D\x54\x24\x08");     /* lea rdx,[rsp+8] */
+    tc2(e, 0x31, 0xC9);                /* xor ecx,ecx */
+    tc3(e, 0x45, 0x31, 0xC0);          /* xor r8d,r8d */
+    tc3(e, 0x45, 0x31, 0xC9);          /* xor r9d,r9d */
+    lea_rip_lbl(e, 7, entry);          /* rdi = entry */
+    tc4u_seq(e, (const unsigned char*)"\x48\x83\xE4\xF0");          /* and rsp,-16 */
+    mov_rip(e, 0, SEG_GOT, (size_t)(3 + imp_idx(e, "__libc_start_main", 0)) * 8);
+    tc2(e, 0xFF, 0xD0);                /* call rax */
+    tc(e, 0xF4);                       /* hlt */
+}
+
+/* ------------------------------------------------------------------ */
+/* ELF writing                                                         */
+/* ------------------------------------------------------------------ */
+
+static uint32_t elf_hash(const char* s) {
+    uint32_t hh = 0;
+    for (; *s; s++) {
+        hh = (hh << 4) + (unsigned char)*s;
+        uint32_t g = hh & 0xF0000000u;
+        if (g) hh ^= g >> 24;
+        hh &= ~g;
+    }
+    return hh;
+}
+
+static const char* host_interp(void) {
+    const char* env = getenv("CRYN_INTERP");
+    if (env && env[0]) return env;
+    static const char* cands[] = {
+        "/lib/ld-musl-x86_64.so.1",
+        "/lib64/ld-linux-x86-64.so.2",
+        "/lib/ld-linux-x86-64.so.2",
+        "/lib64/ld-linux.so.2",
+        "/lib/ld-linux.so.2",
+    };
+    for (size_t i = 0; i < sizeof cands / sizeof cands[0]; i++)
+        if (access(cands[i], F_OK) == 0) return cands[i];
+    return "/lib64/ld-linux-x86-64.so.2";
+}
+
+static const char* elf_soname(const char* path) {
+    static char buf[256];
+    FILE* f = fopen(path, "rb");
+    if (!f) return NULL;
+    unsigned char eh[64];
+    if (fread(eh, 1, 64, f) != 64) { fclose(f); return NULL; }
+    if (memcmp(eh, "\x7F""ELF", 4) != 0) { fclose(f); return NULL; }
+    uint64_t phoff;
+    memcpy(&phoff, eh + 32, 8);
+    uint16_t phentsize, phnum;
+    memcpy(&phentsize, eh + 54, 2);
+    memcpy(&phnum, eh + 56, 2);
+    uint64_t dynv = 0;
+    int has_dyn = 0;
+    for (int i = 0; i < phnum; i++) {
+        unsigned char ph[56];
+        if (fseek(f, (long)(phoff + (uint64_t)i * phentsize), SEEK_SET) != 0) break;
+        if (fread(ph, 1, 56, f) != 56) break;
+        uint32_t ty;
+        memcpy(&ty, ph, 4);
+        if (ty == 2) {
+            memcpy(&dynv, ph + 16, 8);
+            has_dyn = 1;
+        }
+    }
+    if (!has_dyn) { fclose(f); return NULL; }
+    if (fseek(f, (long)dynv, SEEK_SET) != 0) { fclose(f); return NULL; }
+    uint64_t strtab = 0;
+    int64_t soname = -1;
+    for (;;) {
+        unsigned char d[16];
+        if (fread(d, 1, 16, f) != 16) break;
+        uint64_t tag, val;
+        memcpy(&tag, d, 8);
+        memcpy(&val, d + 8, 8);
+        if (tag == 0) break;
+        if (tag == 14) soname = (int64_t)val;
+        if (tag == 5) strtab = val;
+    }
+    if (soname < 0) { fclose(f); return NULL; }
+    if (fseek(f, (long)(strtab + (uint64_t)soname), SEEK_SET) != 0) { fclose(f); return NULL; }
+    size_t n = fread(buf, 1, sizeof buf - 1, f);
+    buf[n] = 0;
+    fclose(f);
+    return buf[0] ? buf : NULL;
+}
+
+static void w8b(Buf* b, uint64_t v) { buf_put(b, &v, 8); }
+static void w4b(Buf* b, uint32_t v) { buf_put(b, &v, 4); }
+static void w2b(Buf* b, uint16_t v) { buf_put(b, &v, 2); }
+
+static int write_elf(XE* e, const char* soname, const char* libm_needed, Buf* out) {
+    /* dynamic symbol table: null + imports */
+    int nsym = 1 + e->nimps;
+    Buf dynstr = {0};
+    uint32_t* nameoff = xmalloc((size_t)e->nimps * sizeof(uint32_t));
+    for (int i = 0; i < e->nimps; i++) {
+        nameoff[i] = (uint32_t)dynstr.len;
+        buf_puts(&dynstr, e->imps[i].name);
+        buf_putc(&dynstr, 0);
+    }
+    uint32_t soname_off = (uint32_t)dynstr.len;
+    buf_puts(&dynstr, soname);
+    buf_putc(&dynstr, 0);
+    uint32_t libm_off = 0;
+    if (libm_needed) {
+        libm_off = (uint32_t)dynstr.len;
+        buf_puts(&dynstr, libm_needed);
+        buf_putc(&dynstr, 0);
+    }
+
+    Buf dynsym = {0};
+    for (int k = 0; k < nsym; k++) { /* null + entries (fill below) */
+        for (int z = 0; z < 24; z++) buf_putc(&dynsym, 0);
+    }
+    for (int i = 0; i < e->nimps; i++) {
+        size_t s = 24 + (size_t)i * 24;
+        memcpy(dynsym.data + s, &nameoff[i], 4);
+        dynsym.data[s + 4] = (unsigned char)(e->imps[i].is_data ? 0x11 : 0x12);
+    }
+
+    /* relocations */
+    Buf rela = {0};
+    size_t got_slots = 3 + (size_t)e->nimps;
+    for (int i = 0; i < e->nimps; i++) {
+        uint64_t roff = (uint64_t)(24 + 8 * (size_t)i); /* + base at patch */
+        w8b(&rela, roff);
+        w8b(&rela, ((uint64_t)(i + 1) << 32) | 6); /* GLOB_DAT */
+        w8b(&rela, 0);
+    }
+
+    /* sysv hash */
+    uint32_t nbucket = nsym < 4 ? 4 : (uint32_t)nsym;
+    uint32_t* buckets = xmalloc(nbucket * 4);
+    uint32_t* chain = xmalloc((size_t)nsym * 4);
+    uint32_t* last = xmalloc(nbucket * 4);
+    memset(buckets, 0, nbucket * 4);
+    memset(chain, 0, (size_t)nsym * 4);
+    memset(last, 0, nbucket * 4);
+    for (int i = 0; i < e->nimps; i++) {
+        uint32_t hh = elf_hash(e->imps[i].name) % nbucket;
+        uint32_t sym = (uint32_t)(i + 1);
+        if (!buckets[hh]) buckets[hh] = sym;
+        else chain[last[hh]] = sym;
+        last[hh] = sym;
+    }
+    Buf hash = {0};
+    w4b(&hash, nbucket);
+    w4b(&hash, (uint32_t)nsym);
+    for (uint32_t i = 0; i < nbucket; i++) w4b(&hash, buckets[i]);
+    for (int i = 0; i < nsym; i++) w4b(&hash, chain[i]);
+
+    /* layout */
+    uint64_t base = 0x400000;
+    size_t phnum = 5;
+    size_t text_off = 64 + phnum * 56;
+    size_t ro_off = text_off + e->text.len;
+    size_t sym_off = (ro_off + e->ro.len + 7) & ~(size_t)7;
+    size_t str_off = sym_off + dynsym.len;
+    size_t rela_off = str_off + dynstr.len;
+    size_t hash_off = rela_off + rela.len;
+    const char* interp = host_interp();
+    size_t interp_off = hash_off + hash.len;
+    size_t ilen = strlen(interp) + 1;
+    size_t rx_filesz = interp_off + ilen;
+    size_t rw_off = (rx_filesz + 0xFFF) & ~(size_t)0xFFF;
+    uint64_t rw_va = base + rw_off;
+    size_t got_off = rw_off;
+    size_t dyn_off = got_off + got_slots * 8;
+    int ndyn = 10 + (libm_needed ? 1 : 0);
+    size_t data_off = dyn_off + (size_t)ndyn * 16;
+    size_t data_start_in_seg = data_off - rw_off; /* offset of .data within RW */
+    size_t rw_filesz = data_off - rw_off + e->data.len;
+
+    e->base = base;
+    e->text_va = base + text_off;
+    e->ro_va = base + ro_off;
+    e->got_va = rw_va;
+    e->dyn_va = rw_va + (got_slots * 8);
+    e->data_va = rw_va + data_start_in_seg;
+
+    /* patch fixups */
+    for (int i = 0; i < e->nfix; i++) {
+        Fix* f = &e->fixes[i];
+        if (f->kind == 0 || f->kind == 4) {
+            int64_t target = e->labels[f->label] < 0 ? 0 : e->labels[f->label];
+            int64_t disp = target - (int64_t)(f->at + 4);
+            memcpy(e->text.data + f->at, &disp, 4);
+        } else if (f->kind == 1) {
+            uint64_t t = f->seg == SEG_RO ? e->ro_va + f->segoff :
+                         f->seg == SEG_GOT ? e->got_va + f->segoff :
+                         e->data_va + f->segoff;
+            int64_t disp = (int64_t)t - (int64_t)(e->text_va + f->at + 4);
+            memcpy(e->text.data + f->at, &disp, 4);
+        } else if (f->kind == 2) {
+            uint64_t t = e->text_va + (uint64_t)e->labels[f->label];
+            memcpy(e->data.data + f->at, &t, 8);
+        }
+    }
+    /* patch reloc offsets with real vaddrs */
+    for (size_t i = 0; i < rela.len / 24; i++) {
+        uint64_t v = e->got_va + 24 + 8 * (uint64_t)i;
+        memcpy(rela.data + i * 24, &v, 8);
+    }
+
+    /* dynamic table content */
+    Buf dyn = {0};
+    #define DTB(t, v) do { w4b(&dyn, (t)); w4b(&dyn, 0); w8b(&dyn, (uint64_t)(v)); } while (0)
+    DTB(1, soname_off);
+    DTB(4, base + hash_off);
+    DTB(5, base + str_off);
+    DTB(6, base + sym_off);
+    DTB(10, dynstr.len);
+    DTB(11, 24);
+    DTB(7, base + rela_off);
+    DTB(8, rela.len);
+    DTB(9, 24);
+    if (libm_needed) DTB(1, libm_off);
+    DTB(0, 0);
+    #undef DTB
+
+    /* ehdr */
+    buf_puts(out, "");
+    buf_putc(out, 0x7F); buf_puts(out, "ELF");
+    buf_putc(out, 2); buf_putc(out, 1); buf_putc(out, 1);
+    for (int i = 7; i < 16; i++) buf_putc(out, 0);
+    w2b(out, 2);                      /* ET_EXEC */
+    w2b(out, 62);                     /* EM_X86_64 */
+    w4b(out, 1);                      /* version */
+    w8b(out, e->text_va + e->start_off); /* e_entry = _start */
+    w8b(out, 64);                     /* phoff */
+    w8b(out, 0);                      /* shoff */
+    w4b(out, 0);                      /* flags */
+    w2b(out, 64);                     /* ehsize */
+    w2b(out, 56);                     /* phentsize */
+    w2b(out, (uint16_t)phnum);
+    w2b(out, 0); w2b(out, 0); w2b(out, 0); /* shentsize, shnum, shstrndx */
+    struct { uint32_t t, fl; uint64_t o, v, fs, ms, a; } ph[5] = {
+        {1, 5, 0, base, rx_filesz, rx_filesz, 0x1000},
+        {1, 6, rw_off, rw_va, rw_filesz, rw_filesz + e->bss_size, 0x1000},
+        {3, 4, interp_off, base + interp_off, ilen, ilen, 1},
+        {2, 6, dyn_off, e->dyn_va, (size_t)ndyn * 16, (size_t)ndyn * 16, 8},
+        {0x6474E551, 6, 0, 0, 0, 0, 0x10},
+    };
+    /* pad phdr area */
+    while (out->len < 64) buf_putc(out, 0);
+    for (int i = 0; i < 5; i++) {
+        w4b(out, ph[i].t); w4b(out, ph[i].fl);
+        w8b(out, ph[i].o); w8b(out, ph[i].v); w8b(out, ph[i].v);
+        w8b(out, ph[i].fs); w8b(out, ph[i].ms); w8b(out, ph[i].a);
+    }
+    buf_put(out, e->text.data, e->text.len);
+    buf_put(out, e->ro.data, e->ro.len);
+    while (out->len < sym_off) buf_putc(out, 0);
+    buf_put(out, dynsym.data, dynsym.len);
+    buf_put(out, dynstr.data, dynstr.len);
+    buf_put(out, rela.data, rela.len);
+    buf_put(out, hash.data, hash.len);
+    buf_put(out, interp, ilen);
+    while (out->len < rw_off) buf_putc(out, 0);
+    /* GOT */
+    w8b(out, e->dyn_va);              /* GOT[0] = _DYNAMIC */
+    w8b(out, 0); w8b(out, 0);
+    for (int i = 0; i < e->nimps; i++) w8b(out, 0);
+    /* dynamic */
+    buf_put(out, dyn.data, dyn.len);
+    /* data */
+    buf_put(out, e->data.data, e->data.len);
+
+    buf_free(&dynstr); buf_free(&dynsym); buf_free(&rela); buf_free(&hash); buf_free(&dyn);
+    free(nameoff); free(buckets); free(chain); free(last);
+    return 0;
+}
+
+/* ------------------------------------------------------------------ */
+/* driver                                                              */
+/* ------------------------------------------------------------------ */
+
+int be_x64_emit(ProgramIR* p, int release, Buf* out) {
+    XE e;
+    memset(&e, 0, sizeof e);
+    H h;
+    memset(&h, 0, sizeof h);
+
+    h.fmt_err = ro_str(&e, "cryn: runtime error: %s\n");
+    h.fmt_range = ro_str(&e, "cryn: runtime error: index %llu out of bounds (size %llu)\n");
+    h.fmt_lld = ro_str(&e, "%lld");
+    h.fmt_llu = ro_str(&e, "%llu");
+    h.fmt_hex = ro_str(&e, "0x%llx");
+    h.fmt_g = ro_str(&e, "%.*g");
+    h.s_nan = ro_str(&e, "nan");
+    h.s_true = ro_str(&e, "true");
+    h.s_false = ro_str(&e, "false");
+    h.modetab = ro_put(&e, "rb\0\0wb\0\0w+b\0ab\0\0", 16);
+    h.k_2p63 = ro_f64(&e, 9223372036854775808.0);
+    h.k_2n63 = ro_f64(&e, -9223372036854775808.0);
+    h.k_2p64 = ro_f64(&e, 18446744073709551616.0);
+    h.fbuf = e.data.len;
+    for (int i = 0; i < 64; i++) buf_putc(&e.data, 0);
+    h.done_ctr = e.data.len;
+    for (int i = 0; i < 8; i++) buf_putc(&e.data, 0);
+
+    int ids[] = {
+        /* dies */ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+        /* stack */ 4, /* heap */ 5, /* conv */ 8, /* out */ 8, /* io */ 7, /* spawn */ 4,
+    };
+    (void)ids;
+    h.die = lbl_new(&e); h.die_range = lbl_new(&e);
+    h.die_oom = lbl_new(&e); h.die_div0 = lbl_new(&e); h.die_flushfail = lbl_new(&e);
+    h.die_focus = lbl_new(&e); h.die_toolarge = lbl_new(&e);
+    h.die_notblock = lbl_new(&e); h.die_free_null = lbl_new(&e);
+    h.die_usebad = lbl_new(&e); h.die_closebad = lbl_new(&e);
+    h.die_indcall = lbl_new(&e); h.die_spawnpar = lbl_new(&e);
+    h.die_spawnmax = lbl_new(&e); h.die_threadfail = lbl_new(&e);
+    h.die_seekstreams = lbl_new(&e);
+    h.push = lbl_new(&e); h.pop = lbl_new(&e);
+    h.fpush = lbl_new(&e); h.fpop = lbl_new(&e);
+    h.alloc = lbl_new(&e); h.free_ = lbl_new(&e); h.blockn = lbl_new(&e);
+    h.chk_block = lbl_new(&e); h.chk_range = lbl_new(&e);
+    h.d2i64 = lbl_new(&e); h.d2u64 = lbl_new(&e); h.d2i8 = lbl_new(&e);
+    h.d2i16 = lbl_new(&e); h.d2i32 = lbl_new(&e); h.d2u8 = lbl_new(&e);
+    h.d2u16 = lbl_new(&e); h.d2u32 = lbl_new(&e);
+    h.out_i64 = lbl_new(&e); h.out_u64 = lbl_new(&e);
+    h.out_f64 = lbl_new(&e); h.out_f32 = lbl_new(&e);
+    h.out_bool = lbl_new(&e); h.out_char = lbl_new(&e);
+    h.out_addr = lbl_new(&e); h.out_text = lbl_new(&e);
+    h.input = lbl_new(&e); h.open = lbl_new(&e); h.close = lbl_new(&e);
+    h.use = lbl_new(&e); h.flush = lbl_new(&e); h.seek = lbl_new(&e);
+    h.system = lbl_new(&e); h.spawn = lbl_new(&e);
+    h.thread_main = lbl_new(&e); h.wait = lbl_new(&e); h.stop = lbl_new(&e);
+
+    const char* interp = host_interp();
+    e.style_musl = strstr(interp, "musl") != NULL;
+    const char* soname;
+    const char* libm = NULL;
+    if (e.style_musl) {
+        soname = elf_soname(interp);
+        if (!soname) soname = "libc.musl-x86_64.so.1";
+    } else {
+        soname = "libc.so.6";
+        libm = "libm.so.6";
+    }
+
+    h_all(&e, &h);
+
+    /* program objects */
+    size_t* glob_off = xmalloc((size_t)(p->nglobs ? p->nglobs : 1) * sizeof(size_t));
+    for (int i = 0; i < p->nglobs; i++) {
+        GlobIR* gb = &p->globs[i];
+        uint64_t sz = type_size(gb->type);
+        if (gb->str_init) {
+            glob_off[i] = data_put(&e, gb->str_init, gb->str_initlen);
+        } else {
+            glob_off[i] = e.data.len;
+            e.bss_size += 0; /* keep zero bytes in .data for simplicity */
+            for (uint64_t k = 0; k < sz; k++) buf_putc(&e.data, 0);
+        }
+    }
+    size_t* str_off = xmalloc((size_t)(p->nstrs ? p->nstrs : 1) * sizeof(size_t));
+    for (int i = 0; i < p->nstrs; i++) {
+        StrIR* s = &p->strs[i];
+        str_off[i] = data_put(&e, s->bytes, s->len + 1);
+    }
+    int* flabels = xmalloc((size_t)(p->nfns ? p->nfns : 1) * sizeof(int));
+    size_t* ref_off = xmalloc((size_t)(p->nfns ? p->nfns : 1) * sizeof(size_t));
+    for (int i = 0; i < p->nfns; i++) flabels[i] = lbl_new(&e);
+    for (int i = 0; i < p->nfns; i++) {
+        ref_off[i] = e.data.len;
+        fix_add(&e, 2, ref_off[i], flabels[i], 0, 0);
+        for (int k = 0; k < 8; k++) buf_putc(&e.data, 0);   /* fn ptr */
+        int32_t np = p->fns[i].nparams;
+        buf_put(&e.data, &np, 4);
+        buf_putc(&e.data, 0); buf_putc(&e.data, 0); buf_putc(&e.data, 0); buf_putc(&e.data, 0);
+    }
+
+    /* functions; label ids for IR labels continue after existing labels */
+    int lblbase = e.nlabels;
+    for (int i = 0; i < p->nfns; i++) {
+        FnIR* fn = &p->fns[i];
+        int base = e.nlabels;
+        for (int k = 0; k < fn->nlabels; k++) lbl_new(&e);
+        FnGen g;
+        memset(&g, 0, sizeof g);
+        g.e = &e; g.h = &h; g.p = p; g.fn = fn; g.release = release;
+        g.flabels = flabels; g.glob_off = glob_off; g.str_off = str_off;
+        g.ref_off = ref_off; g.lblbase = base; g.ro_2p63 = h.k_2p63; g.fi = i;
+        gen_fn(&g);
+        (void)lblbase;
+    }
+
+    /* entry needs main's label */
+    int mi = prog_find_fn(p, "main");
+    emit_entry(&e, &h, flabels[mi]);
+
+    return write_elf(&e, soname, libm, out);
+}
diff --git a/src/be_x64.h b/src/be_x64.h
new file mode 100644
index 0000000..eb2f36b
--- /dev/null
+++ b/src/be_x64.h
@@ -0,0 +1,12 @@
+#ifndef CR_BE_X64_H
+#define CR_BE_X64_H
+
+#include "ir.h"
+
+/* Native x86-64 backend: lower the IR directly to x86-64 machine code
+   and emit a static ELF64 executable that links against the host C
+   library (the system-call interface). No host C compiler is involved.
+   Returns 0 on success. */
+int be_x64_emit(ProgramIR* p, int release, Buf* out);
+
+#endif
diff --git a/src/check.c b/src/check.c
index 5abac09..314f616 100644
--- a/src/check.c
+++ b/src/check.c
@@ -483,6 +483,19 @@ static void check_insn(Chk* c, Node* n) {
                 type_name(st->wv, b, sizeof b));
             return;
         }
+        /* statically provable index: an integer-literal working value
+           within the static bound becomes a literal move, eliminating
+           the bounds check at compile time */
+        if (st->wv->kind == TY_INTLIT && !st->wv->litneg && f->cell->count &&
+            st->wv->litval < f->cell->count && st->wv->litval <= 0x7FFFFFFFULL) {
+            Insn* in = emit(c, I_MOVE_LIT, n->loc);
+            in->imm.kind = IV_UINT;
+            in->imm.u = st->wv->litval;
+            in->t = f->cell->inner;
+            in->b = (int)f->cell->count;
+            focus_push_cell(c, f->cell->inner, n->loc);
+            return;
+        }
         Insn* in = emit(c, I_MOVE_INDEX, n->loc);
         in->t = f->cell->inner;
         in->b = f->cell->count ? (int)f->cell->count : -1;
@@ -1313,6 +1326,30 @@ static void check_insn(Chk* c, Node* n) {
     case A_FLUSH:
         emit(c, I_FLUSH, n->loc);
         return;
+    case A_SEEK: {
+        if (n->has_imm) {
+            Insn* in = emit(c, I_SEEK, n->loc);
+            in->b = 1;
+            in->imm.kind = IV_UINT;
+            in->imm.u = n->imm.u;
+            st->wv = ty_int(64);
+            return;
+        }
+        if (!ty_is_any_int(st->wv)) {
+            char b[128];
+            err(n->loc, "'seek' requires an integer offset working value (found %s)",
+                type_name(st->wv, b, sizeof b));
+            return;
+        }
+        if (st->wv->kind == TY_INTLIT) {
+            Insn* cv = emit(c, I_CONV, n->loc);
+            cv->t = ty_int(64);
+            st->wv = ty_int(64);
+        }
+        emit(c, I_SEEK, n->loc)->b = 0;
+        st->wv = ty_int(64);
+        return;
+    }
     case A_SYSTEM: {
         Type* wv = st->wv;
         if (!(wv->kind == TY_ADDR &&
diff --git a/src/ir.h b/src/ir.h
index 25994a0..ff72c8b 100644
--- a/src/ir.h
+++ b/src/ir.h
@@ -56,6 +56,7 @@ typedef enum {
     I_CLOSE,
     I_USE,          /* b = 0 handle in working value, 1 imm handle */
     I_FLUSH,
+    I_SEEK,         /* b = 0 offset in working value, 1 imm offset; result in wv */
     I_SYSTEM,
     I_SPAWN,        /* a = fn index */
     I_WAIT,
diff --git a/src/lex.c b/src/lex.c
index c1e9806..2d35ec0 100644
--- a/src/lex.c
+++ b/src/lex.c
@@ -60,6 +60,7 @@ static const Kw k_keywords[] = {
     {"stop", T_STOP},     {"input", T_INPUT}, {"output", T_OUTPUT}, {"open", T_OPEN},
     {"close", T_CLOSE}, {"system", T_SYSTEM}, {"spawn", T_SPAWN}, {"wait", T_WAIT},
     {"use", T_USE}, {"flush", T_FLUSH}, {"exchange", T_EXCHANGE},
+    {"seek", T_SEEK},
     {"define", T_DEFINE}, {"end", T_END}, {"mark", T_MARK},
     {"nothing", T_NOTHING}, {"bool", T_BOOL}, {"char", T_CHAR_T},
     {"int8", T_INT8}, {"int16", T_INT16}, {"int32", T_INT32}, {"int64", T_INT64},
diff --git a/src/lex.h b/src/lex.h
index f37450a..86c88d8 100644
--- a/src/lex.h
+++ b/src/lex.h
@@ -23,7 +23,7 @@ typedef enum {
     T_JUMP, T_CALL, T_RETURN, T_STOP,
     T_INPUT, T_OUTPUT, T_OPEN, T_CLOSE, T_SYSTEM,
     T_SPAWN, T_WAIT,
-    T_USE, T_FLUSH, T_EXCHANGE,
+    T_USE, T_FLUSH, T_EXCHANGE, T_SEEK,
     T_DEFINE, T_END, T_MARK,
 
     /* type words */
diff --git a/src/main.c b/src/main.c
index 8ba3f7b..0f9cbab 100644
--- a/src/main.c
+++ b/src/main.c
@@ -1,5 +1,6 @@
 /* cryn - The Cryn Programming Language compiler (bootstrap) */
 #include "be_c.h"
+#include "be_x64.h"
 #include "check.h"
 #include "diag.h"
 #include "lex.h"
@@ -10,11 +11,12 @@
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <sys/stat.h>
 #include <sys/wait.h>
 #include <time.h>
 #include <unistd.h>
 
-#define CRYN_VERSION "0.2.1"
+#define CRYN_VERSION "0.3.0"
 
 static void usage(void) {
     printf(
@@ -22,8 +24,11 @@ static void usage(void) {
         "\n"
         "usage:\n"
         "  cryn <file.cryn>               compile to an executable\n"
-        "  cryn build <file.cryn> [-o out]\n"
-        "  cryn run <file.cryn>           compile and execute\n"
+        "  cryn build <file.cryn> [-o out] [--release] [--native]\n"
+        "                                release: no runtime bounds checks\n"
+        "                                native: x86-64 ELF, no host cc\n"
+        "  cryn run <file.cryn> [--release] [--native]\n"
+        "                                compile and execute\n"
         "  cryn check <file.cryn>         type check only\n"
         "  cryn emit-c <file.cryn> [-o out.c]\n"
         "                                 write generated C\n"
@@ -62,7 +67,7 @@ static const char* tmp_dir(void) {
 
 typedef enum { CMD_COMPILE, CMD_RUN, CMD_CHECK, CMD_EMITC, CMD_EMITIR } CmdKind;
 
-static int compile_file(const char* path, CmdKind kind, const char* out) {
+static int compile_file(const char* path, CmdKind kind, const char* out, int release, int native) {
     char* src = NULL;
     size_t len = 0;
     if (read_file(path, &src, &len) != 0) {
@@ -92,7 +97,7 @@ static int compile_file(const char* path, CmdKind kind, const char* out) {
     }
     case CMD_EMITC: {
         Buf b = {0};
-        be_c_emit(ir, &b);
+        be_c_emit(ir, &b, release);
         const char* dst = out ? out : "out.c";
         if (write_file(dst, b.data, b.len) != 0) {
             fprintf(stderr, "cryn: cannot write '%s'\n", dst);
@@ -114,19 +119,33 @@ static int compile_file(const char* path, CmdKind kind, const char* out) {
             snprintf(cfile, sizeof cfile, "%s.c", base);
             snprintf(exefile, sizeof exefile, "%s", base);
         }
-        Buf b = {0};
-        be_c_emit(ir, &b);
-        if (write_file(cfile, b.data, b.len) != 0) {
-            fprintf(stderr, "cryn: cannot write '%s'\n", cfile);
-            return 1;
-        }
-        char* cc = find_host_cc();
-        int rc = run_host_cc(cc, cfile, exefile);
-        free(cc);
-        if (rc != 0) {
-            fprintf(stderr, "cryn: host C compiler failed (exit %d)\n", rc);
-            if (kind == CMD_RUN) remove(cfile);
-            return 1;
+        if (native) {
+            /* native x86-64 backend: emit the ELF directly, no host cc */
+            Buf b = {0};
+            if (be_x64_emit(ir, release, &b) != 0) {
+                fprintf(stderr, "cryn: native backend failed\n");
+                return 1;
+            }
+            if (write_file(exefile, b.data, b.len) != 0) {
+                fprintf(stderr, "cryn: cannot write '%s'\n", exefile);
+                return 1;
+            }
+            chmod(exefile, 0755);
+        } else {
+            Buf b = {0};
+            be_c_emit(ir, &b, release);
+            if (write_file(cfile, b.data, b.len) != 0) {
+                fprintf(stderr, "cryn: cannot write '%s'\n", cfile);
+                return 1;
+            }
+            char* cc = find_host_cc();
+            int rc = run_host_cc(cc, cfile, exefile);
+            free(cc);
+            if (rc != 0) {
+                fprintf(stderr, "cryn: host C compiler failed (exit %d)\n", rc);
+                if (kind == CMD_RUN) remove(cfile);
+                return 1;
+            }
         }
         if (kind == CMD_COMPILE) {
             remove(cfile);
@@ -134,8 +153,8 @@ static int compile_file(const char* path, CmdKind kind, const char* out) {
         }
         fflush(NULL);
         int prc = system(exefile);
+        if (native) remove(exefile);
         remove(cfile);
-        remove(exefile);
         if (prc == -1) return 1;
         if (WIFEXITED(prc)) return WEXITSTATUS(prc);
         return 1;
@@ -164,6 +183,7 @@ int main(int argc, char** argv) {
     const char* out = NULL;
     const char* file = NULL;
     int has_sub = 1;
+    int release = 0, native = 0;
     if (strcmp(cmd, "build") == 0) kind = CMD_COMPILE;
     else if (strcmp(cmd, "run") == 0) kind = CMD_RUN;
     else if (strcmp(cmd, "check") == 0) kind = CMD_CHECK;
@@ -181,6 +201,8 @@ int main(int argc, char** argv) {
     int start = has_sub ? 3 : 2;
     for (int i = start; i < argc; i++) {
         if (strcmp(argv[i], "-o") == 0 && i + 1 < argc) out = argv[++i];
+        else if (strcmp(argv[i], "--release") == 0) release = 1;
+        else if (strcmp(argv[i], "--native") == 0) native = 1;
     }
-    return compile_file(file, kind, out);
+    return compile_file(file, kind, out, release, native);
 }
diff --git a/src/parse.c b/src/parse.c
index 887ef60..26f4ea0 100644
--- a/src/parse.c
+++ b/src/parse.c
@@ -305,6 +305,18 @@ static Node* parse_command(Parser* p) {
         return n;
     }
     case T_FLUSH: next(p); return node_new(p, A_FLUSH);
+    case T_SEEK: {
+        next(p);
+        n = node_new(p, A_SEEK);
+        Token* v = peek(p);
+        if (v->kind == T_INT) {
+            if (v->neg) crn_error(v->loc, "seek offset literal cannot be negative");
+            next(p);
+            n->imm = imm_from_token(v);
+            n->has_imm = 1;
+        }
+        return n;
+    }
     case T_SYSTEM: next(p); return node_new(p, A_SYSTEM);
     case T_SPAWN: {
         next(p);
diff --git a/src/rt/cryn_rt.c b/src/rt/cryn_rt.c
index d4d769f..1724ee1 100644
--- a/src/rt/cryn_rt.c
+++ b/src/rt/cryn_rt.c
@@ -453,6 +453,26 @@ void crn_flush(void) {
     if (fflush(cr_ctx.out) != 0) crn_die("flush failed");
 }
 
+int64_t crn_seek(int64_t off) {
+    FILE* a = cr_ctx.in;
+    FILE* b = cr_ctx.out;
+    int any = 0;
+    int bad = 0;
+    /* standard streams are not positionable; only handle streams are */
+    if (a && a != stdin) {
+        any = 1;
+        if (fseek(a, (long)off, SEEK_SET) != 0) bad = 1;
+    }
+    if (b && b != a && b != stdout) {
+        any = 1;
+        if (fseek(b, (long)off, SEEK_SET) != 0) bad = 1;
+    }
+    if (!any)
+        crn_die("seek requires a stream routed through an open handle "
+                "('use' a handle from 'open' first)");
+    return bad ? -1 : 0;
+}
+
 int64_t crn_close(int64_t h) {
     if (h == 0)
         crn_die("close of handle 0 (the standard streams cannot be closed)");
diff --git a/src/rt/cryn_rt.h b/src/rt/cryn_rt.h
index cc82ed2..9bccb8d 100644
--- a/src/rt/cryn_rt.h
+++ b/src/rt/cryn_rt.h
@@ -128,6 +128,13 @@ int64_t crn_open(uint8_t* path, int mode);
    the standard streams (input: stdin, output: stdout). */
 void crn_use(int64_t h);
 void crn_flush(void);
+/* Position the context's file streams at a byte offset from the start.
+   Applies fseek(SEEK_SET) to every current stream that belongs to an
+   open handle (both sides of an 'open both' stream are the same file and
+   are positioned once). Standard streams are not positionable; if no
+   current stream belongs to a handle, it is a runtime error. Returns 0
+   on success and -1 if the position could not be set. */
+int64_t crn_seek(int64_t off);
 int64_t crn_close(int64_t h);
 int64_t crn_system(uint8_t* cmd);
 void crn_stop(int64_t code) CRN_NORETURN;
diff --git a/tests/31_seek.cryn b/tests/31_seek.cryn
new file mode 100644
index 0000000..a9dafd5
--- /dev/null
+++ b/tests/31_seek.cryn
@@ -0,0 +1,123 @@
+[# seek: positioning an 'open both' stream so read and write interleave
+   on one file — the one thing the sequential byte-stream model plus
+   flush cannot express #]
+define main
+  make string "cryn_test_31.txt" path
+  make string "ABCDE" data
+  make array 8 of uint8 buf
+  make int64 h
+  make int64 r
+  make int64 c1
+  make int64 c2
+  make int64 n
+  make int64 i
+
+  address path
+  open both
+  home
+  compare 0
+  jump fail when less
+  move h
+  write
+  push
+  use
+  home
+
+  address data
+  output text
+  flush
+
+  set 0
+  seek
+  move r
+  write
+
+  input
+  move c1
+  write
+  input
+  move c2
+  write
+
+  seek 3
+  set 88
+  convert uint8
+  output
+  flush
+
+  set 0
+  seek
+  set 0
+  move n
+  write
+  mark rd
+  input
+  compare -1
+  jump fin when equal
+  convert uint8
+  push
+  move n
+  read
+  move buf
+  move index
+  pop
+  write
+  move n
+  read
+  add 1
+  move n
+  write
+  jump rd
+  mark fin
+
+  pop
+  close
+  use 0
+
+  move r
+  read
+  output
+  move c1
+  read
+  convert uint8
+  output
+  move c2
+  read
+  convert uint8
+  output
+
+  set 0
+  home
+  mark pr
+  move n
+  read
+  push
+  move i
+  read
+  compare
+  jump donePr when greater-or-equal
+  set 0
+  home
+  move i
+  read
+  move buf
+  move index
+  read
+  output
+  move i
+  read
+  add 1
+  move i
+  write
+  set 0
+  home
+  jump pr
+  mark donePr
+  set 0
+  home
+  stop
+
+  mark fail
+  set 1
+  stop
+end
diff --git a/tests/32_cas.cryn b/tests/32_cas.cryn
new file mode 100644
index 0000000..a3b0ed8
--- /dev/null
+++ b/tests/32_cas.cryn
@@ -0,0 +1,137 @@
+[# the atomic-primitives evaluation: compare-and-swap and atomic
+   increment built from 'exchange' (which builds locks) alone — the
+   demonstration that closed "further atomic primitives": anything a new
+   primitive would express can be built from exchange and wait #]
+make int64 lock
+make int64 value
+make string "\n" nl
+
+define acquire
+  set 0
+  home
+  mark again
+  set 1
+  move lock
+  exchange
+  compare 0
+  set 0
+  home
+  jump again when different
+  return
+end
+
+define release
+  set 0
+  move lock
+  exchange
+  return
+end
+
+[# compare-and-swap: when value holds expected, store newv.
+   returns the value value held before the attempt #]
+define cas int64 expected int64 newv result int64
+  make int64 old
+  call acquire
+  move value
+  read
+  move old
+  write
+  move expected
+  read
+  push
+  move old
+  read
+  compare
+  jump skip when different
+  move newv
+  read
+  move value
+  write
+  mark skip
+  call release
+  move old
+  read
+  return
+end
+
+define worker
+  make int64 k
+  make int64 seen
+  set 0
+  move k
+  write
+  set 0
+  home
+  mark w
+  move k
+  read
+  compare 50
+  jump done when greater-or-equal
+  set 0
+  home
+
+  mark retry
+  move value
+  read
+  move seen
+  write
+  move seen
+  read
+  push
+  move seen
+  read
+  add 1
+  push
+  call cas
+  push
+  move seen
+  read
+  compare
+  set 0
+  home
+  jump retry when different
+
+  move k
+  read
+  add 1
+  move k
+  write
+  set 0
+  home
+  jump w
+
+  mark done
+  return
+end
+
+define main
+  set 7
+  move value
+  write
+  set 5
+  push
+  set 9
+  push
+  call cas
+  compare 7
+  jump bad when different
+  move value
+  read
+  compare 7
+  jump bad when different
+
+  spawn worker
+  spawn worker
+  wait
+
+  move value
+  read
+  output
+  address nl
+  output text
+  return
+
+  mark bad
+  set 1
+  stop
+end
diff --git a/tests/33_conversions.cryn b/tests/33_conversions.cryn
new file mode 100644
index 0000000..19e6925
--- /dev/null
+++ b/tests/33_conversions.cryn
@@ -0,0 +1,134 @@
+[# the conversion matrix: integer truncation keeps low bits, floats
+   saturate into integers, float32 narrows and widens exactly #]
+make string "\n" nl
+
+define newline result nothing
+  address nl
+  output text
+  set nothing
+  return
+end
+
+define main
+  [# integer to integer: low bits kept, then sign extended #]
+  set 300
+  convert uint8
+  convert int64
+  output
+  call newline
+
+  set -1
+  convert uint8
+  convert int64
+  output
+  call newline
+
+  set 130
+  convert int8
+  convert int64
+  output
+  call newline
+
+  set -1
+  convert uint16
+  convert int64
+  output
+  call newline
+
+  set 100000
+  convert uint16
+  convert int64
+  output
+  call newline
+
+  set -1
+  convert uint64
+  output
+  call newline
+
+  set 5000000000
+  convert uint32
+  convert int64
+  output
+  call newline
+
+  [# integer to float and back #]
+  set 7
+  convert float64
+  output
+  call newline
+
+  set -3
+  convert float32
+  output
+  call newline
+
+  [# float to integer saturates at the target range #]
+  set 300.5
+  convert uint8
+  convert int64
+  output
+  call newline
+
+  set -5.5
+  convert uint8
+  convert int64
+  output
+  call newline
+
+  set 1e18
+  convert int64
+  output
+  call newline
+
+  set 1e19
+  convert int64
+  output
+  call newline
+
+  set 0.9999999
+  convert int64
+  output
+  call newline
+
+  [# float32 narrows and the shortest form still round trips #]
+  set 0.1
+  convert float32
+  output
+  call newline
+
+  set 3.14159265358979
+  convert float32
+  output
+  call newline
+
+  set 16777217
+  convert float32
+  convert int64
+  output
+  call newline
+
+  [# bool conversion: nonzero is true, including fractions #]
+  set 5
+  convert bool
+  output
+  call newline
+
+  set 0
+  convert bool
+  output
+  call newline
+
+  set 0.5
+  convert bool
+  output
+  call newline
+
+  set 0.0
+  convert bool
+  output
+  call newline
+
+  set nothing
+  return
+end
diff --git a/tests/34_xchgwidths.c b/tests/34_xchgwidths.c
new file mode 100644
index 0000000..056ed85
--- /dev/null
+++ b/tests/34_xchgwidths.c
@@ -0,0 +1,863 @@
+/* generated by the Cryn compiler - do not edit */
+/* Cryn source compiled to portable C11 */
+
+#ifndef CRYN_RT_H
+#define CRYN_RT_H
+
+/*
+ * Minimal runtime support for compiled Cryn programs.
+ * Everything here exists only because it cannot be expressed as inline C
+ * in generated code (context management, heap headers, threads, output
+ * formatting). The language's semantics live in the compiler.
+ */
+
+#include <stdint.h>
+#include <stdio.h>
+#include <pthread.h>
+
+#if defined(__GNUC__) || defined(__clang__)
+#define CRN_NORETURN __attribute__((noreturn))
+#else
+#define CRN_NORETURN
+#endif
+
+typedef union Val {
+    uint64_t u;   /* unsigned, address (as integer), bool, char */
+    int64_t i;    /* signed integers, integer literals */
+    double d;     /* float32 (widened) and float64 */
+    void* p;      /* focus target / data pointer */
+} Val;
+
+/* A transformation reference: function pointer + parameter count.
+   Indirect calls and spawns are rejected at runtime unless np == 0. */
+typedef struct CrFnRef {
+    Val (*fn)(void);
+    int np;
+} CrFnRef;
+
+#define CR_FH_MAX 1024
+#define CR_SPAWN_MAX 256
+#define CR_FILES_MAX 256
+
+typedef struct CrCtx {
+    Val* stk;        /* value stack */
+    uint64_t sp, cap;
+    void* fh[CR_FH_MAX]; /* focus history */
+    uint64_t fh_sp;
+    FILE* in;        /* current input stream (default stdin) */
+    FILE* out;       /* current output stream (default stdout) */
+    FILE* files[CR_FILES_MAX];      /* open files; slot i has handle i+1 */
+    unsigned char fmodes[CR_FILES_MAX]; /* 0 read, 1 write, 2 both, 3 append */
+    pthread_t spawned[CR_SPAWN_MAX];
+    int nspawned;
+} CrCtx;
+
+extern _Thread_local CrCtx cr_ctx;
+
+void crn_init(void);
+void crn_die(const char* msg) CRN_NORETURN;
+void crn_die_range(uint64_t i, uint64_t n) CRN_NORETURN;
+
+void crn_push(Val v);
+Val crn_pop(void);
+
+void crn_focus_push(void* f);
+void* crn_focus_pop(void);
+
+void* crn_alloc(uint64_t nbytes);
+void crn_free(void* p);
+uint64_t crn_block_n(void* data, uint64_t elemsz);
+void crn_check_block(void* data, uint64_t i, uint64_t elemsz);
+void crn_check_range(uint64_t i, uint64_t n);
+
+/* Relaxed atomic scalar access. Cryn defines a data race on an aligned
+   scalar cell (<= 8 bytes) as: the access yields an unspecified value of
+   the cell's type and never tears. To keep that promise (and avoid C data
+   races, which are undefined behavior), the backend performs every scalar
+   load/store through these helpers. Racily accessed transform-reference
+   cells (16 bytes) are undefined. No ordering is provided beyond wait. */
+uint8_t  crn_ld_u8(const uint8_t* p);
+uint16_t crn_ld_u16(const uint16_t* p);
+uint32_t crn_ld_u32(const uint32_t* p);
+uint64_t crn_ld_u64(const uint64_t* p);
+float    crn_ld_f32(const float* p);
+double   crn_ld_f64(const double* p);
+void crn_st_u8(uint8_t* p, uint8_t v);
+void crn_st_u16(uint16_t* p, uint16_t v);
+void crn_st_u32(uint32_t* p, uint32_t v);
+void crn_st_u64(uint64_t* p, uint64_t v);
+void crn_st_f32(float* p, float v);
+void crn_st_f64(double* p, double v);
+
+/* Atomic exchange (acquire-release). `exchange` swaps the working value
+   with the focused cell in one indivisible step and is the only ordering
+   primitive besides wait: an exchange that stores a value releases the
+   accesses that preceded it in program order, and one that loads a value
+   acquires for the accesses that follow it. It never tears. This is
+   enough to build locks (test-and-set). */
+uint8_t  crn_xch_u8(uint8_t* p, uint8_t v);
+uint16_t crn_xch_u16(uint16_t* p, uint16_t v);
+uint32_t crn_xch_u32(uint32_t* p, uint32_t v);
+uint64_t crn_xch_u64(uint64_t* p, uint64_t v);
+float    crn_xch_f32(float* p, float v);
+double   crn_xch_f64(double* p, double v);
+
+int64_t crn_d2i64(double d);
+uint64_t crn_d2u64(double d);
+int64_t crn_d2i8(double d);
+int64_t crn_d2i16(double d);
+int64_t crn_d2i32(double d);
+uint64_t crn_d2u8(double d);
+uint64_t crn_d2u16(double d);
+uint64_t crn_d2u32(double d);
+
+void crn_out_i64(int64_t v);
+void crn_out_u64(uint64_t v);
+void crn_out_f64(double v);
+void crn_out_f32(float v);
+void crn_out_bool(int b);
+void crn_out_char(unsigned c);
+void crn_out_addr(uint64_t a);
+void crn_out_text(uint8_t* p);
+
+int64_t crn_input(void);
+/* open modes: 0 read, 1 write, 2 both, 3 append.
+   Returns a handle >= 1, or -1 on failure. A handle is an index into the
+   context's file table; closing a handle frees its number for reuse. */
+int64_t crn_open(uint8_t* path, int mode);
+/* Route this context's streams through a handle: the readable side of the
+   file becomes the input stream, the writable side the output stream;
+   sides the mode does not support keep their current stream. Handle 0 is
+   the standard streams (input: stdin, output: stdout). */
+void crn_use(int64_t h);
+void crn_flush(void);
+/* Position the context's file streams at a byte offset from the start.
+   Applies fseek(SEEK_SET) to every current stream that belongs to an
+   open handle (both sides of an 'open both' stream are the same file and
+   are positioned once). Standard streams are not positionable; if no
+   current stream belongs to a handle, it is a runtime error. Returns 0
+   on success and -1 if the position could not be set. */
+int64_t crn_seek(int64_t off);
+int64_t crn_close(int64_t h);
+int64_t crn_system(uint8_t* cmd);
+void crn_stop(int64_t code) CRN_NORETURN;
+
+void crn_spawn(CrFnRef* f);
+void crn_wait(void);
+
+#endif
+
+#include <stdlib.h>
+#include <string.h>
+
+
+#include <errno.h>
+#include <math.h>
+#include <stdlib.h>
+#include <string.h>
+
+_Thread_local CrCtx cr_ctx = {0};
+
+void crn_init(void) {
+    memset(&cr_ctx, 0, sizeof cr_ctx);
+    cr_ctx.in = stdin;
+    cr_ctx.out = stdout;
+}
+
+void crn_die(const char* msg) {
+    fprintf(stderr, "cryn: runtime error: %s\n", msg);
+    exit(1);
+}
+
+void crn_die_range(uint64_t i, uint64_t n) {
+    fprintf(stderr, "cryn: runtime error: index %llu out of bounds (size %llu)\n",
+            (unsigned long long)i, (unsigned long long)n);
+    exit(1);
+}
+
+/* ---- value stack ---- */
+
+void crn_push(Val v) {
+    if (cr_ctx.sp == cr_ctx.cap) {
+        cr_ctx.cap = cr_ctx.cap ? cr_ctx.cap * 2 : 64;
+        cr_ctx.stk = realloc(cr_ctx.stk, cr_ctx.cap * sizeof(Val));
+        if (!cr_ctx.stk) crn_die("out of memory");
+    }
+    cr_ctx.stk[cr_ctx.sp++] = v;
+}
+
+Val crn_pop(void) { return cr_ctx.stk[--cr_ctx.sp]; }
+
+/* ---- focus ---- */
+
+void crn_focus_push(void* f) {
+    if (cr_ctx.fh_sp >= CR_FH_MAX) {
+        char msg[64];
+        snprintf(msg, sizeof msg, "focus history overflow (limit %d)", CR_FH_MAX);
+        crn_die(msg);
+    }
+    cr_ctx.fh[cr_ctx.fh_sp++] = f;
+}
+
+void* crn_focus_pop(void) {
+    if (cr_ctx.fh_sp == 0) crn_die("focus history underflow");
+    return cr_ctx.fh[--cr_ctx.fh_sp];
+}
+
+/* ---- heap ---- */
+
+typedef struct CrHdr {
+    uint64_t magic;
+    uint64_t size; /* data bytes */
+} CrHdr;
+
+#define CR_MAGIC 0x4352594E424C4BULL /* "CRYNBLK" */
+
+/* Registry of live block addresses. Validation must not read the header
+   of a block that free() may already have returned to the allocator:
+   reading freed memory is undefined in C, so stale-address detection
+   (double free, indexing through a freed or shifted address) goes
+   through this table instead. One global table, mutex protected,
+   because blocks can be shared across spawned contexts. */
+typedef struct CrReg {
+    void** keys;      /* NULL = empty, CR_TOMB = deleted */
+    size_t cap;       /* power of two */
+    size_t used;      /* live + tombstones */
+    pthread_mutex_t mu;
+} CrReg;
+
+#define CR_TOMB ((void*)1)
+
+static CrReg cr_reg = {0, 0, 0, PTHREAD_MUTEX_INITIALIZER};
+static pthread_once_t cr_reg_once = PTHREAD_ONCE_INIT;
+
+static size_t reg_slot(void* p, size_t cap) {
+    uintptr_t x = (uintptr_t)p >> 4; /* malloc blocks are 16-aligned */
+    x ^= x >> 17;
+    x *= 0x9E3779B97F4A7C15ULL;
+    x ^= x >> 29;
+    return (size_t)x & (cap - 1);
+}
+
+static void reg_init(void) {
+    cr_reg.cap = 256;
+    cr_reg.keys = malloc(cr_reg.cap * sizeof(void*));
+    if (!cr_reg.keys) crn_die("out of memory");
+    memset(cr_reg.keys, 0, cr_reg.cap * sizeof(void*));
+    cr_reg.used = 0;
+}
+
+static void reg_grow_locked(void) {
+    size_t ncap = cr_reg.cap * 2;
+    void** nk = malloc(ncap * sizeof(void*));
+    if (!nk) crn_die("out of memory");
+    memset(nk, 0, ncap * sizeof(void*));
+    for (size_t i = 0; i < cr_reg.cap; i++) {
+        void* k = cr_reg.keys[i];
+        if (!k || k == CR_TOMB) continue;
+        size_t j = reg_slot(k, ncap);
+        while (nk[j]) j = (j + 1) & (ncap - 1);
+        nk[j] = k;
+    }
+    free(cr_reg.keys);
+    cr_reg.keys = nk;
+    cr_reg.cap = ncap;
+}
+
+/* returns the slot of p, or cap when absent (probing stops at NULL) */
+static size_t reg_find_locked(void* p) {
+    size_t j = reg_slot(p, cr_reg.cap);
+    for (;;) {
+        void* k = cr_reg.keys[j];
+        if (!k) return cr_reg.cap;
+        if (k == p) return j;
+        j = (j + 1) & (cr_reg.cap - 1);
+    }
+}
+
+static void reg_add(void* p) {
+    pthread_once(&cr_reg_once, reg_init);
+    pthread_mutex_lock(&cr_reg.mu);
+    if ((cr_reg.used + 1) * 10 >= cr_reg.cap * 7) {
+        reg_grow_locked();
+        /* collapse tombstones into the new table */
+        cr_reg.used = 0;
+        for (size_t i = 0; i < cr_reg.cap; i++)
+            if (cr_reg.keys[i]) cr_reg.used++;
+    }
+    size_t j = reg_slot(p, cr_reg.cap);
+    while (cr_reg.keys[j] && cr_reg.keys[j] != CR_TOMB) j = (j + 1) & (cr_reg.cap - 1);
+    cr_reg.keys[j] = p;
+    cr_reg.used++;
+    pthread_mutex_unlock(&cr_reg.mu);
+}
+
+static int reg_has(void* p) {
+    pthread_once(&cr_reg_once, reg_init);
+    pthread_mutex_lock(&cr_reg.mu);
+    int found = reg_find_locked(p) != cr_reg.cap;
+    pthread_mutex_unlock(&cr_reg.mu);
+    return found;
+}
+
+static int reg_take(void* p) {
+    pthread_once(&cr_reg_once, reg_init);
+    pthread_mutex_lock(&cr_reg.mu);
+    size_t j = reg_find_locked(p);
+    if (j == cr_reg.cap) {
+        pthread_mutex_unlock(&cr_reg.mu);
+        return 0;
+    }
+    cr_reg.keys[j] = CR_TOMB;
+    pthread_mutex_unlock(&cr_reg.mu);
+    return 1;
+}
+
+void* crn_alloc(uint64_t nbytes) {
+    /* the header and padding additions must not wrap */
+    if (nbytes > (uint64_t)-32) crn_die("allocation too large");
+    CrHdr* h = malloc(sizeof(CrHdr) + (size_t)nbytes + 16);
+    if (!h) crn_die("out of memory");
+    h->magic = CR_MAGIC;
+    h->size = nbytes;
+    unsigned char* p = (unsigned char*)h;
+    p += sizeof(CrHdr);
+    /* keep data 16-aligned: sizeof(CrHdr)==16 on 64-bit */
+    memset(p, 0, (size_t)nbytes);
+    reg_add(p);
+    return p;
+}
+
+void crn_free(void* p) {
+    if (!p) crn_die("free of the null address");
+    if (!reg_take(p)) {
+        crn_die("free of an address that is not a heap block "
+                "(only live blocks made with 'make <size> of <type>' or anonymous "
+                "'make' can be freed)");
+    }
+    CrHdr* h = (CrHdr*)((unsigned char*)p - sizeof(CrHdr));
+    h->magic = 0;
+    free(h);
+}
+
+uint64_t crn_block_n(void* data, uint64_t elemsz) {
+    if (elemsz == 0) elemsz = 1;
+    if (!reg_has(data)) {
+        crn_die("index into memory that is not a heap block "
+                "(only the base address of a live block made with "
+                "'make <size> of <type>' is bounds checked)");
+    }
+    CrHdr* h = (CrHdr*)((unsigned char*)data - sizeof(CrHdr));
+    return h->size / elemsz;
+}
+
+void crn_check_block(void* data, uint64_t i, uint64_t elemsz) {
+    uint64_t n = crn_block_n(data, elemsz);
+    if (i >= n) crn_die_range(i, n);
+}
+
+void crn_check_range(uint64_t i, uint64_t n) {
+    if (i >= n) crn_die_range(i, n);
+}
+
+/* ---- relaxed atomic scalar access ---- */
+
+#if defined(__GNUC__) || defined(__clang__)
+#define CRN_LD(P) __atomic_load_n((P), __ATOMIC_RELAXED)
+#define CRN_ST(P, V) __atomic_store_n((P), (V), __ATOMIC_RELAXED)
+#else
+/* fallback for hosts without the builtins: plain access (single aligned
+   scalar loads/stores do not tear on such hosts in practice) */
+#define CRN_LD(P) (*(P))
+#define CRN_ST(P, V) (*(P) = (V))
+#endif
+
+uint8_t  crn_ld_u8(const uint8_t* p) { return CRN_LD((uint8_t*)p); }
+uint16_t crn_ld_u16(const uint16_t* p) { return CRN_LD((uint16_t*)p); }
+uint32_t crn_ld_u32(const uint32_t* p) { return CRN_LD((uint32_t*)p); }
+uint64_t crn_ld_u64(const uint64_t* p) { return CRN_LD((uint64_t*)p); }
+float    crn_ld_f32(const float* p) {
+    uint32_t b = crn_ld_u32((const uint32_t*)(const void*)p);
+    float v;
+    memcpy(&v, &b, sizeof v);
+    return v;
+}
+double   crn_ld_f64(const double* p) {
+    uint64_t b = crn_ld_u64((const uint64_t*)(const void*)p);
+    double v;
+    memcpy(&v, &b, sizeof v);
+    return v;
+}
+void crn_st_u8(uint8_t* p, uint8_t v) { CRN_ST(p, v); }
+void crn_st_u16(uint16_t* p, uint16_t v) { CRN_ST(p, v); }
+void crn_st_u32(uint32_t* p, uint32_t v) { CRN_ST(p, v); }
+void crn_st_u64(uint64_t* p, uint64_t v) { CRN_ST(p, v); }
+void crn_st_f32(float* p, float v) {
+    uint32_t b;
+    memcpy(&b, &v, sizeof b);
+    crn_st_u32((uint32_t*)(void*)p, b);
+}
+void crn_st_f64(double* p, double v) {
+    uint64_t b;
+    memcpy(&b, &v, sizeof b);
+    crn_st_u64((uint64_t*)(void*)p, b);
+}
+
+/* ---- atomic exchange (acquire-release) ---- */
+
+uint8_t crn_xch_u8(uint8_t* p, uint8_t v) {
+#if defined(__GNUC__) || defined(__clang__)
+    return __atomic_exchange_n(p, v, __ATOMIC_ACQ_REL);
+#else
+    /* fallback for hosts without the builtins: a plain swap (correct for
+       a single context; such hosts are not targets for racing programs) */
+    uint8_t t = *p;
+    *p = v;
+    return t;
+#endif
+}
+uint16_t crn_xch_u16(uint16_t* p, uint16_t v) {
+#if defined(__GNUC__) || defined(__clang__)
+    return __atomic_exchange_n(p, v, __ATOMIC_ACQ_REL);
+#else
+    uint16_t t = *p;
+    *p = v;
+    return t;
+#endif
+}
+uint32_t crn_xch_u32(uint32_t* p, uint32_t v) {
+#if defined(__GNUC__) || defined(__clang__)
+    return __atomic_exchange_n(p, v, __ATOMIC_ACQ_REL);
+#else
+    uint32_t t = *p;
+    *p = v;
+    return t;
+#endif
+}
+uint64_t crn_xch_u64(uint64_t* p, uint64_t v) {
+#if defined(__GNUC__) || defined(__clang__)
+    return __atomic_exchange_n(p, v, __ATOMIC_ACQ_REL);
+#else
+    uint64_t t = *p;
+    *p = v;
+    return t;
+#endif
+}
+float crn_xch_f32(float* p, float v) {
+    uint32_t b, nv;
+    memcpy(&nv, &v, sizeof nv);
+    b = crn_xch_u32((uint32_t*)(void*)p, nv);
+    float r;
+    memcpy(&r, &b, sizeof r);
+    return r;
+}
+double crn_xch_f64(double* p, double v) {
+    uint64_t b, nv;
+    memcpy(&nv, &v, sizeof nv);
+    b = crn_xch_u64((uint64_t*)(void*)p, nv);
+    double r;
+    memcpy(&r, &b, sizeof r);
+    return r;
+}
+
+/* ---- conversions ---- */
+
+int64_t crn_d2i64(double d) {
+    if (d >= 9223372036854775808.0) return INT64_MAX;
+    if (d <= -9223372036854775808.0) return INT64_MIN;
+    return (int64_t)d;
+}
+
+uint64_t crn_d2u64(double d) {
+    if (d >= 18446744073709551616.0) return UINT64_MAX;
+    if (d <= 0.0) return 0;
+    return (uint64_t)d;
+}
+
+int64_t crn_d2i8(double d) {
+    if (d >= 127.0) return 127;
+    if (d <= -128.0) return -128;
+    return (int64_t)(int8_t)d;
+}
+
+int64_t crn_d2i16(double d) {
+    if (d >= 32767.0) return 32767;
+    if (d <= -32768.0) return -32768;
+    return (int64_t)(int16_t)d;
+}
+
+int64_t crn_d2i32(double d) {
+    if (d >= 2147483648.0) return 2147483647;
+    if (d <= -2147483649.0) return -2147483648;
+    return (int64_t)(int32_t)d;
+}
+
+uint64_t crn_d2u8(double d) {
+    if (d >= 255.0) return 255;
+    if (d <= 0.0) return 0;
+    return (uint64_t)(uint8_t)d;
+}
+
+uint64_t crn_d2u16(double d) {
+    if (d >= 65535.0) return 65535;
+    if (d <= 0.0) return 0;
+    return (uint64_t)(uint16_t)d;
+}
+
+uint64_t crn_d2u32(double d) {
+    if (d >= 4294967296.0) return 4294967295ULL;
+    if (d <= 0.0) return 0;
+    return (uint64_t)(uint32_t)d;
+}
+
+/* ---- output ---- */
+
+static void crn_out_str(const char* s) { fputs(s, cr_ctx.out); }
+
+void crn_out_i64(int64_t v) { fprintf(cr_ctx.out, "%lld", (long long)v); }
+void crn_out_u64(uint64_t v) { fprintf(cr_ctx.out, "%llu", (unsigned long long)v); }
+
+static void crn_out_f(double v, int maxprec) {
+    char buf[64];
+    if (v != v) { /* NaN: the sign is meaningless; print one spelling */
+        crn_out_str("nan");
+        return;
+    }
+    for (int prec = 1; prec <= maxprec; prec++) {
+        snprintf(buf, sizeof buf, "%.*g", prec, v);
+        if (strtod(buf, NULL) == v) break;
+    }
+    crn_out_str(buf);
+}
+
+void crn_out_f64(double v) { crn_out_f(v, 17); }
+void crn_out_f32(float v) {
+    char buf[64];
+    if (v != v) {
+        crn_out_str("nan");
+        return;
+    }
+    for (int prec = 1; prec <= 9; prec++) {
+        snprintf(buf, sizeof buf, "%.*g", prec, v);
+        if ((float)strtod(buf, NULL) == v) break;
+    }
+    crn_out_str(buf);
+}
+void crn_out_bool(int b) { crn_out_str(b ? "true" : "false"); }
+void crn_out_char(unsigned c) { fputc((int)(c & 0xFF), cr_ctx.out); }
+void crn_out_addr(uint64_t a) { fprintf(cr_ctx.out, "0x%llx", (unsigned long long)a); }
+
+void crn_out_text(uint8_t* p) {
+    FILE* f = cr_ctx.out;
+    while (*p) fputc(*p++, f);
+}
+
+/* ---- input / files / system ---- */
+
+int64_t crn_input(void) {
+    int c = fgetc(cr_ctx.in);
+    if (c == EOF) return -1;
+    return c;
+}
+
+int64_t crn_open(uint8_t* path, int mode) {
+    const char* m;
+    switch (mode) {
+    case 0: m = "rb"; break;
+    case 1: m = "wb"; break;
+    case 2: m = "w+b"; break;
+    case 3: m = "ab"; break;
+    default: return -1;
+    }
+    FILE* f = fopen((const char*)path, m);
+    if (!f) return -1;
+    /* find a free slot; handles are slot+1 */
+    for (int i = 0; i < CR_FILES_MAX; i++) {
+        if (cr_ctx.files[i]) continue;
+        cr_ctx.files[i] = f;
+        cr_ctx.fmodes[i] = (unsigned char)mode;
+        return i + 1;
+    }
+    fclose(f);
+    return -1; /* table full */
+}
+
+void crn_use(int64_t h) {
+    if (h == 0) {
+        cr_ctx.in = stdin;
+        cr_ctx.out = stdout;
+        return;
+    }
+    if (h < 1 || h > CR_FILES_MAX || !cr_ctx.files[h - 1])
+        crn_die("use of a handle that is not open in this context "
+                "(handles come from 'open'; 0 means the standard streams)");
+    FILE* f = cr_ctx.files[h - 1];
+    switch (cr_ctx.fmodes[h - 1]) {
+    case 0: cr_ctx.in = f; break;                       /* read */
+    case 2: cr_ctx.in = f; cr_ctx.out = f; break;       /* both */
+    default: cr_ctx.out = f; break;                     /* write / append */
+    }
+}
+
+void crn_flush(void) {
+    if (fflush(cr_ctx.out) != 0) crn_die("flush failed");
+}
+
+int64_t crn_seek(int64_t off) {
+    FILE* a = cr_ctx.in;
+    FILE* b = cr_ctx.out;
+    int any = 0;
+    int bad = 0;
+    /* standard streams are not positionable; only handle streams are */
+    if (a && a != stdin) {
+        any = 1;
+        if (fseek(a, (long)off, SEEK_SET) != 0) bad = 1;
+    }
+    if (b && b != a && b != stdout) {
+        any = 1;
+        if (fseek(b, (long)off, SEEK_SET) != 0) bad = 1;
+    }
+    if (!any)
+        crn_die("seek requires a stream routed through an open handle "
+                "('use' a handle from 'open' first)");
+    return bad ? -1 : 0;
+}
+
+int64_t crn_close(int64_t h) {
+    if (h == 0)
+        crn_die("close of handle 0 (the standard streams cannot be closed)");
+    if (h < 1 || h > CR_FILES_MAX || !cr_ctx.files[h - 1])
+        crn_die("close of a handle that is not open in this context "
+                "(handles come from 'open' and are closed once)");
+    FILE* f = cr_ctx.files[h - 1];
+    cr_ctx.files[h - 1] = NULL;
+    if (cr_ctx.in == f) cr_ctx.in = stdin;
+    if (cr_ctx.out == f) cr_ctx.out = stdout;
+    return fclose(f) == 0 ? 0 : -1;
+}
+
+int64_t crn_system(uint8_t* cmd) { return (int64_t)system((const char*)cmd); }
+
+void crn_stop(int64_t code) { exit((int)(code & 0xFF)); }
+
+/* ---- concurrency ---- */
+
+typedef struct CrSpawnArg {
+    CrFnRef* fn;
+} CrSpawnArg;
+
+static void* crn_thread_main(void* argp) {
+    CrSpawnArg* a = argp;
+    CrFnRef* fn = a->fn;
+    free(a);
+    crn_init();
+    fn->fn();
+    return NULL;
+}
+
+void crn_spawn(CrFnRef* f) {
+    if (f->np != 0)
+        crn_die("spawn of a transformation that takes parameters");
+    if (cr_ctx.nspawned >= CR_SPAWN_MAX) {
+        char msg[80];
+        snprintf(msg, sizeof msg,
+                 "too many outstanding spawned contexts (limit %d)", CR_SPAWN_MAX);
+        crn_die(msg);
+    }
+    CrSpawnArg* a = malloc(sizeof(CrSpawnArg));
+    if (!a) crn_die("out of memory");
+    a->fn = f;
+    if (pthread_create(&cr_ctx.spawned[cr_ctx.nspawned++], NULL, crn_thread_main, a) != 0)
+        crn_die("thread creation failed");
+}
+
+void crn_wait(void) {
+    for (int i = 0; i < cr_ctx.nspawned; i++)
+        pthread_join(cr_ctx.spawned[i], NULL);
+    cr_ctx.nspawned = 0;
+}
+
+
+static uint8_t g_nl[2] = {0};
+static int8_t g_c8 = 0;
+static uint8_t g_u8 = 0;
+static int16_t g_c16 = 0;
+static uint16_t g_u16 = 0;
+static int32_t g_c32 = 0;
+static uint32_t g_u32 = 0;
+static int64_t g_c64 = 0;
+static uint64_t g_u64 = 0;
+static uint8_t g_b = 0;
+static uint8_t g_ch = 0;
+static float g_f32 = 0;
+static double g_f64 = 0;
+static const uint8_t g_nl_init[] = "\n\000";
+
+static Val cr_fn_newline(void);
+static Val cr_fn_main(void);
+
+static CrFnRef cr_ref_newline = {(Val(*)(void))cr_fn_newline, 0};
+static CrFnRef cr_ref_main = {(Val(*)(void))cr_fn_main, 0};
+
+static void crn_init_world(void) {
+    memcpy(g_nl, g_nl_init, 2);
+}
+
+static Val cr_fn_newline(void) {
+    uint64_t cr_fh0 = cr_ctx.fh_sp;
+    int cr_cmp = 0;
+    Val wv; wv.u = 0;
+    void* F = 0;
+    wv.u = (uint64_t)(uintptr_t)&g_nl;
+    crn_out_text((uint8_t*)(uintptr_t)wv.u);
+    wv.u = (uint64_t)0ULL;
+    cr_ctx.fh_sp = cr_fh0; return wv;
+    cr_ctx.fh_sp = cr_fh0; return wv;
+}
+
+static Val cr_fn_main(void) {
+    uint64_t cr_fh0 = cr_ctx.fh_sp;
+    int cr_cmp = 0;
+    Val wv; wv.u = 0;
+    void* F = 0;
+    wv.u = (uint64_t)18446744073709551607ULL;
+    cr_ctx.fh_sp = cr_fh0; F = &g_c8;
+    crn_st_u8((uint8_t*)F, (uint8_t)(int64_t)wv.u);
+    wv.u = (uint64_t)47ULL;
+    cr_ctx.fh_sp = cr_fh0; F = &g_c8;
+    wv.i = (int64_t)(int8_t)wv.u;
+    wv.i = (int64_t)(int8_t)crn_xch_u8((uint8_t*)F, (int8_t)wv.i);
+    wv.i = (int64_t)(int64_t)wv.u;
+    crn_out_i64((int64_t)wv.u);
+    cr_fn_newline();
+    cr_ctx.fh_sp = cr_fh0; F = &g_c8;
+    wv.i = (int64_t)(int8_t)crn_ld_u8((const uint8_t*)F);
+    wv.i = (int64_t)(int64_t)wv.u;
+    crn_out_i64((int64_t)wv.u);
+    cr_fn_newline();
+    wv.u = (uint64_t)200ULL;
+    cr_ctx.fh_sp = cr_fh0; F = &g_u8;
+    crn_st_u8((uint8_t*)F, (uint8_t)(int64_t)wv.u);
+    wv.u = (uint64_t)1ULL;
+    cr_ctx.fh_sp = cr_fh0; F = &g_u8;
+    wv.u = (uint64_t)(uint8_t)wv.u;
+    wv.u = (uint64_t)(uint8_t)crn_xch_u8((uint8_t*)F, (uint8_t)wv.u);
+    wv.i = (int64_t)(int64_t)wv.u;
+    crn_out_i64((int64_t)wv.u);
+    cr_fn_newline();
+    cr_ctx.fh_sp = cr_fh0; F = &g_u8;
+    wv.u = (uint64_t)crn_ld_u8((const uint8_t*)F);
+    wv.i = (int64_t)(int64_t)wv.u;
+    crn_out_i64((int64_t)wv.u);
+    cr_fn_newline();
+    wv.u = (uint64_t)18446744073709550616ULL;
+    cr_ctx.fh_sp = cr_fh0; F = &g_c16;
+    crn_st_u16((uint16_t*)F, (uint16_t)(int64_t)wv.u);
+    wv.u = (uint64_t)1000ULL;
+    cr_ctx.fh_sp = cr_fh0; F = &g_c16;
+    wv.i = (int64_t)(int16_t)wv.u;
+    wv.i = (int64_t)(int16_t)crn_xch_u16((uint16_t*)F, (int16_t)wv.i);
+    wv.i = (int64_t)(int64_t)wv.u;
+    crn_out_i64((int64_t)wv.u);
+    cr_fn_newline();
+    wv.u = (uint64_t)60000ULL;
+    cr_ctx.fh_sp = cr_fh0; F = &g_u16;
+    crn_st_u16((uint16_t*)F, (uint16_t)(int64_t)wv.u);
+    wv.u = (uint64_t)60001ULL;
+    cr_ctx.fh_sp = cr_fh0; F = &g_u16;
+    wv.u = (uint64_t)(uint16_t)wv.u;
+    wv.u = (uint64_t)(uint16_t)crn_xch_u16((uint16_t*)F, (uint16_t)wv.u);
+    wv.i = (int64_t)(int64_t)wv.u;
+    crn_out_i64((int64_t)wv.u);
+    cr_fn_newline();
+    wv.u = (uint64_t)18446744071709551616ULL;
+    cr_ctx.fh_sp = cr_fh0; F = &g_c32;
+    crn_st_u32((uint32_t*)F, (uint32_t)(int64_t)wv.u);
+    wv.u = (uint64_t)7ULL;
+    cr_ctx.fh_sp = cr_fh0; F = &g_c32;
+    wv.i = (int64_t)(int32_t)wv.u;
+    wv.i = (int64_t)(int32_t)crn_xch_u32((uint32_t*)F, (int32_t)wv.i);
+    wv.i = (int64_t)(int64_t)wv.u;
+    crn_out_i64((int64_t)wv.u);
+    cr_fn_newline();
+    wv.u = (uint64_t)4000000000ULL;
+    cr_ctx.fh_sp = cr_fh0; F = &g_u32;
+    crn_st_u32((uint32_t*)F, (uint32_t)(int64_t)wv.u);
+    wv.u = (uint64_t)8ULL;
+    cr_ctx.fh_sp = cr_fh0; F = &g_u32;
+    wv.u = (uint64_t)(uint32_t)wv.u;
+    wv.u = (uint64_t)(uint32_t)crn_xch_u32((uint32_t*)F, (uint32_t)wv.u);
+    wv.i = (int64_t)(int64_t)wv.u;
+    crn_out_i64((int64_t)wv.u);
+    cr_fn_newline();
+    wv.u = (uint64_t)18446744068709551616ULL;
+    cr_ctx.fh_sp = cr_fh0; F = &g_c64;
+    crn_st_u64((uint64_t*)F, (uint64_t)(int64_t)wv.u);
+    wv.u = (uint64_t)5000000000ULL;
+    cr_ctx.fh_sp = cr_fh0; F = &g_c64;
+    wv.i = (int64_t)(int64_t)wv.u;
+    wv.i = (int64_t)(int64_t)crn_xch_u64((uint64_t*)F, (int64_t)wv.i);
+    crn_out_i64((int64_t)wv.u);
+    cr_fn_newline();
+    wv.u = (uint64_t)18000000000000000000ULL;
+    cr_ctx.fh_sp = cr_fh0; F = &g_u64;
+    crn_st_u64((uint64_t*)F, (uint64_t)(int64_t)wv.u);
+    wv.u = (uint64_t)2ULL;
+    cr_ctx.fh_sp = cr_fh0; F = &g_u64;
+    wv.u = (uint64_t)(uint64_t)wv.u;
+    wv.u = (uint64_t)(uint64_t)crn_xch_u64((uint64_t*)F, (uint64_t)wv.u);
+    crn_out_u64(wv.u);
+    cr_fn_newline();
+    wv.u = (uint64_t)0ULL;
+    cr_ctx.fh_sp = cr_fh0; F = &g_b;
+    crn_st_u8((uint8_t*)F, (uint8_t)wv.u);
+    wv.u = (uint64_t)1ULL;
+    cr_ctx.fh_sp = cr_fh0; F = &g_b;
+    wv.u = (uint64_t)(uint8_t)crn_xch_u8((uint8_t*)F, (uint8_t)wv.u);
+    crn_out_bool((int)wv.u);
+    cr_fn_newline();
+    cr_ctx.fh_sp = cr_fh0; F = &g_b;
+    wv.u = (uint64_t)crn_ld_u8((const uint8_t*)F);
+    crn_out_bool((int)wv.u);
+    cr_fn_newline();
+    wv.u = (uint64_t)81ULL;
+    cr_ctx.fh_sp = cr_fh0; F = &g_ch;
+    crn_st_u8((uint8_t*)F, (uint8_t)wv.u);
+    wv.u = (uint64_t)90ULL;
+    cr_ctx.fh_sp = cr_fh0; F = &g_ch;
+    wv.u = (uint64_t)(uint8_t)crn_xch_u8((uint8_t*)F, (uint8_t)wv.u);
+    crn_out_char((unsigned)wv.u);
+    cr_fn_newline();
+    wv.d = (double)1.5;
+    cr_ctx.fh_sp = cr_fh0; F = &g_f32;
+    crn_st_f32((float*)F, (float)wv.d);
+    wv.d = (double)2.75;
+    cr_ctx.fh_sp = cr_fh0; F = &g_f32;
+    wv.d = (double)(float)wv.d;
+    wv.d = (double)crn_xch_f32((float*)F, (float)wv.d);
+    crn_out_f32((float)wv.d);
+    cr_fn_newline();
+    cr_ctx.fh_sp = cr_fh0; F = &g_f32;
+    wv.d = (double)crn_ld_f32((const float*)F);
+    crn_out_f32((float)wv.d);
+    cr_fn_newline();
+    wv.d = (double)0.125;
+    cr_ctx.fh_sp = cr_fh0; F = &g_f64;
+    crn_st_f64((double*)F, wv.d);
+    wv.d = (double)6.5;
+    cr_ctx.fh_sp = cr_fh0; F = &g_f64;
+    wv.d = crn_xch_f64((double*)F, wv.d);
+    crn_out_f64(wv.d);
+    cr_fn_newline();
+    wv.u = (uint64_t)0ULL;
+    cr_ctx.fh_sp = cr_fh0; return wv;
+    cr_ctx.fh_sp = cr_fh0; return wv;
+}
+
+int main(void) {
+    crn_init();
+    crn_init_world();
+    cr_fn_main();
+    fflush(NULL);
+    return 0;
+}
diff --git a/tests/34_xchgwidths.cryn b/tests/34_xchgwidths.cryn
new file mode 100644
index 0000000..dd46880
--- /dev/null
+++ b/tests/34_xchgwidths.cryn
@@ -0,0 +1,171 @@
+[# exchange on every scalar width: the old cell value leaves, the new
+   one lands, and the type rules hold for each width #]
+make string "\n" nl
+make int8 c8
+make uint8 u8
+make int16 c16
+make uint16 u16
+make int32 c32
+make uint32 u32
+make int64 c64
+make uint64 u64
+make bool b
+make char ch
+make float32 f32
+make float64 f64
+
+define newline result nothing
+  address nl
+  output text
+  set nothing
+  return
+end
+
+define main
+  [# int8 #]
+  set -9
+  move c8
+  write
+  set 47
+  move c8
+  exchange
+  convert int64
+  output
+  call newline
+  move c8
+  read
+  convert int64
+  output
+  call newline
+
+  [# uint8 #]
+  set 200
+  move u8
+  write
+  set 1
+  move u8
+  exchange
+  convert int64
+  output
+  call newline
+  move u8
+  read
+  convert int64
+  output
+  call newline
+
+  [# int16 #]
+  set -1000
+  move c16
+  write
+  set 1000
+  move c16
+  exchange
+  convert int64
+  output
+  call newline
+
+  [# uint16 #]
+  set 60000
+  move u16
+  write
+  set 60001
+  move u16
+  exchange
+  convert int64
+  output
+  call newline
+
+  [# int32 #]
+  set -2000000000
+  move c32
+  write
+  set 7
+  move c32
+  exchange
+  convert int64
+  output
+  call newline
+
+  [# uint32 #]
+  set 4000000000
+  move u32
+  write
+  set 8
+  move u32
+  exchange
+  convert int64
+  output
+  call newline
+
+  [# int64 #]
+  set -5000000000
+  move c64
+  write
+  set 5000000000
+  move c64
+  exchange
+  output
+  call newline
+
+  [# uint64 #]
+  set 18000000000000000000
+  move u64
+  write
+  set 2
+  move u64
+  exchange
+  output
+  call newline
+
+  [# bool #]
+  set false
+  move b
+  write
+  set true
+  move b
+  exchange
+  output
+  call newline
+  move b
+  read
+  output
+  call newline
+
+  [# char #]
+  set 'Q'
+  move ch
+  write
+  set 'Z'
+  move ch
+  exchange
+  output
+  call newline
+
+  [# float32 #]
+  set 1.5
+  move f32
+  write
+  set 2.75
+  move f32
+  exchange
+  output
+  call newline
+  move f32
+  read
+  output
+  call newline
+
+  [# float64 #]
+  set 0.125
+  move f64
+  write
+  set 6.5
+  move f64
+  exchange
+  output
+  call newline
+
+  set nothing
+  return
+end
diff --git a/tests/35_seekedge.cryn b/tests/35_seekedge.cryn
new file mode 100644
index 0000000..2eb9bb2
--- /dev/null
+++ b/tests/35_seekedge.cryn
@@ -0,0 +1,129 @@
+[# seek edges: offset in the working value and as a literal, seeking
+   before the start fails without moving the position, seeking past the
+   end reads end of file and writes extend the file #]
+define main
+  make string "cryn_test_35.txt" path
+  make int64 h
+  make int64 r1
+  make int64 r2
+  make int64 r3
+
+  address path
+  open both
+  home
+  compare 0
+  jump bad when less
+  move h
+  write
+  push
+  use
+  home
+
+  make string "abcdef" data
+  address data
+  output text
+  flush
+
+  [# literal form: land on byte 3 and read it #]
+  seek 3
+  home
+  compare 0
+  jump ok1 when equal
+  set 1
+  stop
+  mark ok1
+  input
+  move r1
+  write
+
+  [# working value form: land on byte 0 and read it #]
+  set 0
+  seek
+  home
+  compare 0
+  jump ok2 when equal
+  set 1
+  stop
+  mark ok2
+  input
+  move r2
+  write
+
+  [# negative offset fails, reports -1, position stays put at byte 1 #]
+  set -1
+  seek
+  home
+  compare -1
+  jump ok3 when equal
+  set 1
+  stop
+  mark ok3
+  input
+  move r3
+  write
+
+  [# past the end reads end of file #]
+  seek 100
+  home
+  compare 0
+  jump ok4 when equal
+  set 1
+  stop
+  mark ok4
+  input
+  compare -1
+  jump past when equal
+  set 1
+  stop
+  mark past
+
+  [# writing there extends the file: byte 100 becomes G over a gap #]
+  set 71
+  convert uint8
+  output
+  flush
+
+  pop
+  close
+  use 0
+
+  move r1
+  read
+  convert uint8
+  output
+  move r2
+  read
+  convert uint8
+  output
+  move r3
+  read
+  convert uint8
+  output
+
+  [# read the whole file back: six letters, one gap byte, one G #]
+  address path
+  open read
+  move h
+  write
+  push
+  use
+  home
+  set 0
+  mark rd
+  input
+  compare -1
+  jump fin when equal
+  convert uint8
+  output
+  jump rd
+  mark fin
+
+  pop
+  close
+  set 0
+  stop
+
+  mark bad
+  set 1
+  stop
+end
diff --git a/tests/expected/31_seek.txt b/tests/expected/31_seek.txt
new file mode 100644
index 0000000..8042f2b
--- /dev/null
+++ b/tests/expected/31_seek.txt
@@ -0,0 +1 @@
+0ABABCXE
\ No newline at end of file
diff --git a/tests/expected/32_cas.txt b/tests/expected/32_cas.txt
new file mode 100644
index 0000000..e3b5acb
--- /dev/null
+++ b/tests/expected/32_cas.txt
@@ -0,0 +1 @@
+107
\ No newline at end of file
diff --git a/tests/expected/33_conversions.txt b/tests/expected/33_conversions.txt
new file mode 100644
index 0000000..3335444
--- /dev/null
+++ b/tests/expected/33_conversions.txt
@@ -0,0 +1,21 @@
+44
+255
+-126
+65535
+34464
+18446744073709551615
+705032704
+7
+-3
+255
+0
+1000000000000000000
+9223372036854775807
+0
+0.1
+3.1415927
+16777216
+true
+false
+true
+false
diff --git a/tests/expected/34_xchgwidths.txt b/tests/expected/34_xchgwidths.txt
new file mode 100644
index 0000000..6ffb08b
--- /dev/null
+++ b/tests/expected/34_xchgwidths.txt
@@ -0,0 +1,16 @@
+-9
+47
+200
+1
+-1000
+60000
+-2000000000
+4000000000
+-5000000000
+18000000000000000000
+false
+true
+Q
+1.5
+2.75
+0.125
diff --git a/tests/expected/35_seekedge.txt b/tests/expected/35_seekedge.txt
new file mode 100644
index 0000000..7285fb0
Binary files /dev/null and b/tests/expected/35_seekedge.txt differ
diff --git a/tests/run.sh b/tests/run.sh
index d51c3e4..9b19bcb 100644
--- a/tests/run.sh
+++ b/tests/run.sh
@@ -1,9 +1,11 @@
 #!/bin/sh
 # Cryn test suite: compile and run each tests/*.cryn and compare with
 # tests/expected/<name>.txt. Error tests in tests/errors must fail to compile.
+# CRYN_ARGS: extra flags passed to the compiler (e.g. --release).
 set -u
 cd "$(dirname "$0")"
 CRYN=../cryn
+CRYN_ARGS=${CRYN_ARGS:-}
 pass=0
 fail=0
 
@@ -12,9 +14,9 @@ for src in *.cryn; do
     exp="expected/$name.txt"
     [ -f "$exp" ] || continue
     if [ -f "input/$name.txt" ]; then
-        out=$($CRYN run "$src" < "input/$name.txt" 2>/dev/null)
+        out=$($CRYN run "$src" $CRYN_ARGS < "input/$name.txt" 2>/dev/null)
     else
-        out=$($CRYN run "$src" 2>/dev/null)
+        out=$($CRYN run "$src" $CRYN_ARGS 2>/dev/null)
     fi
     if [ "$out" = "$(cat "$exp")" ]; then
         pass=$((pass + 1))