NURLNURL registrynurl-lang.org →

← nwasm

nwasm 3.1.3 API

rjit.nu

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:

  1. every record's slot uses and defs are decoded once;
  2. records split into basic blocks, and every branch that moves values

(a block's results, a loop's params) gets a synthetic EDGE block that holds those moves as one parallel copy;

  1. slot liveness is solved over the blocks;
  2. each slot's definitions and uses are grouped into WEBS — the

independent values that happen to share a storage slot (the operand-stack slots are reused for every expression in the body);

  1. webs get a register by linear scan over their live hulls — a web

live across a call takes a callee-saved register or memory;

  1. records are emitted with operands wherever their webs live.

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.

API

: 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 (a copy only on terms — rj_lcar_moves)
    ( Vec i ) rpi32  // per record: a call's callee parameters that are i32, bit k for parameter k (rj_set_pi32)
    ( Vec i ) lhead  // per record: the head of the innermost loop holding it (-1 none) …
    ( Vec i ) lend  // … and that loop's furthest back edge
}

── 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 → i

scalar 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_raxs → i

The frame slot whose value rax still holds because the last thing emitted stored rax there (a spilled web's def), and the code offset right after that store; -1 when none (rj_ldf_fwd).

@ rjs_raxpos → 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 → i

a 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 → v

patch 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 → v

the same with a memory operand

@ rj_home i s → i

frame 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 → v

mov dst, [rbx + 8*s]

@ rj_stf Rj c i w i s i src → v

mov [rbx + 8*s], src

@ rj_mov_ri Rj c i dst i imm i keep → v

dst ← 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 → v

ALU 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 → v

imm must fit 32 bits (sign-extended for w=1)

@ rj_alu_fi Rj c i w i ext i s i imm → v

op 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 → v

shifts: 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 -1

x86-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 → v

BMI2 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 → i

A 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 → v

movsxd dst, src32

@ rj_mov32 Rj c i dst i src → v

mov 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 → v

lea 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 → i

a 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

A forward jump lands here: control arrives from somewhere rax was not necessarily stored from, so rax no longer mirrors a slot (rj_ldf_fwd).

@ rj_stub_oob Rj c → i

stub 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 → i

condition 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 → i

the 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 → b

two-operand integer ALU: dst = B op C

@ rj_isrot i op → b

@ rj_isfused i op → b

fused i64 pairs: dst = (B op1 C) op2 D

@ rj_hastarget i op → b

a record whose A field is a branch target (a word offset)

@ rj_isbranch i op → b

a record that leaves the block: anything with a target, or no fall-through

@ rj_iscall i op → b

a record whose emitted code calls out of the function (clobbers every caller-saved register)

@ rj_op_ok i op → b

The records this tier lowers; a function holding anything else stays on the template tier (or the interpreter).

@ rj_isfbin i op → b

the 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 → i

a 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 → v

per 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 → b

argument 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 → v

Decode 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 → i

branch target of a record with one (A field), else -1

@ rj_mark Rj c i t i r → v

t 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 → i

new 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 → i

logical 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 → i

a 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 → v

the 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 → v

loop 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 → b

Does 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 → b

is use k of record r a constant in [0, 2^32)?

@ rj_lowk Rj c i r i op i k → i

The 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 → b

Does 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 → v

the sensitivity facts: direct consumers, then back through every copy

@ rj_lcar_moves Rj c ( Vec i ) movr → v

A loop's MOVs move values from slot to slot, and whether a copy makes its destination loop-carried in the sense rj_dens's ×8 means — a chain whose next link waits on this write — depends on what it copies. A copy of a value this iteration computed is that computation's result and counts as it would. A copy of the previous iteration's value of another web (SHA-2's h = g, g = f, f = e, through as many copies as the compiler routed them), or of a value from before the loop, or of a constant, is a delay line: spilled, its store has the rest of the iteration before the read that waits on it. Such a destination earns the ×8 only when one of its reads sits within a few records of the write it reads (rj_gap) — SHA-2's a, copied at the top of the round and read right behind, is one. Without this, the eight rotating words ranked alike, and the scan spilled the two the round computes — a and e, each a store and a reload on the round's own critical path.

@ rj_lastdef ( Vec i ) dh ( Vec i ) lr ( Vec i ) ln i w i lo i hi → i

The last def of web w at a record in [lo, hi): -1 none, -2 when the list ran past 256 nodes first (the caller then assumes the worst).

@ rj_mov_delay Rj c ( Vec i ) dh ( Vec i ) lr ( Vec i ) ln i m i h i hops → b

Does MOV record m, in the loop headed at h, copy a value no record of this iteration computed — through any chain of up to six copies?

@ rj_gap ( Vec i ) dh ( Vec i ) uh ( Vec i ) lr ( Vec i ) ln i w i h i e → i

The fewest records from a def of web w to a read of it inside the loop [h, e]; a read ahead of the iteration's first def reads the previous iteration's last one, around the back edge. 0 when there are too many to look at, a large number when the loop never reads w.

@ rj_dcanon Rj c i od → b

must 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 → b

records 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 → b

is use k of record r a constant-pool slot?

@ rj_clobcx Rj c i r → b

Records 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 → b

is 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 → b

is 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 → i

spill 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 -1

NURL_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 → i

What 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 → b

is the value a constant-pool slot holds zero-extended as mov_ri materialises it?

@ rj_pzx Rj c i wv → b

A parameter's GPR web that no consumer reads bits 32..63 of: the entry zero-extends it (rj_prologue, rj_entry1), so the web counts as clear — a pointer parameter then indexes memory as it stands at every access, instead of each access zero-extending it into rax first.

@ rj_zx Rj c → v

wzx: every def of the web leaves bits 32..63 clear. Parameters arrive canonical (sign-extended) unless rj_pzx, 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 → b

bits [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 → i

the GPRs a cached read may take: rdx rsi r8 r9 r10 rbp r12..r15

@ rj_cache Rj c → v

@ rj_cxbusy Rj c → v

cxb: 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 → i

use #k / def #k of record r

@ rj_dd Rj c i r i k → i

@ rj_movq_xg Rj c i x i g → v

movq xmm, r64 / movq r64, xmm

@ rj_movq_gx Rj c i g i x → v

@ rj_ldfx Rj c i x i s → v

movsd 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 → v

GPR reg ← operand (64-bit). keep = 1: the flags must survive.

@ rj_ldx Rj c i x i loc i s → v

XMM x ← operand

@ rj_stg Rj c i loc i s i reg → v

def location ← GPR reg

@ rj_ldf_fwd Rj c i reg i s → v

reg ← slot s's frame home. A spilled web lives in memory, so a value the last record computed in rax and stored there used to be loaded straight back by the next one: its dependency chain went through a store and a store-forwarded reload, four or five cycles a link — what kept ChaCha20 and BLAKE2b, whose state is sixteen words all updated in place, at half the reference JIT's speed. When nothing at all was emitted since that store (no instruction can have touched rax, and no label lets control in from elsewhere — rj_emit forgets the slot at a branch target) the value is taken from rax instead. The store stays: later reads still find it.

@ rj_inrax Rj c i s → b

rax holds slot s's value: nothing was emitted since rj_stg stored it there

@ rj_keeprax Rj c → v

the instruction just emitted read rax and wrote some other register

@ rj_stx Rj c i loc i s i x → v

def location ← XMM x

@ rj_move Rj c i dl i ds i sl i ss → v

a whole-value copy between two operand places (dl/ds ← sl/ss)

@ rj_greg i loc → i

A 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 → v

op 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 → v

dst = 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 → v

shifts 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 → v

imul 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 → v

the fused i64 pairs: dst = (s1 op1 s2) op2 x

@ rj_shrx_by Rj c i tr i l i s → v

tr >>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 → v

tr = 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 → i

cmp for the condition nibble cc; returns the nibble that holds after any operand exchange

@ rj_cfuse Rj c i r → i

Does 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 → i

the 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 → v

materialise 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 → v

test operand (w) — sets ZF from it; an immediate goes through rax

@ rj_e_cfuse Rj c i r i kind → v

test 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 → i

Is 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 → i

the 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 → v

cmov<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 → v

i32.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 → v

i32.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 → i

a 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 → i

use 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 → v

load wid bytes (sg: sign-extend) from [r11+rax+off] into GPR tr

@ rj_ldto Rj c i od i wid i sg i off → v

the 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 → v

LOADSHL: dst = mem[(x << k) w32 + off]; LOADSHLADD adds a base first

@ rj_e_loadop Rj c i r → v

LOADMULI64 / LOADADDI64: dst = mem64[base w32 + off] * / + x

@ rj_goto Rj c i r i t → v

── lowering: control ───────────────────────────────────────────

@ rj_place i loc i s → i

the 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 → v

One 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 → v

An edge block's parallel copy.

@ rj_e_brm Rj c i r → v

the 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 → i

dst = 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 → v

ADDBRIFC: 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 → v

rdi back to the context (a callee and every call-out need it there)

@ rj_reglob Rj c → v

r9 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 → b

r9/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 → v

The 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 → i

the args pointer's stack offset: above the saved context when rdi was handed out

@ rj_frameless Rj c → b

@ rj_frame_end Rj c → v

lea rcx,[rbx + nslots*8] — the callee's frame base (the end of ours)

@ rj_args_home Rj c i r i np i ab → v

a 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 → v

reg ← 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 → v

one-operand group 3 on rcx: mul 4, imul 5, neg 3 (on rm)

@ rj_imul_k Rj c i k → v

rax ← rax * k (64-bit)

@ rj_rem_tail Rj c → v

rax ← rcx − rax: the remainder from x (rcx) and q·d (rax)

@ rj_e_divk Rj c i r → b

Division 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_mulhu Rj c i r → v

── lowering: the rest ────────────────────────────────────────── MULHU64: rdx:rax ← rax·rcx, and the high word is the result — the registers a div uses, so the same rules keep them clear: no web touching the record lives in rcx (rj_clobcx) and none live across it in rdx (rj_clobdx).

@ rj_e_div Rj c i r → v

@ rj_globmode Rj c → v

Globals 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 → i

the 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 → v

one 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 → v

multi-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 → b

Compile 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 → v

x 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 → v

vop 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 → v

dst = a op b into dst's place

@ rj_e_fbin Rj c i r → v

@ rj_fun_dst Rj c i od → i

a 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 → v

f64.abs / f64.neg: clear / flip the sign bit, on whichever side the web lives

@ rj_e_fround Rj c i r → v

f64 ceil / floor / trunc / nearest: roundsd (SSE4.1)

@ rj_fcmp1 Rj c i pf i al i as i bl i bs → v

ucomisd/ucomiss a, b with a in an xmm register

@ rj_e_fcmp Rj c i r → v

@ rj_e_fconv Rj c i r → v

int → float and float ↔ float conversions

@ rj_e_ffused Rj c i r → v

the 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 → v

min / 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 → v

f32/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 → v

call_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 → v


interp.nu

packages/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.

API

& c @ nurl_code_alloc i n → *u

The 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 → i32

Page 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 → i32

NURL_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 → *u

Guard-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 → i32

The 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 → i32

unlink(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 → sink 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 → sink 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 → sink s

& c @ nurl_dns_resolve_port s host i port → sink s

& c @ nurl_dns_reverse s ip → sink 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.

: WasiCtx

: WasiCtx {
    ( Vec WFd ) fds
    ( Vec u ) capout  // captured module stdout (raw bytes, NULs preserved)
    ( Vec u ) caperr  // captured module stderr
}

What WASI calls see of the host, per process in the guest's terms: the argument and environment lists, the descriptor table (0/1/2 stdio, 3 preopen, …; a closed one is kind 0) and the captured output. A spawned thread's instance shares its owner's (one Locked handle), so a file one thread opens is the same fd in another, and two threads' calls take turns at it instead of writing one record at once.

: ArgEnv { ( Vec Arg ) argv ( Vec Arg ) envp }

The program's arguments (argv[0] = program) and environment entries ("NAME=VALUE"): set before it runs and read by every thread, so they are frozen — reading them takes no lock — and a push makes a new set.

: 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
    ( Locked WasiCtx ) wasi  // the WASI host state, shared with every thread of the instance
    ( Arc ArgEnv ) argenv  // the program's arguments and environment, shared and frozen
    b cap  // capture stdout/stderr into the WasiCtx's capout/caperr instead of the host streams
    // 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
    // The storage behind the raw words above, owned here: the slab, the
    // anchor block, every call context ever made (the freelist only links
    // them) and the call-out bridge's closure environment (jit_co_env).
    ( Vec u ) jit_slab_mem
    ( Vec u ) jit_spcell_mem
    ( Vec ( Vec i ) ) jit_ctx_mem
    JitCoEnv jit_co
    // 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)
    ( Vec s ) pfuncs  // *PFunc per defined function, predecoded lazily (#s 0 until first call)
    ( Vec ( Box PFunc ) ) pf_owned  // the records `pfuncs` points at: this instance owns them
    // 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)
    ( Vec i ) import_ids  // per import: its dispatch number (__wasi_resolve; -3 = not yet)
    ( Vec i ) import_sigs  // per import: params | results << 16 (-1 = not yet)
    // The module this instance runs, held so it outlives every user of `mod`
    // (owner only; a thread borrows its owner's).
    Module modh
}

: JitCoEnv { s env }

The call-out bridge's closure environment, decomposed for the emitted code (which calls fn(env)) and released with the instance. Not a closure field: a closure may capture an Interp, and an Arc's payload must not be able to reach its own Arc (docs/MEMORY.md §7.7) — this one captures the raw *InterpImpl only.

% Drop JitCoEnv { unsafe @ drop JitCoEnv c → v { ( nurl_closure_drop # *u . c env ) } }

: Interp { ( Locked InterpImpl ) l }

An Interp is a handle on its instance state in an Arc (stdlib/std/arc.nu): every copy is the same instance, and the last owner releases it. A spawned thread's instance is an Arc too, held by the thread that runs it (__wasi_thread_spawn); it owns only its own stacks, globals and caches — the memory, table, segments and argv it shares are its owner's, reached through __host.

@ Interp_share Interp h → Interp

@ 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 pool ( Vec i ) bytes s jit i jitlen ( Vec i ) refl ( Vec ( Box Frame ) ) frames }

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.

% Drop PFunc

% Drop PFunc {
    unsafe @ drop PFunc pf → v {
        ? != . pf jitlen 0 {
            ( nurl_guard_code_del # *u . pf jit )  // no-op when never registered
            ? > . pf jitlen 0 { ( nurl_code_free # *u . pf jit . pf jitlen ) } {}
        } {}
    }
}

A function's compiled code goes with its record: its own mapping (jitlen

0), or a stretch of the code arena (< 0), which the arena releases.

: ~ i g_guard 1

Guard-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 ) )
        } {}
        ( vec_clear [( Box PFunc )] . it pf_owned )
        ( __jit_arena_unmap . it jit_arena )
    }
}

What an instance holds outside its fields — the guard reservation, the JIT's code arena, the guest's open sockets — goes with its last owner (Interp_drop); its fields (the predecoded records first, which may point into the arena) 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 → v

Let go of it now rather than at the end of its owner's scope.

@ interp_capture Interp h → v

Capture the module's stdout/stderr into buffers instead of writing them to the host's own streams. This is the embedder's switch: a host program that runs a module as a function call wants the module's output back as a VALUE, not interleaved into its own console. Enable before exec_func; read the buffers after it.

@ interp_stdout_bytes Interp h → ( Vec u )

The captured bytes — a copy of the Interp's buffers. Raw bytes, exactly as the module wrote them: a NUL neither truncates nor terminates.

@ interp_stderr_bytes Interp h → ( Vec u )

@ interp_trapped Interp h → b

@ interp_exited Interp h → b

@ interp_trapmsg Interp h → ( Vec u )

The trap message (empty unless interp_trapped), copied.

@ interp_exit_code Interp h → i

The guest's proc_exit code (0 when it returned from _start instead).

@ interp_stack_push Interp h i v → v

The value stack: push an invoked export's arguments before exec_func, and read its results off the top after it.

@ interp_stack_len Interp h → i

@ interp_stack_at Interp h i k → i

Slot k of the value stack (0 = bottom); 0 past its end.

@ interp_set_fuel Interp h i n → v

Meter execution: at most n predecoded records, then trap (--fuel).

@ interp_push_arg Interp h s str → v

Append a program argument (copied from a NUL-terminated host string).

@ interp_push_env Interp h s str → v

Append an environment entry ("NAME=VALUE", copied).

@ interp_allow_gpu Interp h → v

Enable the module "env" GPU/CUDA host-import bridge. Off by default: those imports hand the guest raw host pointers into linear memory and forward them to libcuda, so they are only safe for trusted compute — the embedder must opt in explicitly (the CLI does so with --allow-gpu).

@ interp_allow_net Interp h → v

Enable the module "nurl_net" socket bridge — the guest's TCP/UDP/DNS calls become this process's. Off by default for the same reason the GPU bridge is: it is the guest reaching the network through us (the CLI opts in with --allow-net).

@ interp_set_preopen Interp it__h s host_path s guest_name → v

Grant the module one preopened host directory, visible to it as guest_name (the path it resolves opens against). Installed as fd 3.

@ interp_run_start Interp h → v

: 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 default

Execute 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 → v

NURL_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 0

Set 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 h i fidx → v

@ interp_flush Interp h → v

@ interp_net_close_all Interp 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 value

: ~ ( Vec u ) g_atom_mem 0

The lock and the condvar live side by side in this buffer, which is never grown, so their addresses stay put.


module.nu

packages/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 Arc (stdlib/std/arc.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.

API

: Wc { ( Slice 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 → v

Nothing to release: a Wc owns no memory.

@ wc_eof inout Wc c → b

@ wc_u8 inout Wc c → i

@ wc_peek inout Wc c → i

@ wc_uleb inout Wc c → i

Unsigned 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 → i

Signed LEB128 → i (sign-extended).

@ wc_skip inout Wc c i n → v

Skip n bytes.

@ wc_avail inout Wc c → i

Bytes 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 ( Box FuncType ) ) types
    ( Vec i ) functypes  // type index per defined function
    ( Vec ( Box WFunc ) ) funcs
    ( Vec ( Box WExport ) ) exports
    ( 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 ( Box DataSeg ) ) datas
    ( 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 ( Box ElemSeg ) ) elems  // element segments, in order
    ( Vec ( Box WImport ) ) imports  // 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 ( Box NameBuf ) ) name_str  // …and their names, parallel (sparse)
    b ok
    ( Vec u ) err
}

: Module { ( Arc ModuleImpl ) a }

A Module is a handle on its decoded state in an Arc (stdlib/std/arc.nu): every copy is the same module, and the last owner releases it.

@ Module_share Module h → Module

@ Module_drop sink Module h → v

@ module_impl Module h → ModuleImpl

The decoded state, read-only (a Module is frozen once decoded).

@ module_free sink Module m → v

Let go of m now rather than at the end of its owner's scope.

@ module_ok Module p__h → b

Did the module decode? When not, module_err says why.

@ module_err Module p__h → ( Vec u )

The decode error message (empty when module_ok) — the module's own bytes, lent.

@ module_num_import_funcs Module p__h → i

Imported functions occupy function indices 0 .. this-1.

@ _module_func_name ModuleImpl m i fidx → ( Vec u )

The name-section name of function fidx as a fresh byte vector (empty if unknown). Cold path — linear scan is fine (used only for trap backtraces).

@ module_func_name Module m__h i fidx → ( Vec u )

@ module_decode sink ( Vec u ) bytes → Module

Decode a whole module. On error, module_ok is F and module_err says why. The module takes over bytes as its image (functions index into it).

@ _module_export_global ModuleImpl m s name → i

Find an exported GLOBAL's index by name (-1 if absent). wasi-threads needs one: __stack_pointer is what gives a spawned thread its own stack, and only the host can set it before the thread's first call.

@ module_export_global Module m__h s name → i

@ _module_export_func ModuleImpl m s name → i

Find an exported function index by name (-1 if absent).

@ module_export_func Module m__h s name → i

@ module_func_sig Module m__h i fidx → ?FuncType

The type of function fidx (imported or defined), copied out of the module — what a caller outside the interpreter (the CLI parsing its arguments) reads; F when the index is out of range.

@ functype_eq FuncType a FuncType b → b

Structural function-type equality (the call_indirect runtime check): same parameter and result valtypes, in order.


main.nu

packages/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).

API

@ 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 → i

WASI command: run the module's _start with argv = [module, prog args…], the given preopened directories and environment entries.

@ main → i

Every 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.