Copyright (c) 2026 The NURL Project Developers SPDX-License-Identifier: MIT OR Apache-2.0
rjit.nu — tier 8: a register-allocating JIT over the predecoded records.
The template tier (interp.nu, __jit_try) lowers every record through rax against a frame in memory, with the hottest few slots pinned to a handful of registers for the whole function. That is a baseline compiler: whatever does not fit the pins pays a store and a reload on every access, and a value that crosses a loop edge pays a store-forward on every iteration of its dependency chain. This tier is an optimizing one over the same records and the same calling convention:
(a block's results, a loop's params) gets a synthetic EDGE block that holds those moves as one parallel copy;
independent values that happen to share a storage slot (the operand-stack slots are reused for every expression in the body);
live across a call takes a callee-saved register or memory;
A spilled web lives in its slot's frame home, [rbx + 8*slot]: two webs of one slot are never live on the same path at once, so they can share it. The frame, the argument and result windows, every call-out and every trap stub are the template tier's own, so the two tiers call each other through the same direct-entry table and fall back to the same interpreter bridges.
This file is pure: it turns records into machine code plus the patch lists that need the page address. interp.nu owns the page.
: Rj: Rj {
( Vec u ) buf // the emitted code
( Vec i ) st // scalars — rjs_*
( Vec i ) code // the records, 6 words each (copy)
( Vec i ) aux // br_table rows, 4 words each (copy)
( Vec i ) kv // the constant pool (copy)
( Vec i ) ltype // per local slot: 0 i32, 1 i64, 2 f32, 3 f64, 4 a reference (null = -1)
( Vec i ) rsig // per record: a call's callee nparams*65536 + nresults (-1 none/unknown)
( Vec i ) uoff // per record (n+1): its uses are [uoff[r], uoff[r+1])
( Vec i ) uslot
( Vec i ) uweb
( Vec i ) doff // per record (n+1): its defs are [doff[r], doff[r+1])
( Vec i ) dslot
( Vec i ) dweb
( Vec i ) bfirst // per block: first record (real) / branch record (edge)
( Vec i ) blast // per block: one past its last record (real) / -1 (edge)
( Vec i ) btgt // per block: edge → its target record, real → -1
( Vec i ) bmd // edge block moves: dst base
( Vec i ) bms // … src base
( Vec i ) bmn // … count
( Vec i ) soff // successor lists: [soff[k], soff[k+1]) in ssucc
( Vec i ) ssucc
( Vec i ) rblk // record → the real block holding it
( Vec i ) eblk // per record: first edge block it owns (-1 none)
( Vec i ) lin // live-in bitsets, nb x W words
( Vec i ) lout // live-out bitsets
( Vec i ) emoff // per block: edge-move occurrences [emoff[k], emoff[k+1])
( Vec i ) emu // … source web per moved value
( Vec i ) emd // … destination web per moved value
( Vec i ) uf // union-find parent over web ids
( Vec i ) nslot // per raw web id: its slot
( Vec i ) xpos // interval extension list: (raw id, position) pairs
( Vec i ) entw // entry-defined raw web ids (params / zeroed locals)
( Vec i ) wslot // per web: slot
( Vec i ) wcls // per web: votes (int*65536 + float); then 0 gpr / 1 xmm
( Vec i ) wwt // per web: spill weight
( Vec i ) wst // per web: first position
( Vec i ) wen // per web: last position
( Vec i ) wuse // per web: number of uses
( Vec i ) wloc // per web: location (rjl_*)
( Vec i ) whint // per web: a web whose register it would like
( Vec i ) wcc // per web: 1 when live across a call
( Vec i ) wsens // per web: 1 when some consumer reads bits 32..63 (an i32 def must sign-extend)
( Vec i ) wdx // per web: 1 when live across a record that clobbers rdx (div/rem, bit counts)
( Vec i ) dxr // those records, ascending
( Vec i ) wcx // per web: 1 when it touches a record whose lowering writes rcx
( Vec i ) cxr // those records, ascending
( Vec i ) cxb // per record: 1 when a web in rcx is live across it (a borrower parks rcx)
( Vec i ) wzx // per web: 1 when every value it holds has bits 32..63 clear (an address can index as it stands)
( Vec i ) cs_off // direct call sites: the rel32's buffer offset …
( Vec i ) cs_fx // … the callee …
( Vec i ) cs_ab // … its argument base …
( Vec i ) cs_nr // … its result count (the site's slow stub needs all four) …
( Vec i ) cs_kind // … 1 for a register-argument site …
( Vec i ) cs_np // … and its argument count
( Vec i ) depth // per record: loop weight (8^depth, capped)
( Vec i ) callr // records that clobber caller-saved registers, ascending
( Vec i ) tgt // per record: 1 when some branch targets it
( Vec i ) lab // record / stub label → code offset
( Vec i ) pat_at // rel32 sites …
( Vec i ) pat_rec // … and the label each one names
( Vec i ) pta_off // absolute jump-table entries (code offset) …
( Vec i ) pta_stub // … and the code offset they hold
( Vec i ) ptr_off // absolute entries naming a record label …
( Vec i ) ptr_rec // … resolved into pta_* once labels are known
( Vec i ) litv // the literal pool: wide constants read RIP-relative …
( Vec i ) lsite // … the disp32 sites that read them …
( Vec i ) lidx // … and which literal each one names
( Vec i ) uover // per use: the register a cached read of a spilled web takes (-1 none — rj_cache)
( Vec i ) wflag // per web: 1 when only fused flags read it (it gets no place)
( Vec i ) clhead // per record: its first cache load (-1 none) …
( Vec i ) clnext // … the next one at the same record …
( Vec i ) clreg // … the register it fills …
( Vec i ) clslot // … from this slot's frame home
( Vec i ) wlcar // per web: 1 when a record inside a loop writes it
( Vec i ) rpi32 // per record: a call's callee parameters that are i32, bit k for parameter k (rj_set_pi32)
}
── the per-function compile context ──────────────────────────── Every field is a growable vector, so helpers mutate through the handle; the few scalars live in st under the rjs_* indices below.
@ rjs_n → iscalar state indices
@ rjs_nl → i@ rjs_ns → i@ rjs_np → i@ rjs_nr → i@ rjs_sb → i@ rjs_nb → i@ rjs_nreal → i@ rjs_w → i@ rjs_nw → i@ rjs_nimp → i@ rjs_spcell → i@ rjs_trapfn → i@ rjs_cofn → i@ rjs_coenv → i@ rjs_fidx → i@ rjs_used → i@ rjs_fweb → i@ rjs_fcc → i@ rjs_fail → i@ rjs_nraw → i@ rjs_fsel → i@ rjs_forbid → i@ rjs_sigoff → i@ rjs_tbloff → i@ rjs_fl → i@ rjs_fast → i@ rjs_fastoff → i@ rjs_glob → i@ rjs_pcc → i@ rjs_aidx → i@ rjs_nst → i@ rj_get Rj c i k → i@ rj_set Rj c i k i v → v@ rj_fail Rj c i why → v@ rj_failed Rj c → b@ rj_rw Rj c i r i k → ia record word: 0 op, 1 A, 2 B, 3 C, 4 D, 5 W5
@ rjl_mem → i── locations ─────────────────────────────────────────────────── 0..15 a GPR (rax=0 … r15=15), 16..31 an XMM register (16 + n), rjl_mem the slot's frame home, rjl_imm a constant-pool value.
@ rjl_imm → i@ rj_isg i l → b@ rj_isx i l → b@ rj_b Rj c i x → v── the byte stream ─────────────────────────────────────────────
@ rj_d Rj c i x → v@ rj_q Rj c i x → v@ rj_here Rj c → i@ rj_patch32 Rj c i at i dst → vpatch a rel32 at at to land on code offset dst
@ rj_fits8 i v → b@ rj_fits32 i v → b@ rj_rex Rj c i w i r i x i bb i force → v── x86-64 encoding ───────────────────────────────────────────── REX = 0100WRXB; force emits it even when every bit is clear (the spl/bpl/sil/dil byte registers exist only under a REX prefix).
@ rj_modrr Rj c i reg i rm → v@ rj_modmem Rj c i reg i base i idx i sc i disp → v[base + idx<<sc + disp]; idx < 0 = no index
@ rj_rr Rj c i pfx i w i esc i opc i reg i rm i b8 → v[pfx] [REX] [0F] opc ModRM(reg, rm) — register form. b8 forces REX for the 8-bit view of registers 4..7 in whichever operand is a byte register (1 = rm, 2 = reg, 3 = both).
@ rj_rm Rj c i pfx i w i esc i opc i reg i base i idx i sc i disp i b8 → vthe same with a memory operand
@ rj_home i s → iframe home of slot s
@ rj_mov_rr Rj c i w i dst i src → v@ rj_ldf Rj c i w i dst i s → vmov dst, [rbx + 8*s]
@ rj_stf Rj c i w i s i src → vmov [rbx + 8*s], src
@ rj_mov_ri Rj c i dst i imm i keep → vdst ← imm, shortest form. keep = 1 when the flags must survive (no xor).
@ rj_alu_rr Rj c i w i ext i dst i src → vALU groups: add 0, or 1, and 4, sub 5, xor 6, cmp 7
@ rj_alu_rf Rj c i w i ext i dst i s → v@ rj_alu_ri Rj c i w i ext i dst i imm → vimm must fit 32 bits (sign-extended for w=1)
@ rj_alu_fi Rj c i w i ext i s i imm → vop qword/dword [rbx+8s], imm32
@ rj_test_rr Rj c i w i a i bb → v@ rj_imul_rr Rj c i w i dst i src → v@ rj_imul_rf Rj c i w i dst i s → v@ rj_imul_rri Rj c i w i dst i src i imm → v@ rj_shift_ri Rj c i w i ext i dst i imm → vshifts: rol 0, ror 1, shl 4, shr 5, sar 7
@ rj_shift_cl Rj c i w i ext i dst → v: ~ i g_rjbmi -1x86-64-v3 (BMI1/BMI2, LZCNT) in the emitted code: -1 = ask the CPU on first use, 0 = baseline x86-64 only, 1 = use it
@ rj_set_bmi i v → v@ rj_bmi → b@ rj_shx Rj c i w i pp i dst i src i cnt → vBMI2 shlx (pp 1) / sarx (pp 2) / shrx (pp 3): dst = src shifted by cnt, any three registers, the count masked like the legacy forms; flags untouched. VEX.LZ.0F38.W F7 /r, the count in vvvv.
@ rj_tmp_on Rj c i reg → iA second scratch GPR for an instruction whose own register is reg: rax when that is not rax, else rcx — parked in xmm1 until rj_tmp_off, since rcx may hold a web (movq leaves the flags alone)
@ rj_tmp_off Rj c i t → v@ rj_movsxd Rj c i dst i src → vmovsxd dst, src32
@ rj_mov32 Rj c i dst i src → vmov dst32, src32 (zero-extends)
@ rj_setcc Rj c i cc i rm → v@ rj_movzx8 Rj c i dst i src → v@ rj_cmov_rr Rj c i w i cc i dst i src → v@ rj_cmov_rf Rj c i w i cc i dst i s → v@ rj_push Rj c i r → v@ rj_pop Rj c i r → v@ rj_lea Rj c i w i dst i base i idx i sc i disp → vlea dst, [base + idx<<sc + disp] (w=0: the 32-bit form, which wraps) An rbp / r13 base takes a displacement byte even for 0, and base + index
scaled, dst takes the base first (a mov — no flags touched)
@ rj_ripop Rj c i pfx i w i esc i opc i reg i v → v[pfx] [REX] [0F] opc ModRM(reg, [rip + literal v]) — a wide constant as a memory operand. The disp32 is the instruction's last field (no immediate may follow), patched once the pool's place is known.
@ rj_jcc Rj c i cc i label → v── jumps and labels ──────────────────────────────────────────── Labels 0..n-1 are records; n.. are the trap stubs (see rj_stub_*).
@ rj_jmp Rj c i label → v@ rj_jcc_fwd Rj c i cc → ia forward jump to a not-yet-known local offset: returns the rel32 site
@ rj_jmp_fwd Rj c → i@ rj_land Rj c i site → v@ rj_stub_oob Rj c → istub labels, after the n record labels
@ rj_stub_div0 Rj c → i@ rj_stub_gate Rj c → i@ rj_stub_ovf Rj c → i@ rj_stub_iovf Rj c → i@ rj_stub_inval Rj c → i@ rj_lab_entry Rj c → i@ rj_lab_fast Rj c → i@ rj_cmpcc i op → icondition nibbles: o 0 no 1 b 2 ae 3 e 4 ne 5 be 6 a 7 s 8 ns 9 p 10 np 11 l 12 ge 13 le 14 g 15 compare micro-op (56..75) → the nibble that means "true"
@ rj_ccswap i cc → ithe nibble for the same test with the operands exchanged
@ rj_memkind i op → i── what a record reads and writes ────────────────────────────── The template tier's memory-op table, verbatim: width4 + signed2 + store.
@ rj_isalu i op → btwo-operand integer ALU: dst = B op C
@ rj_isrot i op → b@ rj_isfused i op → bfused i64 pairs: dst = (B op1 C) op2 D
@ rj_hastarget i op → ba record whose A field is a branch target (a word offset)
@ rj_isbranch i op → ba record that leaves the block: anything with a target, or no fall-through
@ rj_iscall i op → ba record whose emitted code calls out of the function (clobbers every caller-saved register)
@ rj_op_ok i op → bThe records this tier lowers; a function holding anything else stays on the template tier (or the interpreter).
@ rj_isfbin i op → bthe float records this tier lowers
@ rj_isfcmp i op → b@ rj_isfconv i op → b@ rj_isfminmax i op → b@ rj_isfloat i op → b@ rj_use Rj c i s → v@ rj_def Rj c i s → v@ rj_sig_np i sig → ia call record's signature word: canon<<32 | nparams<<16 | nresults (canon: call_indirect's expected type, canonicalised; -1 = unknown)
@ rj_sig_nr i sig → i@ rj_sig_canon i sig → i@ rj_set_pi32 Rj c ( Vec i ) v → vper call record: which of the callee's parameters are i32 (bit k), so an i32 argument is sign-extended where it is passed rather than where it is made; without it every argument counts as read in full
@ rj_pi32 Rj c i r i k → b@ rj_argext Rj c i r i k i o → bargument k of call record r (use o) is an i32 whose web skips the sign extension — the call must do it
@ rj_decode Rj c i r → vDecode record r's uses and defs, in the fixed per-op order the emitter reads them back in.
@ rj_tgtrec i w → i── blocks ──────────────────────────────────────────────────────
@ rj_target Rj c i r → ibranch target of a record with one (A field), else -1
@ rj_mark Rj c i t i r → vt is a branch target of record r: 2 marks a loop head (a backward target)
@ rj_edge Rj c i r i t i dst i src i n → inew edge block for branch record r → target record t moving n values
@ rj_blocks Rj c → v@ rj_isconst Rj c i s → b── liveness ────────────────────────────────────────────────────
@ rj_shr i a i n → ilogical shift right (>> is arithmetic on i)
@ rj_bit ( Vec i ) v i base i s → b@ rj_bset ( Vec i ) v i base i s → v@ rj_gen1 Rj c ( Vec i ) gen ( Vec i ) kill i base i s → v@ rj_live Rj c → v@ rj_newraw Rj c i s → i── webs ──────────────────────────────────────────────────────── A raw web id names one definition (or one block's live-in value of a slot); union-find merges the ids that reach a common use. Every id of a set belongs to the same slot — merges only ever join a block's live-out value of slot s with a successor's live-in value of s.
@ rj_find Rj c i x0 → i@ rj_union Rj c i a i bb → v@ rj_bpos0 Rj c i k → ia block's first and last position: 2r = record r's reads, 2r+1 its writes
@ rj_bpos1 Rj c i k → i@ rj_bits ( Vec i ) v i base i w ( Vec i ) out → vthe set bits of words [base, base+w) of v, ascending
@ rj_xpos Rj c i id i pos → v@ rj_webs Rj c → v@ rj_ucls i op i k → i── per-web facts ─────────────────────────────────────────────── Use k / the def of op: 1 when it is an integer value, 2 a float, 0 no opinion (moves, selects and calls carry whatever they are handed).
@ rj_dcls i op → i@ rj_vote Rj c i wv i cls i wt → v@ rj_ext Rj c i wv i pos → v@ rj_addwt Rj c i wv i wt → v@ rj_hint Rj c i dw i sw → v@ rj_canon Rj c → v@ rj_depths Rj c → vloop weights: every backward branch multiplies the span it closes by 8 A loop counts once, from its head to its furthest back edge — each continue is another branch to the same head, not another level.
@ rj_usens i op i k → bDoes use k of op read bits 32..63 of its operand? Every i32 a slot holds is sign-extended (the predecoder drops i64.extend_i32_s on the strength of it), but only these consumers can tell: an i32 def whose web feeds none of them skips the movsxd. Copies (MOV, SEL's values, edge moves) pass the question on to their destination instead of answering it.
@ rj_kin32 Rj c i r i k → bis use k of record r a constant in [0, 2^32)?
@ rj_lowk Rj c i r i op i k → iThe i64 ops whose result's low half needs only their operands' low halves (+ - * & | ^, shl, and the fused pairs built from them) answer for use k themselves: 1 the operand's high half matters exactly when the result's does (the def decides, as for a copy — rj_narrow then computes it with a 32-bit instruction), 0 never (a shift count is read mod 64; the other side of an and with a constant below 2^32 lands in bits the mask clears), 2 always; -1 for any other op (rj_usens decides).
@ rj_narrow Rj c i r i op → bDoes record r compute its i64 result with a 32-bit instruction? Only when no consumer reads the high half, and only where the low half comes out the same: + - * & | ^, shl by a constant below 32, the fused pairs of those. The 32-bit instruction leaves the high half clear (rj_defzx).
@ rj_sens Rj c → vthe sensitivity facts: direct consumers, then back through every copy
@ rj_dcanon Rj c i od → bmust record r's i32 def number k come out sign-extended?
@ rj_facts Rj c → v@ rj_gpool i k → i── linear scan ───────────────────────────────────────────────── GPR order: caller-saved first (no prologue cost), callee-saved after; a web live across a call may only take a callee-saved one. Ordered by what a register costs the function that takes it: rsi and rdx cost nothing; r9/r10/r8 one instruction at every call and return; the callee-saved ones a push and a pop per invocation; rdi (the context) a push, a pop and a reload before every call.
@ rj_calleesaved i l → b@ rj_allowed i l i cls i cross → b@ rj_clobdx i op → brecords whose lowering uses rdx as scratch (its webs may not be live across them): div/rem, and the bit scans on a CPU without lzcnt/tzcnt
@ rj_ukonst Rj c i r i k → bis use k of record r a constant-pool slot?
@ rj_clobcx Rj c i r → bRecords whose lowering may leave something of its own in rcx — no web that touches one may live there. Every other record's lowering was checked to keep rcx intact: rax is its scratch, and where it needs a second one it parks rcx in xmm1 (rj_tmp_on, the stores' cxb test).
@ rj_crosses ( Vec i ) rs i ws i we → bis some record c of the ascending list rs inside (ws, we) — 2c ≥ ws, 2c+2 ≤ we?
@ rj_touches ( Vec i ) rs i ws i we → bis some record c of the ascending list rs touched by [ws, we] — its reads (2c) or writes (2c+1) inside it?
@ rj_dens Rj c i wv → ispill priority: weight per unit of live length — a short temporary on a dependency chain keeps its register, a sparse long-lived value gives it up. A value a loop writes counts eight times over: spilled, every write is a store its next read waits on (a loop-carried chain through memory), where a loop-invariant one only reloads, and the load issues early.
@ rj_alloc Rj c → v: ~ i g_rjtrace -1NURL_NWASM_RJIT_TRACE=<fidx>: that function's webs and where they went
@ rj_set_trace i f → v@ rj_trace Rj c → v@ rj_defzx Rj c i r i op i o → iWhat a def leaves in bits 32..63 of its web: 1 clear (a 32-bit x86 instruction wrote it, or a small non-negative value), 0 unknown, 2 a copy of use 0, 3 a copy of uses 0 and 1 (SEL's two values).
@ rj_kzx Rj c i s → bis the value a constant-pool slot holds zero-extended as mov_ri materialises it?
@ rj_zx Rj c → vwzx: every def of the web leaves bits 32..63 clear. Parameters arrive canonical (sign-extended), a reference local starts at -1, anything not listed in rj_defzx is unknown; copies (MOV, SEL, edge moves) are clear exactly when their sources are, settled by a fixpoint.
@ rj_bfree ( Vec i ) bm i words i l i p i q → bbits [p, q] of register l's row in the position bitmap: all clear?
@ rj_bmark ( Vec i ) bm i words i l i p i q → v@ rj_cgpr i k → ithe GPRs a cached read may take: rdx rsi r8 r9 r10 rbp r12..r15
@ rj_cache Rj c → v@ rj_cxbusy Rj c → vcxb: per record, 1 when a web in rcx is live across it — read before, still needed after — so a record that borrows rcx parks it first. The webs in rcx never overlap, so this is linear in the function.
@ rj_uloc Rj c i o → i── operands ────────────────────────────────────────────────────
@ rj_us Rj c i o → i@ rj_dloc Rj c i o → i@ rj_ds Rj c i o → i@ rj_dlive Rj c i o → b@ rj_kval Rj c i s → i@ rj_u Rj c i r i k → iuse #k / def #k of record r
@ rj_dd Rj c i r i k → i@ rj_movq_xg Rj c i x i g → vmovq xmm, r64 / movq r64, xmm
@ rj_movq_gx Rj c i g i x → v@ rj_ldfx Rj c i x i s → vmovsd xmm, [rbx+8s] / movsd [rbx+8s], xmm
@ rj_stfx Rj c i s i x → v@ rj_movx Rj c i dst i src → v@ rj_ldg Rj c i reg i loc i s i keep → vGPR reg ← operand (64-bit). keep = 1: the flags must survive.
@ rj_ldx Rj c i x i loc i s → vXMM x ← operand
@ rj_stg Rj c i loc i s i reg → vdef location ← GPR reg
@ rj_stx Rj c i loc i s i x → vdef location ← XMM x
@ rj_move Rj c i dl i ds i sl i ss → va whole-value copy between two operand places (dl/ds ← sl/ss)
@ rj_greg i loc → iA register operand's GPR, or -1 when the operand lives elsewhere.
@ rj_opsrc Rj c i w i ext i reg i loc i s → vop R, operand for the ALU group ext, width w. An operand in an xmm register goes through a scratch GPR (rj_tmp_on).
@ rj_sx32 i v → i── lowering: integer arithmetic ────────────────────────────────
@ rj_e_bin Rj c i r i ext i w → vdst = a OP b, OP one of the ALU groups (add 0, or 1, and 4, sub 5, xor 6); w = 1 for i64, 0 for i32 (the result is then sign-extended — every i32 a slot holds is canonical, and the predecoder relies on it).
@ rj_e_shift Rj c i r i ext i w0 → vshifts and rotates: ext shl 4, shr 5, sar 7, rol 0, ror 1
@ rj_imulsrc Rj c i w i reg i loc i s → vimul R, operand
@ rj_e_mul Rj c i r i w0 → v@ rj_fop1 Rj c i op i w i tr i l2 i s2 → vthe fused i64 pairs: dst = (s1 op1 s2) op2 x
@ rj_shrx_by Rj c i tr i l i s → vtr >>u= operand with shrx: the count from its register, else a scratch (tr is never the count's register — the callers keep them apart)
@ rj_fop1_from Rj c i op i w i tr i l1 i s1 i l2 i s2 → vtr = s1 op1 s2 for the fused pairs, straight from s1's register when it has one: imul tr, s1, imm / lea tr, [s1 + s2 or imm] — no copy of s1 first
@ rj_e_fused Rj c i r → v@ rj_cmp1 Rj c i w i al i as i bl i bs → v── compares and selects ──────────────────────────────────────── cmp a, b (width w), a not an immediate
@ rj_cmp Rj c i w i al i as i bl i bs i cc → icmp for the condition nibble cc; returns the nibble that holds after any operand exchange
@ rj_cfuse Rj c i r → iDoes record r's value feed only the IFZ / BRIF right behind it, so the pair can become one flag-setting instruction and a jcc? 1: i32/i64.and with an imm32 → test; 2: SHRUAND by anything, mask 1 → bt. 0: neither.
@ rj_selrun Rj c i r i wd → ithe fused-SEL run right behind a compare whose 0/1 is web wd
@ rj_setbool Rj c i cc i dl i ds i pre → vmaterialise a compare's 0/1 into dst after the flags are set; pre = 1 when dst was zeroed before the compare (then setcc alone finishes it)
@ rj_e_cmp Rj c i r → v@ rj_testop Rj c i w i loc i s → vtest operand (w) — sets ZF from it; an immediate goes through rax
@ rj_e_cfuse Rj c i r i kind → vtest a, imm / bt x, s for the IFZ / BRIF behind (rj_cfuse): only the flags; rjs_pcc tells the branch which jcc means "the value is zero"
@ rj_andz Rj c i q → iIs record q an and with a constant whose value only the eqz right behind it reads (same width, no branch lands between)? 1: test x, imm32; 2: bt x, b for a single-bit 64-bit mask; 0: neither. The and itself emits nothing.
@ rj_e_andtest Rj c i q i ak → ithe flags for eqz(and(x, k)) from x itself; returns the jcc/cmov nibble that means "the and is zero"
@ rj_e_eqz Rj c i r i w → v@ rj_cmovsrc Rj c i cc i reg i loc i s → vcmov<cc> R, operand (64-bit); immediates and xmm through a scratch GPR (the flags survive: the loads are movs, the park is a movq)
@ rj_e_sel Rj c i r → v@ rj_e_ext Rj c i r → vi32.wrap_i64 (dst = sign-extended low half), i64.extend_i32_u (zero-extended low half), the extendN_s family
@ rj_e_clz Rj c i r → vi32.clz: bsr + cmov (baseline x86-64, no lzcnt)
@ rj_addr Rj c i bl i bs i xl i xs i hasx i bz → v── lowering: linear memory (guard-page mode: no bounds checks) ── eax ← the wrapped 32-bit address (base + idx), idx an operand or none — or, when the base alone is the address and its web is zero-extended (bz), nothing at all: its own register becomes the index (rjs_aidx)
@ rj_rj32 Rj c i s → ia constant's low 32 bits, sign-extended (a displacement / imm32)
@ rj_mrm Rj c i pfx i w i esc i opc i reg i off i b8 → v[r11 + idx + off], idx the register rj_addr left the address in (rax unless a zero-extended base)
@ rj_uzx Rj c i o → iuse o's web holds only zero-extended values (a constant: no — rj_addr folds those)
@ rj_ldmem Rj c i tr i wid i sg i off → vload wid bytes (sg: sign-extend) from [r11+rax+off] into GPR tr
@ rj_ldto Rj c i od i wid i sg i off → vthe load into its def's place: xmm defs take an 8- or 4-byte float load directly
@ rj_e_load Rj c i r → v@ rj_e_store Rj c i r → v@ rj_e_loadshl Rj c i r → vLOADSHL: dst = mem[(x << k) w32 + off]; LOADSHLADD adds a base first
@ rj_e_loadop Rj c i r → vLOADMULI64 / LOADADDI64: dst = mem64[base w32 + off] * / + x
@ rj_goto Rj c i r i t → v── lowering: control ───────────────────────────────────────────
@ rj_place i loc i s → ithe destination of an edge move, as a place no other location aliases
@ rj_pmove Rj c ( Vec i ) dl ( Vec i ) ds ( Vec i ) sl ( Vec i ) ss → vOne parallel copy: dl[k]/ds[k] ← sl[k]/ss[k] for every k at once. A move goes once nothing still pending reads its destination; a cycle is broken through xmm1 (a scratch no web lives in — any GPR may hold one).
@ rj_e_moves Rj c i e → vAn edge block's parallel copy.
@ rj_e_brm Rj c i r → vthe edge block a branch record owns (BRM / BRIFM)
@ rj_e_brifm Rj c i r → v@ rj_e_brtbl Rj c i r → v@ rj_e_brifc Rj c i r → v@ rj_alu3 Rj c i ext i w i al i as i bl i bs i dl i ds b cn b nolea → idst = a OP b into dst's place; returns the GPR holding the result nolea: the flags must come from the op itself (an lea sets none)
@ rj_e_addbr Rj c i r → vADDBRIFC: dst = s1 + s2; branch when (dst cmp rhs) holds
@ rj_areg i k → i── the register calling convention (tier 8 → tier 8) ────────── A callee with at most five parameters and at most one result gets a second, FAST entry: argument k arrives in rj_areg(k), the result leaves in rax, and nothing is stored to or reloaded from the caller's window. rcx is still the callee's frame base and rsi the window (the slab-full path spills the arguments there), rdi the context. Neither side keeps r8/r9/r10 as invariants across a fast call: tier 8 rematerialises the anchor wherever it reads it, reserves r9 for the globals base only in a function that touches globals (and reloads it after every call), and reads the memory size from the context. The memory entry (+28) — the driver's, the template tier's and the bridges' way in — is a wrapper: it loads the window into the argument registers, calls the fast entry, stores the result back and re-establishes the invariants its callers rely on.
@ rj_fastsig i sig → b@ rj_restore_ctx Rj c → vrdi back to the context (a callee and every call-out need it there)
@ rj_reglob Rj c → vr9 back to the globals base after something may have clobbered it
@ rj_callout Rj c i kind → v── lowering: calls ───────────────────────────────────────────── the inline call-out into the runtime (the template tier's own sequence)
@ rj_uses Rj c i l → br9/r10 are cross-call invariants (globals base, memory bytes); a function that allocated them puts them back before control leaves it
@ rj_restore_inv Rj c → vThe invariants a template-tier function, a bridge or a memory-entry caller expects: rdi the context, r8 the anchor, r9 the globals base, r10 the memory size. Re-established before every memory-ABI call, call-out and memory-entry return — unconditionally, since a fast callee may have left anything in r8..r10.
@ rj_argsp Rj c → ithe args pointer's stack offset: above the saved context when rdi was handed out
@ rj_frameless Rj c → b@ rj_frame_end Rj c → vlea rcx,[rbx + nslots*8] — the callee's frame base (the end of ours)
@ rj_args_home Rj c i r i np i ab → va memory-ABI call's arguments into the window [rbx + 8*(ab + k)], each i32 one the web left zero-extended sign-extended there
@ rj_e_call Rj c i r → v@ rj_e_ret Rj c i r → v@ rj_magicu i dd i w → ( Vec i )── division by a constant ────────────────────────────────────── Hacker's Delight magicu2, W = 32 (w 0) or 64 (w 1): unsigned division by d (3 ≤ d < 2^W, not a power of two) is q = mulhu(x, M) >> s, or — the add indicator a = 1 — the same with the W+1-bit magic M + 2^W. [M, a, s]
@ rj_magics i dd i w → ( Vec i )Hacker's Delight magic (signed), W = 32 / 64, 3 ≤ |d| < 2^(W-1): q = mulhs(x, M) (+ x when d > 0 and M < 0, − x when d < 0 and M > 0), arithmetic-shifted by s, plus its own sign bit. M comes back as the signed W-bit value, sign-extended. [M, s]
@ rj_ldw Rj c i reg i loc i s i w i sgn → vreg ← operand; an i32 (w 0) widened from its low half — sign-extended when sgn, zero-extended otherwise (a div/rem operand need not be canonical)
@ rj_grp3 Rj c i ext i rm → vone-operand group 3 on rcx: mul 4, imul 5, neg 3 (on rm)
@ rj_imul_k Rj c i k → vrax ← rax * k (64-bit)
@ rj_rem_tail Rj c → vrax ← rcx − rax: the remainder from x (rcx) and q·d (rax)
@ rj_e_divk Rj c i r → bDivision by a constant divisor: shifts for a power of two, otherwise a multiply by the reciprocal (Hacker's Delight ch. 10) — no div, no zero test. Leaves the record to rj_e_div (F) when the divisor is not a constant, is 0, is −1 under a signed quotient (MIN / −1 traps), is the signed minimum, or is ≥ 2^63 unsigned.
@ rj_e_div Rj c i r → v── lowering: the rest ──────────────────────────────────────────
@ rj_globmode Rj c → vGlobals touched only outside loops leave r9 to the allocator: each access then loads their base from the context (rj_globase)
@ rj_globase Rj c i s → ithe register holding the globals base: r9 when reserved, else s loaded from the context (in rdi, or saved at [rsp] when rdi was handed out)
@ rj_e_gget Rj c i r → v@ rj_e_gset Rj c i r → v@ rj_e_const Rj c i r → v@ rj_e_mov Rj c i r → v@ rj_saved i k → i── prologue, epilogue, stubs ───────────────────────────────────
@ rj_epilogue Rj c → v@ rj_entry1 Rj c i wv → vone entry web's initial value: a parameter from the caller's window (arg0 in rdx when there is exactly one), anything else zero
@ rj_prologue Rj c → v@ rj_stubs Rj c → v@ rj_align16 Rj c → vmulti-byte NOPs up to the next 16-byte boundary
@ rj_e_rec Rj c i r → v── the record walk ─────────────────────────────────────────────
@ rj_emit Rj c → v@ rj_new ( Vec i ) code ( Vec i ) aux ( Vec i ) kv ( Vec i ) ltype i n i nl i ns i np i nr i sb → Rj── entry ───────────────────────────────────────────────────────
@ rj_compile Rj c ( Vec i ) rsig i nimp i spcell i trapfn i cofn i coenv i fidx i sigoff i tbloff → bCompile the context's records. rsig: per record, a call's callee nparams*65536 + nresults (-1 for every other record). True when buf holds the function — record and stub labels in lab, absolute jump-table entries in pta_off/pta_stub (page-relative) — false to leave it to the template tier (rjs_fail says why).
@ rj_fsrc Rj c i pf i opc i x i loc i s → vx op= operand (opc: 88 add, 89 mul, 92 sub, 94 div, 81 sqrt)
@ rj_fopc i op → i@ rj_vex Rj c i pp i dst i v1 i bb i xx → v── AVX (VEX) scalar forms, on an x86-64-v3 CPU ───────────────── vop x, v1, src: three operands, so a result in a register of its own needs no movaps copy first, and none carries a dependency on the destination's old value. pp: 2 = the ss forms (F3), 3 = sd (F2). Only the 128-bit forms run — they clear bits 128..255, so mixing them with the SSE forms costs no transition.
@ rj_vfsrc Rj c i pp i opc i x i v1 i loc i s → vvop x, v1, operand (the float operand of rj_fsrc's kinds)
@ rj_fbin3 Rj c i pf i opc i al i as i bl i bs i dl i ds → vdst = a op b into dst's place
@ rj_e_fbin Rj c i r → v@ rj_fun_dst Rj c i od → ia one-operand float result computed in place: x ← a (a full-register write, which also breaks the false dependency the scalar forms carry on their destination's upper bits), then body runs on x
@ rj_fun_fin Rj c i od i x → v@ rj_e_fsqrt Rj c i r → v@ rj_e_fsign Rj c i r → vf64.abs / f64.neg: clear / flip the sign bit, on whichever side the web lives
@ rj_e_fround Rj c i r → vf64 ceil / floor / trunc / nearest: roundsd (SSE4.1)
@ rj_fcmp1 Rj c i pf i al i as i bl i bs → vucomisd/ucomiss a, b with a in an xmm register
@ rj_e_fcmp Rj c i r → v@ rj_e_fconv Rj c i r → vint → float and float ↔ float conversions
@ rj_e_ffused Rj c i r → vthe fused f64 records
@ rj_e_bits Rj c i r → v── lowering: the remaining integer unaries ───────────────────── ctz / popcnt / clz on 32 or 64 bits; bsf/bsr + cmov, popcnt (POPCNT)
@ rj_e_isnull Rj c i r → v@ rj_e_f32sign Rj c i r → v── lowering: f32 sign / rounding, min / max / copysign ─────────
@ rj_e_f32round Rj c i r → v@ rj_e_fminmax Rj c i r → vmin / max with wasm semantics (any NaN → the canonical NaN; equal operands combine bitwise so min(±0,∓0) = -0 and max(±0,∓0) = +0); copysign on the bits
@ rj_e_trunc Rj c i r → v── lowering: truncations and the unsigned 64-bit conversions ─── float → int, trapping: NaN → "invalid conversion to integer", a value whose truncation leaves the target range → "integer overflow". An f32 source is widened to f64 first (exact), so one set of bounds serves.
@ rj_e_cvtu64 Rj c i r → vf32/f64.convert_i64_u: halve with a sticky bit, convert, double
@ rj_reload_defs Rj c i r → v── lowering: call-outs (memory.grow, call_indirect, the 0xfc bridge) ── Every call-out reads its operands from frame homes and leaves its results there.
@ rj_e_memsize Rj c i r → v@ rj_e_memgrow Rj c i r → v@ rj_e_callind Rj c i r → vcall_indirect: resolve the table entry inline and call the callee's direct entry; an index out of range, a null entry, a signature mismatch, an import or a callee not compiled yet all take the bridge, which traps with the interpreter's own message or runs the call there.
@ rj_fcb_fast Rj c i r → ( Vec i )memory.copy (fc 10) and memory.fill (fc 11) without the bridge: the operands in rax/rdx/rcx, both bounds checked up front against the context's memory size (no partial writes), then rep movsb / rep stosb. An out-of-bounds operand — and a copy whose destination starts inside its own source, which a forward copy would smear — takes the bridge, which traps with the interpreter's message or does the overlap right. Returns the bridge-path sites (empty: no fast path for this op).
@ rj_e_fcb Rj c i r → vpackages/nwasm/src/interp.nu — a small WebAssembly interpreter (pure NURL).
Executes wasm: i32/i64/f32/f64 const, locals, globals, the integer and float arithmetic/comparison/bitwise ops, all int↔float conversions, structured control flow (block / loop / if / else / br / br_if / return), drop / select, direct and indirect calls, and linear memory load/store. Each value occupies one 64-bit cell; i32 wraps + sign-extends to 32 bits, and floats are held as their IEEE-754 bit pattern (reinterpreted via std/floatbits for arithmetic). Imports (the WASI surface) are the remaining milestone — so this runs self-contained compute modules today.
Control flow uses an explicit control stack. Entering a block/if computes its matching end by a one-pass immediate-skipping scan; br to a loop re-enters at the loop start, to a block jumps past its end.
& m @ rint f x → fround-to-nearest-even (wasm f*.nearest) — libm rint honours the default mode.
& c @ nurl_rand_fill *u buf i n → iOS entropy (runtime helper; getrandom/urandom under the hood).
& c @ nurl_code_alloc i n → *uThe JIT substrate: an executable page and a call through a raw address. On wasm32 alloc returns 0 (no executable memory) and the runtime stays on the interpreter — a capability probe, not an error.
& c @ nurl_code_seal *u p i n → i& c @ nurl_call_code *u fn *u a → i& c @ nurl_call_code_at *u fn *u a i off → i& c @ nurl_call_code2 *u fn *u a *u b → i& c @ nurl_call_code2_sj *u fn *u a *u b → i& c @ nurl_code_trap_addr → i& c @ nurl_call_code_at2 *u fn i off *u a *u b → i& c @ nurl_code_free *u p i n → v& c @ mprotect *u addr i len i32 prot → i32Page permissions for the JIT code arena (PROT_READ|PROT_WRITE = 3, PROT_READ|PROT_EXEC = 5): functions are packed into shared pages, so a page holding sealed code turns writable only while a neighbour is copied in, then executable again.
& c @ getpid → i32NURL_NWASM_PERFMAP=1: a perf JIT map (/tmp/perf-<pid>.map, "start size name" per line), which perf report reads to name generated code
& c @ creat s path i32 mode → i32& c @ nurl_vmem_reserve i span → *uGuard-page linear memory: an 8 GiB PROT_NONE reservation swallows every address a 32-bit index + 32-bit offset can form, so JIT code needs no bounds checks — an out-of-bounds access faults and the runtime's SIGSEGV handler steers the faulting frame to its function's trap stub. Reserve returning 0 means the platform has no fault-to-trap plumbing and the bounds-checked path stays — a capability probe, like nurl_code_alloc.
& c @ nurl_vmem_commit *u base i old i new → i& c @ nurl_vmem_release *u base i span → v& c @ nurl_guard_mem_add *u base i span → i& c @ nurl_guard_mem_del *u base → v& c @ nurl_guard_code_room → i& c @ nurl_guard_code_add *u code i len *u stub → i& c @ nurl_guard_code_del *u code → v& c @ pthread_mutex_init *u m *u attr → i32The one lock behind the 0xfe atomics (see __atom_init). Declared here rather than taken from std/thread.nu because the handles live in file globals, and a NURL global holds a pointer as an integer.
& c @ pthread_mutex_lock *u m → i32& c @ pthread_mutex_unlock *u m → i32& c @ pthread_cond_init *u c *u attr → i32& c @ pthread_cond_wait *u c *u m → i32& c @ pthread_cond_broadcast *u c → i32& c @ unlink s path → i32unlink(2): path_unlink_file must NOT remove directories (remove(3) would).
& c @ nurl_tcp_connect s host i port → i── raw sockets, for the "nurl_net" host-import bridge ──────────── The listen/accept/read/write/close/err_kind/peer_addr/set_timeout half of this ABI is preamble-declared by the compiler; the rest lives in stdlib FFI declares, so name it here the same way std/dns.nu and ext/http_pure.nu do.
& c @ nurl_tcp_timeout_ms i handle → i& c @ nurl_tcp_set_send_buffer i handle i bytes → i& c @ nurl_tcp_set_write_deadline i handle i ns → v& c @ nurl_tcp_write_deadline i handle → i& c @ nurl_tcp_write_wait_ms i handle → i& c @ nurl_tcp_write_nowait i handle s bytes i length → i& c @ nurl_tcp_read_nowait i handle s bytes i length → i& c @ nurl_tcp_wait_io i handle i events i timeout_ms → i& c @ nurl_tcp_get_fd i handle → i& c @ nurl_tcp_set_nonblock i handle i on → v& c @ nurl_tcp_ref i handle → v& c @ nurl_tcp_unref i handle → v& c @ nurl_tcp_local_addr i handle → s& c @ nurl_udp_bind s host i port → i& c @ nurl_udp_connect i handle s host i port → i& c @ nurl_udp_send i handle s buf i n → i& c @ nurl_udp_recv i handle s buf i n → i& c @ nurl_udp_send_to i handle s buf i n s host i port → i& c @ nurl_udp_recv_from i handle s buf i n → i& c @ nurl_udp_close i handle → v& c @ nurl_udp_err_kind i handle → i& c @ nurl_udp_peer_addr i handle → s& c @ nurl_udp_local_addr i handle → s& c @ nurl_udp_set_timeout i handle i ms → v& c @ nurl_udp_set_nonblock i handle i on → v& c @ nurl_udp_family i handle → i& c @ nurl_udp_get_fd i handle → i& c @ nurl_udp_set_broadcast i handle i on → i& c @ nurl_udp_join_group i handle s group s iface → i& c @ nurl_udp_leave_group i handle s group s iface → i& c @ nurl_udp_set_multicast_ttl i handle i ttl → i& c @ nurl_udp_set_multicast_loop i handle i on → i& c @ nurl_dns_resolve s host → s& c @ nurl_dns_resolve_port s host i port → s& c @ nurl_dns_reverse s ip → s& cuda @ cuInit i32 flags → i32── CUDA driver + NVRTC (the GPU host-import bridge) ────────────── A wasm module built from a GPU-using NURL package (packages/gpu → onnx → objdet) imports these under module "env"; nwasm resolves them to the real libcuda/libnvrtc here, marshalling guest linear memory ↔ host. nurl.sh auto-links libcuda/libnvrtc when these symbols appear, and links stub objects on a GPU-less host (so nwasm always builds; the guest then just sees nonzero CUresult codes). Handle types match the portable i64 model in packages/gpu/src/cuda.nu.
& cuda @ cuDeviceGetCount *u count → i32& cuda @ cuDeviceGet *u device i32 ordinal → i32& cuda @ cuDeviceGetName *u name i32 len i32 dev → i32& cuda @ cuCtxCreate *u pctx i32 flags i32 dev → i32& cuda @ cuCtxDestroy i ctx → i32& cuda @ cuCtxSynchronize → i32& cuda @ cuModuleLoadData *u module *u image → i32& cuda @ cuModuleUnload i module → i32& cuda @ cuModuleGetFunction *u hfunc i hmod s name → i32& cuda @ cuMemAlloc *u dptr i bytesize → i32& cuda @ cuMemFree i dptr → i32& cuda @ cuMemcpyHtoD i dst *u src i n → i32& cuda @ cuMemcpyDtoH *u dst i src i n → i32& cuda @ cuLaunchKernel i f i32 gx i32 gy i32 gz i32 bx i32 by i32 bz i32 sh i stream *u params i extra → i32& nvrtc @ nvrtcCreateProgram *u prog s src s name i32 nh *u headers *u incs → i32& nvrtc @ nvrtcCompileProgram i prog i32 nopt *u opts → i32& nvrtc @ nvrtcGetPTXSize i prog *u sz → i32& nvrtc @ nvrtcGetPTX i prog *u ptx → i32& nvrtc @ nvrtcGetProgramLogSize i prog *u sz → i32& nvrtc @ nvrtcGetProgramLog i prog *u log → i32& nvrtc @ nvrtcDestroyProgram *u prog → i32: Arg { ( Vec u ) bytes }: Ctrl { i is_loop i start_pc i end_pc i height i arity }: WFd { i kind ( Vec u ) data i pos ( Vec u ) host ( Vec u ) name b writable b dirty b append }A WASI file descriptor. kind: 0 closed, 1 stdio, 2 preopened dir, 3 file, 4 opened (non-preopen) directory. For a file, data holds the contents, pos the read/write offset, host the on-disk path (for open/flush), and name (a dir) the guest-visible preopen name reported by fd_prestat_*. append = O_APPEND: every write lands at the end regardless of pos.
: InterpImpl: InterpImpl {
s mod // *ModuleImpl
( Vec i ) vs // value stack
( Vec u ) mem // linear memory (bytes)
i mem_pages // current size in 64 KiB pages
// Bytes the guest may address = mem_pages * 64 KiB. Usually the whole
// buffer, but a SHARED memory reserves its declared maximum up front —
// growth must never move the buffer, because another thread is reading
// it through the same pointer — so there the buffer is larger than
// what the guest may touch, and every bounds check reads this instead
// of the Vec's length.
i mem_bytes
// Guard-page mode: the raw base of an 8 GiB PROT_NONE reservation
// (0 = the memory lives in the `mem` Vec instead). Growth commits
// pages in place, so this address never moves; every reader goes
// through __mem_base, which picks whichever representation is live.
i mem_raw
( Vec i ) globals // mutable global values
( Vec i ) table // runtime funcref table (mutable via table.set/grow/…)
( Vec i ) data_dropped // 1 per data segment once dropped / active
( Vec i ) elem_dropped // 1 per element segment once dropped / active
( Vec s ) argv // *Arg — WASI program arguments (argv[0] = program)
( Vec s ) envp // *Arg — WASI environment entries ("NAME=VALUE")
( Vec s ) fds // *WFd — file-descriptor table (0/1/2 stdio, 3 preopen, …)
// Why execution stopped, as one word so the driver's hot loop tests one
// load instead of two byte fields: bit 0 = trapped, bit 1 = exited (via
// proc_exit). `interp_trapped` / `interp_exited` are the readers; nothing
// outside `__trap` and `__wasi_proc_exit` sets it.
i halt
i exit_code
( Vec u ) trapmsg
i pending_call // callee set by call/call_indirect for the driver (-1 none)
i max_depth // frame-stack depth limit (trap when exceeded)
i fuel // remaining budget in predecoded records (-1 = unlimited)
i jit_ctx_free // freelist of reusable 7-word JIT call contexts (0 = empty)
i jit_lc_fidx // last JIT callee fidx (-1 = none) and its resolved PFunc
i jit_lc_pf
// Inline call-out bridge: a capture-decomposed closure the emitted
// code calls C-ABI-style instead of parking the whole native frame
// chain. cur_cd is the live JIT context for the innermost run.
i jit_co_fn
i jit_co_env
i jit_cur_cd
// Tier-6 direct-call state. The slab is a bump stack of raw slot
// frames for JIT execution. jit_spcell is the JIT anchor block, held
// in r8 for the whole native execution: word 0 = the absolute
// address of the next free slab slot, word 1 = the slab end, words
// 2.. = one direct-entry address per defined function (0 = call
// through the driver).
i jit_slab // raw slab base address (0 = not yet allocated)
i jit_slab_end // slab base + byte length
i jit_spcell // address of the anchor block [sp, slab end, ftab..]
// The code arena: compiled functions are packed into shared chunks
// instead of one mapping each — a page per function put every entry
// point at the same page offset, so the hot ones fought over the same
// L1i sets, and spread the code over an iTLB entry apiece. Each chunk
// starts with a 64-byte header [previous chunk, chunk bytes].
i jit_arena // current chunk (0 = none yet)
i jit_arena_used // bytes handed out in it
i jit_arena_cap // its size
// Direct call sites waiting for their callee (tier 8 calls rel32; a site
// aims at its own stub until the callee's entry exists): per defined
// function the head of a list of sites, linked through jit_lnext, each
// the absolute address of a rel32 in the arena.
( Vec i ) jit_lhead
( Vec i ) jit_lnext
( Vec i ) jit_laddr
( Vec i ) jit_lkind // 1: a register-argument site (links to the callee's fast entry)
( Vec i ) jit_fast // per defined function: tier 8's register-argument entry (0 = none)
b gpu_ok // env/CUDA host imports enabled (opt-in; default off)
b net_ok // nurl_net host imports (real sockets) enabled (opt-in; default off)
// Shared memory changes what a narrow store is allowed to touch: see
// __mem_store. Cached here because every store reads it.
b shared_mem
// Guest socket handle → host handle. The guest never sees a host
// pointer: it gets an index into this table, so a forged handle can
// only miss, never become a host address the runtime dereferences.
// Slot 0 is reserved — the stdlib reads handle 0 as "failed".
( Vec i ) nethandles
( Vec i ) netkinds // 1 = TCP, 2 = UDP (which close the runtime owes it)
b cap // capture stdout/stderr into capout/caperr instead of the host streams
( Vec u ) capout // captured module stdout (raw bytes, NULs preserved)
( Vec u ) caperr // captured module stderr
( Vec s ) pfuncs // *PFunc per defined function, predecoded lazily (#s 0 until first call)
// Threads (wasi-threads). A spawned thread runs its OWN Interp — own
// value stack, frames, globals and therefore its own `__stack_pointer`,
// exactly as the proposal's "one instance per thread" says — over the
// SHARED linear memory, table and module. `owner` points at the
// instantiating Interp (0 in it), and the host-side tables that must
// stay common (file descriptors, socket handles, captured output) are
// reached through it; `__host` is that hop.
s owner // *InterpImpl of the instantiating thread, 0 when this IS it
i next_tid // owner only: thread ids handed to wasi.thread-spawn
( Vec i ) thread_kids // owner only: live child *InterpImpl, for memory.grow
( Vec Thread ) thread_joins // owner only: host threads, joined before free
i tstart_fidx // cached export index of `wasi_thread_start` (-2 = not looked up)
i sp_global // cached global index of `__stack_pointer` (-2 = not looked up)
// The module this instance runs, held so it outlives every user of `mod`
// (owner only; a thread borrows its owner's).
Module modh
}
: Interp { s ctl }An Interp is a handle on its instance state in an rcbox (stdlib/core/rcbox.nu): every copy is the same instance, and the last owner releases it. A spawned thread's instance is not one of these: it is a raw *InterpImpl made and released by the thread that runs it (see __wasi_thread_spawn), because it shares its owner's memory, table and argv by handle and must never drop them.
@ Interp_drop sink Interp h → v: Frame { i fidx ( Vec i ) regs i pos i end i code_start s pins i ret_dst s prev i depth }One activation record on the explicit call stack: the function, its locals, its control stack, and the instruction cursor [pos, end) — pos/end are record indices into the function's predecoded instruction array, not byte positions; pins borrows the *PFunc owned by it.pfuncs.
: PFunc { ( Vec i ) code ( Vec i ) aux i count i nlocals i nslots i nparams i nresults i code_start i sbase ( Vec i ) kv s free ( Vec i ) bytes s jit i jitlen ( Vec i ) refl }A predecoded function body in REGISTER FORM. wasm validation guarantees a static stack height at every instruction, so the value at stack position h lives in slot (nlocals + h) of one flat per-frame array — locals first, stack after — and predecode resolves every operand to an absolute slot index. There is no value-stack traffic and no runtime control stack left: local.get/set/tee become register moves, block/loop/end emit nothing at all, and branches are direct jumps that carry (dst, src, n) result-move triples computed statically. Layout: 6 x i64 per record — [0] micro-op a dense internal opcode, NOT the wasm byte — see __iop [1..4] A B C D operands: slot indices, jump targets (record indices), immediates, packed move triples. A load's D is its index slot — the address add folded into the access, or the pool's zero slot when there was none (__fuse_addr). [5] BYTE the instruction's byte offset in the module image, for trap backtraces — the one thing byte positions are still good for aux holds br_table rows, 4 words per label: target, dst, src, n. The 0xfc family is the one group still bridged through the old value stack: the record copies its operands from slots onto it.vs, runs the existing executor arm, and copies the result back — correctness identical, speed unchanged for them, and no duplicated semantics. Floats and the int/float conversions are register form like everything else. nparams / nresults are the callee's arity, copied out of the type section once at predecode time. Every call and every return needs them, and re-deriving them per call means walking funcs → typeidx → types and two vec_lens inside module_func_type — pure repeat work on a value that is fixed for the life of the module.
: ~ i g_guard 1Guard-page memory is on wherever the runtime supports it; main.nu turns it off for NURL_NWASM_GUARD=0 (the A/B and debugging escape).
@ interp_disable_guard → v% Drop InterpImpl% Drop InterpImpl {
unsafe @ drop InterpImpl it → v {
: i tjn ( vec_len [Thread] . it thread_joins )
: ~ i tji 0
~ < tji tjn {
?? ( vec_get [Thread] . it thread_joins tji ) { T t → { ( thread_join t ) } F → {} }
= tji + tji 1
}
( __net_close_handles . it nethandles . it netkinds )
? != 0 . it mem_raw {
( nurl_guard_mem_del # *u . it mem_raw )
( nurl_vmem_release # *u . it mem_raw ( __vmem_span ) )
} {}
( __free_args . it argv )
( __free_args . it envp )
: i fn ( vec_len [s] . it fds )
: ~ i fi 0
~ < fi fn { ?? ( vec_get [s] . it fds fi ) { T pp → ( __freefd pp ) F → {} } = fi + fi 1 }
( __free_pfuncs . it pfuncs )
( __jit_state_free . it jit_ctx_free . it jit_slab . it jit_spcell . it jit_co_env . it jit_arena )
}
}
The raw parts of an instance — the Arg / WFd / *PFunc records behind its tables, the guard reservation, the JIT state, the guest's open sockets — released by its last owner (Interp_drop); its Vecs and its module handle are dropped after this returns. Threads first: the guest's own join only proves the thread's BODY finished, and the host thread is still inside the interpreter for a moment after that, reading this memory and table.
@ interp_free sink Interp it → vLet go of it now rather than at the end of its owner's scope.
@ interp_trapped Interp it__h → b@ interp_exited Interp it__h → b: PBlk { i kind i base i params i results i live_entry i t0 i else_br ( Vec i ) patches }One open block/loop/if during predecode. patches holds forward-branch sites that must learn "the record index just past this frame's end": value site2 → record A-field, site2+1 → aux word site.
: ~ i g_jit 0 // tier-0 JIT opt-in (NURL_NWASM_JIT); off by defaultExecute function fidx to completion on an EXPLICIT frame stack — guest recursion depth is bounded by max_depth, not by the host's native stack. Arguments are already on the value stack; results are left on top.
Two loops, not one. The outer loop owns the frame stack: it loads the top frame's bases into registers and, when the inner loop comes back, either returns from that frame or picks up the one a call just pushed. The inner loop owns the records of a single frame and its condition is one compare — everything that has to leave a frame (a call, a return, a trap, exhausted fuel) parks the cursor at pend and falls out of it. That is what the old single loop paid for on every record: a vec_len on the frame stack, a reload test, and two byte loads for the trap and exit flags, none of which can change between two records of the same basic block.
: ~ i g_jit_depth 0 // JIT call-out nesting; beyond a cap, callees interpret (1M-deep-safe): ~ i g_pin 1 // tier-7 slot pinning (NURL_NWASM_PIN=0 keeps every slot in memory; A/B, debug): ~ i g_jitdump 0 // NURL_NWASM_JIT_DUMP=1: emit every sealed page as decimal bytes on stderr: ~ i g_rjit 1 // tier 8 (rjit.nu) first; NURL_NWASM_RJIT=0 keeps the template tier alone (A/B, debug): ~ i g_rjdbg 0 // NURL_NWASM_RJIT_DBG=1: say on stderr which functions tier 8 declined, and why: ~ i g_perfmap -1 // NURL_NWASM_PERFMAP=1: fd of /tmp/perf-<pid>.map (-1 = off)@ interp_enable_jit → v@ interp_disable_rjit → v@ interp_disable_bmi2 → vNURL_NWASM_BMI2=0: tier 8 sticks to baseline x86-64 (no shlx/sarx/shrx, lzcnt, tzcnt) even on a CPU that has them — for testing those paths
@ interp_rjit_trace i f → v@ interp_enable_rjdbg → v@ interp_disable_pin → v@ interp_enable_jitdump → v: ~ i g_jit_noax 0Set by an emitter that used a pin-direct form: the record ran without touching rax, so the walk keeps its rax cache instead of assuming the result went through it. Consumed (and reset) once per record.
@ exec_func Interp it__h i fidx → v@ interp_flush Interp it__h → v@ interp_net_close_all Interp it__h → v: ~ i g_atom_mx 0 // *pthread_mutex_t, the one atomics lock: ~ i g_atom_cv 0 // *pthread_cond_t, where waiters sleep: ~ i g_atom_gen 0 // bumped by every notify; a waiter watches it change: ~ i g_atom_waiters 0 // sleepers right now, for notify's return valuepackages/nwasm/src/module.nu — WebAssembly binary decoder (pure NURL).
Decodes a wasm32 module's structure: the magic/version header and the sections this runtime understands (type, function, table, memory, global, export, element, code, data). Imports and the custom/start sections are skipped. The byte cursor + LEB128 readers here are reused by the interpreter (interp.nu) to walk instruction streams.
Memory model: a Module is a handle on the decoded module in an rcbox (stdlib/core/rcbox.nu) — every copy is the same module, and the last owner releases it (module_free is an early release, optional). A Wc cursor is a plain value that owns nothing.
: Wc { * u data i pos i len i cap }A cursor over bytes it does not own (the module image outlives it): a value in a mutable local, passed inout, so there is nothing to release. len is where the cursor's input ends (wc_eof / wc_avail; callers narrow it to one function body), cap the buffer's real end, which bounds every read — past it a read yields 0, as an out-of-range vec_get did.
@ wc_new ( Vec u ) buf → Wc@ wc_free sink Wc c → vNothing to release: a Wc owns no memory.
@ wc_eof inout Wc c → b@ wc_uleb inout Wc c → iUnsigned LEB128 → i (NURL i is 64-bit, covers u32/u64).
Only the first ten groups can carry a 64-bit value. A longer encoding is malformed wasm, and << x shift with shift >= 64 is poison in LLVM (docs/spec.md, § operators) — a corrupted continuation byte turning a terminal group into a running one is exactly how a module reaches it. Groups past the tenth are consumed, so pos still lands after the encoding, but they contribute nothing: the value stays inside 64 bits and every caller's range check still sees a number it can judge.
@ wc_sleb inout Wc c → iSigned LEB128 → i (sign-extended).
@ wc_skip inout Wc c i n → vSkip n bytes.
@ wc_avail inout Wc c → iBytes physically remaining in the input (never negative).
: FuncType { ( Vec i ) params ( Vec i ) results }: WFunc { i typeidx ( Vec i ) locals i code_start i code_end }A defined function: its type index, its declared (non-param) local valtypes expanded flat, and the [start,end) byte range of its instruction stream.
: WExport { ( Vec u ) name i kind i index }: DataSeg { i offset ( Vec u ) bytes i passive }A data segment. Active (passive=0): copied to memory at offset during instantiation. Passive (passive=1): source material for memory.init only.
: ElemSeg { ( Vec i ) funcs i passive }An element segment: function indices (−1 = null ref). Active segments are applied to the table at decode; passive ones feed table.init; declared (and applied active) ones count as dropped at instantiation.
: NameBuf { ( Vec u ) bytes }An owned byte buffer behind an opaque pointer (name-section entries).
: WImport { ( Vec u ) module ( Vec u ) field i typeidx }An imported function (the only import kind this runtime resolves): module + field name select the host (WASI) implementation; typeidx gives its signature. Non-function imports are a decode error (nothing satisfies them).
: ModuleImpl: ModuleImpl {
( Vec s ) types // *FuncType
( Vec i ) functypes // type index per defined function
( Vec s ) funcs // *WFunc
( Vec s ) exports // *WExport
( Vec u ) code // the whole module byte image (functions index into it)
i has_mem
i mem_min // initial pages (64 KiB each)
i mem_max // 0 if unbounded
i mem_shared // 1 = threads-proposal shared memory (max is mandatory)
( Vec s ) datas // *DataSeg
( Vec i ) global_init // initial value per global
( Vec i ) global_mut // 1 if mutable
i has_table
( Vec i ) table // initial table image: function indices (−1 = null)
i table_max // declared table maximum (0 = none)
( Vec s ) elems // *ElemSeg — element segments, in order
( Vec s ) imports // *WImport (imported functions, in index order)
i num_import_funcs // imported funcs occupy func indices 0..n-1
i start_func // start-section function index (-1 = none)
( Vec i ) name_idx // function indices with a "name"-section entry…
( Vec s ) name_str // …and their names (*( Vec u )), parallel (sparse)
b ok
( Vec u ) err
}
: Module { s ctl }A Module is a handle on its decoded state in an rcbox (stdlib/core/rcbox.nu): every copy is the same module, and the last owner releases it.
@ Module_drop sink Module h → v% Drop ModuleImpl% Drop ModuleImpl {
unsafe @ drop ModuleImpl m → v {
: i tn ( vec_len [s] . m types )
: ~ i k 0
~ < k tn { ?? ( vec_get [s] . m types k ) { T pp → ? != # i pp 0 { ( __ft_free # *FuncType pp ) } {} F → {} } = k + k 1 }
: i fn ( vec_len [s] . m funcs )
: ~ i j 0
~ < j fn { ?? ( vec_get [s] . m funcs j ) { T pp → ? != # i pp 0 { : *WFunc f # *WFunc pp ( vec_free [i] . f locals ) ( nurl_free # s f ) } {} F → {} } = j + j 1 }
: i en ( vec_len [s] . m exports )
: ~ i e 0
~ < e en { ?? ( vec_get [s] . m exports e ) { T pp → ? != # i pp 0 { : *WExport x # *WExport pp ( vec_free [u] . x name ) ( nurl_free # s x ) } {} F → {} } = e + e 1 }
: i dn ( vec_len [s] . m datas )
: ~ i d 0
~ < d dn { ?? ( vec_get [s] . m datas d ) { T pp → ? != # i pp 0 { : *DataSeg ds # *DataSeg pp ( vec_free [u] . ds bytes ) ( nurl_free # s ds ) } {} F → {} } = d + d 1 }
: i eln ( vec_len [s] . m elems )
: ~ i el 0
~ < el eln { ?? ( vec_get [s] . m elems el ) { T pp → ? != # i pp 0 { : *ElemSeg es # *ElemSeg pp ( vec_free [i] . es funcs ) ( nurl_free # s es ) } {} F → {} } = el + el 1 }
: i in ( vec_len [s] . m imports )
: ~ i ii 0
~ < ii in { ?? ( vec_get [s] . m imports ii ) { T pp → ? != # i pp 0 { : *WImport w # *WImport pp ( vec_free [u] . w module ) ( vec_free [u] . w field ) ( nurl_free # s w ) } {} F → {} } = ii + ii 1 }
: i nn ( vec_len [s] . m name_str )
: ~ i ni 0
~ < ni nn { ?? ( vec_get [s] . m name_str ni ) { T pp → ? != # i pp 0 { : *NameBuf nb # *NameBuf pp ( vec_free [u] . nb bytes ) ( nurl_free # s nb ) } {} F → {} } = ni + ni 1 }
}
}
The section records are raw blocks behind the ( Vec s ) tables; the last owner of the Module releases them here, and the tables themselves (with every other Vec of the module) are dropped after this returns.
@ module_free sink Module m → vLet go of m now rather than at the end of its owner's scope.
@ module_func_name Module m__h i fidx → ( Vec u )@ module_export_global Module m__h s name → i@ module_export_func Module m__h s name → i@ module_func_type Module m__h i fidx → spackages/nwasm/src/main.nu — nwasm: a WebAssembly runtime in pure NURL.
nwasm run --invoke <export> <module.wasm> [int args…]
Loads a wasm module and either runs its _start as a wasm32-wasi command or invokes one exported function directly, printing the result. The engine itself is module.nu (decoder) + interp.nu (interpreter and template JIT).
@ usage → v@ run_invoke s export s path i first_arg i argc i allow_gpu i allow_net → i@ run_command s path i prog_start i argc ( Vec String ) dirs ( Vec String ) envs i fuel i allow_gpu i allow_net → iWASI command: run the module's _start with argv = [module, prog args…], the given preopened directories and environment entries.
@ main → iEvery host option is read from the argv PREFIX that ends at the module path — a guest's own --help, --version or --allow-gpu belongs to the guest. (Scanning all of argv for those three meant nwasm run app.wasm --help printed the RUNTIME's usage and never started the module; the bug surfaced on the first guest with a CLI of its own.) -- ends the host options explicitly, for a module path that starts with a dash.