nx_wasm.nx source
↩ module page · 722 lines · 30376 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// ---- operand emission ----------------------------------------------
39//
40// Push an operand onto the WASM stack. Constants become literals;
41// everything else reads from its $vN mirror local.
42
43func wat_push_operand(f: *Function, o: *OutBuf, ind: i64, v: i64) -> i64 {
44 let val: *Value = val_at(f, v)
45 out_indent(o, ind * 2)
46 if val.kind == VK_CONST_INT {
47 out_str(o, "i64.const ")
48 out_i64(o, val.const_int)
49 out_char(o, 0x0A)
50 } else {
51 out_str(o, "local.get $v")
52 out_i64(o, v)
53 out_char(o, 0x0A)
54 }
55 return 0
56}
57
58func wat_store_result(o: *OutBuf, ind: i64, v: i64) -> i64 {
59 out_indent(o, ind * 2)
60 out_str(o, "local.set $v")
61 out_i64(o, v)
62 out_char(o, 0x0A)
63 return 0
64}
65
66// ---- opcode -> WAT mnemonic ----------------------------------------
67
68func wat_binop_mnem(op: i64, o: *OutBuf) -> i64 {
69 if op == OP_ADD { out_str(o, "i64.add"); return 1 }
70 if op == OP_SUB { out_str(o, "i64.sub"); return 1 }
71 if op == OP_MUL { out_str(o, "i64.mul"); return 1 }
72 if op == OP_DIV_S { out_str(o, "i64.div_s"); return 1 }
73 if op == OP_REM_S { out_str(o, "i64.rem_s"); return 1 }
74 if op == OP_AND { out_str(o, "i64.and"); return 1 }
75 if op == OP_OR { out_str(o, "i64.or"); return 1 }
76 if op == OP_XOR { out_str(o, "i64.xor"); return 1 }
77 if op == OP_SHL { out_str(o, "i64.shl"); return 1 }
78 if op == OP_SHR_S { out_str(o, "i64.shr_s"); return 1 }
79 if op == OP_SHR_U { out_str(o, "i64.shr_u"); return 1 }
80 return 0
81}
82
83func wat_cmp_mnem(op: i64, o: *OutBuf) -> i64 {
84 if op == OP_EQ { out_str(o, "i64.eq"); return 1 }
85 if op == OP_NE { out_str(o, "i64.ne"); return 1 }
86 if op == OP_LT_S { out_str(o, "i64.lt_s"); return 1 }
87 if op == OP_LE_S { out_str(o, "i64.le_s"); return 1 }
88 if op == OP_GT_S { out_str(o, "i64.gt_s"); return 1 }
89 if op == OP_GE_S { out_str(o, "i64.ge_s"); return 1 }
90 return 0
91}
92
93// ---- instruction emission ------------------------------------------
94
95func wat_emit_binop(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
96 wat_push_operand(f, o, ind, i.op0)
97 wat_push_operand(f, o, ind, i.op1)
98 out_indent(o, ind * 2)
99 wat_binop_mnem(i.op, o)
100 out_char(o, 0x0A)
101 wat_store_result(o, ind, i.result)
102 return 0
103}
104
105func wat_emit_cmp(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
106 wat_push_operand(f, o, ind, i.op0)
107 wat_push_operand(f, o, ind, i.op1)
108 out_indent(o, ind * 2)
109 wat_cmp_mnem(i.op, o)
110 out_char(o, 0x0A)
111 // Lift i32 compare result to i64 for uniform storage.
112 out_indent(o, ind * 2)
113 out_str(o, "i64.extend_i32_u\n")
114 wat_store_result(o, ind, i.result)
115 return 0
116}
117
118// ---- F618 v128 SAD intercept --------------------------------------
119// exact NUL-terminated name compare (kw passed as an ARG so the const-index trap never applies)
120func wat_nameq(name: *u8, kw: *u8) -> i64 {
121 var i: i64 = 0
122 while kw[i] != (0 as u8) {
123 if name[i] != kw[i] { return 0 }
124 i = i + 1
125 }
126 if name[i] != (0 as u8) { return 0 }
127 return 1
128}
129func wat_call_is_sad16(i: *Instr) -> i64 {
130 if i.callee == (0 as *Function) { return 0 }
131 let na: i64 = i.callee.name_start
132 if na == 0 { return 0 }
133 let nm: *u8 = na as *u8
134 return wat_nameq(nm, "v128_sad16" as *u8)
135}
136// emit the fused SIMD SAD-of-16-bytes sequence for a v128_sad16(a,b) call site.
137// stack discipline: everything ends as one i64 stored to the call's result local.
138// uses the per-function scratch locals $wa/$wb (v128), declared by wat_emit_function iff needed.
139func wat_emit_v128sad(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
140 wat_push_operand(f, o, ind, i.op0)
141 out_indent(o, ind * 2); out_str(o, "i32.wrap_i64\n")
142 out_indent(o, ind * 2); out_str(o, "v128.load\n")
143 out_indent(o, ind * 2); out_str(o, "local.set $wa\n")
144 wat_push_operand(f, o, ind, i.op1)
145 out_indent(o, ind * 2); out_str(o, "i32.wrap_i64\n")
146 out_indent(o, ind * 2); out_str(o, "v128.load\n")
147 out_indent(o, ind * 2); out_str(o, "local.set $wb\n")
148 out_indent(o, ind * 2); out_str(o, "local.get $wa\n")
149 out_indent(o, ind * 2); out_str(o, "local.get $wb\n")
150 out_indent(o, ind * 2); out_str(o, "i8x16.sub_sat_u\n")
151 out_indent(o, ind * 2); out_str(o, "local.get $wb\n")
152 out_indent(o, ind * 2); out_str(o, "local.get $wa\n")
153 out_indent(o, ind * 2); out_str(o, "i8x16.sub_sat_u\n")
154 out_indent(o, ind * 2); out_str(o, "v128.or\n")
155 out_indent(o, ind * 2); out_str(o, "i16x8.extadd_pairwise_i8x16_u\n")
156 out_indent(o, ind * 2); out_str(o, "i32x4.extadd_pairwise_i16x8_u\n")
157 out_indent(o, ind * 2); out_str(o, "local.set $wa\n")
158 out_indent(o, ind * 2); out_str(o, "local.get $wa\n")
159 out_indent(o, ind * 2); out_str(o, "i32x4.extract_lane 0\n")
160 out_indent(o, ind * 2); out_str(o, "i64.extend_i32_u\n")
161 out_indent(o, ind * 2); out_str(o, "local.get $wa\n")
162 out_indent(o, ind * 2); out_str(o, "i32x4.extract_lane 1\n")
163 out_indent(o, ind * 2); out_str(o, "i64.extend_i32_u\n")
164 out_indent(o, ind * 2); out_str(o, "i64.add\n")
165 out_indent(o, ind * 2); out_str(o, "local.get $wa\n")
166 out_indent(o, ind * 2); out_str(o, "i32x4.extract_lane 2\n")
167 out_indent(o, ind * 2); out_str(o, "i64.extend_i32_u\n")
168 out_indent(o, ind * 2); out_str(o, "i64.add\n")
169 out_indent(o, ind * 2); out_str(o, "local.get $wa\n")
170 out_indent(o, ind * 2); out_str(o, "i32x4.extract_lane 3\n")
171 out_indent(o, ind * 2); out_str(o, "i64.extend_i32_u\n")
172 out_indent(o, ind * 2); out_str(o, "i64.add\n")
173 wat_store_result(o, ind, i.result)
174 return 0
175}
176
177func wat_emit_call(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
178 if wat_call_is_sad16(i) == 1 { return wat_emit_v128sad(f, o, ind, i) }
179 let n: i64 = i.n_operands
180 if n > 0 { wat_push_operand(f, o, ind, i.op0) }
181 if n > 1 { wat_push_operand(f, o, ind, i.op1) }
182 if n > 2 { wat_push_operand(f, o, ind, i.op2) }
183 if n > 3 { wat_push_operand(f, o, ind, i.op3) }
184 if n > 4 { wat_push_operand(f, o, ind, i.op4) }
185 if n > 5 { wat_push_operand(f, o, ind, i.op5) }
186 if n > 6 { wat_push_operand(f, o, ind, i.op6) }
187 if n > 7 { wat_push_operand(f, o, ind, i.op7) }
188 if n > 8 { wat_push_operand(f, o, ind, i.op8) }
189 if n > 9 { wat_push_operand(f, o, ind, i.op9) }
190 if n > 10 { wat_push_operand(f, o, ind, i.op10) }
191 if n > 11 { wat_push_operand(f, o, ind, i.op11) }
192 if n > 12 { wat_push_operand(f, o, ind, i.op12) }
193 if n > 13 { wat_push_operand(f, o, ind, i.op13) }
194 if n > 14 { wat_push_operand(f, o, ind, i.op14) }
195 if n > 15 { wat_push_operand(f, o, ind, i.op15) }
196 out_indent(o, ind * 2)
197 out_str(o, "call $")
198 if i.callee != (0 as *Function) {
199 let name_addr: i64 = i.callee.name_start
200 let name: *u8 = name_addr as *u8
201 if name != (0 as *u8) {
202 out_str(o, name)
203 } else {
204 out_str(o, "fn_unknown")
205 }
206 } else {
207 out_str(o, "fn_unknown")
208 }
209 out_char(o, 0x0A)
210 wat_store_result(o, ind, i.result)
211 return 0
212}
213
214func wat_emit_return(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
215 if i.n_operands > 0 {
216 wat_push_operand(f, o, ind, i.op0)
217 out_indent(o, ind * 2)
218 out_str(o, "local.set $ret\n")
219 }
220 out_indent(o, ind * 2)
221 out_str(o, "br $exit\n")
222 return 0
223}
224
225func wat_emit_branch(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
226 if i.op == OP_BR {
227 out_indent(o, ind * 2)
228 out_str(o, "i32.const ")
229 out_i64(o, i.op0)
230 out_char(o, 0x0A)
231 out_indent(o, ind * 2)
232 out_str(o, "local.set $target\n")
233 out_indent(o, ind * 2)
234 out_str(o, "br $dispatch\n")
235 return 0
236 }
237 // br_cond cond ? op1 : op2
238 wat_push_operand(f, o, ind, i.op0)
239 out_indent(o, ind * 2)
240 out_str(o, "i64.const 0\n")
241 out_indent(o, ind * 2)
242 out_str(o, "i64.ne\n")
243 out_indent(o, ind * 2)
244 out_str(o, "if\n")
245 out_indent(o, (ind + 1) * 2)
246 out_str(o, "i32.const ")
247 out_i64(o, i.op1)
248 out_char(o, 0x0A)
249 out_indent(o, (ind + 1) * 2)
250 out_str(o, "local.set $target\n")
251 out_indent(o, ind * 2)
252 out_str(o, "else\n")
253 out_indent(o, (ind + 1) * 2)
254 out_str(o, "i32.const ")
255 out_i64(o, i.op2)
256 out_char(o, 0x0A)
257 out_indent(o, (ind + 1) * 2)
258 out_str(o, "local.set $target\n")
259 out_indent(o, ind * 2)
260 out_str(o, "end\n")
261 out_indent(o, ind * 2)
262 out_str(o, "br $dispatch\n")
263 return 0
264}
265
266// hardware f32 on the wasm lane (R2 -- browser lane). i64 CARRIER (low 32 = IEEE binary32 bits, same layout
267// as the x86 __f32_* lowering) <-> native wasm f32 via reinterpret. Same NishiLang f32 targets native + browser.
268func wat_emit_f32(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
269 let op: i64 = i.op
270 if op == OP_FCAST_I_TO_F {
271 wat_push_operand(f, o, ind, i.op0)
272 out_indent(o, ind * 2); out_str(o, "f32.convert_i64_s\n")
273 out_indent(o, ind * 2); out_str(o, "i32.reinterpret_f32\n")
274 out_indent(o, ind * 2); out_str(o, "i64.extend_i32_u\n")
275 wat_store_result(o, ind, i.result)
276 return 0
277 }
278 if op == OP_FCAST_F_TO_I {
279 wat_push_operand(f, o, ind, i.op0)
280 out_indent(o, ind * 2); out_str(o, "i32.wrap_i64\n")
281 out_indent(o, ind * 2); out_str(o, "f32.reinterpret_i32\n")
282 out_indent(o, ind * 2); out_str(o, "i64.trunc_f32_s\n")
283 wat_store_result(o, ind, i.result)
284 return 0
285 }
286 wat_push_operand(f, o, ind, i.op0)
287 out_indent(o, ind * 2); out_str(o, "i32.wrap_i64\n")
288 out_indent(o, ind * 2); out_str(o, "f32.reinterpret_i32\n")
289 wat_push_operand(f, o, ind, i.op1)
290 out_indent(o, ind * 2); out_str(o, "i32.wrap_i64\n")
291 out_indent(o, ind * 2); out_str(o, "f32.reinterpret_i32\n")
292 out_indent(o, ind * 2)
293 if op == OP_FADD { out_str(o, "f32.add\n") }
294 if op == OP_FSUB { out_str(o, "f32.sub\n") }
295 if op == OP_FMUL { out_str(o, "f32.mul\n") }
296 if op == OP_FDIV { out_str(o, "f32.div\n") }
297 out_indent(o, ind * 2); out_str(o, "i32.reinterpret_f32\n")
298 out_indent(o, ind * 2); out_str(o, "i64.extend_i32_u\n")
299 wat_store_result(o, ind, i.result)
300 return 0
301}
302
303// ---- memory model (alloca / load / store / gep) --------------------
304// A SCALAR alloca's own $vN local IS its storage: wasm locals are per-INVOCATION, so nested calls can't
305// collide -- no shadow stack needed for scalars. load/store whose pointer is an alloca-result become
306// local.get/set; on a COMPUTED address (fixed-offset buffer + gep) they become real i64.load/i64.store in
307// linear memory (8-byte; the renderer's fb + matrices are all i64). gep = base + offset (front-end pre-
308// scales the index). LIMIT: scalar/address-not-taken allocas only; array/address-taken allocas need a real
309// shadow stack (future) -- the wasm renderer uses fixed linear-memory offsets for its buffers to avoid that.
310func wat_is_alloca(f: *Function, vid: i64) -> i64 {
311 let val: *Value = val_at(f, vid)
312 if val.kind != VK_INSTR { return 0 }
313 let ins: *Instr = val.instr
314 if ins == (0 as *Instr) { return 0 }
315 if ins.op == OP_ALLOCA { return 1 }
316 return 0
317}
318func wat_emit_load(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
319 if wat_is_alloca(f, i.op0) == 1 {
320 out_indent(o, ind * 2); out_str(o, "local.get $v"); out_i64(o, i.op0); out_char(o, 0x0A)
321 wat_store_result(o, ind, i.result)
322 return 0
323 }
324 wat_push_operand(f, o, ind, i.op0)
325 out_indent(o, ind * 2); out_str(o, "i32.wrap_i64\n")
326 // width from the loaded element type (i.ty) -- a *u8 must NOT read 8 bytes (close T#wasm-001-byte-mem).
327 // 2026-07-10 sext debt fix (WASM lane): subword loads SIGN-extend when the pointee was declared
328 // signed (Type.sext=1, minted by alloc_type_s for i8/i16/i32) and ZERO-extend otherwise -- the SAME
329 // semantic the x86 + RV64 backends now share. wasm has native _s/_u twins for every width.
330 var lsz: i64 = 8
331 var lsx: i64 = 0
332 let lty: *Type = i.ty
333 if lty != (0 as *Type) { lsz = lty.size; lsx = lty.sext }
334 out_indent(o, ind * 2)
335 if lsz == 1 { if lsx == 1 { out_str(o, "i64.load8_s\n") } else { out_str(o, "i64.load8_u\n") } }
336 if lsz == 2 { if lsx == 1 { out_str(o, "i64.load16_s\n") } else { out_str(o, "i64.load16_u\n") } }
337 if lsz == 4 { if lsx == 1 { out_str(o, "i64.load32_s\n") } else { out_str(o, "i64.load32_u\n") } }
338 if lsz >= 8 { out_str(o, "i64.load\n") }
339 wat_store_result(o, ind, i.result)
340 return 0
341}
342func wat_emit_store(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
343 if wat_is_alloca(f, i.op0) == 1 {
344 wat_push_operand(f, o, ind, i.op1)
345 out_indent(o, ind * 2); out_str(o, "local.set $v"); out_i64(o, i.op0); out_char(o, 0x0A)
346 return 0
347 }
348 wat_push_operand(f, o, ind, i.op0)
349 out_indent(o, ind * 2); out_str(o, "i32.wrap_i64\n")
350 wat_push_operand(f, o, ind, i.op1)
351 // width from the stored element type (i.ty) -- a *u8 write must NOT clobber the next 7 bytes (the codec-wasm bug).
352 var ssz: i64 = 8
353 let sty: *Type = i.ty
354 if sty != (0 as *Type) { ssz = sty.size }
355 out_indent(o, ind * 2)
356 if ssz == 1 { out_str(o, "i64.store8\n") }
357 if ssz == 2 { out_str(o, "i64.store16\n") }
358 if ssz == 4 { out_str(o, "i64.store32\n") }
359 if ssz >= 8 { out_str(o, "i64.store\n") }
360 return 0
361}
362func wat_emit_gep(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
363 wat_push_operand(f, o, ind, i.op0)
364 wat_push_operand(f, o, ind, i.op1)
365 out_indent(o, ind * 2); out_str(o, "i64.add\n")
366 wat_store_result(o, ind, i.result)
367 return 0
368}
369// tail call (`return f(...)`): wasm has no tail-call op, so emit a normal call then return its result.
370func wat_emit_tail_call(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
371 if wat_call_is_sad16(i) == 1 {
372 wat_emit_v128sad(f, o, ind, i)
373 out_indent(o, ind * 2); out_str(o, "local.get $v"); out_i64(o, i.result); out_char(o, 0x0A)
374 out_indent(o, ind * 2); out_str(o, "local.set $ret\n")
375 out_indent(o, ind * 2); out_str(o, "br $exit\n")
376 return 0
377 }
378 let n: i64 = i.n_operands
379 if n > 0 { wat_push_operand(f, o, ind, i.op0) }
380 if n > 1 { wat_push_operand(f, o, ind, i.op1) }
381 if n > 2 { wat_push_operand(f, o, ind, i.op2) }
382 if n > 3 { wat_push_operand(f, o, ind, i.op3) }
383 if n > 4 { wat_push_operand(f, o, ind, i.op4) }
384 if n > 5 { wat_push_operand(f, o, ind, i.op5) }
385 if n > 6 { wat_push_operand(f, o, ind, i.op6) }
386 if n > 7 { wat_push_operand(f, o, ind, i.op7) }
387 if n > 8 { wat_push_operand(f, o, ind, i.op8) }
388 if n > 9 { wat_push_operand(f, o, ind, i.op9) }
389 if n > 10 { wat_push_operand(f, o, ind, i.op10) }
390 if n > 11 { wat_push_operand(f, o, ind, i.op11) }
391 if n > 12 { wat_push_operand(f, o, ind, i.op12) }
392 if n > 13 { wat_push_operand(f, o, ind, i.op13) }
393 if n > 14 { wat_push_operand(f, o, ind, i.op14) }
394 if n > 15 { wat_push_operand(f, o, ind, i.op15) }
395 out_indent(o, ind * 2)
396 out_str(o, "call $")
397 if i.callee != (0 as *Function) {
398 let name_addr: i64 = i.callee.name_start
399 let name: *u8 = name_addr as *u8
400 if name != (0 as *u8) { out_str(o, name) } else { out_str(o, "fn_unknown") }
401 } else {
402 out_str(o, "fn_unknown")
403 }
404 out_char(o, 0x0A)
405 out_indent(o, ind * 2); out_str(o, "local.set $ret\n")
406 out_indent(o, ind * 2); out_str(o, "br $exit\n")
407 return 0
408}
409
410// Dispatch table for one IR instruction.
411func wat_emit_instr(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 {
412 let op: i64 = i.op
413 // Arithmetic / bitwise (1..15 minus 9=NEG which we skip).
414 if op == OP_ADD { wat_emit_binop(f, o, ind, i); return 0 }
415 if op == OP_SUB { wat_emit_binop(f, o, ind, i); return 0 }
416 if op == OP_MUL { wat_emit_binop(f, o, ind, i); return 0 }
417 if op == OP_DIV_S { wat_emit_binop(f, o, ind, i); return 0 }
418 if op == OP_REM_S { wat_emit_binop(f, o, ind, i); return 0 }
419 if op == OP_AND { wat_emit_binop(f, o, ind, i); return 0 }
420 if op == OP_OR { wat_emit_binop(f, o, ind, i); return 0 }
421 if op == OP_XOR { wat_emit_binop(f, o, ind, i); return 0 }
422 if op == OP_SHL { wat_emit_binop(f, o, ind, i); return 0 }
423 if op == OP_SHR_S { wat_emit_binop(f, o, ind, i); return 0 }
424 if op == OP_SHR_U { wat_emit_binop(f, o, ind, i); return 0 }
425 // Hardware f32 (browser lane) -- reinterpret carrier <-> native wasm f32.
426 if op == OP_FADD { wat_emit_f32(f, o, ind, i); return 0 }
427 if op == OP_FSUB { wat_emit_f32(f, o, ind, i); return 0 }
428 if op == OP_FMUL { wat_emit_f32(f, o, ind, i); return 0 }
429 if op == OP_FDIV { wat_emit_f32(f, o, ind, i); return 0 }
430 if op == OP_FCAST_I_TO_F { wat_emit_f32(f, o, ind, i); return 0 }
431 if op == OP_FCAST_F_TO_I { wat_emit_f32(f, o, ind, i); return 0 }
432 // Compare.
433 if op == OP_EQ { wat_emit_cmp(f, o, ind, i); return 0 }
434 if op == OP_NE { wat_emit_cmp(f, o, ind, i); return 0 }
435 if op == OP_LT_S { wat_emit_cmp(f, o, ind, i); return 0 }
436 if op == OP_LE_S { wat_emit_cmp(f, o, ind, i); return 0 }
437 if op == OP_GT_S { wat_emit_cmp(f, o, ind, i); return 0 }
438 if op == OP_GE_S { wat_emit_cmp(f, o, ind, i); return 0 }
439 // Call / return / branches.
440 if op == OP_CALL { wat_emit_call(f, o, ind, i); return 0 }
441 if op == OP_TAIL_CALL { wat_emit_tail_call(f, o, ind, i); return 0 }
442 if op == OP_RETURN { wat_emit_return(f, o, ind, i); return 0 }
443 if op == OP_BR { wat_emit_branch(f, o, ind, i); return 0 }
444 if op == OP_BR_COND { wat_emit_branch(f, o, ind, i); return 0 }
445 // COPY: push op0 then store result.
446 if op == OP_COPY {
447 wat_push_operand(f, o, ind, i.op0)
448 wat_store_result(o, ind, i.result)
449 return 0
450 }
451 // memory model: scalar alloca = its $vN local; load/store/gep on computed addresses = real linear memory.
452 if op == OP_ALLOCA { return 0 }
453 if op == OP_LOAD { wat_emit_load(f, o, ind, i); return 0 }
454 if op == OP_STORE { wat_emit_store(f, o, ind, i); return 0 }
455 if op == OP_GEP { wat_emit_gep(f, o, ind, i); return 0 }
456 // Scalar bit unops (wasm has no i64.not/i64.neg) -- lower to identities. Same silent-no-op hazard the native
457 // backend hit on rotates (SHA-512/Ed25519 break, SITES-LIVE 2026-05-27); NOT was breaking SHA-256 in the wat lane.
458 if op == OP_NOT { // ~a == a XOR -1
459 wat_push_operand(f, o, ind, i.op0)
460 out_indent(o, ind * 2); out_str(o, "i64.const -1\n")
461 out_indent(o, ind * 2); out_str(o, "i64.xor\n")
462 wat_store_result(o, ind, i.result)
463 return 0
464 }
465 if op == OP_NEG { // -a == 0 - a
466 out_indent(o, ind * 2); out_str(o, "i64.const 0\n")
467 wat_push_operand(f, o, ind, i.op0)
468 out_indent(o, ind * 2); out_str(o, "i64.sub\n")
469 wat_store_result(o, ind, i.result)
470 return 0
471 }
472 // OP_SYSCALL (2026-07-29 root fix): this used to fall through to the TODO comment below = a SILENT
473 // NO-OP whose result local stayed default-0 -- the artifact known as "the wasm sys_mmap 0-stub"
474 // (every allocation aliased address 0; the partition-MV bug + the dormant SATD landmine, seq234).
475 // Now: SYS_MMAP (x86-64 9 / rv64 222; __syscall op0=number, op2=size) lowers to the stateless
476 // $nx_wasm_mmap grow-per-allocation helper emitted in the module preamble. Every OTHER syscall
477 // keeps the historical EXPLICIT result-0 no-op: wasm has no kernel, and turning the no-op into a
478 // trap would change the behavior of shipped modules that harmlessly pass through sys_write paths.
479 if op == OP_SYSCALL {
480 var ismmap: i64 = 0
481 if i.n_operands >= 3 {
482 let nval: *Value = val_at(f, i.op0)
483 if nval.kind == VK_CONST_INT {
484 if nval.const_int == 9 { ismmap = 1 }
485 if nval.const_int == 222 { ismmap = 1 }
486 }
487 }
488 if ismmap == 1 {
489 wat_push_operand(f, o, ind, i.op2)
490 out_indent(o, ind * 2)
491 out_str(o, "call $nx_wasm_mmap\n")
492 } else {
493 out_indent(o, ind * 2)
494 out_str(o, "i64.const 0\n")
495 }
496 wat_store_result(o, ind, i.result)
497 return 0
498 }
499 // Unhandled: leave a TODO marker.
500 out_indent(o, ind * 2)
501 out_str(o, ";; TODO opcode ")
502 out_i64(o, op)
503 out_char(o, 0x0A)
504 return 0
505}
506
507// ---- function emission ---------------------------------------------
508
509func wat_emit_function(f: *Function, o: *OutBuf) -> i64 {
510 // Signature.
511 out_str(o, " (func $")
512 let name_addr: i64 = f.name_start
513 let fn_name: *u8 = name_addr as *u8
514 if fn_name != (0 as *u8) {
515 out_str(o, fn_name)
516 } else {
517 out_str(o, "fn")
518 }
519 // Params: scan values for VAL_PARAM by param_index in order.
520 var p: i64 = 0
521 while p < f.n_params {
522 var v_idx: i64 = 0
523 while v_idx < f.n_values {
524 let val: *Value = val_at(f, v_idx)
525 if val.kind == VK_PARAM {
526 if val.param_index == p {
527 out_str(o, " (param $arg")
528 out_i64(o, p)
529 out_str(o, " i64)")
530 v_idx = f.n_values // break
531 }
532 }
533 v_idx = v_idx + 1
534 }
535 p = p + 1
536 }
537 if f.ret_ty != (0 as *Type) {
538 if f.ret_ty.kind != 0 { // TY_VOID = 0
539 out_str(o, " (result i64)")
540 }
541 }
542 out_char(o, 0x0A)
543
544 // Locals: one i64 per non-constant SSA value.
545 var v: i64 = 0
546 while v < f.n_values {
547 let val2: *Value = val_at(f, v)
548 if val2.kind != VK_CONST_INT {
549 out_str(o, " (local $v")
550 out_i64(o, v)
551 out_str(o, " i64)\n")
552 }
553 v = v + 1
554 }
555 out_str(o, " (local $target i32)\n")
556 out_str(o, " (local $ret i64)\n")
557 // F618: declare the v128 scratch pair IFF this function contains an intercepted v128_sad16 call
558 // (keeps every other function's wasm byte-identical to the stock emitter's output).
559 var wv_has: i64 = 0
560 var wv_b: i64 = 0
561 while wv_b < f.n_blocks {
562 let wv_bb: *BasicBlock = block_at(f, wv_b)
563 var wv_ins: *Instr = wv_bb.head
564 while wv_ins != (0 as *Instr) {
565 if wv_ins.op == OP_CALL { if wat_call_is_sad16(wv_ins) == 1 { wv_has = 1 } }
566 if wv_ins.op == OP_TAIL_CALL { if wat_call_is_sad16(wv_ins) == 1 { wv_has = 1 } }
567 wv_ins = wv_ins.next
568 }
569 wv_b = wv_b + 1
570 }
571 if wv_has == 1 {
572 out_str(o, " (local $wa v128)\n")
573 out_str(o, " (local $wb v128)\n")
574 }
575
576 // Copy incoming params into their $vI mirror.
577 var vp: i64 = 0
578 while vp < f.n_values {
579 let val3: *Value = val_at(f, vp)
580 if val3.kind == VK_PARAM {
581 out_str(o, " local.get $arg")
582 out_i64(o, val3.param_index)
583 out_char(o, 0x0A)
584 out_str(o, " local.set $v")
585 out_i64(o, vp)
586 out_char(o, 0x0A)
587 }
588 vp = vp + 1
589 }
590
591 // Initial dispatch target = 0 (entry block).
592 out_str(o, " i32.const 0\n")
593 out_str(o, " local.set $target\n")
594
595 // Open block-dispatch scaffolding. (block $exit / (loop $dispatch
596 // / (block $bb_default / nested (block $bbN) ... (block $bb0) ...
597 let N: i64 = f.n_blocks
598 out_str(o, " (block $exit\n")
599 out_str(o, " (loop $dispatch\n")
600 out_str(o, " (block $bb_default\n")
601 var bi: i64 = N - 1
602 while bi >= 0 {
603 let ind_lvl: i64 = 4 + (N - 1 - bi)
604 out_indent(o, ind_lvl * 2)
605 out_str(o, "(block $bb")
606 out_i64(o, bi)
607 out_char(o, 0x0A)
608 bi = bi - 1
609 }
610
611 // br_table at deepest indent. local.get $target FIRST (it pushes the dispatch index that
612 // br_table consumes from the stack), THEN br_table. Fixes the operand-order bug the C wasm.c
613 // already fixed -- this NishiLang port emitted them reversed, producing INVALID wat.
614 let deepest: i64 = 4 + N
615 out_indent(o, deepest * 2)
616 out_str(o, "local.get $target\n")
617 out_indent(o, deepest * 2)
618 out_str(o, "br_table")
619 var bt: i64 = 0
620 while bt < N {
621 out_str(o, " $bb")
622 out_i64(o, bt)
623 bt = bt + 1
624 }
625 out_str(o, " $bb_default\n")
626
627 // Close each bbB, emit its body at (4+N-B-1) indent.
628 var b: i64 = 0
629 while b < N {
630 let close_ind: i64 = 4 + N - b - 1
631 out_indent(o, close_ind * 2)
632 out_str(o, ")\n")
633 let bb: *BasicBlock = block_at(f, b)
634 var inst: *Instr = bb.head
635 while inst != (0 as *Instr) {
636 wat_emit_instr(f, o, close_ind, inst)
637 inst = inst.next
638 }
639 b = b + 1
640 }
641
642 // Close bb_default + body (unreachable).
643 out_str(o, " )\n")
644 out_str(o, " unreachable\n")
645 out_str(o, " )\n") // close loop $dispatch
646 out_str(o, " )\n") // close block $exit
647
648 // Return the stashed ret value.
649 if f.ret_ty != (0 as *Type) {
650 if f.ret_ty.kind != 0 {
651 out_str(o, " local.get $ret\n")
652 }
653 }
654 out_str(o, " )\n") // close (func
655
656 // Export under the function's own name.
657 if fn_name != (0 as *u8) {
658 out_str(o, " (export \"")
659 out_str(o, fn_name)
660 out_str(o, "\" (func $")
661 out_str(o, fn_name)
662 out_str(o, "))\n")
663 }
664 return 0
665}
666
667// ---- module emission -----------------------------------------------
668
669func wat_emit_module(m: *Module, o: *OutBuf) -> i64 {
670 out_str(o, ";; Auto-generated by nxc2.nx WASM (WAT) backend.\n")
671 out_str(o, ";; Module: ")
672 if m.name != (0 as *u8) { out_str(o, m.name) }
673 out_char(o, 0x0A)
674 out_str(o, "(module\n")
675 // Declare + export 192 pages (12 MiB) of linear memory so programs can use fixed-offset buffers
676 // (framebuffers, matrices) and JS can read them via exports.memory.buffer. Raised 32->192 (2026-07-09)
677 // for the 960x720 explodelab framebuffers (11.75 MB layout); browsers commit pages lazily, so smaller
678 // surfaces pay nothing. The VM sizes mod.mem from this same declaration.
679 out_str(o, " (memory (export ")
680 out_char(o, 34)
681 out_str(o, "memory")
682 out_char(o, 34)
683 out_str(o, ") 192)\n")
684 // $nx_wasm_mmap (2026-07-29): the REAL sys_mmap for the wasm tier. Historically OP_SYSCALL fell
685 // through to a TODO comment = silent no-op with a default-0 result, so EVERY wasm allocation
686 // aliased address 0 (the seq234 landmine class; the codec grew "slab, NEVER sys_mmap" workarounds).
687 // Design: STATELESS grow-per-allocation -- pages = ceil(n/64K), base = memory.grow(pages)*64K.
688 // The returned base is the OLD memory end = past every fixed carve BY CONSTRUCTION (no global, no
689 // heap pointer, no collision surface). grow failure (-1) returns 0 = the historical failure value.
690 // Branchless select: (g != 0xFFFFFFFF) * g * 65536 (compares yield i32 0/1; extend_u then multiply).
691 out_str(o, " (func $nx_wasm_mmap (param $arg0 i64) (result i64)\n")
692 out_str(o, " (local $g i64)\n")
693 out_str(o, " local.get $arg0\n")
694 out_str(o, " i64.const 65535\n")
695 out_str(o, " i64.add\n")
696 out_str(o, " i64.const 65536\n")
697 out_str(o, " i64.div_s\n")
698 out_str(o, " i32.wrap_i64\n")
699 out_str(o, " memory.grow\n")
700 out_str(o, " i64.extend_i32_u\n")
701 out_str(o, " local.set $g\n")
702 out_str(o, " local.get $g\n")
703 out_str(o, " i64.const 4294967295\n")
704 out_str(o, " i64.ne\n")
705 out_str(o, " i64.extend_i32_u\n")
706 out_str(o, " local.get $g\n")
707 out_str(o, " i64.mul\n")
708 out_str(o, " i64.const 65536\n")
709 out_str(o, " i64.mul\n")
710 out_str(o, " )\n")
711 var i: i64 = 0
712 while i < m.n_functions {
713 let fn_base: i64 = m.functions as i64
714 let f: *Function = (fn_base + i * 176) as *Function
715 wat_emit_function(f, o)
716 i = i + 1
717 }
718 out_str(o, ")\n")
719 return 0
720}
721
722// Library only; self-test lives in wasm_test.nx.