code wiki / (root) / wasm.nx

wasm.nx source

↩ module page · 424 lines · 15189 B

1// wasm.nx -- NishiLang port of wasm.c (WAT backend). 2// 3// Lowers our SSA IR to WebAssembly Text format. The core challenge 4// WAT imposes is structured control flow: there are no arbitrary 5// branches, only block/loop/if scopes with break-to-label. Our IR 6// has free-form CFG edges, so we use the universal block-dispatch 7// pattern (`br_table` driven by a `$target` local) that encodes any 8// CFG at small constant cost. 9// 10// Per-op semantics match wasm.c exactly; every Value gets a $vN 11// local, comparisons extend to i64 after the i32 WASM result, and 12// constants materialize inline as i64.const. 13// 14// Functions are prefixed `wat_` so they don't collide with riscv.nx's 15// asm-side `rv_*` / `emit_*` naming when both libraries are imported 16// into a driver. 17 18import "syscalls.nx" 19import "types.nx" 20import "ir.nx" 21import "outbuf.nx" 22 23// ---- operand emission ---------------------------------------------- 24// 25// Push an operand onto the WASM stack. Constants become literals; 26// everything else reads from its $vN mirror local. 27 28func wat_push_operand(f: *Function, o: *OutBuf, ind: i64, v: i64) -> i64 { 29 let val: *Value = val_at(f, v) 30 out_indent(o, ind * 2) 31 if val.kind == VK_CONST_INT { 32 out_str(o, "i64.const ") 33 out_i64(o, val.const_int) 34 out_char(o, 0x0A) 35 } else { 36 out_str(o, "local.get $v") 37 out_i64(o, v) 38 out_char(o, 0x0A) 39 } 40 return 0 41} 42 43func wat_store_result(o: *OutBuf, ind: i64, v: i64) -> i64 { 44 out_indent(o, ind * 2) 45 out_str(o, "local.set $v") 46 out_i64(o, v) 47 out_char(o, 0x0A) 48 return 0 49} 50 51// ---- opcode -> WAT mnemonic ---------------------------------------- 52 53func wat_binop_mnem(op: i64, o: *OutBuf) -> i64 { 54 if op == OP_ADD { out_str(o, "i64.add"); return 1 } 55 if op == OP_SUB { out_str(o, "i64.sub"); return 1 } 56 if op == OP_MUL { out_str(o, "i64.mul"); return 1 } 57 if op == OP_DIV_S { out_str(o, "i64.div_s"); return 1 } 58 if op == OP_REM_S { out_str(o, "i64.rem_s"); return 1 } 59 if op == OP_AND { out_str(o, "i64.and"); return 1 } 60 if op == OP_OR { out_str(o, "i64.or"); return 1 } 61 if op == OP_XOR { out_str(o, "i64.xor"); return 1 } 62 if op == OP_SHL { out_str(o, "i64.shl"); return 1 } 63 if op == OP_SHR_S { out_str(o, "i64.shr_s"); return 1 } 64 if op == OP_SHR_U { out_str(o, "i64.shr_u"); return 1 } 65 return 0 66} 67 68func wat_cmp_mnem(op: i64, o: *OutBuf) -> i64 { 69 if op == OP_EQ { out_str(o, "i64.eq"); return 1 } 70 if op == OP_NE { out_str(o, "i64.ne"); return 1 } 71 if op == OP_LT_S { out_str(o, "i64.lt_s"); return 1 } 72 if op == OP_LE_S { out_str(o, "i64.le_s"); return 1 } 73 if op == OP_GT_S { out_str(o, "i64.gt_s"); return 1 } 74 if op == OP_GE_S { out_str(o, "i64.ge_s"); return 1 } 75 return 0 76} 77 78// ---- instruction emission ------------------------------------------ 79 80func wat_emit_binop(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 { 81 wat_push_operand(f, o, ind, i.op0) 82 wat_push_operand(f, o, ind, i.op1) 83 out_indent(o, ind * 2) 84 wat_binop_mnem(i.op, o) 85 out_char(o, 0x0A) 86 wat_store_result(o, ind, i.result) 87 return 0 88} 89 90func wat_emit_cmp(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 { 91 wat_push_operand(f, o, ind, i.op0) 92 wat_push_operand(f, o, ind, i.op1) 93 out_indent(o, ind * 2) 94 wat_cmp_mnem(i.op, o) 95 out_char(o, 0x0A) 96 // Lift i32 compare result to i64 for uniform storage. 97 out_indent(o, ind * 2) 98 out_str(o, "i64.extend_i32_u\n") 99 wat_store_result(o, ind, i.result) 100 return 0 101} 102 103func wat_emit_call(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 { 104 let n: i64 = i.n_operands 105 if n > 0 { wat_push_operand(f, o, ind, i.op0) } 106 if n > 1 { wat_push_operand(f, o, ind, i.op1) } 107 if n > 2 { wat_push_operand(f, o, ind, i.op2) } 108 if n > 3 { wat_push_operand(f, o, ind, i.op3) } 109 out_indent(o, ind * 2) 110 out_str(o, "call $") 111 if i.callee != (0 as *Function) { 112 let name_addr: i64 = i.callee.name_start 113 let name: *u8 = name_addr as *u8 114 if name != (0 as *u8) { 115 out_str(o, name) 116 } else { 117 out_str(o, "fn_unknown") 118 } 119 } else { 120 out_str(o, "fn_unknown") 121 } 122 out_char(o, 0x0A) 123 wat_store_result(o, ind, i.result) 124 return 0 125} 126 127func wat_emit_return(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 { 128 if i.n_operands > 0 { 129 wat_push_operand(f, o, ind, i.op0) 130 out_indent(o, ind * 2) 131 out_str(o, "local.set $ret\n") 132 } 133 out_indent(o, ind * 2) 134 out_str(o, "br $exit\n") 135 return 0 136} 137 138func wat_emit_branch(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 { 139 if i.op == OP_BR { 140 out_indent(o, ind * 2) 141 out_str(o, "i32.const ") 142 out_i64(o, i.op0) 143 out_char(o, 0x0A) 144 out_indent(o, ind * 2) 145 out_str(o, "local.set $target\n") 146 out_indent(o, ind * 2) 147 out_str(o, "br $dispatch\n") 148 return 0 149 } 150 // br_cond cond ? op1 : op2 151 wat_push_operand(f, o, ind, i.op0) 152 out_indent(o, ind * 2) 153 out_str(o, "i64.const 0\n") 154 out_indent(o, ind * 2) 155 out_str(o, "i64.ne\n") 156 out_indent(o, ind * 2) 157 out_str(o, "if\n") 158 out_indent(o, (ind + 1) * 2) 159 out_str(o, "i32.const ") 160 out_i64(o, i.op1) 161 out_char(o, 0x0A) 162 out_indent(o, (ind + 1) * 2) 163 out_str(o, "local.set $target\n") 164 out_indent(o, ind * 2) 165 out_str(o, "else\n") 166 out_indent(o, (ind + 1) * 2) 167 out_str(o, "i32.const ") 168 out_i64(o, i.op2) 169 out_char(o, 0x0A) 170 out_indent(o, (ind + 1) * 2) 171 out_str(o, "local.set $target\n") 172 out_indent(o, ind * 2) 173 out_str(o, "end\n") 174 out_indent(o, ind * 2) 175 out_str(o, "br $dispatch\n") 176 return 0 177} 178 179// hardware f32 on the wasm lane (R2 -- the browser lane). The i64 CARRIER (low 32 = IEEE binary32 bits, 180// same layout as the x86 __f32_* lowering) is reinterpreted to a native wasm f32, computed with f32.*, 181// then reinterpreted back to the i64 carrier. So the SAME NishiLang f32 code targets native (SSE) AND browser. 182func wat_emit_f32(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 { 183 let op: i64 = i.op 184 if op == OP_FCAST_I_TO_F { 185 wat_push_operand(f, o, ind, i.op0) 186 out_indent(o, ind * 2); out_str(o, "f32.convert_i64_s\n") 187 out_indent(o, ind * 2); out_str(o, "i32.reinterpret_f32\n") 188 out_indent(o, ind * 2); out_str(o, "i64.extend_i32_u\n") 189 wat_store_result(o, ind, i.result) 190 return 0 191 } 192 if op == OP_FCAST_F_TO_I { 193 wat_push_operand(f, o, ind, i.op0) 194 out_indent(o, ind * 2); out_str(o, "i32.wrap_i64\n") 195 out_indent(o, ind * 2); out_str(o, "f32.reinterpret_i32\n") 196 out_indent(o, ind * 2); out_str(o, "i64.trunc_f32_s\n") 197 wat_store_result(o, ind, i.result) 198 return 0 199 } 200 wat_push_operand(f, o, ind, i.op0) 201 out_indent(o, ind * 2); out_str(o, "i32.wrap_i64\n") 202 out_indent(o, ind * 2); out_str(o, "f32.reinterpret_i32\n") 203 wat_push_operand(f, o, ind, i.op1) 204 out_indent(o, ind * 2); out_str(o, "i32.wrap_i64\n") 205 out_indent(o, ind * 2); out_str(o, "f32.reinterpret_i32\n") 206 out_indent(o, ind * 2) 207 if op == OP_FADD { out_str(o, "f32.add\n") } 208 if op == OP_FSUB { out_str(o, "f32.sub\n") } 209 if op == OP_FMUL { out_str(o, "f32.mul\n") } 210 if op == OP_FDIV { out_str(o, "f32.div\n") } 211 out_indent(o, ind * 2); out_str(o, "i32.reinterpret_f32\n") 212 out_indent(o, ind * 2); out_str(o, "i64.extend_i32_u\n") 213 wat_store_result(o, ind, i.result) 214 return 0 215} 216 217// Dispatch table for one IR instruction. 218func wat_emit_instr(f: *Function, o: *OutBuf, ind: i64, i: *Instr) -> i64 { 219 let op: i64 = i.op 220 // Arithmetic / bitwise (1..15 minus 9=NEG which we skip). 221 if op == OP_ADD { wat_emit_binop(f, o, ind, i); return 0 } 222 if op == OP_SUB { wat_emit_binop(f, o, ind, i); return 0 } 223 if op == OP_MUL { wat_emit_binop(f, o, ind, i); return 0 } 224 if op == OP_DIV_S { wat_emit_binop(f, o, ind, i); return 0 } 225 if op == OP_REM_S { wat_emit_binop(f, o, ind, i); return 0 } 226 if op == OP_AND { wat_emit_binop(f, o, ind, i); return 0 } 227 if op == OP_OR { wat_emit_binop(f, o, ind, i); return 0 } 228 if op == OP_XOR { wat_emit_binop(f, o, ind, i); return 0 } 229 if op == OP_SHL { wat_emit_binop(f, o, ind, i); return 0 } 230 if op == OP_SHR_S { wat_emit_binop(f, o, ind, i); return 0 } 231 if op == OP_SHR_U { wat_emit_binop(f, o, ind, i); return 0 } 232 // Hardware f32 (browser lane) -- reinterpret carrier <-> native wasm f32. 233 if op == OP_FADD { wat_emit_f32(f, o, ind, i); return 0 } 234 if op == OP_FSUB { wat_emit_f32(f, o, ind, i); return 0 } 235 if op == OP_FMUL { wat_emit_f32(f, o, ind, i); return 0 } 236 if op == OP_FDIV { wat_emit_f32(f, o, ind, i); return 0 } 237 if op == OP_FCAST_I_TO_F { wat_emit_f32(f, o, ind, i); return 0 } 238 if op == OP_FCAST_F_TO_I { wat_emit_f32(f, o, ind, i); return 0 } 239 // Compare. 240 if op == OP_EQ { wat_emit_cmp(f, o, ind, i); return 0 } 241 if op == OP_NE { wat_emit_cmp(f, o, ind, i); return 0 } 242 if op == OP_LT_S { wat_emit_cmp(f, o, ind, i); return 0 } 243 if op == OP_LE_S { wat_emit_cmp(f, o, ind, i); return 0 } 244 if op == OP_GT_S { wat_emit_cmp(f, o, ind, i); return 0 } 245 if op == OP_GE_S { wat_emit_cmp(f, o, ind, i); return 0 } 246 // Call / return / branches. 247 if op == OP_CALL { wat_emit_call(f, o, ind, i); return 0 } 248 if op == OP_RETURN { wat_emit_return(f, o, ind, i); return 0 } 249 if op == OP_BR { wat_emit_branch(f, o, ind, i); return 0 } 250 if op == OP_BR_COND { wat_emit_branch(f, o, ind, i); return 0 } 251 // COPY: push op0 then store result. 252 if op == OP_COPY { 253 wat_push_operand(f, o, ind, i.op0) 254 wat_store_result(o, ind, i.result) 255 return 0 256 } 257 // Unhandled: leave a TODO marker. 258 out_indent(o, ind * 2) 259 out_str(o, ";; TODO opcode ") 260 out_i64(o, op) 261 out_char(o, 0x0A) 262 return 0 263} 264 265// ---- function emission --------------------------------------------- 266 267func wat_emit_function(f: *Function, o: *OutBuf) -> i64 { 268 // Signature. 269 out_str(o, " (func $") 270 let name_addr: i64 = f.name_start 271 let fn_name: *u8 = name_addr as *u8 272 if fn_name != (0 as *u8) { 273 out_str(o, fn_name) 274 } else { 275 out_str(o, "fn") 276 } 277 // Params: scan values for VAL_PARAM by param_index in order. 278 var p: i64 = 0 279 while p < f.n_params { 280 var v_idx: i64 = 0 281 while v_idx < f.n_values { 282 let val: *Value = val_at(f, v_idx) 283 if val.kind == VK_PARAM { 284 if val.param_index == p { 285 out_str(o, " (param $arg") 286 out_i64(o, p) 287 out_str(o, " i64)") 288 v_idx = f.n_values // break 289 } 290 } 291 v_idx = v_idx + 1 292 } 293 p = p + 1 294 } 295 if f.ret_ty != (0 as *Type) { 296 if f.ret_ty.kind != 0 { // TY_VOID = 0 297 out_str(o, " (result i64)") 298 } 299 } 300 out_char(o, 0x0A) 301 302 // Locals: one i64 per non-constant SSA value. 303 var v: i64 = 0 304 while v < f.n_values { 305 let val2: *Value = val_at(f, v) 306 if val2.kind != VK_CONST_INT { 307 out_str(o, " (local $v") 308 out_i64(o, v) 309 out_str(o, " i64)\n") 310 } 311 v = v + 1 312 } 313 out_str(o, " (local $target i32)\n") 314 out_str(o, " (local $ret i64)\n") 315 316 // Copy incoming params into their $vI mirror. 317 var vp: i64 = 0 318 while vp < f.n_values { 319 let val3: *Value = val_at(f, vp) 320 if val3.kind == VK_PARAM { 321 out_str(o, " local.get $arg") 322 out_i64(o, val3.param_index) 323 out_char(o, 0x0A) 324 out_str(o, " local.set $v") 325 out_i64(o, vp) 326 out_char(o, 0x0A) 327 } 328 vp = vp + 1 329 } 330 331 // Initial dispatch target = 0 (entry block). 332 out_str(o, " i32.const 0\n") 333 out_str(o, " local.set $target\n") 334 335 // Open block-dispatch scaffolding. (block $exit / (loop $dispatch 336 // / (block $bb_default / nested (block $bbN) ... (block $bb0) ... 337 let N: i64 = f.n_blocks 338 out_str(o, " (block $exit\n") 339 out_str(o, " (loop $dispatch\n") 340 out_str(o, " (block $bb_default\n") 341 var bi: i64 = N - 1 342 while bi >= 0 { 343 let ind_lvl: i64 = 4 + (N - 1 - bi) 344 out_indent(o, ind_lvl * 2) 345 out_str(o, "(block $bb") 346 out_i64(o, bi) 347 out_char(o, 0x0A) 348 bi = bi - 1 349 } 350 351 // br_table at deepest indent. 352 let deepest: i64 = 4 + N 353 out_indent(o, deepest * 2) 354 out_str(o, "br_table") 355 var bt: i64 = 0 356 while bt < N { 357 out_str(o, " $bb") 358 out_i64(o, bt) 359 bt = bt + 1 360 } 361 out_str(o, " $bb_default\n") 362 out_indent(o, deepest * 2) 363 out_str(o, "local.get $target\n") 364 365 // Close each bbB, emit its body at (4+N-B-1) indent. 366 var b: i64 = 0 367 while b < N { 368 let close_ind: i64 = 4 + N - b - 1 369 out_indent(o, close_ind * 2) 370 out_str(o, ")\n") 371 let bb: *BasicBlock = block_at(f, b) 372 var inst: *Instr = bb.head 373 while inst != (0 as *Instr) { 374 wat_emit_instr(f, o, close_ind, inst) 375 inst = inst.next 376 } 377 b = b + 1 378 } 379 380 // Close bb_default + body (unreachable). 381 out_str(o, " )\n") 382 out_str(o, " unreachable\n") 383 out_str(o, " )\n") // close loop $dispatch 384 out_str(o, " )\n") // close block $exit 385 386 // Return the stashed ret value. 387 if f.ret_ty != (0 as *Type) { 388 if f.ret_ty.kind != 0 { 389 out_str(o, " local.get $ret\n") 390 } 391 } 392 out_str(o, " )\n") // close (func 393 394 // Export under the function's own name. 395 if fn_name != (0 as *u8) { 396 out_str(o, " (export \"") 397 out_str(o, fn_name) 398 out_str(o, "\" (func $") 399 out_str(o, fn_name) 400 out_str(o, "))\n") 401 } 402 return 0 403} 404 405// ---- module emission ----------------------------------------------- 406 407func wat_emit_module(m: *Module, o: *OutBuf) -> i64 { 408 out_str(o, ";; Auto-generated by nxc2.nx WASM (WAT) backend.\n") 409 out_str(o, ";; Module: ") 410 if m.name != (0 as *u8) { out_str(o, m.name) } 411 out_char(o, 0x0A) 412 out_str(o, "(module\n") 413 var i: i64 = 0 414 while i < m.n_functions { 415 let fn_base: i64 = m.functions as i64 416 let f: *Function = (fn_base + i * 176) as *Function 417 wat_emit_function(f, o) 418 i = i + 1 419 } 420 out_str(o, ")\n") 421 return 0 422} 423 424// Library only; self-test lives in wasm_test.nx.