diff --git a/core/rexcode/ir/module.odin b/core/rexcode/ir/module.odin index 224b760f8..5764608b9 100644 --- a/core/rexcode/ir/module.odin +++ b/core/rexcode/ir/module.odin @@ -130,6 +130,7 @@ Function :: struct { blocks: []Block, name: string, signature: Type_Ref, // a FUNCTION type in Module.types + id: Id, // not used by all formats } // A module-level mutable/immutable value. diff --git a/core/rexcode/ir/status.odin b/core/rexcode/ir/status.odin index 169ada9ac..d3e39c3d5 100644 --- a/core/rexcode/ir/status.odin +++ b/core/rexcode/ir/status.odin @@ -37,6 +37,6 @@ Error_Code :: enum u8 { Error :: struct #packed { location: u32, code: Error_Code, - _: [3]u8, + _: [3]u8 `fmt:"-"`, } #assert(size_of(Error) == 8) diff --git a/core/rexcode/ir/wasm/module.odin b/core/rexcode/ir/wasm/module.odin index 388e06aff..e966bbfca 100644 --- a/core/rexcode/ir/wasm/module.odin +++ b/core/rexcode/ir/wasm/module.odin @@ -98,7 +98,6 @@ Module :: struct { using base: ir.Module, // functions, globals, symbols, dataflow (= .STACK), target version: u32, // WASM_VERSION if 0 on encode - name: string, // Module's name // --- container sections (the WASM-specific data the ir core has no slot for) --- func_types: []Func_Type, // the type section; ir.Function.signature indexes here @@ -202,3 +201,28 @@ section_name :: #force_inline proc "contextless" (id: Section_Id) -> string { return "unknown" } + +@(require_results) +count_import_kind :: proc "contextless" (m: Module, k: External_Kind) -> u32 { + n: u32 = 0 + for imp in m.imports { + if imp.kind == k { + n += 1 + } + } + return n +} + + +@(require_results) +module_name :: proc "contextless" (m: Module) -> (string, bool) { + for c in m.customs { + #partial switch v in c.variant { + case Custom_Section_Name: + if v.module_name != "" { + return v.module_name, true + } + } + } + return "", false +} \ No newline at end of file diff --git a/core/rexcode/ir/wasm/parse.odin b/core/rexcode/ir/wasm/parse.odin index 6f7a58d76..2edddd3cf 100644 --- a/core/rexcode/ir/wasm/parse.odin +++ b/core/rexcode/ir/wasm/parse.odin @@ -69,7 +69,7 @@ decode :: proc( return } -@(private="file", require_results) +@(private, require_results) parse_error_code :: proc "contextless" (err: Reader_Error) -> Error_Code { #partial switch e in err { case Parse_Error: @@ -168,12 +168,12 @@ Reader :: struct { off: u32, } -@(private="file", require_results) +@(private, require_results) reader :: proc "contextless" (data: []u8, off: u32) -> Reader { return Reader{data = data, off = off} } -@(private="file", require_results) +@(private, require_results) rd_byte :: proc "contextless" (r: ^Reader) -> (u8, Parse_Error) { if r.off >= u32(len(r.data)) { return 0, .TRUNCATED } b := r.data[r.off] @@ -181,7 +181,7 @@ rd_byte :: proc "contextless" (r: ^Reader) -> (u8, Parse_Error) { return b, .NONE } -@(private="file", require_results) +@(private, require_results) rd_u32le_block :: proc "contextless" (r: ^Reader) -> (u32, Parse_Error) { if r.off + 4 > u32(len(r.data)) { return 0, .TRUNCATED } v := u32(r.data[r.off]) | @@ -192,7 +192,7 @@ rd_u32le_block :: proc "contextless" (r: ^Reader) -> (u32, Parse_Error) { return v, .NONE } -@(private="file", require_results) +@(private, require_results) rd_uleb :: proc "contextless" (r: ^Reader) -> (u64, Parse_Error) { shift: uint = 0 value: u64 = 0 @@ -207,7 +207,7 @@ rd_uleb :: proc "contextless" (r: ^Reader) -> (u64, Parse_Error) { return 0, .BAD_ULEB } -@(private="file", require_results) +@(private, require_results) rd_sleb :: proc "contextless" (r: ^Reader) -> (i64, Parse_Error) { shift: uint = 0 value: i64 = 0 @@ -226,13 +226,13 @@ rd_sleb :: proc "contextless" (r: ^Reader) -> (i64, Parse_Error) { return value, .NONE } -@(private="file", require_results) +@(private, require_results) rd_u32 :: proc "contextless" (r: ^Reader) -> (u32, Parse_Error) { v, err := rd_uleb(r) return u32(v), err } -@(private="file", require_results) +@(private, require_results) rd_name :: proc "contextless" (r: ^Reader) -> (val: string, err: Parse_Error) { n := rd_u32(r) or_return if r.off + n > u32(len(r.data)) { err = .TRUNCATED; return } @@ -241,7 +241,7 @@ rd_name :: proc "contextless" (r: ^Reader) -> (val: string, err: Parse_Error) { return } -@(private="file", require_results) +@(private, require_results) rd_valtype_vec :: proc(r: ^Reader, allocator: runtime.Allocator) -> (out: []Value_Type, err: Reader_Error) { n := rd_u32(r) or_return out = make([]Value_Type, int(n), allocator) or_return @@ -251,7 +251,7 @@ rd_valtype_vec :: proc(r: ^Reader, allocator: runtime.Allocator) -> (out: []Valu return } -@(private="file", require_results) +@(private, require_results) rd_limits :: proc "contextless" (r: ^Reader) -> (min: u64, max: Maybe(u64), err: Parse_Error) { flags := rd_byte(r) or_return min = rd_uleb(r) or_return @@ -265,7 +265,7 @@ rd_limits :: proc "contextless" (r: ^Reader) -> (min: u64, max: Maybe(u64), err: // Structured section parsers // ============================================================================= -@(private="file", require_results) +@(private, require_results) parse_types :: proc(r: ^Reader, allocator: runtime.Allocator) -> (out: []Func_Type, err: Reader_Error) { n := rd_u32(r) or_return out = make([]Func_Type, int(n), allocator) or_return @@ -278,7 +278,7 @@ parse_types :: proc(r: ^Reader, allocator: runtime.Allocator) -> (out: []Func_Ty return } -@(private="file", require_results) +@(private, require_results) parse_imports :: proc(r: ^Reader, allocator: runtime.Allocator) -> (out: []Import, err: Reader_Error) { n := rd_u32(r) or_return out = make([]Import, int(n), allocator) or_return @@ -302,7 +302,7 @@ parse_imports :: proc(r: ^Reader, allocator: runtime.Allocator) -> (out: []Impor return } -@(private="file", require_results) +@(private, require_results) parse_function_section :: proc(r: ^Reader, allocator: runtime.Allocator) -> (out: []u32, err: Reader_Error) { n := rd_u32(r) or_return out = make([]u32, int(n), allocator) or_return @@ -312,7 +312,7 @@ parse_function_section :: proc(r: ^Reader, allocator: runtime.Allocator) -> (out return } -@(private="file", require_results) +@(private, require_results) parse_exports :: proc(r: ^Reader, allocator: runtime.Allocator) -> (out: []Export, err: Reader_Error) { n := rd_u32(r) or_return out = make([]Export, int(n), allocator) or_return @@ -326,20 +326,20 @@ parse_exports :: proc(r: ^Reader, allocator: runtime.Allocator) -> (out: []Expor // One code entry's locals (still compressed as `count x type` groups) and the // file span of its body `expr`. -@(private="file") +@(private) Code_Body :: struct { locals: []Local_Group, body_offset: u32, // file offset of the instruction stream body_size: u32, // instruction-stream length in bytes } -@(private="file") +@(private) Local_Group :: struct { count: u32, type: Value_Type, } -@(private="file", require_results) +@(private, require_results) parse_code :: proc(r: ^Reader, allocator: runtime.Allocator) -> (out: []Code_Body, err: Reader_Error) { n := rd_u32(r) or_return out = make([]Code_Body, int(n), allocator) or_return @@ -364,7 +364,7 @@ parse_code :: proc(r: ^Reader, allocator: runtime.Allocator) -> (out: []Code_Bod } -@(private="file", require_results) +@(private, require_results) parse_custom_sections :: proc(m: ^Module, allocator: runtime.Allocator) -> Reader_Error { custom_count := 0 for &sec in m.sections { @@ -465,7 +465,7 @@ parse_custom_sections :: proc(m: ^Module, allocator: runtime.Allocator) -> Reade // Function index space (imports ++ defined) with eagerly-decoded bodies // ============================================================================= -@(private="file", require_results) +@(private, require_results) build_functions :: proc( m: ^Module, func_typeidx: []u32, @@ -530,6 +530,7 @@ build_functions :: proc( blocks := make([]Block, 1, allocator) or_return blocks[0] = Block{id = ID_NONE, ops = ops} f.blocks = blocks + f.id = Id(fi) } funcs[fi] = f } @@ -550,7 +551,7 @@ build_functions :: proc( // be relative to the body start (so `decode_ops` -- whose pc starts at 0 -- // matches them). Returns nil when there are none (the common, linked-module // case), so no allocation happens. -@(private="file", require_results) +@(private, require_results) relocs_for_body :: proc(all: []Relocation, body_off_in_sec, size: u32, allocator: runtime.Allocator) -> []Relocation { if len(all) == 0 { return nil } lo := body_off_in_sec @@ -571,7 +572,7 @@ relocs_for_body :: proc(all: []Relocation, body_off_in_sec, size: u32, allocator // Relocations (object-file `reloc.*` custom sections) // ============================================================================= -@(private="file", require_results) +@(private, require_results) parse_relocations :: proc(m: Module, allocator: runtime.Allocator) -> (groups_out: []Reloc_Group, err: Reader_Error) { groups: [dynamic]Reloc_Group groups.allocator = allocator @@ -611,7 +612,7 @@ parse_relocations :: proc(m: Module, allocator: runtime.Allocator) -> (groups_ou // Override function names with the "name" custom section's function-name // subsection (id 1) when present -- these are the authoritative debug names. -@(private="file") +@(private) apply_name_section :: proc(m: ^Module) { for sec in m.sections { if sec.id != .CUSTOM || sec.name != "name" { continue } diff --git a/core/rexcode/ir/wasm/print_wat.odin b/core/rexcode/ir/wasm/print_wat.odin new file mode 100644 index 000000000..f7e4d1719 --- /dev/null +++ b/core/rexcode/ir/wasm/print_wat.odin @@ -0,0 +1,547 @@ +package rexcode_wasm + +import "base:runtime" +import "core:fmt" +import "core:io" +import "core:os" +import "core:strings" + +// ============================================================================= +// WebAssembly TEXT (WAT) PRINTER +// ============================================================================= +// +// `print.odin` emits a compact, custom *disassembly* of a parsed `Module`. +// This file emits the standard WebAssembly text format (`.wat`): the nested +// s-expression module that `wat2wasm` consumes and that `wasm2wat` produces. +// +// (module +// (type (;0;) (func (param i32 i32) (result i32))) +// (func $add (type 0) (param i32 i32) (result i32) +// (i32.add +// (local.get 0) +// (local.get 1))) +// (export "add" (func 0))) +// +// Folding +// ------- +// A WASM code section is a *linear* stack-machine stream; WAT bodies are +// *folded* operator trees. Reconstructing the trees needs each instruction's +// stack arity (operands consumed / results produced), which the linear stream +// does not carry, so it is recovered here: +// * fixed for the numeric / memory / variable / parametric / reference / +// bulk-memory operators (`instruction_arity`), +// * computed from the module's own type table for `call` (callee signature), +// `call_indirect` (the referenced type), and `block`/`loop`/`if` results. +// +// The folder is the algorithm `wat2wasm`/`wasm2wat` use: keep a stack of +// partially built operand trees; an operator with N operands claims the top N +// trees as children; an operator that yields no value (a "statement", e.g. a +// store / drop / void call) flushes the stack so nothing is reordered. Where +// folding would be unsound (not enough operands contiguously on top, e.g. +// across a side-effecting statement) it degrades to flat folded exprs such as +// `(i32.add)` whose operands are the preceding sibling exprs -- still valid, +// still correct, just less nested. SIMD / atomic operators are conservatively +// treated as statements (rendered flat) rather than risk an incorrect tree. +// +// Index operands print numerically (`call 3`, `local.get 0`): the decoder +// marks `call` targets `symbolic` (which would print `$3`); that flag is +// cleared per-instruction so references always resolve. Names from the "name" +// custom section are attached to *definitions* as `$name` (or kept as an index +// comment when the text would not be a legal WAT identifier). +// +// Coverage: TYPE, IMPORT (all four kinds), FUNCTION/CODE, TABLE, MEMORY, +// GLOBAL, EXPORT, START, ELEMENT (active table-0 funcref form), DATA. + +WAT_Options :: struct { + indent_unit: string, // one indentation step; default " " +} + +DEFAULT_WAT_OPTIONS :: WAT_Options{ + indent_unit = " ", +} + +print_wat :: proc(m: Module, file: ^os.File, opts := DEFAULT_WAT_OPTIONS) { + sb := strings.builder_make(context.allocator) + defer strings.builder_destroy(&sb) + sbprint_wat(&sb, m, opts) + os.write_string(file, strings.to_string(sb)) +} + +fprint_wat :: proc(w: io.Writer, m: Module, opts := DEFAULT_WAT_OPTIONS) { + sb := strings.builder_make(context.temp_allocator) + sbprint_wat(&sb, m, opts) + io.write_string(w, strings.to_string(sb)) +} + +// Allocates the result with `allocator` (caller owns it). +aprint_wat :: proc(m: Module, opts := DEFAULT_WAT_OPTIONS, allocator := context.allocator) -> string { + sb := strings.builder_make(allocator) + sbprint_wat(&sb, m, opts) + return strings.to_string(sb) +} + +// Result lives in the temp allocator. +tprint_wat :: proc(m: Module, opts := DEFAULT_WAT_OPTIONS) -> string { + sb := strings.builder_make(context.temp_allocator) + sbprint_wat(&sb, m, opts) + return strings.to_string(sb) +} + +sbprint_wat :: proc(sb: ^strings.Builder, m: Module, opts := DEFAULT_WAT_OPTIONS) { + unit := opts.indent_unit + if unit == "" { + unit = " " + } + + strings.write_string(sb, "(module") + defer strings.write_string(sb, ")\n") + + if name, ok := module_name(m); ok { + if wat_ident_ok(name) { + strings.write_string(sb, " $") + strings.write_string(sb, name) + } else { + fmt.sbprintf(sb, " (;%q;)", name) + } + } + strings.write_byte(sb, '\n') + + for t, i in m.func_types { + fmt.sbprintf(sb, "%s(type (;%d;) ", unit, i) + wat_write_functype(sb, t) + strings.write_string(sb, ")\n") + } + + wat_write_imports(sb, m, unit) + + func_relocs := relocations_for_section(m.relocations, .FUNCTION) + + for f in m.functions { + if len(f.blocks) == 0 { + // assume this is the equivalent of an imported block + continue + } + wat_write_function(sb, m, func_relocs, f, unit) + } + + wat_write_tables (sb, m, unit) + wat_write_memories(sb, m, unit) + wat_write_globals (sb, m, unit) + + for e in m.exports { + fmt.sbprintf(sb, "%s(export %q (%s %d))\n", unit, e.name, external_kind_name(e.kind), e.index) + } + + if m.start >= 0 { + fmt.sbprintf(sb, "%s(start %d)\n", unit, m.start) + } + + wat_write_elements(sb, m, unit) + wat_write_data (sb, m, unit) +} + +wat_write_functype :: proc(sb: ^strings.Builder, t: Func_Type) { + strings.write_string(sb, "(func") + defer strings.write_byte(sb, ')') + if len(t.params) > 0 { + strings.write_string(sb, " (param") + defer strings.write_byte(sb, ')') + for p in t.params { + strings.write_byte(sb, ' ') + strings.write_string(sb, valtype_name(p)) + } + } + if len(t.results) > 0 { + strings.write_string(sb, " (result") + defer strings.write_byte(sb, ')') + for rt in t.results { + strings.write_byte(sb, ' ') + strings.write_string(sb, valtype_name(rt)) + } + } +} + +wat_write_imports :: proc(sb: ^strings.Builder, m: Module, unit: string) -> Parse_Error { + sec: Section + { + found := false + for s in m.sections { + if s.id == .IMPORT { + sec = s + found = true + break + } + } + if !found { + return nil + } + } + + r := reader(m.data, sec.offset) + count := rd_u32(&r) or_return + + fi: u32 + ti: u32 + mi: u32 + gi: u32 + + for _ in 0.. 0 { + strings.write_string(sb, " (param") + defer strings.write_byte(sb, ')') + for p in type.params { + strings.write_byte(sb, ' ') + strings.write_string(sb, valtype_name(p)) + } + } + if len(type.results) > 0 { + strings.write_string(sb, " (result") + defer strings.write_byte(sb, ')') + for rt in type.results { + strings.write_byte(sb, ' ') + strings.write_string(sb, valtype_name(rt)) + } + } + locals := m.function_locals[f.id] + if len(locals) > 0 { + strings.write_string(sb, "\n") + strings.write_string(sb, " (local") + defer strings.write_byte(sb, ')') + for g in locals { + strings.write_byte(sb, ' ') + strings.write_string(sb, valtype_name(g)) + } + } + strings.write_byte(sb, '\n') + + opts := DEFAULT_PRINT_OPTIONS + opts.indent = " " + assert(len(f.blocks) <= 1) + for blk in f.blocks { + for &op, i in blk.ops { + if i+1 == len(blk.ops) && Opcode(op.opcode) == .END { + // TODO(bill): Should this be rendered or NOT? + // break + } + write_operation(sb, &op, &opts) + strings.write_string(sb, opts.separator) + } + } + + strings.write_string(sb, unit) +} + +wat_write_tables :: proc(sb: ^strings.Builder, m: Module, unit: string) { + idx := count_import_kind(m, .TABLE) + for sec in m.sections { + if sec.id != .TABLE { + continue + } + r := reader(m.data, sec.offset) + count := rd_u32(&r) or_break + for _ in 0.. Parse_Error { + elem_idx := 0 + for sec in m.sections { + if sec.id != .ELEMENT { + continue + } + + relocs := relocations_for_section(m.relocations, sec.id) + + r := reader(m.data, sec.offset) + count := rd_u32(&r) or_break + for _ in 0.. Parse_Error { + data_idx := 0 + for sec in m.sections { + if sec.id != .DATA { + continue + } + r := reader(m.data, sec.offset) + count := rd_u32(&r) or_break + for _ in 0.. 0 { strings.write_byte(sb, ' ') } + // wat_render(sb, &s, 0, unit) + // } +} + +wat_write_id_or_comment :: proc(sb: ^strings.Builder, name: string, index: Id) { + if name != "" && wat_ident_ok(name) { + strings.write_string(sb, " $") + strings.write_string(sb, name) + } else { + fmt.sbprintf(sb, " (;%d;)", index) + } +} + +@(private) +indent_str :: proc(sb: ^strings.Builder, unit: string, level: int) { + for _ in 0.. bool { + if len(s) == 0 { + return false + } + #no_bounds_check for i in 0..', '?', '@', '\\', '^', '_', '`', '|', + '~': + // okay + case: + return false + } + } + return true +} + +wat_write_quoted_string :: proc(sb: ^strings.Builder, bytes: []u8) { + @(rodata, static) + HEX := "0123456789abcdef" + strings.write_byte(sb, '"') + for b in bytes { + switch b { + case '"': strings.write_string(sb, "\\\"") + case '\\': strings.write_string(sb, "\\\\") + case 0x20..<0x7F: strings.write_byte(sb, b) + case: + strings.write_byte(sb, '\\') + strings.write_byte(sb, HEX[b >> 4]) + strings.write_byte(sb, HEX[b & 0xF]) + } + } + strings.write_byte(sb, '"') +} + + +@(require_results) +valtype_name :: #force_inline proc "contextless" (t: Value_Type) -> string { + switch t { + case .I32: return "i32" + case .I64: return "i64" + case .F32: return "f32" + case .F64: return "f64" + case .V128: return "v128" + case .FUNCREF: return "funcref" + case .EXTERNREF: return "externref" + } + return "?" +} + +@(require_results) +external_kind_name :: #force_inline proc "contextless" (k: External_Kind) -> string { + switch k { + case .FUNC: return "func" + case .TABLE: return "table" + case .MEMORY: return "memory" + case .GLOBAL: return "global" + } + return "?" +} \ No newline at end of file diff --git a/core/rexcode/ir/wasm/reloc.odin b/core/rexcode/ir/wasm/reloc.odin index 31480da0f..b27f5a5ad 100644 --- a/core/rexcode/ir/wasm/reloc.odin +++ b/core/rexcode/ir/wasm/reloc.odin @@ -54,7 +54,9 @@ Reloc_Group :: struct { @(require_results) relocations_for_section :: proc "contextless" (groups: []Reloc_Group, id: Section_Id) -> []Relocation { for g in groups { - if g.target_section == id { return g.relocs } + if g.target_section == id { + return g.relocs + } } return nil } @@ -86,7 +88,8 @@ reloc_type_from_wire :: proc "contextless" (code: u8) -> (Relocation_Type, bool) @(require_results) reloc_has_addend :: proc "contextless" (code: u8) -> bool { switch code { - case 3, 4, 5, 8, 9: return true + case 3, 4, 5, 8, 9: + return true } return false }