// rexcode · Brendan Punsky (dotbmp@github), original author // Ginger Bill (gingerBill@github) package rexcode_wasm import "core:fmt" import "core:strings" import "core:strconv" // ============================================================================= // SECTION: Printer (Module -> WebAssembly text listing) // ============================================================================= // // The third ir verb (docs/ir_design.md §4), same signature as the SPIR-V // printer: `print(m, sb, options)`, disassembling the module into a caller-owned // builder. It walks functions -> blocks -> operations and emits one instruction // per line in the linear (unfolded) WAT form // // * // // e.g. // // i32.const 42 // local.get 0 // i32.add // call 3 // block (result i32) // i32.load offset=8 align=2 // br_table 0 1 2 ; cases 0 1, default 2 // ref.null func // f64.const 3.14 // v128.const i8x16 0x01 0x02 ... // // The folded-stack form is not reconstructed (that needs structure the linear // stream does not carry). Mnemonic spelling comes from the explicit // MNEMONIC_NAMES table (WASM mixes '.' and '_' irregularly, e.g. `local.get` // vs `i32.trunc_f32_s`). Immediates are recovered from the shared `ir.Operand` // model via the operands.odin accessors -- structured payloads (memarg, // blocktype, reftype, lanes, v128) ride in ATTRIBUTE operands tagged in `aux`. // print: disassemble `m` into the caller-owned builder `sb`. The canonical ir // print verb; `sbprint` / `tprint` below are thin convenience sinks over it. print :: proc(m: Module, sb: ^strings.Builder, options: ^Print_Options = nil) { m := m opts := DEFAULT_PRINT_OPTIONS if options != nil { opts = options^ } for fn in m.functions { if fn.name != "" { strings.write_string(sb, "func ") strings.write_string(sb, fn.name) strings.write_string(sb, ":") strings.write_string(sb, opts.separator) } for blk in fn.blocks { for &op in blk.ops { write_operation(sb, &op, &opts, &m) strings.write_string(sb, opts.separator) } } } } // sbprint: disassemble a bare operation stream (a WASM `expr`) -- the reusable // core that `print` runs per block, and the mirror of the encoder's // `encode_ops` / decoder's `decode_ops`. sbprint :: proc(sb: ^strings.Builder, ops: []Operation, options: ^Print_Options = nil) { opts := DEFAULT_PRINT_OPTIONS if options != nil { opts = options^ } for &op in ops { write_operation(sb, &op, &opts, nil) strings.write_string(sb, opts.separator) } } // tprint: temp-allocated string of an operation stream (spot-checks / debugging). @(require_results) tprint :: proc(ops: []Operation, options: ^Print_Options = nil) -> string { sb := strings.builder_make(context.temp_allocator) sbprint(&sb, ops, options) return strings.to_string(sb) } // aprint: caller-allocated string of an operation stream. @(require_results) aprint :: proc(ops: []Operation, options: ^Print_Options = nil, allocator := context.allocator) -> string { sb := strings.builder_make(allocator) sbprint(&sb, ops, options) return strings.to_string(sb) } @(require_results) mnemonic_to_string :: proc(op: Opcode, uppercase := false, allocator := context.temp_allocator) -> string { sb := strings.builder_make(allocator) write_mnemonic(&sb, op, uppercase) return strings.to_string(sb) } // ============================================================================= // Per-operation writer // ============================================================================= write_operation :: proc(sb: ^strings.Builder, op: ^Operation, opts: ^Print_Options, module: Maybe(^Module)) { strings.write_string(sb, opts.indent) opcode := Opcode(op.opcode) write_mnemonic(sb, opcode, opts.uppercase) // A couple of opcodes carry a bespoke operand layout the generic per-operand // walk would mis-order; specialise those, and fall through for the rest. #partial switch opcode { case .BR_TABLE: // operands = [default, case0, case1, ...]; WAT prints the cases first, // then the default depth, matching the binary order. for c in op.operands[1:] { strings.write_byte(sb, ' ') strings.write_u64(sb, u64(operand_index(c))) } if len(op.operands) > 0 { strings.write_byte(sb, ' ') strings.write_u64(sb, u64(operand_index(op.operands[0]))) } case .V128_CONST: strings.write_string(sb, " i8x16") if len(op.operands) >= 2 { bytes := operand_v128(op.operands[0], op.operands[1]) for bb in bytes { strings.write_byte(sb, ' ') print_hex(sb, u64(bb), opts) } } case .I8X16_SHUFFLE: if len(op.operands) >= 2 { bytes := operand_v128(op.operands[0], op.operands[1]) for bb in bytes { strings.write_byte(sb, ' ') strings.write_u64(sb, u64(bb)) } } case .CALL: if m, ok := module.?; ok { assert(len(op.operands) == 1) o := op.operands[0] if o.kind == .REF { if o.imm < i64(len(m.functions)) { name := m.functions[o.imm].name write_name_or_id(sb, name, Id(o.imm)) break } } } fallthrough case: for &o in op.operands { write_operand(sb, &o, opcode, opts) } } } @(private, require_results) ident_ok :: proc(s: string) -> bool { if len(s) == 0 { return false } #no_bounds_check for i in 0..', '?', '@', '\\', '^', '_', '`', '|', '~': // okay case: return false } } return true } write_name_or_id :: proc(sb: ^strings.Builder, name: string, index: Id) { if name != "" && ident_ok(name) { strings.write_string(sb, " $") strings.write_string(sb, name) } else { fmt.sbprintf(sb, " %d", index) } } write_id_or_comment :: proc(sb: ^strings.Builder, name: string, index: Id) { if name != "" && ident_ok(name) { strings.write_string(sb, " $") strings.write_string(sb, name) } else { fmt.sbprintf(sb, " (;%d;)", index) } } // ============================================================================= // Internal writers // ============================================================================= write_mnemonic :: proc(sb: ^strings.Builder, op: Opcode, uppercase: bool) { name := MNEMONIC_NAMES[op] if name == "" { strings.write_string(sb, "") return } if uppercase { for i in 0..= 0 { // a type index (positive s33) strings.write_string(sb, " (type ") strings.write_u64(sb, u64(u32(v))) strings.write_byte(sb, ')') return true } } return false } write_heap_type :: proc(sb: ^strings.Builder, b: u8) { #partial switch Value_Type(b) { case .FUNCREF: strings.write_string(sb, "func") case .EXTERNREF: strings.write_string(sb, "extern") case: strings.write_u64(sb, u64(u32(b))) } } write_float :: proc(sb: ^strings.Builder, o: ^Operand) { buf: [40]u8 if o.aux == 32 { f := transmute(f32)u32(o.imm) s := strconv.write_float(buf[:], f64(f), 'g', -1, 32) strings.write_string(sb, s) } else { f := transmute(f64)u64(o.imm) s := strconv.write_float(buf[:], f, 'g', -1, 64) strings.write_string(sb, s) } } write_label :: proc(sb: ^strings.Builder, label_id: u32) { strings.write_byte(sb, '$') strings.write_u64(sb, u64(label_id)) }