From 23c5c4e668ea56155f34525a7a23826158f28261 Mon Sep 17 00:00:00 2001 From: gingerBill Date: Mon, 13 Jul 2026 15:24:45 +0100 Subject: [PATCH] Add module parsing --- core/rexcode/ir/wasm/decoder.odin | 19 +- core/rexcode/ir/wasm/module.odin | 24 ++ core/rexcode/ir/wasm/parse.odin | 539 ++++++++++++++++++++++++++++++ core/rexcode/ir/wasm/reloc.odin | 59 ++++ 4 files changed, 632 insertions(+), 9 deletions(-) create mode 100644 core/rexcode/ir/wasm/parse.odin diff --git a/core/rexcode/ir/wasm/decoder.odin b/core/rexcode/ir/wasm/decoder.odin index f780f16f3..8343b0111 100644 --- a/core/rexcode/ir/wasm/decoder.odin +++ b/core/rexcode/ir/wasm/decoder.odin @@ -20,9 +20,11 @@ import "base:runtime" // relocations *are* re-attached: when an input relocation lands on a decoded // index field, that operand is marked symbolic and carries the label id. -// decode: parse a WASM `expr` byte stream into a single-function, single-block -// Module (dataflow = .STACK). The reusable stream decoder is `decode_ops`. -decode :: proc( +// decode_expr: parse a bare WASM `expr` byte stream (one instruction stream, +// no container) into a single-function, single-block Module (dataflow = .STACK). +// The full-container module verb is `decode` (parse.odin); the reusable +// stream-level decoder both share is `decode_ops`. +decode_expr :: proc( data: []u8, m: ^Module, errors: ^[dynamic]Error, @@ -109,7 +111,6 @@ decode_one :: proc( form := encoding_form(m) operands := make([dynamic]Operand, allocator) - defer if !ok { delete(operands) } for k, ki in form.imm { switch k { @@ -166,15 +167,15 @@ decode_one :: proc( case .BR_TABLE: count := int(read_uleb(data, &off) or_return) - reserve(&operands, len(operands)+count+1) - operands_default_index := len(operands) - for _ in 0..= u32(len(data)) { diff --git a/core/rexcode/ir/wasm/module.odin b/core/rexcode/ir/wasm/module.odin index acb34593e..b45bf3a94 100644 --- a/core/rexcode/ir/wasm/module.odin +++ b/core/rexcode/ir/wasm/module.odin @@ -55,6 +55,17 @@ External_Kind :: enum u8 { GLOBAL = 3, } +// A parsed binary section header: where its *contents* live in the file and how +// many entries it declares. Kept so sections the ir core does not model +// structurally (table/memory/global/element/data) stay re-readable from `data`. +Section :: struct { + id: Section_Id, + offset: u32, // file offset of the section contents + size: u32, // contents length in bytes + count: u32, // declared element count (0 for CUSTOM / START) + name: string, // custom-section name (borrowed from `data`) +} + // A WASM function signature: value-typed params and results. Func_Type :: struct { params: []Value_Type, @@ -76,10 +87,18 @@ Export :: struct { // The WASM module -- the unit the verbs operate on. Embeds the ir core; adds the // container metadata parsed from / emitted to the binary sections. +// +// After `decode`, `base.functions` spans the whole WASM function index space: +// imported functions occupy the low indices (their `blocks` are empty -- an +// import has no body) followed by the module-defined functions (each a single +// `ir.Block` whose ops are the decoded body). `ir.Function.signature` is the +// function's typeidx (an index into `func_types`, by convention), and its +// `name` is resolved from the export and "name" custom sections. 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 @@ -91,6 +110,11 @@ Module :: struct { // A WASM function's declared locals (the code section's local groups), kept // parallel to base.functions since ir.Function has no locals slot. function_locals: [][]Value_Type, + + // --- preserved binary framing (so nothing decoded is lost / is re-readable) --- + sections: []Section, // every section header, in file order + relocations: []Reloc_Group, // object-file relocations, grouped by target section + data: []u8, // borrowed whole-file bytes (table/memory/... re-readable) } // A freshly-made WASM module declares STACK dataflow so the shared verbs and the diff --git a/core/rexcode/ir/wasm/parse.odin b/core/rexcode/ir/wasm/parse.odin new file mode 100644 index 000000000..bf73f9bc5 --- /dev/null +++ b/core/rexcode/ir/wasm/parse.odin @@ -0,0 +1,539 @@ +// rexcode ยท Brendan Punsky (dotbmp@github), original author +// Ginger Bill (gingerBill@github) + +package rexcode_wasm + +import "base:runtime" +import "core:strings" + +// ============================================================================= +// SECTION: Container decode (a whole .wasm binary module -> ir Module) +// ============================================================================= +// +// `decode_ops` / `decode_expr` (decoder.odin) turn a single WASM `expr` -- one +// instruction stream -- into ir.Operations. That is only the *code* of one +// function; a real `.wasm` file is a container: an 8-byte header (`\0asm`, a +// version) followed by a sequence of length-prefixed sections (type / import / +// function / table / memory / global / export / start / element / code / data, +// plus custom sections). This file is the missing outer layer: `decode` reads +// that whole container and populates the full `wasm.Module`. +// +// How the container maps onto the ir core: +// +// * TYPE -> `func_types`. +// * FUNCTION -> the typeidx per defined function (folded into signatures). +// * CODE -> for *each* defined function, the locals go to +// `function_locals` and the body `expr` is decoded (via the +// shared `decode_ops`) into a single `ir.Block` of Operations +// under an `ir.Function`. This is the plural fix: the old +// `decode` produced one function; a module has many. +// * IMPORT -> `imports`; imported functions also occupy the low function +// indices (with empty bodies) so the funcidx space is intact. +// * EXPORT -> `exports`, and names attached to the referenced functions. +// * START -> `start`. +// * reloc.* -> `relocations`; the CODE group is threaded into body decode +// so relocatable index fields decode as symbolic refs. +// +// TABLE / MEMORY / GLOBAL / ELEMENT / DATA are recorded as `sections` and left +// re-readable from `data` (the shared core has no structural slot for them and +// there is, as yet, no container *emitter* -- the codec's symmetric unit is the +// instruction stream, mirroring the original which only parsed the container). +// +// Structural problems surface as an `ir.Error` (BUFFER_TOO_SHORT for a +// truncation, MALFORMED_MODULE otherwise) and `ok = false`; per-instruction +// body errors are pushed by `decode_ops` as usual. + +// decode: parse a whole `.wasm` binary module into `m`. Returns the number of +// bytes consumed (the file length on success) and whether it fully succeeded. +decode :: proc( + data: []u8, + m: ^Module, + errors: ^[dynamic]Error, + allocator := context.allocator, +) -> (byte_count: u32, ok: bool) { + context.allocator = allocator + errors_start := u32(len(errors)) + + m.base.dataflow = .STACK + m.version = WASM_VERSION + m.start = -1 + m.data = data + + if err := parse_container(data, m, errors, allocator); err != nil { + append(errors, Error{location = 0, code = parse_error_code(err)}) + return 0, false + } + + byte_count = u32(len(data)) + ok = u32(len(errors)) == errors_start + return +} + +@(private="file", require_results) +parse_error_code :: proc "contextless" (err: Reader_Error) -> Error_Code { + #partial switch e in err { + case Parse_Error: + if e == .TRUNCATED { + return .BUFFER_TOO_SHORT + } + } + return .MALFORMED_MODULE // allocation failure / anything else +} + +// ============================================================================= +// Container walk +// ============================================================================= + +parse_container :: proc(data: []u8, m: ^Module, errors: ^[dynamic]Error, allocator: runtime.Allocator) -> Reader_Error { + r := reader(data, 0) + if (rd_u32le_block(&r) or_else 0) != WASM_MAGIC { + return .BAD_MAGIC + } + m.version = rd_u32le_block(&r) or_return + + // Pass 1: collect every section header (id, content offset, size, count). + secs: [dynamic]Section + secs.allocator = allocator + for r.off < u32(len(data)) { + id := Section_Id(rd_byte(&r) or_return) + size := rd_u32(&r) or_return + content := r.off + if content + size > u32(len(data)) { + return .BAD_SECTION + } + sec := Section{id = id, offset = content, size = size} + switch id { + case .CUSTOM: + sub := reader(data, content) + sec.name = rd_name(&sub) or_return + case .START: + // funcidx only, no vector count + case .TYPE, .IMPORT, .FUNCTION, .TABLE, .MEMORY, .GLOBAL, + .EXPORT, .ELEMENT, .CODE, .DATA, .DATA_COUNT: + sub := reader(data, content) + sec.count = rd_u32(&sub) or_return + } + append(&secs, sec) or_return + r.off = content + size + } + m.sections = secs[:] + + // Pass 2: parse the structured sections. + func_typeidx: []u32 + codes: []Code_Body + for &sec in m.sections { + s := reader(data, sec.offset) + #partial switch sec.id { + case .TYPE: m.func_types = parse_types (&s, allocator) or_return + case .IMPORT: m.imports = parse_imports (&s, allocator) or_return + case .FUNCTION: func_typeidx = parse_function_section(&s, allocator) or_return + case .EXPORT: m.exports = parse_exports (&s, allocator) or_return + case .CODE: codes = parse_code (&s, allocator) or_return + case .START: m.start = i64(rd_u32(&s) or_return) + } + } + + // Relocations must be parsed before the bodies: the CODE group is threaded + // into body decode so relocatable index fields become symbolic refs. + m.relocations = parse_relocations(m^, allocator) or_return + + build_functions(m, func_typeidx, codes, errors, allocator) or_return + apply_name_section(m) + return nil +} + +// ============================================================================= +// Byte reader (container-level, distinct from leb.odin's stream primitives: +// it tracks position and reports structural errors -- TRUNCATED / BAD_*) +// ============================================================================= + +Parse_Error :: enum u8 { + NONE = 0, + TRUNCATED, // read past the end of the buffer + BAD_MAGIC, // not a `\0asm` module + BAD_TYPE_FORM, // a functype did not start with 0x60 + BAD_SECTION, // section contents extend past the buffer + BAD_ULEB, // a ULEB did not terminate within 10 bytes +} + +Reader_Error :: union #shared_nil { + Parse_Error, + runtime.Allocator_Error, +} + +Reader :: struct { + data: []u8, + off: u32, +} + +@(private="file", require_results) +reader :: proc "contextless" (data: []u8, off: u32) -> Reader { + return Reader{data = data, off = off} +} + +@(private="file", 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] + r.off += 1 + return b, .NONE +} + +@(private="file", 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]) | + u32(r.data[r.off+1]) << 8 | + u32(r.data[r.off+2]) << 16 | + u32(r.data[r.off+3]) << 24 + r.off += 4 + return v, .NONE +} + +@(private="file", require_results) +rd_uleb :: proc "contextless" (r: ^Reader) -> (u64, Parse_Error) { + shift: uint = 0 + value: u64 = 0 + for _ in 0 ..< 10 { + if r.off >= u32(len(r.data)) { return 0, .TRUNCATED } + b := r.data[r.off] + r.off += 1 + value |= u64(b & 0x7F) << shift + if b & 0x80 == 0 { return value, .NONE } + shift += 7 + } + return 0, .BAD_ULEB +} + +@(private="file", require_results) +rd_sleb :: proc "contextless" (r: ^Reader) -> (i64, Parse_Error) { + shift: uint = 0 + value: i64 = 0 + b: u8 = 0 + for _ in 0 ..< 10 { + if r.off >= u32(len(r.data)) { return 0, .TRUNCATED } + b = r.data[r.off] + r.off += 1 + value |= i64(b & 0x7F) << shift + shift += 7 + if b & 0x80 == 0 { break } + } + if shift < 64 && (b & 0x40) != 0 { + value |= -(i64(1) << shift) + } + return value, .NONE +} + +@(private="file", require_results) +rd_u32 :: proc "contextless" (r: ^Reader) -> (u32, Parse_Error) { + v, err := rd_uleb(r) + return u32(v), err +} + +@(private="file", 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 } + val = string(r.data[r.off:][:n]) + r.off += n + return +} + +@(private="file", 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 + for &v in out { + v = Value_Type(rd_byte(r) or_return) + } + return +} + +@(private="file", 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 + if flags & 0x01 != 0 { + max = rd_uleb(r) or_return + } + return +} + +// ============================================================================= +// Structured section parsers +// ============================================================================= + +@(private="file", 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 + for &ft in out { + form := rd_byte(r) or_return + if form != 0x60 { return out, .BAD_TYPE_FORM } + ft.params = rd_valtype_vec(r, allocator) or_return + ft.results = rd_valtype_vec(r, allocator) or_return + } + return +} + +@(private="file", 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 + for &imp in out { + imp.module_name = rd_name(r) or_return + imp.field_name = rd_name(r) or_return + imp.kind = External_Kind(rd_byte(r) or_return) + switch imp.kind { + case .FUNC: + imp.index = rd_u32(r) or_return + case .TABLE: + _ = rd_byte(r) or_return // reftype + _, _ = rd_limits(r) or_return + case .MEMORY: + _, _ = rd_limits(r) or_return + case .GLOBAL: + _ = rd_byte(r) or_return // valtype + _ = rd_byte(r) or_return // mutability + } + } + return +} + +@(private="file", 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 + for &idx in out { + idx = rd_u32(r) or_return + } + return +} + +@(private="file", 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 + for &e in out { + e.name = rd_name(r) or_return + e.kind = External_Kind(rd_byte(r) or_return) + e.index = rd_u32(r) or_return + } + return +} + +// One code entry's locals (still compressed as `count x type` groups) and the +// file span of its body `expr`. +@(private="file") +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") +Local_Group :: struct { + count: u32, + type: Value_Type, +} + +@(private="file", 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 + for &cb in out { + total := rd_u32(r) or_return + body_end := r.off + total + + nl := rd_u32(r) or_return + locals := make([]Local_Group, int(nl), allocator) or_return + for &g in locals { + g.count = rd_u32(r) or_return + g.type = Value_Type(rd_byte(r) or_return) + } + cb = Code_Body{ + locals = locals, + body_offset = r.off, + body_size = body_end > r.off ? body_end - r.off : 0, + } + r.off = body_end // jump past the expr to the next entry + } + return +} + +// ============================================================================= +// Function index space (imports ++ defined) with eagerly-decoded bodies +// ============================================================================= + +@(private="file", require_results) +build_functions :: proc( + m: ^Module, + func_typeidx: []u32, + codes: []Code_Body, + errors: ^[dynamic]Error, + allocator: runtime.Allocator, +) -> Reader_Error { + num_imports := 0 + for imp in m.imports { + if imp.kind == .FUNC { num_imports += 1 } + } + total := num_imports + len(func_typeidx) + if total == 0 { return nil } + + funcs := make([]Function, total, allocator) or_return + locals := make([][]Value_Type, total, allocator) or_return + + // CODE-section file offset, to rebase its relocations to body-relative. + code_off: u32 = 0 + for s in m.sections { + if s.id == .CODE { code_off = s.offset; break } + } + code_relocs := relocations_for_section(m.relocations, .CODE) + + // Imported functions: low indices, no body. + idx := 0 + for imp in m.imports { + (imp.kind == .FUNC) or_continue + funcs[idx] = Function{ + name = imp.field_name, + signature = Type_Ref(imp.index), // typeidx into func_types + } + idx += 1 + } + + // Defined functions: decode each body into a single ir.Block. + for tidx, i in func_typeidx { + fi := num_imports + i + f := Function{signature = Type_Ref(tidx)} + + if i < len(codes) { + cb := codes[i] + + // Expand the compressed local groups to a flat value-type list. + nlocals := 0 + for g in cb.locals { nlocals += int(g.count) } + flat := make([]Value_Type, nlocals, allocator) or_return + w := 0 + for g in cb.locals { + for _ in 0 ..< int(g.count) { flat[w] = g.type; w += 1 } + } + locals[fi] = flat + + // Decode the body, threading the (rebased) CODE relocations. + body := m.data[cb.body_offset:][:cb.body_size] + body_rel := cb.body_offset - code_off + body_relocs := relocs_for_body(code_relocs, body_rel, cb.body_size, allocator) + + ops, _, _ := decode_ops(body, body_relocs, errors, allocator) + blocks := make([]Block, 1, allocator) or_return + blocks[0] = Block{id = ID_NONE, ops = ops} + f.blocks = blocks + } + funcs[fi] = f + } + + // Names from the export section (kept if the name section overrides later). + for e in m.exports { + if e.kind == .FUNC && int(e.index) < total && funcs[e.index].name == "" { + funcs[e.index].name = e.name + } + } + + m.base.functions = funcs + m.function_locals = locals + return nil +} + +// Filter a section's relocations down to one body and rebase their offsets to +// 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) +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 + hi := body_off_in_sec + size + out: [dynamic]Relocation + out.allocator = allocator + for rr in all { + if rr.offset >= lo && rr.offset < hi { + r2 := rr + r2.offset = rr.offset - lo + append(&out, r2) + } + } + return out[:] +} + +// ============================================================================= +// Relocations (object-file `reloc.*` custom sections) +// ============================================================================= + +@(private="file", require_results) +parse_relocations :: proc(m: Module, allocator: runtime.Allocator) -> (groups_out: []Reloc_Group, err: Reader_Error) { + groups: [dynamic]Reloc_Group + groups.allocator = allocator + for sec in m.sections { + if !(sec.id == .CUSTOM && strings.has_prefix(sec.name, "reloc.")) { continue } + + r := reader(m.data[sec.offset:][:sec.size], 0) + _ = rd_name(&r) or_return // step past the custom-section name + target := Section_Id(rd_u32(&r) or_return) + count := rd_u32(&r) or_return + + out := make([]Relocation, int(count), allocator) or_return + w := 0 + for _ in 0 ..< count { + code := rd_byte(&r) or_return + offset := rd_u32(&r) or_return // field offset within target section + index := rd_u32(&r) or_return // symbol / target index + addend: i32 = 0 + if reloc_has_addend(code) { + addend = i32(rd_sleb(&r) or_return) + } + t := reloc_type_from_wire(code) or_continue + out[w] = Relocation{ + offset = offset, + label_id = index, + addend = addend, + type = t, + size = reloc_field_size(t), + } + w += 1 + } + append(&groups, Reloc_Group{target_section = target, relocs = out[:w]}) or_return + } + groups_out = groups[:] + return +} + +// Override function names with the "name" custom section's function-name +// subsection (id 1) when present -- these are the authoritative debug names. +@(private="file") +apply_name_section :: proc(m: ^Module) { + for sec in m.sections { + if sec.id != .CUSTOM || sec.name != "name" { continue } + + r := reader(m.data, sec.offset) + _ = rd_name(&r) or_break // re-read the section name to reach the subsections + end := sec.offset + sec.size + + for r.off < end { + sub_id := rd_byte(&r) or_break + sub_size := rd_u32(&r) or_break + payload_end := r.off + sub_size + if sub_id == 1 { // function names + count := rd_u32(&r) or_break + for _ in 0 ..< count { + fidx := rd_u32(&r) or_break + name := rd_name(&r) or_break + if int(fidx) < len(m.base.functions) { + m.base.functions[fidx].name = name + } + } + } + r.off = payload_end // skip subsections we do not interpret + } + return + } +} diff --git a/core/rexcode/ir/wasm/reloc.odin b/core/rexcode/ir/wasm/reloc.odin index f3b5d3d46..31480da0f 100644 --- a/core/rexcode/ir/wasm/reloc.odin +++ b/core/rexcode/ir/wasm/reloc.odin @@ -41,3 +41,62 @@ Relocation :: struct #packed { inst_idx: u16, } #assert(size_of(Relocation) == 16) + +// A set of relocations that apply to one section (parsed from a `reloc.` +// custom section). Grouped by the section they target so the decoder can hand +// the CODE-section group to each function body it decodes. +Reloc_Group :: struct { + target_section: Section_Id, + relocs: []Relocation, +} + +// The relocations (if any) that patch fields in the given section. +@(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 } + } + return nil +} + +// ----------------------------------------------------------------------------- +// Object-file (tool-conventions) relocation wire format +// ----------------------------------------------------------------------------- + +// Decode a relocation type byte from a `reloc.*` custom section. `ok` is false +// for a type this codec does not model (the entry is then skipped). +@(require_results) +reloc_type_from_wire :: proc "contextless" (code: u8) -> (Relocation_Type, bool) { + switch code { + case 0: return .FUNCTION_INDEX_LEB, true // R_WASM_FUNCTION_INDEX_LEB + case 1: return .TABLE_INDEX_SLEB, true // R_WASM_TABLE_INDEX_SLEB + case 2: return .TABLE_INDEX_I32, true // R_WASM_TABLE_INDEX_I32 + case 3: return .MEMORY_ADDR_LEB, true // R_WASM_MEMORY_ADDR_LEB + case 4: return .MEMORY_ADDR_SLEB, true // R_WASM_MEMORY_ADDR_SLEB + case 5: return .MEMORY_ADDR_I32, true // R_WASM_MEMORY_ADDR_I32 + case 6: return .TYPE_INDEX_LEB, true // R_WASM_TYPE_INDEX_LEB + case 7: return .GLOBAL_INDEX_LEB, true // R_WASM_GLOBAL_INDEX_LEB + case 20: return .TABLE_NUMBER_LEB, true // R_WASM_TABLE_NUMBER_LEB + } + return .NONE, false +} + +// MEMORY_ADDR_* (3,4,5) and the *_OFFSET_I32 forms (8,9) carry a trailing +// signed-LEB addend; the index-type relocations do not. +@(require_results) +reloc_has_addend :: proc "contextless" (code: u8) -> bool { + switch code { + case 3, 4, 5, 8, 9: return true + } + return false +} + +// On-wire field width of a relocation, in bytes. +@(require_results) +reloc_field_size :: proc "contextless" (t: Relocation_Type) -> u8 { + #partial switch t { + case .TABLE_INDEX_I32, .MEMORY_ADDR_I32: + return 4 // 4-byte LE field + } + return 5 // 5-byte padded (S)LEB field +}