nx_wasm.nx source
↩ module page · 1091 lines · 49886 B
1// nx_wasm.nx F618 wasm-SIMD FLIP (2026-07-21, landed from the proven rung-1 twin nx_wasm_v128.nx).
2// The fused v128 SAD intercept fires ONLY on calls to v128_sad16 (nx_vmotion.nx); any function without one emits byte-identical wasm.
3// THE INTERCEPT: a call to the scalar reference fn `v128_sad16(a,b)` (nx_vmotion.nx) is emitted
4// as an INLINE wasm-SIMD sequence (v128.load x2, sub_sat_u both ways, or -> bytewise |a-b|,
5// extadd_pairwise u8->u16->u32, 4x extract_lane+extend+add) instead of a call. Native + stock-wat
6// builds keep the scalar body = the bit-exact reference; this backend swaps ONLY the call sites.
7// Exact-integer equivalence: sub_sat_u(a,b)|sub_sat_u(b,a) == |a-b| per byte (one side is 0);
8// extadds are exact (max 16*255=4080 << u16/u32); the lane sum is the same integer as the scalar loop.
9// The inliner is OFF by default (opt_inline_module gate) so call sites always reach this emitter.
10// wasm.nx -- NishiLang port of wasm.c (WAT backend).
11//
12// Lowers our SSA IR to WebAssembly Text format. The core challenge
13// WAT imposes is structured control flow: there are no arbitrary
14// branches, only block/loop/if scopes with break-to-label. Our IR
15// has free-form CFG edges, so we use the universal block-dispatch
16// pattern (`br_table` driven by a `$target` local) that encodes any
17// CFG at small constant cost.
18//
19// Per-op semantics match wasm.c exactly; every Value gets a $vN
20// local, comparisons extend to i64 after the i32 WASM result, and
21// constants materialize inline as i64.const.
22//
23// Functions are prefixed `wat_` so they don't collide with riscv.nx's
24// asm-side `rv_*` / `emit_*` naming when both libraries are imported
25// into a driver.
26
27// nx_safety_envelope:
28// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
29// sil_target: SIL1
30// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
31// verdict: NOT_YET_EVALUATED
32
33import "nx_syscalls.nx"
34import "nx_types.nx"
35import "nx_ir.nx"
36import "nx_outbuf.nx"
37import "nx_wasm_data.nx"
38
39// LN33 scale-down (operator 2026-09-03: the browser is a door, never the ceiling): the DRIVER may clear this
40// (nx_compile_wat --nothreads) to emit the UNSHARED twin of a shared-declaring module -- ordinary memory, the
41// atomic calls left as calls to their single-thread reference bodies -- so ONE source ships both modules and
42// the page picks by measured capability (crossOriginIsolated), never by user agent. 0 = honour the module's
43// own declarations (the default; a static must be zero-initialised on both lanes, hence the OFF polarity).
44static wat_threads_off: i64
45// GE30 / the scale envelope. THE MEMORY DECLARATION BELONGS ON THE BUILD LANE, NOT IN THE SOURCE. LN33 let a
46// module declare `static nx_wasm_shared_req/pages_req/pages_max`, which is right for a wasm-ONLY module and
47// wrong for a DUAL-TARGET one: nx_wasm_craft compiles both natively and to wasm, and the native lane refuses a
48// non-zero static initializer outright (G3_NONZERO_INIT_STATIC_UNSUPPORTED_BOTH_LANES), so the declaration
49// could not live there at all. It should not anyway -- the standing order is that a target's limit is a
50// declared door on a ladder, never a constant in the source, and one source must be buildable at a sensor's
51// memory and at a supercomputer's. These are ZERO-INIT (the only initializer the native lane accepts) and are
52// set by nx_compile_wat --shared <req> <max>; zero means "not given", so the module-global path is unchanged
53// and every module that does not pass the flag emits byte-identical WAT.
54static wat_shared_req: i64
55static wat_pages_req_flag: i64
56static wat_pages_max_flag: i64
57
58// ---- operand emission ----------------------------------------------
59//
60// Push an operand onto the WASM stack. Constants become literals;
61// everything else reads from its $vN mirror local.
62
63func wat_push_operand(f: *Function, o: *OutBuf, ind: i64, v: i64) -> i64 {
64 let val: *Value = val_at(f, v)
65 out_indent(o, ind * 2)
66 if val.kind == VK_CONST_INT {
67 out_str(o, "i64.const ")
68 out_i64(o, val.const_int)
69 out_char(o, 0x0A)
70 } else {
71 if val.kind == VK_GLOBAL {
72 out_str(o, "i64.const ")
73 out_i64(o, wd_address(val.const_int))
74 out_char(o, 0x0A)
75 return 0
76 }
77 if val.kind == VK_FUNC_ADDR {
78 return wd_fail("WAT VALUE: function addresses require table lowering; refusing uninitialized local\n" as *u8)
79 }
80 out_str(o, "local.get $v")
81 out_i64(o, v)
82 out_char(o, 0x0A)
83 }
84 return 0
85}
86
87func wat_store_result(o: *OutBuf, ind: i64, v: i64) -> i64 {
88 out_indent(o, ind * 2)
89 out_str(o, "local.set $v")
90 out_i64(o, v)
91 out_char(o, 0x0A)
92 return 0
93}
94
95// ---- opcode -> WAT mnemonic ----------------------------------------
96
97func wat_binop_mnem(op: i64, o: *OutBuf) -> i64 {
98 if op == OP_ADD { out_str(o, "i64.add"); return 1 }
99 if op == OP_SUB { out_str(o, "i64.sub"); return 1 }
100 if op == OP_MUL { out_str(o, "i64.mul"); return 1 }
101 if op == OP_DIV_S { out_str(o, "i64.div_s"); return 1 }
102 if op == OP_REM_S { out_str(o, "i64.rem_s"); return 1 }
103 if op == OP_AND { out_str(o, "i64.and"); return 1 }
104 if op == OP_OR { out_str(o, "i64.or"); return 1 }
105 if op == OP_XOR { out_str(o, "i64.xor"); return 1 }
106 if op == OP_SHL { out_str(o, "i64.shl"); return 1 }
107 if op == OP_SHR_S { out_str(o, "i64.shr_s"); return 1 }
108 if op == OP_SHR_U { out_str(o, "i64.shr_u"); return 1 }
109 return 0
110}
111
112func wat_cmp_mnem(op: i64, o: *OutBuf) -> i64 {
113 if op == OP_EQ { out_str(o, "i64.eq"); return 1 }
114 if op == OP_NE { out_str(o, "i64.ne"); return 1 }
115 if op == OP_LT_S { out_str(o, "i64.lt_s"); return 1 }
116 if op == OP_LE_S { out_str(o, "i64.le_s"); return 1 }
117 if op == OP_GT_S { out_str(o, "i64.gt_s"); return 1 }
118 if op == OP_GE_S { out_str(o, "i64.ge_s"); return 1 }
119 return 0
120}
121
122// ---- instruction emission ------------------------------------------
123
124func wat_emit_binop(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
125 wat_push_operand(f, o, ind, i.op0)
126 wat_push_operand(f, o, ind, i.op1)
127 out_indent(o, ind * 2)
128 wat_binop_mnem(i.op, o)
129 out_char(o, 0x0A)
130 wat_store_result(o, ind, i.result)
131 return 0
132}
133
134func wat_emit_cmp(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
135 wat_push_operand(f, o, ind, i.op0)
136 wat_push_operand(f, o, ind, i.op1)
137 out_indent(o, ind * 2)
138 wat_cmp_mnem(i.op, o)
139 out_char(o, 0x0A)
140 // Lift i32 compare result to i64 for uniform storage.
141 out_indent(o, ind * 2)
142 out_str(o, "i64.extend_i32_u\n")
143 wat_store_result(o, ind, i.result)
144 return 0
145}
146
147// ---- F618 v128 SAD intercept --------------------------------------
148// exact NUL-terminated name compare (kw passed as an ARG so the const-index trap never applies)
149func wat_nameq(name: *u8, kw: *u8) -> i64 {
150 var i: i64 = 0
151 while kw[i] != (0 as u8) {
152 if name[i] != kw[i] { return 0 }
153 i = i + 1
154 }
155 if name[i] != (0 as u8) { return 0 }
156 return 1
157}
158func wat_call_is_sad16(i: *Instr) -> i64 {
159 if i.callee == (0 as *Function) { return 0 }
160 let na: i64 = i.callee.name_start
161 if na == 0 { return 0 }
162 let nm: *u8 = na as *u8
163 return wat_nameq(nm, "v128_sad16" as *u8)
164}
165// ---- LN33/LN34 wasm threads: the IR ATOMIC OPS on the wasm lane (2026-09-03) -------------------------------
166// ONE atomic family for every lane, and it is the incumbent's: nx_atom.nx's __atomic_* builtins lower to
167// OP_ATOMIC_LOAD/STORE/CAS/FAA/FENCE in the IR; the x86-64 backend lowers those to LOCK-prefixed instructions,
168// the rv64 backend to AMO and lr/sc, and THIS backend to the 0xFE family over the module's shared memory.
169// (A first cut of LN33 intercepted a second family by call NAME -- nx_atomic_* -- beside nx_atom; that was a
170// duplicate ruler and was retired the same day: the lowering keys on the IR op, exactly like every backend.)
171// Under --nothreads (wat_threads_off) the SAME ops lower to their plain single-thread twins so one source ships
172// both the shared and the unshared module. Every value is an i64 local; the address is wrapped to i32; the cell
173// is 8 bytes (natural alignment 3). The memory-order operand is accepted and ignored: wasm atomics are
174// sequentially consistent, the strongest order a caller can ask for, so no ordering can be lost here.
175// CAS returns 1 when it swapped and 0 otherwise -- the nx_atom contract and the x86 lowering (sete) -- so the
176// cmpxchg's OLD value is compared against the expectation after the op.
177func wat_at_line(o: *OutBuf, ind: i64, s: *u8) -> i64 { out_indent(o, ind * 2); out_str(o, s); out_char(o, 0x0A); return 0 }
178
179// LN35 memory64: THE ONE PLACE THE wasm32 ADDRESS NARROWING IS DECIDED.
180// Measured 2026-09-04 (corpus_complete=1): `i32.wrap_i64` was spelled inline at 27 SITES across four files.
181// That is the duplicate-ruler defect with a severe failure mode -- under memory64 a SINGLE MISSED SITE is a
182// SILENTLY TRUNCATED POINTER, and 27 hand edits guarantee one is missed. So the decision gets exactly one
183// home instead of 27 copies obliged to agree by discipline.
184// NEUTRALITY IS PROVABLE, NOT ASSERTED: with wat_mem64_on == 0 this emits precisely what every call site
185// emitted before (out_indent + text + LF, byte-for-byte what wat_at_line does), so wasm32 output must be
186// BYTE-IDENTICAL and a rebuild is the proof. The body is written EXPANDED on purpose: spelling it as
187// wat_at_line(o, ind, "i32.wrap_i64") would have been rewritten into infinite recursion by the very
188// replace-all sweep that migrated the call sites.
189static wat_mem64_on: i64
190func wat_addr_narrow(o: *OutBuf, ind: i64) -> i64 {
191 if wat_mem64_on == 1 { return 0 }
192 out_indent(o, ind * 2)
193 out_str(o, "i32.wrap_i64")
194 out_char(o, 0x0A)
195 return 0
196}
197func wat_at_get_result(o: *OutBuf, ind: i64, i: *Instr) -> i64 { out_indent(o, ind * 2); out_str(o, "local.get $v"); out_i64(o, i.result); out_char(o, 0x0A); return 0 }
198func wat_emit_atomic_op(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
199 let op: i64 = i.op
200 if op == OP_ATOMIC_FENCE {
201 if wat_threads_off == 0 { wat_at_line(o, ind, "atomic.fence" as *u8) }
202 return 0
203 }
204 wat_push_operand(f, o, ind, i.op0)
205 wat_addr_narrow(o, ind)
206 if op == OP_ATOMIC_LOAD_I64 {
207 if wat_threads_off == 0 { wat_at_line(o, ind, "i64.atomic.load" as *u8) } else { wat_at_line(o, ind, "i64.load" as *u8) }
208 wat_store_result(o, ind, i.result)
209 return 0
210 }
211 if op == OP_ATOMIC_STORE_I64 {
212 wat_push_operand(f, o, ind, i.op1)
213 if wat_threads_off == 0 { wat_at_line(o, ind, "i64.atomic.store" as *u8) } else { wat_at_line(o, ind, "i64.store" as *u8) }
214 return 0
215 }
216 if op == OP_ATOMIC_FAA_I64 {
217 if wat_threads_off == 0 {
218 wat_push_operand(f, o, ind, i.op1)
219 wat_at_line(o, ind, "i64.atomic.rmw.add" as *u8)
220 wat_store_result(o, ind, i.result)
221 return 0
222 }
223 // single-thread twin: old = *addr; *addr = old + delta; result = old
224 wat_at_line(o, ind, "i64.load" as *u8)
225 wat_store_result(o, ind, i.result)
226 wat_push_operand(f, o, ind, i.op0)
227 wat_addr_narrow(o, ind)
228 wat_at_get_result(o, ind, i)
229 wat_push_operand(f, o, ind, i.op1)
230 wat_at_line(o, ind, "i64.add" as *u8)
231 wat_at_line(o, ind, "i64.store" as *u8)
232 return 0
233 }
234 if op == OP_ATOMIC_CAS_I64 {
235 if wat_threads_off == 0 {
236 wat_push_operand(f, o, ind, i.op1)
237 wat_push_operand(f, o, ind, i.op2)
238 wat_at_line(o, ind, "i64.atomic.rmw.cmpxchg" as *u8)
239 wat_store_result(o, ind, i.result)
240 } else {
241 // single-thread twin: old = *addr; if old == expected { *addr = new }; result carries old for the compare below
242 wat_at_line(o, ind, "i64.load" as *u8)
243 wat_store_result(o, ind, i.result)
244 wat_at_get_result(o, ind, i)
245 wat_push_operand(f, o, ind, i.op1)
246 wat_at_line(o, ind, "i64.eq" as *u8)
247 wat_at_line(o, ind, "if" as *u8)
248 wat_push_operand(f, o, ind + 1, i.op0)
249 wat_addr_narrow(o, ind + 1)
250 wat_push_operand(f, o, ind + 1, i.op2)
251 wat_at_line(o, ind + 1, "i64.store" as *u8)
252 wat_at_line(o, ind, "end" as *u8)
253 }
254 // old == expected -> 1 else 0 (the nx_atom contract)
255 wat_at_get_result(o, ind, i)
256 wat_push_operand(f, o, ind, i.op1)
257 wat_at_line(o, ind, "i64.eq" as *u8)
258 wat_at_line(o, ind, "i64.extend_i32_u" as *u8)
259 wat_store_result(o, ind, i.result)
260 return 0
261 }
262 return 0
263}
264// futex over the shared memory: __syscall(98 rv64 / 202 x86-64, addr, op, val, ...) with a CONSTANT op whose
265// low seven bits are FUTEX_WAIT (0) or FUTEX_WAKE (1) -- the nx_thread_pool and nx_mutex shape -- lowers to
266// memory.atomic.wait32 (addr, val, no timeout) and memory.atomic.notify (addr, count). The futex word is a
267// 32-bit compare on Linux, hence wait32 on the low word of the i64 cell. Any other op keeps the historical
268// syscall no-op (result 0); under --nothreads every futex is that no-op, because a wait on unshared memory traps.
269func wat_syscall_is_futex(f: *Function, i: *Instr) -> i64 {
270 if wat_threads_off == 1 { return 0 }
271 if i.n_operands < 4 { return 0 }
272 let nval: *Value = val_at(f, i.op0)
273 if nval.kind != VK_CONST_INT { return 0 }
274 if nval.const_int != 98 { if nval.const_int != 202 { return 0 } }
275 let oval: *Value = val_at(f, i.op2)
276 if oval.kind != VK_CONST_INT { return 0 }
277 let fop: i64 = oval.const_int & 127
278 if fop == 0 { return 1 }
279 if fop == 1 { return 2 }
280 return 0
281}
282func wat_emit_futex(f: *Function, o: *OutBuf, ind: i64, i: *Instr, kind: i64) -> i64 {
283 wat_push_operand(f, o, ind, i.op1)
284 wat_addr_narrow(o, ind)
285 wat_push_operand(f, o, ind, i.op3)
286 wat_addr_narrow(o, ind)
287 if kind == 1 {
288 wat_at_line(o, ind, "i64.const -1" as *u8)
289 wat_at_line(o, ind, "memory.atomic.wait32" as *u8)
290 } else {
291 wat_at_line(o, ind, "memory.atomic.notify" as *u8)
292 }
293 wat_at_line(o, ind, "i64.extend_i32_u" as *u8)
294 wat_store_result(o, ind, i.result)
295 return 0
296}
297// emit the fused SIMD SAD-of-16-bytes sequence for a v128_sad16(a,b) call site.
298// stack discipline: everything ends as one i64 stored to the call's result local.
299// uses the per-function scratch locals $wa/$wb (v128), declared by wat_emit_function iff needed.
300func wat_emit_v128sad(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
301 wat_push_operand(f, o, ind, i.op0)
302 wat_addr_narrow(o, ind)
303 out_indent(o, ind * 2); out_str(o, "v128.load\n")
304 out_indent(o, ind * 2); out_str(o, "local.set $wa\n")
305 wat_push_operand(f, o, ind, i.op1)
306 wat_addr_narrow(o, ind)
307 out_indent(o, ind * 2); out_str(o, "v128.load\n")
308 out_indent(o, ind * 2); out_str(o, "local.set $wb\n")
309 out_indent(o, ind * 2); out_str(o, "local.get $wa\n")
310 out_indent(o, ind * 2); out_str(o, "local.get $wb\n")
311 out_indent(o, ind * 2); out_str(o, "i8x16.sub_sat_u\n")
312 out_indent(o, ind * 2); out_str(o, "local.get $wb\n")
313 out_indent(o, ind * 2); out_str(o, "local.get $wa\n")
314 out_indent(o, ind * 2); out_str(o, "i8x16.sub_sat_u\n")
315 out_indent(o, ind * 2); out_str(o, "v128.or\n")
316 out_indent(o, ind * 2); out_str(o, "i16x8.extadd_pairwise_i8x16_u\n")
317 out_indent(o, ind * 2); out_str(o, "i32x4.extadd_pairwise_i16x8_u\n")
318 out_indent(o, ind * 2); out_str(o, "local.set $wa\n")
319 out_indent(o, ind * 2); out_str(o, "local.get $wa\n")
320 out_indent(o, ind * 2); out_str(o, "i32x4.extract_lane 0\n")
321 out_indent(o, ind * 2); out_str(o, "i64.extend_i32_u\n")
322 out_indent(o, ind * 2); out_str(o, "local.get $wa\n")
323 out_indent(o, ind * 2); out_str(o, "i32x4.extract_lane 1\n")
324 out_indent(o, ind * 2); out_str(o, "i64.extend_i32_u\n")
325 out_indent(o, ind * 2); out_str(o, "i64.add\n")
326 out_indent(o, ind * 2); out_str(o, "local.get $wa\n")
327 out_indent(o, ind * 2); out_str(o, "i32x4.extract_lane 2\n")
328 out_indent(o, ind * 2); out_str(o, "i64.extend_i32_u\n")
329 out_indent(o, ind * 2); out_str(o, "i64.add\n")
330 out_indent(o, ind * 2); out_str(o, "local.get $wa\n")
331 out_indent(o, ind * 2); out_str(o, "i32x4.extract_lane 3\n")
332 out_indent(o, ind * 2); out_str(o, "i64.extend_i32_u\n")
333 out_indent(o, ind * 2); out_str(o, "i64.add\n")
334 wat_store_result(o, ind, i.result)
335 return 0
336}
337
338// Validate once at the call boundary, before ordinary or specialized emission.
339func wat_call_preflight(i: *Instr) -> i64 {
340 let first: *i64 = &i.op0
341 let end: *i64 = &i.callee as *i64
342 let slots: i64 = ((end as i64) - (first as i64)) / 8
343 if i.n_operands < 0 || i.n_operands > slots {
344 wat_die("nx_wasm: call operand count exceeds actual IR operand storage; no partial call emitted\n" as *u8)
345 }
346 if wat_call_is_sad16(i) == 1 {
347 if i.n_operands != 2 {
348 wat_die("nx_wasm: v128_sad16 requires exactly two operands; no partial call emitted\n" as *u8)
349 }
350 }
351 return 0
352}
353// Internal emission follows the shared call preflight; no second arity list.
354func wat_push_call_operands(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
355 let first: *i64 = &i.op0
356 var k: i64 = 0
357 while k < i.n_operands {
358 wat_push_operand(f, o, ind, first[k])
359 k = k + 1
360 }
361 return 0
362}
363
364func wat_emit_call(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
365 wat_call_preflight(i)
366 if wat_call_is_sad16(i) == 1 { return wat_emit_v128sad(f, o, ind, i) }
367 wat_push_call_operands(f, o, ind, i)
368 out_indent(o, ind * 2)
369 out_str(o, "call $")
370 if i.callee != (0 as *Function) {
371 let name_addr: i64 = i.callee.name_start
372 let name: *u8 = name_addr as *u8
373 if name != (0 as *u8) {
374 out_str(o, name)
375 } else {
376 out_str(o, "fn_unknown")
377 }
378 } else {
379 out_str(o, "fn_unknown")
380 }
381 out_char(o, 0x0A)
382 wat_store_result(o, ind, i.result)
383 return 0
384}
385
386func wat_emit_return(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
387 if i.n_operands > 0 {
388 wat_push_operand(f, o, ind, i.op0)
389 out_indent(o, ind * 2)
390 out_str(o, "local.set $ret\n")
391 }
392 out_indent(o, ind * 2)
393 out_str(o, "br $exit\n")
394 return 0
395}
396
397func wat_emit_branch(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
398 if i.op == OP_BR {
399 out_indent(o, ind * 2)
400 out_str(o, "i32.const ")
401 out_i64(o, i.op0)
402 out_char(o, 0x0A)
403 out_indent(o, ind * 2)
404 out_str(o, "local.set $target\n")
405 out_indent(o, ind * 2)
406 out_str(o, "br $dispatch\n")
407 return 0
408 }
409 // br_cond cond ? op1 : op2
410 wat_push_operand(f, o, ind, i.op0)
411 out_indent(o, ind * 2)
412 out_str(o, "i64.const 0\n")
413 out_indent(o, ind * 2)
414 out_str(o, "i64.ne\n")
415 out_indent(o, ind * 2)
416 out_str(o, "if\n")
417 out_indent(o, (ind + 1) * 2)
418 out_str(o, "i32.const ")
419 out_i64(o, i.op1)
420 out_char(o, 0x0A)
421 out_indent(o, (ind + 1) * 2)
422 out_str(o, "local.set $target\n")
423 out_indent(o, ind * 2)
424 out_str(o, "else\n")
425 out_indent(o, (ind + 1) * 2)
426 out_str(o, "i32.const ")
427 out_i64(o, i.op2)
428 out_char(o, 0x0A)
429 out_indent(o, (ind + 1) * 2)
430 out_str(o, "local.set $target\n")
431 out_indent(o, ind * 2)
432 out_str(o, "end\n")
433 out_indent(o, ind * 2)
434 out_str(o, "br $dispatch\n")
435 return 0
436}
437
438// hardware f32 on the wasm lane (R2 -- browser lane). i64 CARRIER (low 32 = IEEE binary32 bits, same layout
439// as the x86 __f32_* lowering) <-> native wasm f32 via reinterpret. Same NishiLang f32 targets native + browser.
440func wat_emit_f32(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
441 let op: i64 = i.op
442 if op == OP_FCAST_I_TO_F {
443 wat_push_operand(f, o, ind, i.op0)
444 out_indent(o, ind * 2); out_str(o, "f32.convert_i64_s\n")
445 out_indent(o, ind * 2); out_str(o, "i32.reinterpret_f32\n")
446 out_indent(o, ind * 2); out_str(o, "i64.extend_i32_u\n")
447 wat_store_result(o, ind, i.result)
448 return 0
449 }
450 if op == OP_FCAST_F_TO_I {
451 wat_push_operand(f, o, ind, i.op0)
452 wat_addr_narrow(o, ind)
453 out_indent(o, ind * 2); out_str(o, "f32.reinterpret_i32\n")
454 out_indent(o, ind * 2); out_str(o, "i64.trunc_f32_s\n")
455 wat_store_result(o, ind, i.result)
456 return 0
457 }
458 wat_push_operand(f, o, ind, i.op0)
459 wat_addr_narrow(o, ind)
460 out_indent(o, ind * 2); out_str(o, "f32.reinterpret_i32\n")
461 wat_push_operand(f, o, ind, i.op1)
462 wat_addr_narrow(o, ind)
463 out_indent(o, ind * 2); out_str(o, "f32.reinterpret_i32\n")
464 out_indent(o, ind * 2)
465 if op == OP_FADD { out_str(o, "f32.add\n") }
466 if op == OP_FSUB { out_str(o, "f32.sub\n") }
467 if op == OP_FMUL { out_str(o, "f32.mul\n") }
468 if op == OP_FDIV { out_str(o, "f32.div\n") }
469 out_indent(o, ind * 2); out_str(o, "i32.reinterpret_f32\n")
470 out_indent(o, ind * 2); out_str(o, "i64.extend_i32_u\n")
471 wat_store_result(o, ind, i.result)
472 return 0
473}
474
475// ---- memory model (alloca / load / store / gep) --------------------
476// A SCALAR alloca's own $vN local IS its storage: wasm locals are per-INVOCATION, so nested calls can't
477// collide -- no shadow stack needed for scalars. load/store whose pointer is an alloca-result become
478// local.get/set; on a COMPUTED address (fixed-offset buffer + gep) they become real i64.load/i64.store in
479// linear memory (8-byte; the renderer's fb + matrices are all i64). gep = base + offset (front-end pre-
480// scales the index). LIMIT: scalar/address-not-taken allocas only; array/address-taken allocas need a real
481// shadow stack (future) -- the wasm renderer uses fixed linear-memory offsets for its buffers to avoid that.
482func wat_is_alloca(f: *Function, vid: i64) -> i64 {
483 let val: *Value = val_at(f, vid)
484 if val.kind != VK_INSTR { return 0 }
485 let ins: *Instr = val.instr
486 if ins == (0 as *Instr) { return 0 }
487 if ins.op == OP_ALLOCA { return 1 }
488 return 0
489}
490func wat_emit_load(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
491 if wat_is_alloca(f, i.op0) == 1 {
492 out_indent(o, ind * 2); out_str(o, "local.get $v"); out_i64(o, i.op0); out_char(o, 0x0A)
493 wat_store_result(o, ind, i.result)
494 return 0
495 }
496 wat_push_operand(f, o, ind, i.op0)
497 wat_addr_narrow(o, ind)
498 // width from the loaded element type (i.ty) -- a *u8 must NOT read 8 bytes (close T#wasm-001-byte-mem).
499 // 2026-07-10 sext debt fix (WASM lane): subword loads SIGN-extend when the pointee was declared
500 // signed (Type.sext=1, minted by alloc_type_s for i8/i16/i32) and ZERO-extend otherwise -- the SAME
501 // semantic the x86 + RV64 backends now share. wasm has native _s/_u twins for every width.
502 var lsz: i64 = 8
503 var lsx: i64 = 0
504 let lty: *Type = i.ty
505 if lty != (0 as *Type) { lsz = lty.size; lsx = lty.sext }
506 out_indent(o, ind * 2)
507 if lsz == 1 { if lsx == 1 { out_str(o, "i64.load8_s\n") } else { out_str(o, "i64.load8_u\n") } }
508 if lsz == 2 { if lsx == 1 { out_str(o, "i64.load16_s\n") } else { out_str(o, "i64.load16_u\n") } }
509 if lsz == 4 { if lsx == 1 { out_str(o, "i64.load32_s\n") } else { out_str(o, "i64.load32_u\n") } }
510 if lsz >= 8 { out_str(o, "i64.load\n") }
511 wat_store_result(o, ind, i.result)
512 return 0
513}
514func wat_emit_store(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
515 if wat_is_alloca(f, i.op0) == 1 {
516 wat_push_operand(f, o, ind, i.op1)
517 out_indent(o, ind * 2); out_str(o, "local.set $v"); out_i64(o, i.op0); out_char(o, 0x0A)
518 return 0
519 }
520 wat_push_operand(f, o, ind, i.op0)
521 wat_addr_narrow(o, ind)
522 wat_push_operand(f, o, ind, i.op1)
523 // width from the stored element type (i.ty) -- a *u8 write must NOT clobber the next 7 bytes (the codec-wasm bug).
524 var ssz: i64 = 8
525 let sty: *Type = i.ty
526 if sty != (0 as *Type) { ssz = sty.size }
527 out_indent(o, ind * 2)
528 if ssz == 1 { out_str(o, "i64.store8\n") }
529 if ssz == 2 { out_str(o, "i64.store16\n") }
530 if ssz == 4 { out_str(o, "i64.store32\n") }
531 if ssz >= 8 { out_str(o, "i64.store\n") }
532 return 0
533}
534func wat_emit_gep(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
535 wat_push_operand(f, o, ind, i.op0)
536 wat_push_operand(f, o, ind, i.op1)
537 out_indent(o, ind * 2); out_str(o, "i64.add\n")
538 wat_store_result(o, ind, i.result)
539 return 0
540}
541// tail call (`return f(...)`): wasm has no tail-call op, so emit a normal call then return its result.
542func wat_emit_tail_call(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
543 wat_call_preflight(i)
544 if wat_call_is_sad16(i) == 1 {
545 wat_emit_v128sad(f, o, ind, i)
546 out_indent(o, ind * 2); out_str(o, "local.get $v"); out_i64(o, i.result); out_char(o, 0x0A)
547 out_indent(o, ind * 2); out_str(o, "local.set $ret\n")
548 out_indent(o, ind * 2); out_str(o, "br $exit\n")
549 return 0
550 }
551 wat_push_call_operands(f, o, ind, i)
552 out_indent(o, ind * 2)
553 out_str(o, "call $")
554 if i.callee != (0 as *Function) {
555 let name_addr: i64 = i.callee.name_start
556 let name: *u8 = name_addr as *u8
557 if name != (0 as *u8) { out_str(o, name) } else { out_str(o, "fn_unknown") }
558 } else {
559 out_str(o, "fn_unknown")
560 }
561 out_char(o, 0x0A)
562 out_indent(o, ind * 2); out_str(o, "local.set $ret\n")
563 out_indent(o, ind * 2); out_str(o, "br $exit\n")
564 return 0
565}
566
567// Dispatch table for one IR instruction.
568func wat_emit_instr(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
569 let op: i64 = i.op
570 // Arithmetic / bitwise (1..15 minus 9=NEG which we skip).
571 if op == OP_ADD { wat_emit_binop(f, o, ind, i); return 0 }
572 if op == OP_SUB { wat_emit_binop(f, o, ind, i); return 0 }
573 if op == OP_MUL { wat_emit_binop(f, o, ind, i); return 0 }
574 if op == OP_DIV_S { wat_emit_binop(f, o, ind, i); return 0 }
575 if op == OP_REM_S { wat_emit_binop(f, o, ind, i); return 0 }
576 if op == OP_AND { wat_emit_binop(f, o, ind, i); return 0 }
577 if op == OP_OR { wat_emit_binop(f, o, ind, i); return 0 }
578 if op == OP_XOR { wat_emit_binop(f, o, ind, i); return 0 }
579 if op == OP_SHL { wat_emit_binop(f, o, ind, i); return 0 }
580 if op == OP_SHR_S { wat_emit_binop(f, o, ind, i); return 0 }
581 if op == OP_SHR_U { wat_emit_binop(f, o, ind, i); return 0 }
582 // Hardware f32 (browser lane) -- reinterpret carrier <-> native wasm f32.
583 if op == OP_FADD { wat_emit_f32(f, o, ind, i); return 0 }
584 if op == OP_FSUB { wat_emit_f32(f, o, ind, i); return 0 }
585 if op == OP_FMUL { wat_emit_f32(f, o, ind, i); return 0 }
586 if op == OP_FDIV { wat_emit_f32(f, o, ind, i); return 0 }
587 if op == OP_FCAST_I_TO_F { wat_emit_f32(f, o, ind, i); return 0 }
588 if op == OP_FCAST_F_TO_I { wat_emit_f32(f, o, ind, i); return 0 }
589 // Compare.
590 if op == OP_EQ { wat_emit_cmp(f, o, ind, i); return 0 }
591 if op == OP_NE { wat_emit_cmp(f, o, ind, i); return 0 }
592 if op == OP_LT_S { wat_emit_cmp(f, o, ind, i); return 0 }
593 if op == OP_LE_S { wat_emit_cmp(f, o, ind, i); return 0 }
594 if op == OP_GT_S { wat_emit_cmp(f, o, ind, i); return 0 }
595 if op == OP_GE_S { wat_emit_cmp(f, o, ind, i); return 0 }
596 // Call / return / branches.
597 if op == OP_CALL { wat_emit_call(f, o, ind, i); return 0 }
598 if op == OP_TAIL_CALL { wat_emit_tail_call(f, o, ind, i); return 0 }
599 if op == OP_RETURN { wat_emit_return(f, o, ind, i); return 0 }
600 if op == OP_BR { wat_emit_branch(f, o, ind, i); return 0 }
601 if op == OP_BR_COND { wat_emit_branch(f, o, ind, i); return 0 }
602 // COPY: push op0 then store result.
603 if op == OP_COPY {
604 wat_push_operand(f, o, ind, i.op0)
605 wat_store_result(o, ind, i.result)
606 return 0
607 }
608 // LN33/LN34: the IR atomic family (nx_atom.nx builtins) -> 0xFE atomics, or their single-thread twins under --nothreads.
609 if op == OP_ATOMIC_LOAD_I64 { wat_emit_atomic_op(f, o, ind, i); return 0 }
610 if op == OP_ATOMIC_STORE_I64 { wat_emit_atomic_op(f, o, ind, i); return 0 }
611 if op == OP_ATOMIC_CAS_I64 { wat_emit_atomic_op(f, o, ind, i); return 0 }
612 if op == OP_ATOMIC_FAA_I64 { wat_emit_atomic_op(f, o, ind, i); return 0 }
613 if op == OP_ATOMIC_FENCE { wat_emit_atomic_op(f, o, ind, i); return 0 }
614 // memory model: scalar alloca = its $vN local; load/store/gep on computed addresses = real linear memory.
615 if op == OP_ALLOCA { return 0 }
616 if op == OP_LOAD { wat_emit_load(f, o, ind, i); return 0 }
617 if op == OP_STORE { wat_emit_store(f, o, ind, i); return 0 }
618 if op == OP_GEP { wat_emit_gep(f, o, ind, i); return 0 }
619 // Scalar bit unops (wasm has no i64.not/i64.neg) -- lower to identities. Same silent-no-op hazard the native
620 // backend hit on rotates (SHA-512/Ed25519 break, SITES-LIVE 2026-05-27); NOT was breaking SHA-256 in the wat lane.
621 if op == OP_NOT { // ~a == a XOR -1
622 wat_push_operand(f, o, ind, i.op0)
623 out_indent(o, ind * 2); out_str(o, "i64.const -1\n")
624 out_indent(o, ind * 2); out_str(o, "i64.xor\n")
625 wat_store_result(o, ind, i.result)
626 return 0
627 }
628 if op == OP_NEG { // -a == 0 - a
629 out_indent(o, ind * 2); out_str(o, "i64.const 0\n")
630 wat_push_operand(f, o, ind, i.op0)
631 out_indent(o, ind * 2); out_str(o, "i64.sub\n")
632 wat_store_result(o, ind, i.result)
633 return 0
634 }
635 // OP_SYSCALL (2026-07-29 root fix): this used to fall through to the TODO comment below = a SILENT
636 // NO-OP whose result local stayed default-0 -- the artifact known as "the wasm sys_mmap 0-stub"
637 // (every allocation aliased address 0; the partition-MV bug + the dormant SATD landmine, seq234).
638 // Now: SYS_MMAP (x86-64 9 / rv64 222; __syscall op0=number, op2=size) lowers to the stateless
639 // $nx_wasm_mmap grow-per-allocation helper emitted in the module preamble. Every OTHER syscall
640 // keeps the historical EXPLICIT result-0 no-op: wasm has no kernel, and turning the no-op into a
641 // trap would change the behavior of shipped modules that harmlessly pass through sys_write paths.
642 if op == OP_SYSCALL {
643 // LN33 (2026-09-03): the futex pair lowers to wait32/notify over the shared memory (see wat_emit_futex).
644 let fk: i64 = wat_syscall_is_futex(f, i)
645 if fk != 0 { return wat_emit_futex(f, o, ind, i, fk) }
646 var ismmap: i64 = 0
647 if i.n_operands >= 3 {
648 let nval: *Value = val_at(f, i.op0)
649 if nval.kind == VK_CONST_INT {
650 if nval.const_int == 9 { ismmap = 1 }
651 if nval.const_int == 222 { ismmap = 1 }
652 }
653 }
654 if ismmap == 1 {
655 wat_push_operand(f, o, ind, i.op2)
656 out_indent(o, ind * 2)
657 out_str(o, "call $nx_wasm_mmap\n")
658 } else {
659 out_indent(o, ind * 2)
660 out_str(o, "i64.const 0\n")
661 }
662 wat_store_result(o, ind, i.result)
663 return 0
664 }
665 // Unhandled: NO ARM EXISTS for this opcode in this backend, so there is nothing honest to emit. REFUSE.
666 return wat_die_unlowerable(f, op)
667}
668
669// ---- function emission ---------------------------------------------
670
671func wat_emit_function(f: *Function, o: *OutBuf) -> i64 {
672 // Signature.
673 out_str(o, " (func $")
674 let name_addr: i64 = f.name_start
675 let fn_name: *u8 = name_addr as *u8
676 if fn_name != (0 as *u8) {
677 out_str(o, fn_name)
678 } else {
679 out_str(o, "fn")
680 }
681 // Params: scan values for VAL_PARAM by param_index in order.
682 var p: i64 = 0
683 while p < f.n_params {
684 var v_idx: i64 = 0
685 while v_idx < f.n_values {
686 let val: *Value = val_at(f, v_idx)
687 if val.kind == VK_PARAM {
688 if val.param_index == p {
689 out_str(o, " (param $arg")
690 out_i64(o, p)
691 out_str(o, " i64)")
692 v_idx = f.n_values // break
693 }
694 }
695 v_idx = v_idx + 1
696 }
697 p = p + 1
698 }
699 if f.ret_ty != (0 as *Type) {
700 if f.ret_ty.kind != 0 { // TY_VOID = 0
701 out_str(o, " (result i64)")
702 }
703 }
704 out_char(o, 0x0A)
705
706 // Locals: one i64 per non-constant SSA value.
707 var v: i64 = 0
708 while v < f.n_values {
709 let val2: *Value = val_at(f, v)
710 if val2.kind != VK_CONST_INT {
711 out_str(o, " (local $v")
712 out_i64(o, v)
713 out_str(o, " i64)\n")
714 }
715 v = v + 1
716 }
717 out_str(o, " (local $target i32)\n")
718 out_str(o, " (local $ret i64)\n")
719 // F618: declare the v128 scratch pair IFF this function contains an intercepted v128_sad16 call
720 // (keeps every other function's wasm byte-identical to the stock emitter's output).
721 var wv_has: i64 = 0
722 var wv_b: i64 = 0
723 while wv_b < f.n_blocks {
724 let wv_bb: *BasicBlock = block_at(f, wv_b)
725 var wv_ins: *Instr = wv_bb.head
726 while wv_ins != (0 as *Instr) {
727 if wv_ins.op == OP_CALL { if wat_call_is_sad16(wv_ins) == 1 { wv_has = 1 } }
728 if wv_ins.op == OP_TAIL_CALL { if wat_call_is_sad16(wv_ins) == 1 { wv_has = 1 } }
729 wv_ins = wv_ins.next
730 }
731 wv_b = wv_b + 1
732 }
733 if wv_has == 1 {
734 out_str(o, " (local $wa v128)\n")
735 out_str(o, " (local $wb v128)\n")
736 }
737
738 // Copy incoming params into their $vI mirror.
739 var vp: i64 = 0
740 while vp < f.n_values {
741 let val3: *Value = val_at(f, vp)
742 if val3.kind == VK_PARAM {
743 out_str(o, " local.get $arg")
744 out_i64(o, val3.param_index)
745 out_char(o, 0x0A)
746 out_str(o, " local.set $v")
747 out_i64(o, vp)
748 out_char(o, 0x0A)
749 }
750 vp = vp + 1
751 }
752
753 // Initial dispatch target = 0 (entry block).
754 out_str(o, " i32.const 0\n")
755 out_str(o, " local.set $target\n")
756
757 // Open block-dispatch scaffolding. (block $exit / (loop $dispatch
758 // / (block $bb_default / nested (block $bbN) ... (block $bb0) ...
759 let N: i64 = f.n_blocks
760 out_str(o, " (block $exit\n")
761 out_str(o, " (loop $dispatch\n")
762 out_str(o, " (block $bb_default\n")
763 var bi: i64 = N - 1
764 while bi >= 0 {
765 let ind_lvl: i64 = 4 + (N - 1 - bi)
766 out_indent(o, ind_lvl * 2)
767 out_str(o, "(block $bb")
768 out_i64(o, bi)
769 out_char(o, 0x0A)
770 bi = bi - 1
771 }
772
773 // br_table at deepest indent. local.get $target FIRST (it pushes the dispatch index that
774 // br_table consumes from the stack), THEN br_table. Fixes the operand-order bug the C wasm.c
775 // already fixed -- this NishiLang port emitted them reversed, producing INVALID wat.
776 let deepest: i64 = 4 + N
777 out_indent(o, deepest * 2)
778 out_str(o, "local.get $target\n")
779 out_indent(o, deepest * 2)
780 out_str(o, "br_table")
781 var bt: i64 = 0
782 while bt < N {
783 out_str(o, " $bb")
784 out_i64(o, bt)
785 bt = bt + 1
786 }
787 out_str(o, " $bb_default\n")
788
789 // Close each bbB, emit its body at (4+N-B-1) indent.
790 var b: i64 = 0
791 while b < N {
792 let close_ind: i64 = 4 + N - b - 1
793 out_indent(o, close_ind * 2)
794 out_str(o, ")\n")
795 let bb: *BasicBlock = block_at(f, b)
796 var inst: *Instr = bb.head
797 while inst != (0 as *Instr) {
798 wat_emit_instr(f, o, close_ind, inst)
799 inst = inst.next
800 }
801 b = b + 1
802 }
803
804 // Close bb_default + body (unreachable).
805 out_str(o, " )\n")
806 out_str(o, " unreachable\n")
807 out_str(o, " )\n") // close loop $dispatch
808 out_str(o, " )\n") // close block $exit
809
810 // Return the stashed ret value.
811 if f.ret_ty != (0 as *Type) {
812 if f.ret_ty.kind != 0 {
813 out_str(o, " local.get $ret\n")
814 }
815 }
816 out_str(o, " )\n") // close (func
817
818 // Export under the function's own name.
819 if fn_name != (0 as *u8) {
820 out_str(o, " (export \"")
821 out_str(o, fn_name)
822 out_str(o, "\" (func $")
823 out_str(o, fn_name)
824 out_str(o, "))\n")
825 }
826 return 0
827}
828
829// ---- module emission -----------------------------------------------
830
831// ---- linear-memory sizing: the MODULE owns its page count ---------------
832// A module that needs a specific memory size declares
833// static nx_wasm_pages_req: i64 = <pages>
834// (parse const-folds the initializer into the global's LE payload; we read it
835// here). WAT_MEM_PAGES_DEFAULT covers modules that do NOT declare -- browsers
836// commit pages lazily, so undeclaring modules pay nothing for a large default.
837// This kills the three-copy drift class (2026-08-18: the engine raised its
838// ceiling to 364 pages while both backends still emitted 192, and the ship
839// lane fail-closed for 9 days): the consumer that needs the memory is now the
840// only place its size is written. Enforcement stays behavioral, not textual:
841// the vm-gate's FITS tooth refuses any emitted module whose declared memory
842// cannot hold its framebuffer, and craft-gate T60 pins the engine's mirror.
843const WAT_MEM_PAGES_DEFAULT: i64 = 364
844// wasm32 linear memory is capped at 65,536 64KiB pages (4 GiB) by the
845// WebAssembly spec (memtype limits); a declared request past it can never
846// instantiate, so refuse at emit where the author can see it.
847const WAT_PAGES_SPEC_MAX: i64 = 65536
848const WAT_GLOBAL_STRIDE: i64 = 80 // Global slot stride -- matches ir_module_new's allocator
849
850// LN13a -- THE WAT LANE MUST REFUSE WHAT IT CANNOT LOWER (2026-09-04).
851// Until today an IR opcode with no arm in this backend was answered with a WAT COMMENT and `return 0`, and
852// crucially WITHOUT storing a result local. The module still assembled, still instantiated and still ran --
853// silently computing zero wherever that instruction's value was read. That is the worst shape a compiler
854// defect can take: invisible at build time, wrong at run time, and indistinguishable from correct code in
855// every artifact anyone would think to inspect. MEASURED the same day: only 41 of the 141 declared opcodes
856// have an arm in this emitter, so the silent surface was ~100 opcodes wide, and OP_CALL_INDIRECT (144) --
857// what every function-pointer call lowers to -- was one of them.
858// SCOPED DELIBERATELY, AND THE SCOPE IS THE WHOLE SAFETY ARGUMENT. This fires ONLY on the no-arm
859// fallthrough. The OP_SYSCALL path above still emits `i64.const 0` and still stores its result, because that
860// no-op is DECLARED behaviour for non-mmap syscalls and shipped modules pass through it harmlessly; turning
861// THAT into a refusal would break working artifacts. The rule is "no arm at all", never "an arm I judge
862// incomplete".
863// COMPOSES wat_die: the message is BUILT here and TERMINATED there, so this file keeps exactly one exit path.
864// CAP is named rather than guessed: the fixed text is ~400 bytes and the only variable part is one identifier.
865const WAT_UNLOWERABLE_MSG_CAP: i64 = 1024
866func wat_die_unlowerable(f: *Function, op: i64) -> i64 {
867 let e: *OutBuf = out_new(WAT_UNLOWERABLE_MSG_CAP)
868 out_str(e, "nx_wasm: cannot lower IR opcode ")
869 out_i64(e, op)
870 out_str(e, " in function ")
871 let name_addr: i64 = f.name_start
872 let fn_name: *u8 = name_addr as *u8
873 if fn_name != (0 as *u8) {
874 out_str(e, fn_name)
875 } else {
876 out_str(e, "<anonymous>")
877 }
878 out_str(e, " -- this backend has no arm for that opcode. It used to emit a comment and return 0, which produced a module that assembled and ran while computing zero at that instruction: the build looked clean and the program was wrong. Add an arm for this opcode in the emitter, or lower the construct before it reaches the wat backend.
879")
880 e.buf[e.pos] = 0 as u8
881 return wat_die(e.buf)
882}
883
884func wat_die(msg: *u8) -> i64 {
885 var n: i64 = 0
886 while msg[n] != (0 as u8) { n = n + 1 }
887 sys_write(2, msg, n)
888 sys_exit(3)
889 return 0
890}
891
892func wat_mem_pages(m: *Module) -> i64 {
893 if wat_pages_req_flag > 0 { return wat_pages_req_flag }
894 let want: *u8 = "nx_wasm_pages_req" as *u8
895 var wl: i64 = 0
896 while want[wl] != (0 as u8) { wl = wl + 1 }
897 var gi: i64 = 0
898 while gi < m.n_globals {
899 let g: *Global = (m.globals as i64 + gi*WAT_GLOBAL_STRIDE) as *Global
900 if g.name_len == wl {
901 var same: i64 = 1
902 var k: i64 = 0
903 while k < wl {
904 if g.name_bytes[k] != want[k] { same = 0; k = wl }
905 k = k + 1
906 }
907 if same == 1 {
908 if g.bytes == (0 as *u8) {
909 wat_die("nx_wasm: nx_wasm_pages_req is declared but carries no initialized value; declare it as `static nx_wasm_pages_req: i64 = <pages>`\n" as *u8)
910 }
911 var v: i64 = 0
912 var b: i64 = 0
913 while b < 8 {
914 v = v + ((g.bytes[b] as i64) << (b*8))
915 b = b + 1
916 }
917 if v < 1 {
918 wat_die("nx_wasm: nx_wasm_pages_req must be >= 1 page; a zero or negative request declares a memory nothing can live in\n" as *u8)
919 }
920 if v > WAT_PAGES_SPEC_MAX {
921 wat_die("nx_wasm: nx_wasm_pages_req exceeds the wasm32 spec maximum of 65536 pages (4 GiB); the module could never instantiate\n" as *u8)
922 }
923 return v
924 }
925 }
926 gi = gi + 1
927 }
928 return WAT_MEM_PAGES_DEFAULT
929}
930// LN33 (2026-09-03): the wat_mem_pages mechanism generalised -- read a declared `static <name>: i64 = <v>`
931// module global. found[0] is set to 1 when the global exists; a declared-but-uninitialised global is refused
932// by name exactly as nx_wasm_pages_req is, because a value nobody wrote is a value nobody can reason about.
933func wat_global_i64(m: *Module, want: *u8, found: *i64) -> i64 {
934 found[0] = 0
935 var wl: i64 = 0
936 while want[wl] != (0 as u8) { wl = wl + 1 }
937 var gi: i64 = 0
938 while gi < m.n_globals {
939 let g: *Global = (m.globals as i64 + gi*WAT_GLOBAL_STRIDE) as *Global
940 if g.name_len == wl {
941 var same: i64 = 1
942 var k: i64 = 0
943 while k < wl {
944 if g.name_bytes[k] != want[k] { same = 0; k = wl }
945 k = k + 1
946 }
947 if same == 1 {
948 if g.bytes == (0 as *u8) {
949 wat_die("nx_wasm: a wasm memory declaration (nx_wasm_shared_req or nx_wasm_pages_max) is declared but carries no initialized value; declare it as `static <name>: i64 = <value>`\n" as *u8)
950 }
951 var v: i64 = 0
952 var b: i64 = 0
953 while b < 8 {
954 v = v + ((g.bytes[b] as i64) << (b*8))
955 b = b + 1
956 }
957 found[0] = 1
958 return v
959 }
960 }
961 gi = gi + 1
962 }
963 return 0
964}
965// A module that declares `static nx_wasm_shared_req: i64 = 1` asks for a SHARED linear memory (the
966// SharedArrayBuffer the world pages already pay for with COOP+COEP); absent or zero means the ordinary
967// unshared memory every module had before LN33, byte-identical output.
968func wat_mem_shared(m: *Module) -> i64 {
969 if wat_threads_off == 1 { return 0 }
970 if wat_shared_req == 1 { return 1 }
971 let fnd: *i64 = sys_mmap(8) as *i64
972 let v: i64 = wat_global_i64(m, "nx_wasm_shared_req" as *u8, fnd)
973 if fnd[0] == 0 { return 0 }
974 if v == 0 { return 0 }
975 return 1
976}
977// The threads proposal only allows a shared memory WITH a maximum, and a guessed maximum is the buffer-cap
978// defect (rule 11), so the module must declare `static nx_wasm_pages_max: i64 = <pages>` itself; refused
979// by name when it is absent, below the initial size, or past the wasm32 spec ceiling.
980func wat_mem_pages_max(m: *Module, pages: i64) -> i64 {
981 if wat_pages_max_flag > 0 {
982 if wat_pages_max_flag < pages { wat_die("nx_wasm: requested maximum is below the derived memory extent including static data and initialization state\n" as *u8) }
983 if wat_pages_max_flag > WAT_PAGES_SPEC_MAX { wat_die("nx_wasm: requested maximum exceeds wasm32 address space\n" as *u8) }
984 return wat_pages_max_flag
985 }
986 let fnd: *i64 = sys_mmap(8) as *i64
987 let v: i64 = wat_global_i64(m, "nx_wasm_pages_max" as *u8, fnd)
988 if fnd[0] == 0 {
989 wat_die("nx_wasm: nx_wasm_shared_req is set but nx_wasm_pages_max is not declared; a shared memory needs a declared maximum (the threads proposal requires one and a guessed ceiling is the buffer-cap defect): declare `static nx_wasm_pages_max: i64 = <pages>`\n" as *u8)
990 }
991 if v < pages {
992 wat_die("nx_wasm: nx_wasm_pages_max is below nx_wasm_pages_req; a shared memory's maximum must be at least its initial size\n" as *u8)
993 }
994 if v > WAT_PAGES_SPEC_MAX {
995 wat_die("nx_wasm: nx_wasm_pages_max exceeds the wasm32 spec maximum of 65536 pages (4 GiB); the module could never instantiate\n" as *u8)
996 }
997 return v
998}
999
1000func wat_emit_module(m: *Module, o: *OutBuf) -> i64 {
1001 out_str(o, ";; Auto-generated by nxc2.nx WASM (WAT) backend.\n")
1002 out_str(o, ";; Module: ")
1003 if m.name != (0 as *u8) { out_str(o, m.name) }
1004 out_char(o, 0x0A)
1005 out_str(o, "(module\n")
1006 // Linear memory: size comes from the module's own declaration when present
1007 // (see wat_mem_pages above), else the named default. JS reads it via
1008 // exports.memory.buffer; the VM sizes mod.mem from this same declaration.
1009 // LN33 (2026-09-03): a module declaring `static nx_wasm_shared_req: i64 = 1` gets a SHARED memory. It is
1010 // IMPORTED ("env" "memory") rather than defined, because every Worker instantiates the same module bytes
1011 // against ONE WebAssembly.Memory the page created -- a defined memory would give each Worker a private copy
1012 // and no thread would ever see another's write. It stays exported under "memory" so a single-thread consumer
1013 // reads exports.memory exactly as before. The unshared path below is byte-identical to the pre-LN33 emitter.
1014 let wm_shared: i64=wat_mem_shared(m)
1015 let wm_pages: i64 = wd_prepare(m, wat_mem_pages(m),wm_shared)
1016 if wat_mem_shared(m) == 1 {
1017 let wm_max: i64 = wat_mem_pages_max(m, wm_pages)
1018 out_str(o, " (import ")
1019 out_char(o, 34); out_str(o, "env"); out_char(o, 34)
1020 out_str(o, " ")
1021 out_char(o, 34); out_str(o, "memory"); out_char(o, 34)
1022 out_str(o, " (memory ")
1023 out_i64(o, wm_pages)
1024 out_str(o, " ")
1025 out_i64(o, wm_max)
1026 out_str(o, " shared))\n")
1027 out_str(o, " (export ")
1028 out_char(o, 34); out_str(o, "memory"); out_char(o, 34)
1029 out_str(o, " (memory 0))\n")
1030 } else {
1031 out_str(o, " (memory (export ")
1032 out_char(o, 34)
1033 out_str(o, "memory")
1034 out_char(o, 34)
1035 out_str(o, ") ")
1036 out_i64(o, wm_pages)
1037 out_str(o, ")\n")
1038 }
1039 if wm_shared!=1 { wd_emit(m,o) }
1040 // $nx_wasm_mmap (2026-07-29): the REAL sys_mmap for the wasm tier. Historically OP_SYSCALL fell
1041 // through to a TODO comment = silent no-op with a default-0 result, so EVERY wasm allocation
1042 // aliased address 0 (the seq234 landmine class; the codec grew "slab, NEVER sys_mmap" workarounds).
1043 // Design: STATELESS grow-per-allocation -- pages = ceil(n/64K), base = memory.grow(pages)*64K.
1044 // The returned base is the OLD memory end = past every fixed carve BY CONSTRUCTION (no global, no
1045 // heap pointer, no collision surface). grow failure (-1) returns 0 = the historical failure value.
1046 // Branchless select: (g != 0xFFFFFFFF) * g * 65536 (compares yield i32 0/1; extend_u then multiply).
1047 out_str(o, " (func $nx_wasm_mmap (param $arg0 i64) (result i64)\n")
1048 out_str(o, " (local $g i64)\n")
1049 out_str(o, " local.get $arg0\n")
1050 out_str(o, " i64.const 65535\n")
1051 out_str(o, " i64.add\n")
1052 out_str(o, " i64.const 65536\n")
1053 out_str(o, " i64.div_s\n")
1054 out_str(o, " i32.wrap_i64\n")
1055 out_str(o, " memory.grow\n")
1056 out_str(o, " i64.extend_i32_u\n")
1057 out_str(o, " local.set $g\n")
1058 out_str(o, " local.get $g\n")
1059 out_str(o, " i64.const 4294967295\n")
1060 out_str(o, " i64.ne\n")
1061 out_str(o, " i64.extend_i32_u\n")
1062 out_str(o, " local.get $g\n")
1063 out_str(o, " i64.mul\n")
1064 out_str(o, " i64.const 65536\n")
1065 out_str(o, " i64.mul\n")
1066 out_str(o, " )\n")
1067 var i: i64 = 0
1068 while i < m.n_functions {
1069 let fn_base: i64 = m.functions as i64
1070 let f: *Function = (fn_base + i * 176) as *Function
1071 if wm_shared==1 {
1072 let reserved: *u8="_nx_data_init" as *u8
1073 let name: *u8=f.name_start as *u8
1074 if name!=(0 as *u8) {
1075 var k: i64=0
1076 while reserved[k]!=(0 as u8) && name[k]==reserved[k] { k=k+1 }
1077 if reserved[k]==(0 as u8) && name[k]==(0 as u8) {
1078 return wat_die("nx_wasm: _nx_data_init is reserved for shared static-data initialization; rename the source function\n" as *u8)
1079 }
1080 }
1081 }
1082 wat_emit_function(f, o)
1083 i = i + 1
1084 }
1085 // Append compiler-owned initialization so existing function indices remain stable across variants.
1086 if wm_shared==1 { wd_emit_shared(m,o) }
1087 out_str(o, ")\n")
1088 return 0
1089}
1090
1091// Library only; self-test lives in wasm_test.nx.