From 863a2090f8eda7dc75e061b26e0bca252f167e24 Mon Sep 17 00:00:00 2001 From: Hitalo Souza Date: Fri, 26 Jun 2026 15:43:09 -0300 Subject: [PATCH] wasm: add table & element sections, call_indirect, and function values (#27532) --- vlib/v/gen/wasm/gen.v | 185 +++++++++++++++++- vlib/v/gen/wasm/mem.v | 29 +++ vlib/v/gen/wasm/ops.v | 3 + vlib/v/gen/wasm/tests/callback_only.vv | 11 ++ vlib/v/gen/wasm/tests/callback_only.vv.out | 1 + vlib/v/gen/wasm/tests/fn_value.vv | 90 +++++++++ vlib/v/gen/wasm/tests/fn_value.vv.out | 14 ++ vlib/v/gen/wasm/tests_decompile/fn_value.vv | 11 ++ .../tests_decompile/fn_value.wasm.must_have | 3 + vlib/wasm/encoding.v | 82 +++++++- vlib/wasm/instructions.v | 53 +++++ vlib/wasm/module.v | 72 ++++++- vlib/wasm/tests/table_test.v | 97 +++++++++ 13 files changed, 641 insertions(+), 10 deletions(-) create mode 100644 vlib/v/gen/wasm/tests/callback_only.vv create mode 100644 vlib/v/gen/wasm/tests/callback_only.vv.out create mode 100644 vlib/v/gen/wasm/tests/fn_value.vv create mode 100644 vlib/v/gen/wasm/tests/fn_value.vv.out create mode 100644 vlib/v/gen/wasm/tests_decompile/fn_value.vv create mode 100644 vlib/v/gen/wasm/tests_decompile/fn_value.wasm.must_have create mode 100644 vlib/wasm/tests/table_test.v diff --git a/vlib/v/gen/wasm/gen.v b/vlib/v/gen/wasm/gen.v index 4374f6215..8dbbfe830 100644 --- a/vlib/v/gen/wasm/gen.v +++ b/vlib/v/gen/wasm/gen.v @@ -46,6 +46,26 @@ mut: needs_address bool defer_vars []Var is_direct_array_access bool // inside a `[direct_array_access]` function + // function values: fn name -> slot in the `__indirect_function_table`. + // Single owner of the indirect-call table population (reused by later phases). + fn_value_indices map[string]int + pending_anon_fns []ast.FnDecl // non-capturing anon fns to compile after toplevel stmts + compiled_anon_fns map[string]bool // dedup for pending_anon_fns + uses_call_indirect bool // a `call_indirect` was emitted, so the table must exist +} + +// fn_table_index returns the slot of `name` in the indirect function table, +// registering it (and growing the table) on first use. +pub fn (mut g Gen) fn_table_index(name string) int { + if idx := g.fn_value_indices[name] { + return idx + } + // Slot 0 is reserved as the null/trap slot, so a real function value never + // collides with `unsafe { nil }` (which also lowers to `i32.const 0`). Real + // functions therefore start at index 1. + idx := g.fn_value_indices.len + 1 + g.fn_value_indices[name] = idx + return idx } struct Global { @@ -745,10 +765,112 @@ fn (mut g Gen) match_branch_exprs(node ast.MatchExpr, expected ast.Type, unpacke g.func.c_end(blk) } +// fn_value_functype lowers a V function type into the wasm function type used +// by `call_indirect`. It mirrors the parameter/return lowering done in `fn_decl` +// (a non-pure return becomes a leading rvar pointer parameter), so the computed +// type index matches the type the target function was committed with. +fn (mut g Gen) fn_value_functype(fn_typ ast.Type) wasm.FuncType { + // final_sym unwraps any alias layer (e.g. `type Callback = fn (int) int`) + ts := g.table.final_sym(fn_typ) + if ts.info !is ast.FnType { + g.w_error('fn_value_functype: `${ts.name}` is not a function type') + } + func := (ts.info as ast.FnType).func + + mut paraml := []wasm.ValType{} + mut retl := []wasm.ValType{} + + rt := func.return_type + if g.table.sym(rt).info is ast.MultiReturn { + g.w_error('wasm backend: calling a function value with multiple return values is not yet supported') + } + if rt.has_flag(.option) || rt.has_flag(.result) { + g.w_error('wasm backend: option/result function values are not yet supported') + } + if rt.idx() != ast.void_type_idx { + wtyp := g.get_wasm_type(rt) + if g.is_param_type(rt) { + paraml << wtyp // non-pure return -> leading rvar pointer (i32) + } else { + retl << wtyp + } + } + for p in func.params { + paraml << g.get_wasm_type_int_literal(p.typ) + } + + return wasm.FuncType{paraml, retl, none} +} + +// push_fn_value evaluates the callee of a function-value call and leaves its +// i32 index into the indirect function table on the stack. +fn (mut g Gen) push_fn_value(node ast.CallExpr, fn_typ ast.Type) { + if node.is_method { + // a fn-typed struct field, e.g. `b.op` in `b.op(x)`: load the field + g.expr(ast.SelectorExpr{ + expr: node.left + field_name: node.name + expr_type: node.left_type + typ: fn_typ + }, fn_typ) + } else if node.name == '' { + // the callee is the expression itself, e.g. `(expr)(x)` or `make_cb()(x)` + g.expr(node.left, fn_typ) + } else if node.is_fn_a_const { + // a const of function type. The const is registered in the global scope + // under its fully-qualified name (`node.const_name`, e.g. `main.cb`), not + // the lexical call name, so a plain `scope.find(node.name)` misses it. + obj := g.table.global_scope.find(node.const_name) or { + g.w_error('wasm: cannot resolve function const `${node.const_name}`') + } + v := g.get_var_from_ident(ast.Ident{ + name: node.const_name + scope: node.scope + obj: obj + }) + g.get(v) + } else { + // a named local/param variable of function type + obj := node.scope.find(node.name) or { + g.w_error('wasm: cannot resolve function value `${node.name}`') + } + v := g.get_var_from_ident(ast.Ident{ + name: node.name + scope: node.scope + obj: obj + }) + g.get(v) + } +} + pub fn (mut g Gen) call_expr(node ast.CallExpr, expected ast.Type, existing_rvars []Var) { mut wasm_ns := ?string(none) mut name := node.name + // Detect a call to a function value: either a fn-typed local/param/const + // (is_fn_var/is_fn_a_const), a fn-typed struct field, which the checker + // represents as a method call (`b.op()`), or an expression callee that + // evaluates to a function (`make_cb()(x)`, `(expr)(x)`), which the checker + // leaves with an empty name and a fn-typed `left_type`. + mut is_fn_value := false + mut fn_value_typ := ast.void_type + if node.is_fn_var || node.is_fn_a_const { + is_fn_value = true + fn_value_typ = node.fn_var_type + } else if node.is_method && node.left_type != 0 { + if field := g.table.find_field(g.table.sym(node.left_type), node.name) { + if g.table.final_sym(field.typ).info is ast.FnType { + is_fn_value = true + fn_value_typ = field.typ + } + } + } else if node.name == '' && node.left_type != 0 { + if g.table.final_sym(node.left_type).info is ast.FnType { + is_fn_value = true + fn_value_typ = node.left_type + } + } + is_print := name in ['panic', 'println', 'print', 'eprintln', 'eprint'] if node.is_method { @@ -792,7 +914,7 @@ pub fn (mut g Gen) call_expr(node ast.CallExpr, expected ast.Type, existing_rvar // {method self} // - if node.is_method { + if node.is_method && !is_fn_value { expr := if !node.left_type.is_ptr() && node.receiver_type.is_ptr() { ast.Expr(ast.PrefixExpr{ op: .amp right: node.left @@ -805,6 +927,22 @@ pub fn (mut g Gen) call_expr(node ast.CallExpr, expected ast.Type, existing_rvar } } + // {callee} + // + // Evaluate the callee of a function-value call *before* its arguments, so a + // callee with side effects (e.g. `make_box().op(next())`) keeps V's + // left-to-right evaluation order. `call_indirect` wants the table index on + // top of the stack (after the args), so stash it in a temp and reload it + // once the arguments are in place. + mut fn_value_idx := Var{} + if is_fn_value { + g.is_leaf_function = false + g.uses_call_indirect = true + fn_value_idx = g.new_local('', fn_value_typ) + g.push_fn_value(node, fn_value_typ) + g.set(fn_value_idx) + } + // {arguments} // for idx, arg in node.args { @@ -837,7 +975,13 @@ pub fn (mut g Gen) call_expr(node ast.CallExpr, expected ast.Type, existing_rvar } } - if namespace := wasm_ns { + if is_fn_value { + // args are already on the stack; reload the callee's table index (computed + // before the args, above) on top, then dispatch through the indirect table + typeidx := g.mod.new_functype(g.fn_value_functype(fn_value_typ)) + g.get(fn_value_idx) + g.func.call_indirect(typeidx, 0) + } else if namespace := wasm_ns { // import calls won't touch `__vsp` ! g.func.call_import(namespace, name) @@ -1113,9 +1257,15 @@ pub fn (mut g Gen) expr(node ast.Expr, expected ast.Type) { g.func.i32_const(i32(rns)) } ast.Ident { - v := g.get_var_from_ident(node) - g.get(v) - g.cast(v.typ, expected) + if node.kind == .function { + // a function used as a value: push its index into the + // indirect function table (an i32) + g.func.i32_const(i32(g.fn_table_index(node.name))) + } else { + v := g.get_var_from_ident(node) + g.get(v) + g.cast(v.typ, expected) + } } ast.IntegerLiteral, ast.FloatLiteral { g.literal(node.val, expected) @@ -1136,6 +1286,15 @@ pub fn (mut g Gen) expr(node ast.Expr, expected ast.Type) { g.expr(node.expr, node.expr_type) + // A cast involving a function value (e.g. `type Alias = fn (...)`) is an + // i32 table index on both sides, so the value passes through unchanged. + // Return before the numeric-cast path, which would also mis-handle a + // function callee (`Alias(some_fn)`) as a variable in get_var_from_ident. + if g.table.final_sym(node.typ).info is ast.FnType + || g.table.final_sym(node.expr_type).info is ast.FnType { + return + } + // TODO: unbelievable colossal hack mut typ := node.expr_type if node.expr is ast.Ident { @@ -1151,6 +1310,22 @@ pub fn (mut g Gen) expr(node ast.Expr, expected ast.Type) { ast.CallExpr { g.call_expr(node, expected, []) } + ast.AnonFn { + if node.inherited_vars.len > 0 { + // closures lower to runtime-generated executable memory, which + // WebAssembly does not support efficiently yet; reject for now. + g.v_error('closures (capturing anonymous functions) are not yet supported on the `wasm` backend', + node.decl.pos) + } + // compile the anon fn after the current toplevel stmts (when the + // per-function state is clean again), then push its table index + idx := g.fn_table_index(node.decl.name) + if node.decl.name !in g.compiled_anon_fns { + g.compiled_anon_fns[node.decl.name] = true + g.pending_anon_fns << node.decl + } + g.func.i32_const(i32(idx)) + } else { g.w_error('wasm.expr(): unhandled node: ' + node.type_name()) } diff --git a/vlib/v/gen/wasm/mem.v b/vlib/v/gen/wasm/mem.v index 6d0830835..5546a58aa 100644 --- a/vlib/v/gen/wasm/mem.v +++ b/vlib/v/gen/wasm/mem.v @@ -314,6 +314,10 @@ pub fn (g &Gen) is_pure_type(typ ast.Type) bool { ast.Enum { return g.is_pure_type(ts.info.typ) } + ast.FnType { + // a function value is an i32 index into the indirect function table + return true + } else {} } @@ -872,6 +876,31 @@ pub fn (mut g Gen) make_vinit() { pub fn (mut g Gen) housekeeping() { g.make_vinit() + // Compile any pending non-capturing anonymous functions (their bodies may + // reference further anon fns, so loop until the queue drains), then declare + // and populate the indirect function table used by `call_indirect`. + for g.pending_anon_fns.len > 0 { + g.fn_decl(g.pending_anon_fns.pop()) + } + // Declare the indirect function table whenever a `call_indirect` was emitted, + // even if no function value was registered (e.g. a module that only consumes a + // callback), otherwise the emitted `call_indirect` references a table that does + // not exist and the module fails to validate. + if g.uses_call_indirect || g.fn_value_indices.len > 0 { + // names[i] holds the function for table slot `i + 1`; slot 0 is the + // reserved null/trap slot (left as `ref.null func`). + mut names := []string{len: g.fn_value_indices.len} + for name, idx in g.fn_value_indices { + names[idx - 1] = name + } + // +1 for the reserved null slot at index 0 + t := + g.mod.assign_table('__indirect_function_table', false, .funcref_t, u32(names.len + 1), none) + if names.len > 0 { + g.mod.new_active_element(t, 1, names) + } + } + heap_base := calc_align(g.data_base + g.pool.buf.len, 16) // 16? page_boundary := calc_align(g.data_base + g.pool.buf.len, 64 * 1024) preallocated_pages := page_boundary / (64 * 1024) diff --git a/vlib/v/gen/wasm/ops.v b/vlib/v/gen/wasm/ops.v index b53fabcb4..e6f328d4b 100644 --- a/vlib/v/gen/wasm/ops.v +++ b/vlib/v/gen/wasm/ops.v @@ -79,6 +79,9 @@ pub fn (mut g Gen) get_wasm_type(typ_ ast.Type) wasm.ValType { ast.Enum { return g.get_wasm_type(ts.info.typ) } + ast.FnType { + return wasm.ValType.i32_t // index into the indirect function table + } else {} } diff --git a/vlib/v/gen/wasm/tests/callback_only.vv b/vlib/v/gen/wasm/tests/callback_only.vv new file mode 100644 index 000000000..c6bb4379f --- /dev/null +++ b/vlib/v/gen/wasm/tests/callback_only.vv @@ -0,0 +1,11 @@ +// This module declares an indirect call (`f(x)` in `apply`) but never takes a +// concrete function as a value, so `fn_value_indices` stays empty. The indirect +// function table must still be emitted, otherwise the `call_indirect` references +// a table that does not exist and the module fails to validate. +pub fn apply(f fn (int) int, x int) int { + return f(x) +} + +fn main() { + println('ok') +} diff --git a/vlib/v/gen/wasm/tests/callback_only.vv.out b/vlib/v/gen/wasm/tests/callback_only.vv.out new file mode 100644 index 000000000..9766475a4 --- /dev/null +++ b/vlib/v/gen/wasm/tests/callback_only.vv.out @@ -0,0 +1 @@ +ok diff --git a/vlib/v/gen/wasm/tests/fn_value.vv b/vlib/v/gen/wasm/tests/fn_value.vv new file mode 100644 index 000000000..c8c659308 --- /dev/null +++ b/vlib/v/gen/wasm/tests/fn_value.vv @@ -0,0 +1,90 @@ +type Callback = fn (int) int + +fn add1(x int) int { + return x + 1 +} + +fn double(x int) int { + return x * 2 +} + +struct Box { + op fn (int) int @[required] +} + +fn apply(f fn (int) int, x int) int { + return f(x) +} + +// apply_cb takes a function-type *alias* parameter, which must be unwrapped to +// its underlying fn type for the indirect call. +fn apply_cb(f Callback, x int) int { + return f(x) +} + +// a fn-typed const, called through the const name +const cb = add1 + +// make_cb returns a function value, so `make_cb()(x)` is an expression callee +fn make_cb() fn (int) int { + return double +} + +// logged_box / logged_arg make the evaluation order observable: the callee +// (the receiver) must be evaluated before the argument. +fn logged_box() Box { + println('callee') + return Box{ + op: add1 + } +} + +fn logged_arg() int { + println('arg') + return 100 +} + +fn main() { + // a top-level fn taken as a local fn value, then called through the value + f := add1 + println(f(10)) // 11 + + // a fn value stored in a struct field, called through the field + b := Box{ + op: double + } + println(b.op(20)) // 40 + + // a top-level fn passed as a fn-typed argument and called indirectly + println(apply(add1, 41)) // 42 + + // a non-capturing anonymous function as a value + h := fn (x int) int { + return x * x + } + println(h(7)) // 49 + + // reassign a fn value and call again (dynamic dispatch through the table) + mut g := add1 + println(g(0)) // 1 + g = double + println(g(50)) // 100 + + // a fn-typed const, called through the const name + println(cb(99)) // 100 + + // an expression callee that evaluates to a function value + println(make_cb()(5)) // 10 + + // the callee is evaluated before the argument: prints `callee`, then `arg` + println(logged_box().op(logged_arg())) // 101 + + // function type alias as a value: passed as a param, and cast into a local + println(apply_cb(add1, 5)) // 6 + ac := Callback(double) + println(ac(6)) // 12 + + // a real function value must be distinct from `nil` (table slot 0 is reserved) + nf := add1 + println(unsafe { nf == nil }) // false +} diff --git a/vlib/v/gen/wasm/tests/fn_value.vv.out b/vlib/v/gen/wasm/tests/fn_value.vv.out new file mode 100644 index 000000000..213fde234 --- /dev/null +++ b/vlib/v/gen/wasm/tests/fn_value.vv.out @@ -0,0 +1,14 @@ +11 +40 +42 +49 +1 +100 +100 +10 +callee +arg +101 +6 +12 +false diff --git a/vlib/v/gen/wasm/tests_decompile/fn_value.vv b/vlib/v/gen/wasm/tests_decompile/fn_value.vv new file mode 100644 index 000000000..d4721c7f9 --- /dev/null +++ b/vlib/v/gen/wasm/tests_decompile/fn_value.vv @@ -0,0 +1,11 @@ +// vtest vflags: -os wasi +fn add1(x int) int { + return x + 1 +} + +pub fn run() int { + f := add1 + return f(41) +} + +run() diff --git a/vlib/v/gen/wasm/tests_decompile/fn_value.wasm.must_have b/vlib/v/gen/wasm/tests_decompile/fn_value.wasm.must_have new file mode 100644 index 000000000..3fc41d9bc --- /dev/null +++ b/vlib/v/gen/wasm/tests_decompile/fn_value.wasm.must_have @@ -0,0 +1,3 @@ +export function main_run():int { +call_indirect( +funcref diff --git a/vlib/wasm/encoding.v b/vlib/wasm/encoding.v index ef2b84297..7c1168686 100644 --- a/vlib/wasm/encoding.v +++ b/vlib/wasm/encoding.v @@ -63,15 +63,17 @@ fn push_f64(mut buf []u8, v f64) { buf << u8(rv >> u32(56)) } +fn (mod &Module) get_local_func_idx(name string) int { + ftt := mod.functions[name] or { panic('function ${name} does not exist') } + return ftt.idx + mod.fn_imports.len +} + fn (mod &Module) get_function_idx(patch CallPatch) int { mut idx := -1 match patch { FunctionCallPatch { - ftt := mod.functions[patch.name] or { - panic('called function ${patch.name} does not exist') - } - idx = ftt.idx + mod.fn_imports.len + idx = mod.get_local_func_idx(patch.name) } ImportCallPatch { for fnidx, c in mod.fn_imports { @@ -183,6 +185,26 @@ pub fn (mut mod Module) compile() []u8 { } mod.end_section(tpatch) } + // https://webassembly.github.io/spec/core/binary/modules.html#table-section + // + if mod.tables.len > 0 { + tpatch := mod.start_section(.table_section) + { + mod.u32(u32(mod.tables.len)) + for tbl in mod.tables { + mod.buf << u8(tbl.reftype) + if max := tbl.max { + mod.buf << 0x01 // limit, max present + mod.u32(tbl.min) + mod.u32(max) + } else { + mod.buf << 0x00 // limit, max not present + mod.u32(tbl.min) + } + } + } + mod.end_section(tpatch) + } // https://webassembly.github.io/spec/core/binary/modules.html#binary-memsec // if memory := mod.memory { @@ -249,6 +271,15 @@ pub fn (mut mod Module) compile() []u8 { mod.u32(0) } } + for tblidx, tbl in mod.tables { + if !tbl.export { + continue + } + lsz++ + mod.name(tbl.name) + mod.buf << 0x01 // table + mod.u32(u32(tblidx)) + } for idx, gbl in mod.globals { if !gbl.export { continue @@ -272,6 +303,49 @@ pub fn (mut mod Module) compile() []u8 { } mod.end_section(tpatch) } + // https://webassembly.github.io/spec/core/binary/modules.html#element-section + // + if mod.elements.len > 0 { + tpatch := mod.start_section(.element_section) + { + mod.u32(u32(mod.elements.len)) + for el in mod.elements { + match el.mode { + .active { + if el.tableidx == 0 { + mod.buf << 0x00 // active, table 0, vec(funcidx) + // offset constant expression + mod.buf << 0x41 // i32.const + mod.buf << leb128.encode_i32(i32(el.offset)) + mod.buf << 0x0B // END expression opcode + } else { + mod.buf << 0x02 // active, explicit tableidx + elemkind + mod.u32(u32(el.tableidx)) + mod.buf << 0x41 // i32.const + mod.buf << leb128.encode_i32(i32(el.offset)) + mod.buf << 0x0B // END expression opcode + mod.buf << 0x00 // elemkind: funcref + } + } + .declarative { + mod.buf << 0x03 // declarative + elemkind + mod.buf << 0x00 // elemkind: funcref + } + .passive { + mod.buf << 0x01 // passive + elemkind + mod.buf << 0x00 // elemkind: funcref + } + } + + // vec(funcidx) + mod.u32(u32(el.funcs.len)) + for fnname in el.funcs { + mod.u32(u32(mod.get_local_func_idx(fnname))) + } + } + } + mod.end_section(tpatch) + } // https://webassembly.github.io/spec/core/binary/modules.html#data-count-section // if mod.segments.len > 0 { diff --git a/vlib/wasm/instructions.v b/vlib/wasm/instructions.v index c2e7444f6..c7c31a3a0 100644 --- a/vlib/wasm/instructions.v +++ b/vlib/wasm/instructions.v @@ -1219,3 +1219,56 @@ pub fn (mut func Function) ref_func_import(mod string, name string) { pos: func.code.len }) } + +// call_indirect calls a function from table `tableidx` whose type is `typeidx`. +// The function index to call is popped from the stack as an i32. +// Obtain `typeidx` from `Module.new_functype`. +// WebAssembly instruction: `call_indirect`. +pub fn (mut func Function) call_indirect(typeidx TypeIndex, tableidx TableIndex) { + func.code << 0x11 // call_indirect + func.u32(u32(typeidx)) + func.u32(u32(tableidx)) +} + +// table_get places the reference at the index (popped from the stack) of table +// `tableidx` onto the stack. +// WebAssembly instruction: `table.get`. +pub fn (mut func Function) table_get(tableidx TableIndex) { + func.code << 0x25 // table.get + func.u32(u32(tableidx)) +} + +// table_set stores a reference (popped from the stack) at the index (popped from +// the stack) of table `tableidx`. +// WebAssembly instruction: `table.set`. +pub fn (mut func Function) table_set(tableidx TableIndex) { + func.code << 0x26 // table.set + func.u32(u32(tableidx)) +} + +// table_size places the current size of table `tableidx` on the stack as an i32. +// WebAssembly instruction: `table.size`. +pub fn (mut func Function) table_size(tableidx TableIndex) { + func.code << 0xFC + func.code << 0x10 // table.size + func.u32(u32(tableidx)) +} + +// table_grow grows table `tableidx` by `n` entries (popped from the stack), +// filling them with an init reference (popped from the stack). It places the +// previous size on the stack as an i32, or -1 on failure. +// WebAssembly instruction: `table.grow`. +pub fn (mut func Function) table_grow(tableidx TableIndex) { + func.code << 0xFC + func.code << 0x0F // table.grow + func.u32(u32(tableidx)) +} + +// table_fill fills a range of table `tableidx` with a reference. It pops the +// count, the init reference, and the start index from the stack. +// WebAssembly instruction: `table.fill`. +pub fn (mut func Function) table_fill(tableidx TableIndex) { + func.code << 0xFC + func.code << 0x11 // table.fill + func.u32(u32(tableidx)) +} diff --git a/vlib/wasm/module.v b/vlib/wasm/module.v index 4829510a8..7cb71d4ad 100644 --- a/vlib/wasm/module.v +++ b/vlib/wasm/module.v @@ -69,6 +69,8 @@ mut: fn_imports []FunctionImport global_imports []GlobalImport segments []DataSegment + tables []Table + elements []Element debug bool mod_name ?string } @@ -108,10 +110,34 @@ struct DataSegment { name ?string } +struct Table { + name string + export bool + reftype RefType + min u32 + max ?u32 +} + +enum ElementMode { + active + declarative + passive +} + +struct Element { + mode ElementMode + tableidx TableIndex + offset int + funcs []string +} + pub type LocalIndex = int pub type GlobalIndex = int pub type GlobalImportIndex = int pub type DataSegmentIndex = int +pub type TableIndex = int +pub type TypeIndex = int +pub type ElementIndex = int pub struct FuncType { pub: @@ -120,7 +146,9 @@ pub: name ?string } -fn (mut mod Module) new_functype(ft FuncType) int { +// new_functype interns a function type and returns its type index. +// Use it to obtain the type index required by `call_indirect`. +pub fn (mut mod Module) new_functype(ft FuncType) TypeIndex { // interns existing types mut idx := mod.functypes.index(ft) @@ -187,6 +215,48 @@ pub fn (mut mod Module) assign_start(name string) { mod.start = name } +// assign_table declares a table in the current module and returns its index. +// `reftype` is the element type (`funcref_t` or `externref_t`); pass `max` as +// `none` for a growable table. +pub fn (mut mod Module) assign_table(name string, export bool, reftype RefType, min u32, max ?u32) TableIndex { + len := mod.tables.len + mod.tables << Table{ + name: name + export: export + reftype: reftype + min: min + max: max + } + return len +} + +// new_active_element appends an active element segment that initialises table +// `tableidx` starting at `offset` with the given (local) function references, +// and returns its index. An active segment also declares its functions, so a +// later `ref.func` to any of them validates. +pub fn (mut mod Module) new_active_element(tableidx TableIndex, offset int, funcs []string) ElementIndex { + len := mod.elements.len + mod.elements << Element{ + mode: .active + tableidx: tableidx + offset: offset + funcs: funcs + } + return len +} + +// new_declarative_element appends a declarative element segment that declares +// the given (local) functions, so that `ref.func` to a non-exported function +// validates. It allocates no table space. Returns its index. +pub fn (mut mod Module) new_declarative_element(funcs []string) ElementIndex { + len := mod.elements.len + mod.elements << Element{ + mode: .declarative + funcs: funcs + } + return len +} + // new_function_import imports a new function into the current module. pub fn (mut mod Module) new_function_import(modn string, name string, parameters []ValType, results []ValType) { assert !mod.fn_imports.any(it.mod == modn && it.name == name) diff --git a/vlib/wasm/tests/table_test.v b/vlib/wasm/tests/table_test.v new file mode 100644 index 000000000..e650d8885 --- /dev/null +++ b/vlib/wasm/tests/table_test.v @@ -0,0 +1,97 @@ +module main + +import wasm + +// A funcref table populated by an active element segment, dispatched through +// `call_indirect`. +fn test_call_indirect() { + mut m := wasm.Module{} + + mut f0 := m.new_function('f0', [], [.i32_t]) + { + f0.i32_const(10) + } + m.commit(f0, false) + + mut f1 := m.new_function('f1', [], [.i32_t]) + { + f1.i32_const(20) + } + m.commit(f1, false) + + tidx := m.assign_table('__indirect_function_table', true, .funcref_t, 2, none) + m.new_active_element(tidx, 0, ['f0', 'f1']) + + sig := m.new_functype(wasm.FuncType{ + parameters: []wasm.ValType{} + results: [.i32_t] + }) + + mut dispatch := m.new_function('dispatch', [.i32_t], [.i32_t]) + { + dispatch.local_get(0) // table index argument + dispatch.call_indirect(sig, tidx) + } + m.commit(dispatch, true) + + validate(m.compile()) or { panic(err) } +} + +// A growable externref table exercised with table.size/grow/set/get. +fn test_externref_table() { + mut m := wasm.Module{} + + tidx := m.assign_table('refs', true, .externref_t, 1, none) + + // grow by 3 (filling with null); leaves the previous size on the stack + mut grow := m.new_function('grow_refs', [], [.i32_t]) + { + grow.ref_null(.externref_t) // init value + grow.i32_const(3) // n + grow.table_grow(tidx) + } + m.commit(grow, true) + + mut size := m.new_function('refs_size', [], [.i32_t]) + { + size.table_size(tidx) + } + m.commit(size, true) + + // refs[0] = null, then read it back and report whether it is null + mut roundtrip := m.new_function('roundtrip', [], [.i32_t]) + { + roundtrip.i32_const(0) // index + roundtrip.ref_null(.externref_t) // value + roundtrip.table_set(tidx) + roundtrip.i32_const(0) // index + roundtrip.table_get(tidx) + roundtrip.ref_is_null(.externref_t) + } + m.commit(roundtrip, true) + + validate(m.compile()) or { panic(err) } +} + +// A declarative element segment makes `ref.func` to a non-exported, non-tabled +// function validate. Without the segment, validation would fail. +fn test_declarative_element() { + mut m := wasm.Module{} + + mut hidden := m.new_function('f_hidden', [], [.i32_t]) + { + hidden.i32_const(42) + } + m.commit(hidden, false) // NOT exported + + m.new_declarative_element(['f_hidden']) + + mut probe := m.new_function('probe', [], [.i32_t]) + { + probe.ref_func('f_hidden') + probe.ref_is_null(.funcref_t) + } + m.commit(probe, true) + + validate(m.compile()) or { panic(err) } +} -- 2.39.5