nx_wasm_scalararm.nx source
↩ module page · 927 lines · 39960 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"
37
38// LN33 scale-down override, see nx_wasm.nx: 0 = honour the module's declarations, 1 = emit the unshared twin.
39static wat_threads_off: i64
40
41// ---- operand emission ----------------------------------------------
42//
43// Push an operand onto the WASM stack. Constants become literals;
44// everything else reads from its $vN mirror local.
45
46func wat_push_operand(f: *Function, o: *OutBuf, ind: i64, v: i64) -> i64 {
47 let val: *Value = val_at(f, v)
48 out_indent(o, ind * 2)
49 if val.kind == VK_CONST_INT {
50 out_str(o, "i64.const ")
51 out_i64(o, val.const_int)
52 out_char(o, 0x0A)
53 } else {
54 out_str(o, "local.get $v")
55 out_i64(o, v)
56 out_char(o, 0x0A)
57 }
58 return 0
59}
60
61func wat_store_result(o: *OutBuf, ind: i64, v: i64) -> i64 {
62 out_indent(o, ind * 2)
63 out_str(o, "local.set $v")
64 out_i64(o, v)
65 out_char(o, 0x0A)
66 return 0
67}
68
69// ---- opcode -> WAT mnemonic ----------------------------------------
70
71func wat_binop_mnem(op: i64, o: *OutBuf) -> i64 {
72 if op == OP_ADD { out_str(o, "i64.add"); return 1 }
73 if op == OP_SUB { out_str(o, "i64.sub"); return 1 }
74 if op == OP_MUL { out_str(o, "i64.mul"); return 1 }
75 if op == OP_DIV_S { out_str(o, "i64.div_s"); return 1 }
76 if op == OP_REM_S { out_str(o, "i64.rem_s"); return 1 }
77 if op == OP_AND { out_str(o, "i64.and"); return 1 }
78 if op == OP_OR { out_str(o, "i64.or"); return 1 }
79 if op == OP_XOR { out_str(o, "i64.xor"); return 1 }
80 if op == OP_SHL { out_str(o, "i64.shl"); return 1 }
81 if op == OP_SHR_S { out_str(o, "i64.shr_s"); return 1 }
82 if op == OP_SHR_U { out_str(o, "i64.shr_u"); return 1 }
83 return 0
84}
85
86func wat_cmp_mnem(op: i64, o: *OutBuf) -> i64 {
87 if op == OP_EQ { out_str(o, "i64.eq"); return 1 }
88 if op == OP_NE { out_str(o, "i64.ne"); return 1 }
89 if op == OP_LT_S { out_str(o, "i64.lt_s"); return 1 }
90 if op == OP_LE_S { out_str(o, "i64.le_s"); return 1 }
91 if op == OP_GT_S { out_str(o, "i64.gt_s"); return 1 }
92 if op == OP_GE_S { out_str(o, "i64.ge_s"); return 1 }
93 return 0
94}
95
96// ---- instruction emission ------------------------------------------
97
98func wat_emit_binop(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
99 wat_push_operand(f, o, ind, i.op0)
100 wat_push_operand(f, o, ind, i.op1)
101 out_indent(o, ind * 2)
102 wat_binop_mnem(i.op, o)
103 out_char(o, 0x0A)
104 wat_store_result(o, ind, i.result)
105 return 0
106}
107
108func wat_emit_cmp(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
109 wat_push_operand(f, o, ind, i.op0)
110 wat_push_operand(f, o, ind, i.op1)
111 out_indent(o, ind * 2)
112 wat_cmp_mnem(i.op, o)
113 out_char(o, 0x0A)
114 // Lift i32 compare result to i64 for uniform storage.
115 out_indent(o, ind * 2)
116 out_str(o, "i64.extend_i32_u\n")
117 wat_store_result(o, ind, i.result)
118 return 0
119}
120
121// ---- F618 v128 SAD intercept --------------------------------------
122// exact NUL-terminated name compare (kw passed as an ARG so the const-index trap never applies)
123func wat_nameq(name: *u8, kw: *u8) -> i64 {
124 var i: i64 = 0
125 while kw[i] != (0 as u8) {
126 if name[i] != kw[i] { return 0 }
127 i = i + 1
128 }
129 if name[i] != (0 as u8) { return 0 }
130 return 1
131}
132func wat_call_is_sad16(i: *Instr) -> i64 {
133 if i.callee == (0 as *Function) { return 0 }
134 let na: i64 = i.callee.name_start
135 if na == 0 { return 0 }
136 let nm: *u8 = na as *u8
137 return wat_nameq(nm, "zzz_scalar_arm_no_intercept" as *u8)
138}
139// ---- LN33/LN34 wasm threads on the scalar arm: the IR ATOMIC OPS (nx_atom.nx builtins) -> 0xFE, or their
140// plain single-thread twins under --nothreads. Same bytes as nx_wasm.nx; see the note there.
141func 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 }
142
143// LN35 memory64: THE ONE PLACE THE wasm32 ADDRESS NARROWING IS DECIDED (scalar-arm twin of nx_wasm.nx).
144// Kept in step with its sibling DELIBERATELY: a fix that lands in one organ and not its twin is half a fix,
145// and the missing half is invisible until something runs it. Under memory64 a SINGLE MISSED SITE is a
146// SILENTLY TRUNCATED POINTER, so the decision gets one home per emitter instead of 10 inline copies.
147// With wat_mem64_on == 0 this emits exactly what every call site emitted before, so scalar-arm wasm32
148// output must be BYTE-IDENTICAL and a rebuild is the proof. Body written EXPANDED on purpose: spelling it
149// as wat_at_line(o, ind, "i32.wrap_i64") would have been rewritten into infinite recursion by the same
150// replace-all sweep that migrated the call sites.
151static wat_mem64_on: i64
152func wat_addr_narrow(o: *OutBuf, ind: i64) -> i64 {
153 if wat_mem64_on == 1 { return 0 }
154 out_indent(o, ind * 2)
155 out_str(o, "i32.wrap_i64")
156 out_char(o, 0x0A)
157 return 0
158}
159func 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 }
160func wat_emit_atomic_op(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
161 let op: i64 = i.op
162 if op == OP_ATOMIC_FENCE {
163 if wat_threads_off == 0 { wat_at_line(o, ind, "atomic.fence" as *u8) }
164 return 0
165 }
166 wat_push_operand(f, o, ind, i.op0)
167 wat_addr_narrow(o, ind)
168 if op == OP_ATOMIC_LOAD_I64 {
169 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) }
170 wat_store_result(o, ind, i.result)
171 return 0
172 }
173 if op == OP_ATOMIC_STORE_I64 {
174 wat_push_operand(f, o, ind, i.op1)
175 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) }
176 return 0
177 }
178 if op == OP_ATOMIC_FAA_I64 {
179 if wat_threads_off == 0 {
180 wat_push_operand(f, o, ind, i.op1)
181 wat_at_line(o, ind, "i64.atomic.rmw.add" as *u8)
182 wat_store_result(o, ind, i.result)
183 return 0
184 }
185 wat_at_line(o, ind, "i64.load" as *u8)
186 wat_store_result(o, ind, i.result)
187 wat_push_operand(f, o, ind, i.op0)
188 wat_addr_narrow(o, ind)
189 wat_at_get_result(o, ind, i)
190 wat_push_operand(f, o, ind, i.op1)
191 wat_at_line(o, ind, "i64.add" as *u8)
192 wat_at_line(o, ind, "i64.store" as *u8)
193 return 0
194 }
195 if op == OP_ATOMIC_CAS_I64 {
196 if wat_threads_off == 0 {
197 wat_push_operand(f, o, ind, i.op1)
198 wat_push_operand(f, o, ind, i.op2)
199 wat_at_line(o, ind, "i64.atomic.rmw.cmpxchg" as *u8)
200 wat_store_result(o, ind, i.result)
201 } else {
202 wat_at_line(o, ind, "i64.load" as *u8)
203 wat_store_result(o, ind, i.result)
204 wat_at_get_result(o, ind, i)
205 wat_push_operand(f, o, ind, i.op1)
206 wat_at_line(o, ind, "i64.eq" as *u8)
207 wat_at_line(o, ind, "if" as *u8)
208 wat_push_operand(f, o, ind + 1, i.op0)
209 wat_addr_narrow(o, ind + 1)
210 wat_push_operand(f, o, ind + 1, i.op2)
211 wat_at_line(o, ind + 1, "i64.store" as *u8)
212 wat_at_line(o, ind, "end" as *u8)
213 }
214 wat_at_get_result(o, ind, i)
215 wat_push_operand(f, o, ind, i.op1)
216 wat_at_line(o, ind, "i64.eq" as *u8)
217 wat_at_line(o, ind, "i64.extend_i32_u" as *u8)
218 wat_store_result(o, ind, i.result)
219 return 0
220 }
221 return 0
222}
223// emit the fused SIMD SAD-of-16-bytes sequence for a v128_sad16(a,b) call site.
224// stack discipline: everything ends as one i64 stored to the call's result local.
225// uses the per-function scratch locals $wa/$wb (v128), declared by wat_emit_function iff needed.
226func wat_emit_v128sad(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
227 wat_push_operand(f, o, ind, i.op0)
228 wat_addr_narrow(o, ind)
229 out_indent(o, ind * 2); out_str(o, "v128.load\n")
230 out_indent(o, ind * 2); out_str(o, "local.set $wa\n")
231 wat_push_operand(f, o, ind, i.op1)
232 wat_addr_narrow(o, ind)
233 out_indent(o, ind * 2); out_str(o, "v128.load\n")
234 out_indent(o, ind * 2); out_str(o, "local.set $wb\n")
235 out_indent(o, ind * 2); out_str(o, "local.get $wa\n")
236 out_indent(o, ind * 2); out_str(o, "local.get $wb\n")
237 out_indent(o, ind * 2); out_str(o, "i8x16.sub_sat_u\n")
238 out_indent(o, ind * 2); out_str(o, "local.get $wb\n")
239 out_indent(o, ind * 2); out_str(o, "local.get $wa\n")
240 out_indent(o, ind * 2); out_str(o, "i8x16.sub_sat_u\n")
241 out_indent(o, ind * 2); out_str(o, "v128.or\n")
242 out_indent(o, ind * 2); out_str(o, "i16x8.extadd_pairwise_i8x16_u\n")
243 out_indent(o, ind * 2); out_str(o, "i32x4.extadd_pairwise_i16x8_u\n")
244 out_indent(o, ind * 2); out_str(o, "local.set $wa\n")
245 out_indent(o, ind * 2); out_str(o, "local.get $wa\n")
246 out_indent(o, ind * 2); out_str(o, "i32x4.extract_lane 0\n")
247 out_indent(o, ind * 2); out_str(o, "i64.extend_i32_u\n")
248 out_indent(o, ind * 2); out_str(o, "local.get $wa\n")
249 out_indent(o, ind * 2); out_str(o, "i32x4.extract_lane 1\n")
250 out_indent(o, ind * 2); out_str(o, "i64.extend_i32_u\n")
251 out_indent(o, ind * 2); out_str(o, "i64.add\n")
252 out_indent(o, ind * 2); out_str(o, "local.get $wa\n")
253 out_indent(o, ind * 2); out_str(o, "i32x4.extract_lane 2\n")
254 out_indent(o, ind * 2); out_str(o, "i64.extend_i32_u\n")
255 out_indent(o, ind * 2); out_str(o, "i64.add\n")
256 out_indent(o, ind * 2); out_str(o, "local.get $wa\n")
257 out_indent(o, ind * 2); out_str(o, "i32x4.extract_lane 3\n")
258 out_indent(o, ind * 2); out_str(o, "i64.extend_i32_u\n")
259 out_indent(o, ind * 2); out_str(o, "i64.add\n")
260 wat_store_result(o, ind, i.result)
261 return 0
262}
263
264func wat_emit_call(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
265 if wat_call_is_sad16(i) == 1 { return wat_emit_v128sad(f, o, ind, i) }
266 let n: i64 = i.n_operands
267 if n > 0 { wat_push_operand(f, o, ind, i.op0) }
268 if n > 1 { wat_push_operand(f, o, ind, i.op1) }
269 if n > 2 { wat_push_operand(f, o, ind, i.op2) }
270 if n > 3 { wat_push_operand(f, o, ind, i.op3) }
271 if n > 4 { wat_push_operand(f, o, ind, i.op4) }
272 if n > 5 { wat_push_operand(f, o, ind, i.op5) }
273 if n > 6 { wat_push_operand(f, o, ind, i.op6) }
274 if n > 7 { wat_push_operand(f, o, ind, i.op7) }
275 if n > 8 { wat_push_operand(f, o, ind, i.op8) }
276 if n > 9 { wat_push_operand(f, o, ind, i.op9) }
277 if n > 10 { wat_push_operand(f, o, ind, i.op10) }
278 if n > 11 { wat_push_operand(f, o, ind, i.op11) }
279 if n > 12 { wat_push_operand(f, o, ind, i.op12) }
280 if n > 13 { wat_push_operand(f, o, ind, i.op13) }
281 if n > 14 { wat_push_operand(f, o, ind, i.op14) }
282 if n > 15 { wat_push_operand(f, o, ind, i.op15) }
283 out_indent(o, ind * 2)
284 out_str(o, "call $")
285 if i.callee != (0 as *Function) {
286 let name_addr: i64 = i.callee.name_start
287 let name: *u8 = name_addr as *u8
288 if name != (0 as *u8) {
289 out_str(o, name)
290 } else {
291 out_str(o, "fn_unknown")
292 }
293 } else {
294 out_str(o, "fn_unknown")
295 }
296 out_char(o, 0x0A)
297 wat_store_result(o, ind, i.result)
298 return 0
299}
300
301func wat_emit_return(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
302 if i.n_operands > 0 {
303 wat_push_operand(f, o, ind, i.op0)
304 out_indent(o, ind * 2)
305 out_str(o, "local.set $ret\n")
306 }
307 out_indent(o, ind * 2)
308 out_str(o, "br $exit\n")
309 return 0
310}
311
312func wat_emit_branch(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
313 if i.op == OP_BR {
314 out_indent(o, ind * 2)
315 out_str(o, "i32.const ")
316 out_i64(o, i.op0)
317 out_char(o, 0x0A)
318 out_indent(o, ind * 2)
319 out_str(o, "local.set $target\n")
320 out_indent(o, ind * 2)
321 out_str(o, "br $dispatch\n")
322 return 0
323 }
324 // br_cond cond ? op1 : op2
325 wat_push_operand(f, o, ind, i.op0)
326 out_indent(o, ind * 2)
327 out_str(o, "i64.const 0\n")
328 out_indent(o, ind * 2)
329 out_str(o, "i64.ne\n")
330 out_indent(o, ind * 2)
331 out_str(o, "if\n")
332 out_indent(o, (ind + 1) * 2)
333 out_str(o, "i32.const ")
334 out_i64(o, i.op1)
335 out_char(o, 0x0A)
336 out_indent(o, (ind + 1) * 2)
337 out_str(o, "local.set $target\n")
338 out_indent(o, ind * 2)
339 out_str(o, "else\n")
340 out_indent(o, (ind + 1) * 2)
341 out_str(o, "i32.const ")
342 out_i64(o, i.op2)
343 out_char(o, 0x0A)
344 out_indent(o, (ind + 1) * 2)
345 out_str(o, "local.set $target\n")
346 out_indent(o, ind * 2)
347 out_str(o, "end\n")
348 out_indent(o, ind * 2)
349 out_str(o, "br $dispatch\n")
350 return 0
351}
352
353// hardware f32 on the wasm lane (R2 -- browser lane). i64 CARRIER (low 32 = IEEE binary32 bits, same layout
354// as the x86 __f32_* lowering) <-> native wasm f32 via reinterpret. Same NishiLang f32 targets native + browser.
355func wat_emit_f32(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
356 let op: i64 = i.op
357 if op == OP_FCAST_I_TO_F {
358 wat_push_operand(f, o, ind, i.op0)
359 out_indent(o, ind * 2); out_str(o, "f32.convert_i64_s\n")
360 out_indent(o, ind * 2); out_str(o, "i32.reinterpret_f32\n")
361 out_indent(o, ind * 2); out_str(o, "i64.extend_i32_u\n")
362 wat_store_result(o, ind, i.result)
363 return 0
364 }
365 if op == OP_FCAST_F_TO_I {
366 wat_push_operand(f, o, ind, i.op0)
367 wat_addr_narrow(o, ind)
368 out_indent(o, ind * 2); out_str(o, "f32.reinterpret_i32\n")
369 out_indent(o, ind * 2); out_str(o, "i64.trunc_f32_s\n")
370 wat_store_result(o, ind, i.result)
371 return 0
372 }
373 wat_push_operand(f, o, ind, i.op0)
374 wat_addr_narrow(o, ind)
375 out_indent(o, ind * 2); out_str(o, "f32.reinterpret_i32\n")
376 wat_push_operand(f, o, ind, i.op1)
377 wat_addr_narrow(o, ind)
378 out_indent(o, ind * 2); out_str(o, "f32.reinterpret_i32\n")
379 out_indent(o, ind * 2)
380 if op == OP_FADD { out_str(o, "f32.add\n") }
381 if op == OP_FSUB { out_str(o, "f32.sub\n") }
382 if op == OP_FMUL { out_str(o, "f32.mul\n") }
383 if op == OP_FDIV { out_str(o, "f32.div\n") }
384 out_indent(o, ind * 2); out_str(o, "i32.reinterpret_f32\n")
385 out_indent(o, ind * 2); out_str(o, "i64.extend_i32_u\n")
386 wat_store_result(o, ind, i.result)
387 return 0
388}
389
390// ---- memory model (alloca / load / store / gep) --------------------
391// A SCALAR alloca's own $vN local IS its storage: wasm locals are per-INVOCATION, so nested calls can't
392// collide -- no shadow stack needed for scalars. load/store whose pointer is an alloca-result become
393// local.get/set; on a COMPUTED address (fixed-offset buffer + gep) they become real i64.load/i64.store in
394// linear memory (8-byte; the renderer's fb + matrices are all i64). gep = base + offset (front-end pre-
395// scales the index). LIMIT: scalar/address-not-taken allocas only; array/address-taken allocas need a real
396// shadow stack (future) -- the wasm renderer uses fixed linear-memory offsets for its buffers to avoid that.
397func wat_is_alloca(f: *Function, vid: i64) -> i64 {
398 let val: *Value = val_at(f, vid)
399 if val.kind != VK_INSTR { return 0 }
400 let ins: *Instr = val.instr
401 if ins == (0 as *Instr) { return 0 }
402 if ins.op == OP_ALLOCA { return 1 }
403 return 0
404}
405func wat_emit_load(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
406 if wat_is_alloca(f, i.op0) == 1 {
407 out_indent(o, ind * 2); out_str(o, "local.get $v"); out_i64(o, i.op0); out_char(o, 0x0A)
408 wat_store_result(o, ind, i.result)
409 return 0
410 }
411 wat_push_operand(f, o, ind, i.op0)
412 wat_addr_narrow(o, ind)
413 // width from the loaded element type (i.ty) -- a *u8 must NOT read 8 bytes (close T#wasm-001-byte-mem).
414 // 2026-07-10 sext debt fix (WASM lane): subword loads SIGN-extend when the pointee was declared
415 // signed (Type.sext=1, minted by alloc_type_s for i8/i16/i32) and ZERO-extend otherwise -- the SAME
416 // semantic the x86 + RV64 backends now share. wasm has native _s/_u twins for every width.
417 var lsz: i64 = 8
418 var lsx: i64 = 0
419 let lty: *Type = i.ty
420 if lty != (0 as *Type) { lsz = lty.size; lsx = lty.sext }
421 out_indent(o, ind * 2)
422 if lsz == 1 { if lsx == 1 { out_str(o, "i64.load8_s\n") } else { out_str(o, "i64.load8_u\n") } }
423 if lsz == 2 { if lsx == 1 { out_str(o, "i64.load16_s\n") } else { out_str(o, "i64.load16_u\n") } }
424 if lsz == 4 { if lsx == 1 { out_str(o, "i64.load32_s\n") } else { out_str(o, "i64.load32_u\n") } }
425 if lsz >= 8 { out_str(o, "i64.load\n") }
426 wat_store_result(o, ind, i.result)
427 return 0
428}
429func wat_emit_store(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
430 if wat_is_alloca(f, i.op0) == 1 {
431 wat_push_operand(f, o, ind, i.op1)
432 out_indent(o, ind * 2); out_str(o, "local.set $v"); out_i64(o, i.op0); out_char(o, 0x0A)
433 return 0
434 }
435 wat_push_operand(f, o, ind, i.op0)
436 wat_addr_narrow(o, ind)
437 wat_push_operand(f, o, ind, i.op1)
438 // width from the stored element type (i.ty) -- a *u8 write must NOT clobber the next 7 bytes (the codec-wasm bug).
439 var ssz: i64 = 8
440 let sty: *Type = i.ty
441 if sty != (0 as *Type) { ssz = sty.size }
442 out_indent(o, ind * 2)
443 if ssz == 1 { out_str(o, "i64.store8\n") }
444 if ssz == 2 { out_str(o, "i64.store16\n") }
445 if ssz == 4 { out_str(o, "i64.store32\n") }
446 if ssz >= 8 { out_str(o, "i64.store\n") }
447 return 0
448}
449func wat_emit_gep(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
450 wat_push_operand(f, o, ind, i.op0)
451 wat_push_operand(f, o, ind, i.op1)
452 out_indent(o, ind * 2); out_str(o, "i64.add\n")
453 wat_store_result(o, ind, i.result)
454 return 0
455}
456// tail call (`return f(...)`): wasm has no tail-call op, so emit a normal call then return its result.
457func wat_emit_tail_call(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
458 if wat_call_is_sad16(i) == 1 {
459 wat_emit_v128sad(f, o, ind, i)
460 out_indent(o, ind * 2); out_str(o, "local.get $v"); out_i64(o, i.result); out_char(o, 0x0A)
461 out_indent(o, ind * 2); out_str(o, "local.set $ret\n")
462 out_indent(o, ind * 2); out_str(o, "br $exit\n")
463 return 0
464 }
465 let n: i64 = i.n_operands
466 if n > 0 { wat_push_operand(f, o, ind, i.op0) }
467 if n > 1 { wat_push_operand(f, o, ind, i.op1) }
468 if n > 2 { wat_push_operand(f, o, ind, i.op2) }
469 if n > 3 { wat_push_operand(f, o, ind, i.op3) }
470 if n > 4 { wat_push_operand(f, o, ind, i.op4) }
471 if n > 5 { wat_push_operand(f, o, ind, i.op5) }
472 if n > 6 { wat_push_operand(f, o, ind, i.op6) }
473 if n > 7 { wat_push_operand(f, o, ind, i.op7) }
474 if n > 8 { wat_push_operand(f, o, ind, i.op8) }
475 if n > 9 { wat_push_operand(f, o, ind, i.op9) }
476 if n > 10 { wat_push_operand(f, o, ind, i.op10) }
477 if n > 11 { wat_push_operand(f, o, ind, i.op11) }
478 if n > 12 { wat_push_operand(f, o, ind, i.op12) }
479 if n > 13 { wat_push_operand(f, o, ind, i.op13) }
480 if n > 14 { wat_push_operand(f, o, ind, i.op14) }
481 if n > 15 { wat_push_operand(f, o, ind, i.op15) }
482 out_indent(o, ind * 2)
483 out_str(o, "call $")
484 if i.callee != (0 as *Function) {
485 let name_addr: i64 = i.callee.name_start
486 let name: *u8 = name_addr as *u8
487 if name != (0 as *u8) { out_str(o, name) } else { out_str(o, "fn_unknown") }
488 } else {
489 out_str(o, "fn_unknown")
490 }
491 out_char(o, 0x0A)
492 out_indent(o, ind * 2); out_str(o, "local.set $ret\n")
493 out_indent(o, ind * 2); out_str(o, "br $exit\n")
494 return 0
495}
496
497// Dispatch table for one IR instruction.
498func wat_emit_instr(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
499 let op: i64 = i.op
500 // Arithmetic / bitwise (1..15 minus 9=NEG which we skip).
501 if op == OP_ADD { wat_emit_binop(f, o, ind, i); return 0 }
502 if op == OP_SUB { wat_emit_binop(f, o, ind, i); return 0 }
503 if op == OP_MUL { wat_emit_binop(f, o, ind, i); return 0 }
504 if op == OP_DIV_S { wat_emit_binop(f, o, ind, i); return 0 }
505 if op == OP_REM_S { wat_emit_binop(f, o, ind, i); return 0 }
506 if op == OP_AND { wat_emit_binop(f, o, ind, i); return 0 }
507 if op == OP_OR { wat_emit_binop(f, o, ind, i); return 0 }
508 if op == OP_XOR { wat_emit_binop(f, o, ind, i); return 0 }
509 if op == OP_SHL { wat_emit_binop(f, o, ind, i); return 0 }
510 if op == OP_SHR_S { wat_emit_binop(f, o, ind, i); return 0 }
511 if op == OP_SHR_U { wat_emit_binop(f, o, ind, i); return 0 }
512 // Hardware f32 (browser lane) -- reinterpret carrier <-> native wasm f32.
513 if op == OP_FADD { wat_emit_f32(f, o, ind, i); return 0 }
514 if op == OP_FSUB { wat_emit_f32(f, o, ind, i); return 0 }
515 if op == OP_FMUL { wat_emit_f32(f, o, ind, i); return 0 }
516 if op == OP_FDIV { wat_emit_f32(f, o, ind, i); return 0 }
517 if op == OP_FCAST_I_TO_F { wat_emit_f32(f, o, ind, i); return 0 }
518 if op == OP_FCAST_F_TO_I { wat_emit_f32(f, o, ind, i); return 0 }
519 // Compare.
520 if op == OP_EQ { wat_emit_cmp(f, o, ind, i); return 0 }
521 if op == OP_NE { wat_emit_cmp(f, o, ind, i); return 0 }
522 if op == OP_LT_S { wat_emit_cmp(f, o, ind, i); return 0 }
523 if op == OP_LE_S { wat_emit_cmp(f, o, ind, i); return 0 }
524 if op == OP_GT_S { wat_emit_cmp(f, o, ind, i); return 0 }
525 if op == OP_GE_S { wat_emit_cmp(f, o, ind, i); return 0 }
526 // Call / return / branches.
527 if op == OP_CALL { wat_emit_call(f, o, ind, i); return 0 }
528 if op == OP_TAIL_CALL { wat_emit_tail_call(f, o, ind, i); return 0 }
529 if op == OP_RETURN { wat_emit_return(f, o, ind, i); return 0 }
530 if op == OP_BR { wat_emit_branch(f, o, ind, i); return 0 }
531 if op == OP_BR_COND { wat_emit_branch(f, o, ind, i); return 0 }
532 // COPY: push op0 then store result.
533 if op == OP_COPY {
534 wat_push_operand(f, o, ind, i.op0)
535 wat_store_result(o, ind, i.result)
536 return 0
537 }
538 // LN33/LN34: the IR atomic family (nx_atom.nx builtins) -> 0xFE atomics, or their single-thread twins under --nothreads.
539 if op == OP_ATOMIC_LOAD_I64 { wat_emit_atomic_op(f, o, ind, i); return 0 }
540 if op == OP_ATOMIC_STORE_I64 { wat_emit_atomic_op(f, o, ind, i); return 0 }
541 if op == OP_ATOMIC_CAS_I64 { wat_emit_atomic_op(f, o, ind, i); return 0 }
542 if op == OP_ATOMIC_FAA_I64 { wat_emit_atomic_op(f, o, ind, i); return 0 }
543 if op == OP_ATOMIC_FENCE { wat_emit_atomic_op(f, o, ind, i); return 0 }
544 // memory model: scalar alloca = its $vN local; load/store/gep on computed addresses = real linear memory.
545 if op == OP_ALLOCA { return 0 }
546 if op == OP_LOAD { wat_emit_load(f, o, ind, i); return 0 }
547 if op == OP_STORE { wat_emit_store(f, o, ind, i); return 0 }
548 if op == OP_GEP { wat_emit_gep(f, o, ind, i); return 0 }
549 // Scalar bit unops (wasm has no i64.not/i64.neg) -- lower to identities. Same silent-no-op hazard the native
550 // backend hit on rotates (SHA-512/Ed25519 break, SITES-LIVE 2026-05-27); NOT was breaking SHA-256 in the wat lane.
551 if op == OP_NOT { // ~a == a XOR -1
552 wat_push_operand(f, o, ind, i.op0)
553 out_indent(o, ind * 2); out_str(o, "i64.const -1\n")
554 out_indent(o, ind * 2); out_str(o, "i64.xor\n")
555 wat_store_result(o, ind, i.result)
556 return 0
557 }
558 if op == OP_NEG { // -a == 0 - a
559 out_indent(o, ind * 2); out_str(o, "i64.const 0\n")
560 wat_push_operand(f, o, ind, i.op0)
561 out_indent(o, ind * 2); out_str(o, "i64.sub\n")
562 wat_store_result(o, ind, i.result)
563 return 0
564 }
565 // Unhandled: NO ARM EXISTS for this opcode in this backend, so there is nothing honest to emit. REFUSE.
566 return wat_die_unlowerable(f, op)
567}
568
569// ---- function emission ---------------------------------------------
570
571func wat_emit_function(f: *Function, o: *OutBuf) -> i64 {
572 // Signature.
573 out_str(o, " (func $")
574 let name_addr: i64 = f.name_start
575 let fn_name: *u8 = name_addr as *u8
576 if fn_name != (0 as *u8) {
577 out_str(o, fn_name)
578 } else {
579 out_str(o, "fn")
580 }
581 // Params: scan values for VAL_PARAM by param_index in order.
582 var p: i64 = 0
583 while p < f.n_params {
584 var v_idx: i64 = 0
585 while v_idx < f.n_values {
586 let val: *Value = val_at(f, v_idx)
587 if val.kind == VK_PARAM {
588 if val.param_index == p {
589 out_str(o, " (param $arg")
590 out_i64(o, p)
591 out_str(o, " i64)")
592 v_idx = f.n_values // break
593 }
594 }
595 v_idx = v_idx + 1
596 }
597 p = p + 1
598 }
599 if f.ret_ty != (0 as *Type) {
600 if f.ret_ty.kind != 0 { // TY_VOID = 0
601 out_str(o, " (result i64)")
602 }
603 }
604 out_char(o, 0x0A)
605
606 // Locals: one i64 per non-constant SSA value.
607 var v: i64 = 0
608 while v < f.n_values {
609 let val2: *Value = val_at(f, v)
610 if val2.kind != VK_CONST_INT {
611 out_str(o, " (local $v")
612 out_i64(o, v)
613 out_str(o, " i64)\n")
614 }
615 v = v + 1
616 }
617 out_str(o, " (local $target i32)\n")
618 out_str(o, " (local $ret i64)\n")
619 // F618: declare the v128 scratch pair IFF this function contains an intercepted v128_sad16 call
620 // (keeps every other function's wasm byte-identical to the stock emitter's output).
621 var wv_has: i64 = 0
622 var wv_b: i64 = 0
623 while wv_b < f.n_blocks {
624 let wv_bb: *BasicBlock = block_at(f, wv_b)
625 var wv_ins: *Instr = wv_bb.head
626 while wv_ins != (0 as *Instr) {
627 if wv_ins.op == OP_CALL { if wat_call_is_sad16(wv_ins) == 1 { wv_has = 1 } }
628 if wv_ins.op == OP_TAIL_CALL { if wat_call_is_sad16(wv_ins) == 1 { wv_has = 1 } }
629 wv_ins = wv_ins.next
630 }
631 wv_b = wv_b + 1
632 }
633 if wv_has == 1 {
634 out_str(o, " (local $wa v128)\n")
635 out_str(o, " (local $wb v128)\n")
636 }
637
638 // Copy incoming params into their $vI mirror.
639 var vp: i64 = 0
640 while vp < f.n_values {
641 let val3: *Value = val_at(f, vp)
642 if val3.kind == VK_PARAM {
643 out_str(o, " local.get $arg")
644 out_i64(o, val3.param_index)
645 out_char(o, 0x0A)
646 out_str(o, " local.set $v")
647 out_i64(o, vp)
648 out_char(o, 0x0A)
649 }
650 vp = vp + 1
651 }
652
653 // Initial dispatch target = 0 (entry block).
654 out_str(o, " i32.const 0\n")
655 out_str(o, " local.set $target\n")
656
657 // Open block-dispatch scaffolding. (block $exit / (loop $dispatch
658 // / (block $bb_default / nested (block $bbN) ... (block $bb0) ...
659 let N: i64 = f.n_blocks
660 out_str(o, " (block $exit\n")
661 out_str(o, " (loop $dispatch\n")
662 out_str(o, " (block $bb_default\n")
663 var bi: i64 = N - 1
664 while bi >= 0 {
665 let ind_lvl: i64 = 4 + (N - 1 - bi)
666 out_indent(o, ind_lvl * 2)
667 out_str(o, "(block $bb")
668 out_i64(o, bi)
669 out_char(o, 0x0A)
670 bi = bi - 1
671 }
672
673 // br_table at deepest indent. local.get $target FIRST (it pushes the dispatch index that
674 // br_table consumes from the stack), THEN br_table. Fixes the operand-order bug the C wasm.c
675 // already fixed -- this NishiLang port emitted them reversed, producing INVALID wat.
676 let deepest: i64 = 4 + N
677 out_indent(o, deepest * 2)
678 out_str(o, "local.get $target\n")
679 out_indent(o, deepest * 2)
680 out_str(o, "br_table")
681 var bt: i64 = 0
682 while bt < N {
683 out_str(o, " $bb")
684 out_i64(o, bt)
685 bt = bt + 1
686 }
687 out_str(o, " $bb_default\n")
688
689 // Close each bbB, emit its body at (4+N-B-1) indent.
690 var b: i64 = 0
691 while b < N {
692 let close_ind: i64 = 4 + N - b - 1
693 out_indent(o, close_ind * 2)
694 out_str(o, ")\n")
695 let bb: *BasicBlock = block_at(f, b)
696 var inst: *Instr = bb.head
697 while inst != (0 as *Instr) {
698 wat_emit_instr(f, o, close_ind, inst)
699 inst = inst.next
700 }
701 b = b + 1
702 }
703
704 // Close bb_default + body (unreachable).
705 out_str(o, " )\n")
706 out_str(o, " unreachable\n")
707 out_str(o, " )\n") // close loop $dispatch
708 out_str(o, " )\n") // close block $exit
709
710 // Return the stashed ret value.
711 if f.ret_ty != (0 as *Type) {
712 if f.ret_ty.kind != 0 {
713 out_str(o, " local.get $ret\n")
714 }
715 }
716 out_str(o, " )\n") // close (func
717
718 // Export under the function's own name.
719 if fn_name != (0 as *u8) {
720 out_str(o, " (export \"")
721 out_str(o, fn_name)
722 out_str(o, "\" (func $")
723 out_str(o, fn_name)
724 out_str(o, "))\n")
725 }
726 return 0
727}
728
729// ---- module emission -----------------------------------------------
730
731// ---- linear-memory sizing: the MODULE owns its page count ---------------
732// Twin of nx_wasm.nx's wat_mem_pages -- a module that needs a specific memory
733// size declares `static nx_wasm_pages_req: i64 = <pages>` and this backend
734// reads the folded payload; WAT_MEM_PAGES_DEFAULT covers modules that do not
735// declare (browsers commit pages lazily, so a large default costs them
736// nothing). Kills the three-copy drift class (2026-08-18: engine at 364,
737// backends at 192, ship lane fail-closed for 9 days). Enforcement is
738// behavioral: the vm-gate's FITS tooth refuses an emitted module whose
739// declared memory cannot hold its framebuffer.
740const WAT_MEM_PAGES_DEFAULT: i64 = 364
741// wasm32 spec maximum: 65,536 64KiB pages (4 GiB, memtype limits).
742const WAT_PAGES_SPEC_MAX: i64 = 65536
743const WAT_GLOBAL_STRIDE: i64 = 80 // Global slot stride -- matches ir_module_new's allocator
744
745// LN13a -- THE WAT LANE MUST REFUSE WHAT IT CANNOT LOWER (2026-09-04).
746// Until today an IR opcode with no arm in this backend was answered with a WAT COMMENT and `return 0`, and
747// crucially WITHOUT storing a result local. The module still assembled, still instantiated and still ran --
748// silently computing zero wherever that instruction's value was read. That is the worst shape a compiler
749// defect can take: invisible at build time, wrong at run time, and indistinguishable from correct code in
750// every artifact anyone would think to inspect. MEASURED the same day: only 41 of the 141 declared opcodes
751// have an arm in this emitter, so the silent surface was ~100 opcodes wide, and OP_CALL_INDIRECT (144) --
752// what every function-pointer call lowers to -- was one of them.
753// SCOPED DELIBERATELY, AND THE SCOPE IS THE WHOLE SAFETY ARGUMENT. This fires ONLY on the no-arm
754// fallthrough. The OP_SYSCALL path above still emits `i64.const 0` and still stores its result, because that
755// no-op is DECLARED behaviour for non-mmap syscalls and shipped modules pass through it harmlessly; turning
756// THAT into a refusal would break working artifacts. The rule is "no arm at all", never "an arm I judge
757// incomplete".
758// COMPOSES wat_die: the message is BUILT here and TERMINATED there, so this file keeps exactly one exit path.
759// CAP is named rather than guessed: the fixed text is ~400 bytes and the only variable part is one identifier.
760const WAT_UNLOWERABLE_MSG_CAP: i64 = 1024
761func wat_die_unlowerable(f: *Function, op: i64) -> i64 {
762 let e: *OutBuf = out_new(WAT_UNLOWERABLE_MSG_CAP)
763 out_str(e, "nx_wasm_scalararm: cannot lower IR opcode ")
764 out_i64(e, op)
765 out_str(e, " in function ")
766 let name_addr: i64 = f.name_start
767 let fn_name: *u8 = name_addr as *u8
768 if fn_name != (0 as *u8) {
769 out_str(e, fn_name)
770 } else {
771 out_str(e, "<anonymous>")
772 }
773 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.
774")
775 e.buf[e.pos] = 0 as u8
776 return wat_die(e.buf)
777}
778
779func wat_die(msg: *u8) -> i64 {
780 var n: i64 = 0
781 while msg[n] != (0 as u8) { n = n + 1 }
782 sys_write(2, msg, n)
783 sys_exit(3)
784 return 0
785}
786
787func wat_mem_pages(m: *Module) -> i64 {
788 let want: *u8 = "nx_wasm_pages_req" as *u8
789 var wl: i64 = 0
790 while want[wl] != (0 as u8) { wl = wl + 1 }
791 var gi: i64 = 0
792 while gi < m.n_globals {
793 let g: *Global = (m.globals as i64 + gi*WAT_GLOBAL_STRIDE) as *Global
794 if g.name_len == wl {
795 var same: i64 = 1
796 var k: i64 = 0
797 while k < wl {
798 if g.name_bytes[k] != want[k] { same = 0; k = wl }
799 k = k + 1
800 }
801 if same == 1 {
802 if g.bytes == (0 as *u8) {
803 wat_die("nx_wasm_scalararm: nx_wasm_pages_req is declared but carries no initialized value; declare it as `static nx_wasm_pages_req: i64 = <pages>`\n" as *u8)
804 }
805 var v: i64 = 0
806 var b: i64 = 0
807 while b < 8 {
808 v = v + ((g.bytes[b] as i64) << (b*8))
809 b = b + 1
810 }
811 if v < 1 {
812 wat_die("nx_wasm_scalararm: nx_wasm_pages_req must be >= 1 page; a zero or negative request declares a memory nothing can live in\n" as *u8)
813 }
814 if v > WAT_PAGES_SPEC_MAX {
815 wat_die("nx_wasm_scalararm: nx_wasm_pages_req exceeds the wasm32 spec maximum of 65536 pages (4 GiB); the module could never instantiate\n" as *u8)
816 }
817 return v
818 }
819 }
820 gi = gi + 1
821 }
822 return WAT_MEM_PAGES_DEFAULT
823}
824// LN33 (2026-09-03): the wat_mem_pages mechanism generalised -- read a declared `static <name>: i64 = <v>`
825// module global; found[0] = 1 when it exists; declared-but-uninitialised is refused by name.
826func wat_global_i64(m: *Module, want: *u8, found: *i64) -> i64 {
827 found[0] = 0
828 var wl: i64 = 0
829 while want[wl] != (0 as u8) { wl = wl + 1 }
830 var gi: i64 = 0
831 while gi < m.n_globals {
832 let g: *Global = (m.globals as i64 + gi*WAT_GLOBAL_STRIDE) as *Global
833 if g.name_len == wl {
834 var same: i64 = 1
835 var k: i64 = 0
836 while k < wl {
837 if g.name_bytes[k] != want[k] { same = 0; k = wl }
838 k = k + 1
839 }
840 if same == 1 {
841 if g.bytes == (0 as *u8) {
842 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)
843 }
844 var v: i64 = 0
845 var b: i64 = 0
846 while b < 8 {
847 v = v + ((g.bytes[b] as i64) << (b*8))
848 b = b + 1
849 }
850 found[0] = 1
851 return v
852 }
853 }
854 gi = gi + 1
855 }
856 return 0
857}
858func wat_mem_shared(m: *Module) -> i64 {
859 if wat_threads_off == 1 { return 0 }
860 let fnd: *i64 = sys_mmap(8) as *i64
861 let v: i64 = wat_global_i64(m, "nx_wasm_shared_req" as *u8, fnd)
862 if fnd[0] == 0 { return 0 }
863 if v == 0 { return 0 }
864 return 1
865}
866func wat_mem_pages_max(m: *Module, pages: i64) -> i64 {
867 let fnd: *i64 = sys_mmap(8) as *i64
868 let v: i64 = wat_global_i64(m, "nx_wasm_pages_max" as *u8, fnd)
869 if fnd[0] == 0 {
870 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)
871 }
872 if v < pages {
873 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)
874 }
875 if v > WAT_PAGES_SPEC_MAX {
876 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)
877 }
878 return v
879}
880
881func wat_emit_module(m: *Module, o: *OutBuf) -> i64 {
882 out_str(o, ";; Auto-generated by nxc2.nx WASM (WAT) backend.\n")
883 out_str(o, ";; Module: ")
884 if m.name != (0 as *u8) { out_str(o, m.name) }
885 out_char(o, 0x0A)
886 out_str(o, "(module\n")
887 // Linear memory: size comes from the module's own declaration when present
888 // (see wat_mem_pages above), else the named default. JS reads it via
889 // exports.memory.buffer; the VM sizes mod.mem from this same declaration.
890 // LN33: a module declaring `static nx_wasm_shared_req: i64 = 1` gets a SHARED, IMPORTED memory (see nx_wasm.nx
891 // for the why); the unshared path is byte-identical to the pre-LN33 emitter.
892 let wm_pages: i64 = wat_mem_pages(m)
893 if wat_mem_shared(m) == 1 {
894 let wm_max: i64 = wat_mem_pages_max(m, wm_pages)
895 out_str(o, " (import ")
896 out_char(o, 34); out_str(o, "env"); out_char(o, 34)
897 out_str(o, " ")
898 out_char(o, 34); out_str(o, "memory"); out_char(o, 34)
899 out_str(o, " (memory ")
900 out_i64(o, wm_pages)
901 out_str(o, " ")
902 out_i64(o, wm_max)
903 out_str(o, " shared))\n")
904 out_str(o, " (export ")
905 out_char(o, 34); out_str(o, "memory"); out_char(o, 34)
906 out_str(o, " (memory 0))\n")
907 } else {
908 out_str(o, " (memory (export ")
909 out_char(o, 34)
910 out_str(o, "memory")
911 out_char(o, 34)
912 out_str(o, ") ")
913 out_i64(o, wm_pages)
914 out_str(o, ")\n")
915 }
916 var i: i64 = 0
917 while i < m.n_functions {
918 let fn_base: i64 = m.functions as i64
919 let f: *Function = (fn_base + i * 176) as *Function
920 wat_emit_function(f, o)
921 i = i + 1
922 }
923 out_str(o, ")\n")
924 return 0
925}
926
927// Library only; self-test lives in wasm_test.nx.