Skip to content

Commit 0390c1a

Browse files
committed
v3/wasm: scope locals, guard shift counts, wrap narrow ints
Address PR review: - Block/for scoping: declarations in a for-initializer or an inner block overwrote the single name->local binding and never restored it, so a shadowed outer local was read with the inner value after its scope ended. Snapshot the local bindings on entry to a block, if-branch, and for-loop, and restore them on exit (outer `i`/`x` survive a shadowing loop, matching eval_test). - Over-width shifts: WASM masks the shift count modulo the operand width, but V yields 0 once the count reaches the width. Guard each shift with a select so the masked result is kept only while count < width (verified against the v1 runtime; the v3 C backend relies on C UB and differs here). - Narrow integers: i8/u8/i16/u16 were collapsed to i32 with no width, so casts like i8(128) and narrow arithmetic/postfix updates never wrapped. Track each local's declared width and mask (unsigned) or sign-extend (signed) after casts, narrow arithmetic results, and stores to narrow locals. Adds regression tests for all three (output matches the C backend except the intentional over-width-shift divergence, which matches the V runtime).
1 parent 372bf7d commit 0390c1a

2 files changed

Lines changed: 198 additions & 4 deletions

File tree

vlib/v3/gen/wasm/gen.v

Lines changed: 154 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ struct Frame {
4343
tag FrameTag
4444
}
4545

46+
// VarScope snapshots the local-name bindings so an inner block or for-loop can
47+
// shadow an outer local and have the outer binding restored on scope exit.
48+
struct VarScope {
49+
index map[string]int
50+
wtype map[string]WType
51+
unsigned map[string]bool
52+
widths map[string]int
53+
}
54+
4655
enum FrameTag {
4756
plain
4857
brk
@@ -63,6 +72,7 @@ mut:
6372
var_index map[string]int
6473
var_wtype map[string]WType
6574
var_unsigned map[string]bool
75+
var_widths map[string]int // sub-32-bit int locals: 8 or 16; else 32
6676
frames []Frame
6777
cur_ret WType
6878
cur_fn_module string
@@ -327,6 +337,7 @@ fn (mut g Gen) emit_user_fn(f FnInfo) {
327337
g.var_index = map[string]int{}
328338
g.var_wtype = map[string]WType{}
329339
g.var_unsigned = map[string]bool{}
340+
g.var_widths = map[string]int{}
330341
g.frames = []Frame{}
331342
g.cur_ret = f.ret
332343
g.cur_fn_module = f.module
@@ -340,9 +351,11 @@ fn (mut g Gen) emit_user_fn(f FnInfo) {
340351
continue
341352
}
342353
w := f.params[pi]
354+
pt := g.tc.parse_type(p.typ)
343355
g.var_index[p.value] = pi
344356
g.var_wtype[p.value] = w
345-
g.var_unsigned[p.value] = type_is_unsigned(g.tc.parse_type(p.typ))
357+
g.var_unsigned[p.value] = type_is_unsigned(pt)
358+
g.var_widths[p.value] = narrow_width(pt)
346359
wparams << wt_valtype(w)
347360
pi++
348361
}
@@ -377,15 +390,42 @@ fn (mut g Gen) emit_start() int {
377390
}
378391

379392
// new_local allocates a fresh local beyond the params and records its type.
380-
fn (mut g Gen) new_local(name string, w WType, unsigned bool) int {
393+
fn (mut g Gen) new_local(name string, w WType, unsigned bool, width int) int {
381394
idx := g.nparams + g.local_types.len
382395
g.local_types << wt_valtype(w)
383396
g.var_index[name] = idx
384397
g.var_wtype[name] = w
385398
g.var_unsigned[name] = unsigned
399+
g.var_widths[name] = width
400+
return idx
401+
}
402+
403+
// alloc_temp allocates an anonymous function-level local (e.g. for the shift
404+
// over-width guard) and returns its index.
405+
fn (mut g Gen) alloc_temp(w WType) int {
406+
idx := g.nparams + g.local_types.len
407+
g.local_types << wt_valtype(w)
386408
return idx
387409
}
388410

411+
// snapshot_scope / restore_scope save and restore local-name bindings so inner
412+
// declarations that shadow an outer local do not leak past their scope.
413+
fn (g &Gen) snapshot_scope() VarScope {
414+
return VarScope{
415+
index: g.var_index.clone()
416+
wtype: g.var_wtype.clone()
417+
unsigned: g.var_unsigned.clone()
418+
widths: g.var_widths.clone()
419+
}
420+
}
421+
422+
fn (mut g Gen) restore_scope(s VarScope) {
423+
g.var_index = s.index.clone()
424+
g.var_wtype = s.wtype.clone()
425+
g.var_unsigned = s.unsigned.clone()
426+
g.var_widths = s.widths.clone()
427+
}
428+
389429
// ---- statements ----
390430

391431
fn (mut g Gen) gen_stmt(id flat.NodeId) {
@@ -396,9 +436,11 @@ fn (mut g Gen) gen_stmt(id flat.NodeId) {
396436
match node.kind {
397437
.fn_decl, .c_fn_decl {}
398438
.block {
439+
saved := g.snapshot_scope()
399440
for i in 0 .. node.children_count {
400441
g.gen_stmt(g.a.child(&node, i))
401442
}
443+
g.restore_scope(saved)
402444
}
403445
.expr_stmt {
404446
child_id := g.a.child(&node, 0)
@@ -447,9 +489,11 @@ fn (mut g Gen) gen_decl_assign(node flat.Node) {
447489
rhs_id := g.a.child(&node, i + 1)
448490
w := g.expr_wtype(rhs_id, node.typ)
449491
uns := g.decl_is_unsigned(rhs_id, node.typ)
492+
width := g.decl_width(rhs_id, node.typ)
450493
if lhs.kind == .ident {
451-
idx := g.new_local(lhs.value, w, uns)
494+
idx := g.new_local(lhs.value, w, uns, width)
452495
g.gen_expr_as(rhs_id, w)
496+
g.narrow_for_local(lhs.value)
453497
g.cur.local_set(idx)
454498
}
455499
i += 2
@@ -472,6 +516,7 @@ fn (mut g Gen) gen_assign(node flat.Node) {
472516
signed := !g.var_unsigned[lhs.value]
473517
g.emit_arith(compound_to_op(node.op), w, signed)
474518
}
519+
g.narrow_for_local(lhs.value)
475520
g.cur.local_set(idx)
476521
} else {
477522
g.warn('unsupported assign target')
@@ -480,34 +525,65 @@ fn (mut g Gen) gen_assign(node flat.Node) {
480525
}
481526
}
482527

528+
// narrow_for_local masks/sign-extends the value on the stack to the local's
529+
// declared sub-32-bit width before it is stored.
530+
fn (mut g Gen) narrow_for_local(name string) {
531+
width := g.var_widths[name]
532+
if width == 8 || width == 16 {
533+
g.emit_narrow(width, g.var_unsigned[name])
534+
}
535+
}
536+
537+
// decl_width resolves the declared sub-32-bit width (8/16) of an initializer.
538+
fn (mut g Gen) decl_width(rhs_id flat.NodeId, fallback_typ string) int {
539+
w := narrow_width(g.tc.resolve_type(rhs_id))
540+
if w != 32 {
541+
return w
542+
}
543+
if fallback_typ.len > 0 {
544+
return narrow_width(g.tc.parse_type(fallback_typ))
545+
}
546+
return 32
547+
}
548+
483549
fn (mut g Gen) gen_if(node flat.Node) {
484550
cond_id := g.a.child(&node, 0)
485551
g.gen_expr_as_bool(cond_id)
486552
g.cur.if_void()
487553
g.frames << Frame{.plain}
488554
then_block := g.a.child_node(&node, 1)
555+
saved_then := g.snapshot_scope()
489556
for i in 0 .. then_block.children_count {
490557
g.gen_stmt(g.a.child(then_block, i))
491558
}
559+
g.restore_scope(saved_then)
492560
if node.children_count > 2 {
493561
else_id := g.a.child(&node, 2)
494562
if int(else_id) >= 0 {
495563
else_node := g.a.nodes[int(else_id)]
496564
g.cur.else_()
565+
saved_else := g.snapshot_scope()
497566
if else_node.kind == .if_expr {
498567
g.gen_if(else_node)
499568
} else if else_node.kind == .block {
500569
for i in 0 .. else_node.children_count {
501570
g.gen_stmt(g.a.child(&else_node, i))
502571
}
503572
}
573+
g.restore_scope(saved_else)
504574
}
505575
}
506576
g.cur.end()
507577
g.frames.pop()
508578
}
509579

510580
fn (mut g Gen) gen_for(node flat.Node) {
581+
// The initializer and body are scoped to the loop; restore outer bindings
582+
// afterwards so a shadowing `for i := ...` or body-local does not leak.
583+
saved := g.snapshot_scope()
584+
defer {
585+
g.restore_scope(saved)
586+
}
511587
init_node := g.a.child_node(&node, 0)
512588
cond_id := g.a.child(&node, 1)
513589
cond_node := g.a.nodes[int(cond_id)]
@@ -710,6 +786,11 @@ fn (mut g Gen) gen_infix(id flat.NodeId, node flat.Node) WType {
710786
g.gen_expr_as(lhs_id, ow)
711787
g.gen_expr_as(rhs_id, ow)
712788
g.emit_arith(op, ow, signed)
789+
// Sub-32-bit results (e.g. u8 + u8) wrap to their declared width in V.
790+
if ow == .i32 {
791+
rt := g.tc.resolve_type(id)
792+
g.emit_narrow(narrow_width(rt), type_is_unsigned(rt))
793+
}
713794
return ow
714795
}
715796

@@ -815,14 +896,18 @@ fn (mut g Gen) gen_postfix(node flat.Node) {
815896
g.cur.raw(if inc { u8(0x6a) } else { u8(0x6b) }) // i32.add/sub
816897
}
817898
}
899+
g.narrow_for_local(target.value)
818900
g.cur.local_set(idx)
819901
}
820902

821903
fn (mut g Gen) gen_cast(node flat.Node) WType {
822904
child_id := g.a.child(&node, 0)
823-
target := prim_wtype(g.tc.parse_type(node.value)) or { WType.i32 }
905+
tt := g.tc.parse_type(node.value)
906+
target := prim_wtype(tt) or { WType.i32 }
824907
src := g.gen_expr(child_id)
825908
g.coerce(src, target, g.is_signed(child_id))
909+
// Casting to a sub-32-bit type wraps/sign-extends to that width.
910+
g.emit_narrow(narrow_width(tt), type_is_unsigned(tt))
826911
return target
827912
}
828913

@@ -1185,6 +1270,20 @@ fn type_is_unsigned(t types.Type) bool {
11851270
return t is types.USize
11861271
}
11871272

1273+
// narrow_width returns 8 or 16 for sub-32-bit integer types (which the backend
1274+
// stores in an i32 and must mask/sign-extend), or 32 otherwise.
1275+
fn narrow_width(t types.Type) int {
1276+
if t is types.Primitive && t.props.has(.integer) {
1277+
if t.size == 8 {
1278+
return 8
1279+
}
1280+
if t.size == 16 {
1281+
return 16
1282+
}
1283+
}
1284+
return 32
1285+
}
1286+
11881287
fn (mut g Gen) is_signed(id flat.NodeId) bool {
11891288
return !g.is_unsigned(id)
11901289
}
@@ -1233,9 +1332,60 @@ fn (mut g Gen) coerce(from WType, to WType, signed bool) {
12331332
}
12341333

12351334
fn (mut g Gen) emit_arith(op flat.Op, w WType, signed bool) {
1335+
if op in [.left_shift, .right_shift, .right_shift_unsigned] {
1336+
g.emit_shift(op, w, signed)
1337+
return
1338+
}
12361339
g.cur.raw(arith_op(op, w, signed))
12371340
}
12381341

1342+
// emit_shift emits a shift with V's over-width semantics: WASM masks the count
1343+
// modulo the operand width, but V yields 0 when the count >= the width, so the
1344+
// raw result is selected only while count < width. Stack on entry: [value,
1345+
// count]; on exit: [result].
1346+
fn (mut g Gen) emit_shift(op flat.Op, w WType, signed bool) {
1347+
count_t := if w == .i64 { WType.i64 } else { WType.i32 }
1348+
tmp := g.alloc_temp(count_t)
1349+
g.cur.local_tee(tmp) // keep the count, also store it
1350+
g.cur.raw(arith_op(op, w, signed)) // value << (count mod width)
1351+
if w == .i64 {
1352+
g.cur.i64_const(0)
1353+
g.cur.local_get(tmp)
1354+
g.cur.i64_const(64)
1355+
g.cur.raw(0x54) // i64.lt_u
1356+
} else {
1357+
g.cur.i32_const(0)
1358+
g.cur.local_get(tmp)
1359+
g.cur.i32_const(32)
1360+
g.cur.raw(0x49) // i32.lt_u
1361+
}
1362+
g.cur.raw(0x1b) // select: (count < width) ? shifted : 0
1363+
}
1364+
1365+
// emit_narrow masks (unsigned) or sign-extends (signed) the i32 on the stack to
1366+
// a sub-32-bit width; a no-op for 32/64-bit values.
1367+
fn (mut g Gen) emit_narrow(width int, unsigned bool) {
1368+
match width {
1369+
8 {
1370+
if unsigned {
1371+
g.cur.i32_const(0xff)
1372+
g.cur.raw(0x71) // i32.and
1373+
} else {
1374+
g.cur.raw(0xc0) // i32.extend8_s
1375+
}
1376+
}
1377+
16 {
1378+
if unsigned {
1379+
g.cur.i32_const(0xffff)
1380+
g.cur.raw(0x71) // i32.and
1381+
} else {
1382+
g.cur.raw(0xc1) // i32.extend16_s
1383+
}
1384+
}
1385+
else {}
1386+
}
1387+
}
1388+
12391389
fn (mut g Gen) warn(msg string) {
12401390
g.warnings << msg
12411391
}

vlib/v3/tests/wasm_codegen_test.v

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,50 @@ fn run_node(node string, runner string, wasm string) os.Result {
5252
return os.execute('${os.quoted_path(node)} --no-warnings ${os.quoted_path(runner)} ${os.quoted_path(wasm)}')
5353
}
5454

55+
// run_wasi_expect runs a WASI module and asserts its trailing output lines.
56+
// Skips execution (compile/validate already happened) when node is absent.
57+
fn run_wasi_expect(wasm string, expected []string) {
58+
node := node_path() or { return }
59+
runner := os.join_path(os.vtmp_dir(), 'wasm_run_wasi.mjs')
60+
os.write_file(runner, wasi_runner_js) or { panic(err) }
61+
res := run_node(node, runner, wasm)
62+
assert res.exit_code == 0, res.output
63+
lines := res.output.split_into_lines().map(it.trim_space()).filter(it.len > 0)
64+
assert lines.len >= expected.len, res.output
65+
for i, want in expected {
66+
got := lines[lines.len - expected.len + i]
67+
assert got == want, 'line ${i}: got ${got}, want ${want} (full: ${res.output})'
68+
}
69+
}
70+
71+
fn test_wasm_block_scoping_preserves_outer_locals() {
72+
v3_bin := v3_binary()
73+
// A shadowing for-initializer and a loop-body declaration must not leak:
74+
// after the loops the outer i (10) and x (1) are restored.
75+
src := 'fn main() {\n\ti := 10\n\tfor i := 0; i < 1; i++ {\n\t}\n\tprintln(i)\n\tx := 1\n\tfor j := 0; j < 2; j++ {\n\t\tx := j + 2\n\t\tprintln(x)\n\t}\n\tprintln(x)\n}\n'
76+
wasm := compile_to_wasm(v3_bin, src, 'wasm_scope')
77+
assert_valid_wasm(wasm)
78+
run_wasi_expect(wasm, ['10', '2', '3', '1'])
79+
}
80+
81+
fn test_wasm_narrow_integer_casts_and_arithmetic_wrap() {
82+
v3_bin := v3_binary()
83+
src := 'fn main() {\n\tprintln(int(i8(128)))\n\tprintln(int(u8(256)))\n\tprintln(int(u16(65536)))\n\tprintln(int(i16(32768)))\n\tmut a := u8(250)\n\ta += u8(10)\n\tprintln(int(a))\n\tmut b := i8(127)\n\tb++\n\tprintln(int(b))\n\tprintln(int(u8(200) + u8(100)))\n}\n'
84+
wasm := compile_to_wasm(v3_bin, src, 'wasm_narrow')
85+
assert_valid_wasm(wasm)
86+
run_wasi_expect(wasm, ['-128', '0', '0', '-32768', '4', '-128', '44'])
87+
}
88+
89+
fn test_wasm_runtime_oversized_shift_is_zero() {
90+
v3_bin := v3_binary()
91+
// V yields 0 when a runtime shift count is >= the operand width, whereas
92+
// raw WASM masks the count modulo the width; in-range shifts are unchanged.
93+
src := 'fn osc() u64 {\n\treturn u64(64)\n}\n\nfn main() {\n\tshift := osc()\n\tbits := u64(9221120237041090561)\n\tprintln(bits >> shift)\n\tprintln(bits << shift)\n\tmut left := u64(1)\n\tleft <<= shift\n\tprintln(left)\n\tprintln(u64(1) << u64(40))\n}\n'
94+
wasm := compile_to_wasm(v3_bin, src, 'wasm_shiftguard')
95+
assert_valid_wasm(wasm)
96+
run_wasi_expect(wasm, ['0', '0', '0', '1099511627776'])
97+
}
98+
5599
fn test_wasm_hello_world() {
56100
v3_bin := v3_binary()
57101
wasm := compile_to_wasm(v3_bin, "fn main() {\n\tprintln('hello world')\n}\n", 'wasm_hello')

0 commit comments

Comments
 (0)