Skip to content

Commit ee47fe6

Browse files
committed
v3/wasm: resolve import aliases, fix loop post scope
Address PR review: - Import aliases: `import moda as m; m.answer()` keyed the call as m.answer, but candidates are keyed from the module declaration (moda.answer), so the call hit the i32.const 0 fallback. Collect `import mod as alias` mappings (import_decl.value/.typ) and map the selector base back to the real module when resolving call keys. - C-style for post scope: the loop snapshot was only restored after the post statement, so a body-local that shadowed the loop variable was still bound when the post was emitted (e.g. `for ; i < 1; i++ { i := 10 }` incremented the inner local and never advanced the loop). Snapshot the bindings after the initializer and restore them before emitting the post so it rebinds to the loop variable. Also handle blank assignments (`_ = expr`) by evaluating the rhs for side effects and dropping it, instead of warning. Adds regression tests for both review items.
1 parent 0390c1a commit ee47fe6

2 files changed

Lines changed: 62 additions & 6 deletions

File tree

vlib/v3/gen/wasm/gen.v

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,10 @@ mut:
7777
cur_ret WType
7878
cur_fn_module string
7979
// module-wide; keyed by qualified function name (see qualified_fn_key)
80-
fn_index map[string]int
81-
fn_ret map[string]WType
82-
fn_params map[string][]WType
80+
fn_index map[string]int
81+
fn_ret map[string]WType
82+
fn_params map[string][]WType
83+
module_aliases map[string]string // import alias -> real module name
8384
data_pool []u8
8485
data_off map[string]int
8586
uses_print bool
@@ -100,6 +101,7 @@ pub fn Gen.new(a &flat.FlatAst, tc &types.TypeChecker, used_fns map[string]bool)
100101

101102
// gen builds the whole module from the flat AST.
102103
pub fn (mut g Gen) gen() {
104+
g.collect_module_aliases()
103105
user_fns := g.collect_user_fns()
104106
g.uses_print = g.detect_print(user_fns)
105107

@@ -505,7 +507,13 @@ fn (mut g Gen) gen_assign(node flat.Node) {
505507
for i + 1 < node.children_count {
506508
lhs := g.a.child_node(&node, i)
507509
rhs_id := g.a.child(&node, i + 1)
508-
if lhs.kind == .ident && lhs.value in g.var_index {
510+
if lhs.kind == .ident && lhs.value == '_' {
511+
// Blank assignment `_ = expr`: evaluate for side effects, discard.
512+
w := g.gen_expr(rhs_id)
513+
if w != .void {
514+
g.cur.drop()
515+
}
516+
} else if lhs.kind == .ident && lhs.value in g.var_index {
509517
idx := g.var_index[lhs.value]
510518
w := g.var_wtype[lhs.value]
511519
if node.op == .assign {
@@ -592,6 +600,9 @@ fn (mut g Gen) gen_for(node flat.Node) {
592600
if init_node.kind != .empty {
593601
g.gen_stmt(g.a.child(&node, 0))
594602
}
603+
// Bindings visible to the condition and post: the loop variable from the
604+
// initializer, but no body-local shadows.
605+
header_scope := g.snapshot_scope()
595606
g.cur.block_void() // break target
596607
g.frames << Frame{.brk}
597608
g.cur.loop_void() // back-edge target
@@ -611,6 +622,8 @@ fn (mut g Gen) gen_for(node flat.Node) {
611622
g.cur.end()
612623
g.frames.pop()
613624

625+
// Drop body-local shadows so the post statement rebinds to the loop var.
626+
g.restore_scope(header_scope)
614627
if post_node.kind != .empty {
615628
g.gen_stmt(g.a.child(&node, 2))
616629
}
@@ -938,10 +951,21 @@ fn (mut g Gen) gen_call(node flat.Node) WType {
938951
return .i32
939952
}
940953

954+
// collect_module_aliases records `import mod as alias` mappings so aliased
955+
// calls (`m.answer()`) resolve to the real module's qualified key.
956+
fn (mut g Gen) collect_module_aliases() {
957+
for node in g.a.nodes {
958+
if node.kind == .import_decl && node.typ.len > 0 {
959+
g.module_aliases[node.typ] = node.value
960+
}
961+
}
962+
}
963+
941964
// resolve_call_keys returns the candidate fn_index keys for a callee, in
942965
// priority order. A bare ident inside an imported module resolves to the
943966
// same-module function first (`mod.name`) and falls back to a main-module name;
944-
// a `module.fn()` selector resolves to that module's qualified name.
967+
// a `module.fn()` selector resolves to that module's qualified name, with any
968+
// import alias mapped back to the real module.
945969
fn (g &Gen) resolve_call_keys(callee &flat.Node, cur_mod string) []string {
946970
if callee.kind == .ident {
947971
if cur_mod != '' && cur_mod != 'main' {
@@ -952,7 +976,8 @@ fn (g &Gen) resolve_call_keys(callee &flat.Node, cur_mod string) []string {
952976
if callee.kind == .selector && callee.children_count > 0 {
953977
base := g.a.child_node(callee, 0)
954978
if base.kind == .ident {
955-
return ['${base.value}.${callee.value}']
979+
real := g.module_aliases[base.value] or { base.value }
980+
return ['${real}.${callee.value}']
956981
}
957982
}
958983
return []

vlib/v3/tests/wasm_codegen_test.v

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,37 @@ fn test_wasm_imported_module_numeric_call() {
268268
}
269269
}
270270

271+
fn test_wasm_for_post_uses_loop_var_not_body_shadow() {
272+
v3_bin := v3_binary()
273+
// A body-local `i` must not rebind the name used by the post `i++`; the
274+
// outer loop counter must still advance. The count/break bound keeps the
275+
// test terminating even if the fix regresses (it would loop otherwise).
276+
src := 'fn main() {\n\tmut i := 0\n\tmut count := 0\n\tfor ; i < 3; i++ {\n\t\ti := 10\n\t\t_ = i\n\t\tcount++\n\t\tif count > 100 {\n\t\t\tbreak\n\t\t}\n\t}\n\tprintln(i)\n\tprintln(count)\n}\n'
277+
wasm := compile_to_wasm(v3_bin, src, 'wasm_loopshadow')
278+
assert_valid_wasm(wasm)
279+
run_wasi_expect(wasm, ['3', '3'])
280+
}
281+
282+
fn test_wasm_imported_module_alias_call() {
283+
v3_bin := v3_binary()
284+
dir := os.join_path(os.vtmp_dir(), 'wasm_modalias')
285+
os.rmdir_all(dir) or {}
286+
os.mkdir_all(os.join_path(dir, 'moda')) or { panic(err) }
287+
os.write_file(os.join_path(dir, 'main.v'), 'import moda as m\n\nfn main() {\n\tprintln(m.answer())\n\tprintln(m.add(3, 4))\n}\n') or {
288+
panic(err)
289+
}
290+
os.write_file(os.join_path(dir, 'moda', 'moda.v'), 'module moda\n\npub fn answer() int {\n\treturn 42\n}\n\npub fn add(a int, b int) int {\n\treturn a + b\n}\n') or {
291+
panic(err)
292+
}
293+
out_wasm := os.join_path(dir, 'main.wasm')
294+
main_v := os.join_path(dir, 'main.v')
295+
res := os.execute('${os.quoted_path(v3_bin)} -b wasm -o ${os.quoted_path(out_wasm)} ${os.quoted_path(main_v)}')
296+
assert res.exit_code == 0, res.output
297+
assert_valid_wasm(out_wasm)
298+
assert !res.output.contains('unsupported call'), res.output
299+
run_wasi_expect(out_wasm, ['42', '7'])
300+
}
301+
271302
const wasi_runner_js = "import { WASI } from 'node:wasi';
272303
import { readFile } from 'node:fs/promises';
273304
const wasi = new WASI({ version: 'preview1', args: [], env: {} });

0 commit comments

Comments
 (0)