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.