code wiki / (root) / nx_wasm.nx

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.