Skip to content

Commit 8169f7a

Browse files
committed
v3/wasm: buffer multi-decls, fix narrow >>> and ~
Address PR review: - Multi-declaration: gen_decl_assign bound each new local before later initializers were emitted, so `x, y := x+1, x+2` read the rebound x for the second RHS (y==8). Buffer every RHS into temporaries first, then bind the new locals, matching v3's evaluator (y==7). - Unsigned right shift on narrow operands: a signed i8/i16 is stored sign-extended in i32, so `i8(-5) >>> 1` shifted 0xfffffffb and produced -3. Mask the lhs to its declared width before `>>>` so it operates on the 8/16-bit pattern (125 / 32765). - Bit-not on narrow operands: `~` returned the full i32 xor without applying the result width, so `~u8(0)` left -1 (printed as 4294967295 via the unsigned path, and wrong for a u8 return). Narrow the `~` result to its resolved type; the same narrowing is applied to unary minus on narrow types. Adds regression tests for all three (verified against the v1 runtime and v3's eval backend).
1 parent f5039f1 commit 8169f7a

2 files changed

Lines changed: 95 additions & 14 deletions

File tree

vlib/v3/gen/wasm/gen.v

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -491,23 +491,59 @@ fn (mut g Gen) gen_stmt(id flat.NodeId) {
491491
}
492492

493493
fn (mut g Gen) gen_decl_assign(node flat.Node) {
494+
// Multi-declaration (`x, y := a, b`): evaluate every RHS against the outer
495+
// scope before binding any new name, so a shadowing `x, y := x+1, x+2`
496+
// reads the outer x for both initializers.
497+
if node.children_count > 2 {
498+
g.gen_multi_decl(node)
499+
return
500+
}
501+
lhs := g.a.child_node(&node, 0)
502+
rhs_id := g.a.child(&node, 1)
503+
w := g.expr_wtype(rhs_id, node.typ)
504+
uns := g.decl_is_unsigned(rhs_id, node.typ)
505+
width := g.decl_width(rhs_id, node.typ)
506+
if lhs.kind == .ident {
507+
// Emit the initializer before binding the new name, so a shadowing
508+
// `x := x + 1` reads the outer x rather than the fresh zero local.
509+
g.gen_expr_as(rhs_id, w)
510+
idx := g.new_local(lhs.value, w, uns, width)
511+
g.narrow_for_local(lhs.value)
512+
g.cur.local_set(idx)
513+
}
514+
}
515+
516+
fn (mut g Gen) gen_multi_decl(node flat.Node) {
517+
mut temps := []int{}
518+
mut names := []string{}
519+
mut ws := []WType{}
520+
mut unss := []bool{}
521+
mut widths := []int{}
522+
// Phase 1: buffer every RHS into a temporary (outer bindings still in scope).
494523
mut i := 0
495524
for i + 1 < node.children_count {
496525
lhs := g.a.child_node(&node, i)
497526
rhs_id := g.a.child(&node, i + 1)
498-
w := g.expr_wtype(rhs_id, node.typ)
499-
uns := g.decl_is_unsigned(rhs_id, node.typ)
500-
width := g.decl_width(rhs_id, node.typ)
501527
if lhs.kind == .ident {
502-
// Emit the initializer before binding the new name, so a shadowing
503-
// `x := x + 1` reads the outer x rather than the fresh zero local.
528+
w := g.expr_wtype(rhs_id, node.typ)
504529
g.gen_expr_as(rhs_id, w)
505-
idx := g.new_local(lhs.value, w, uns, width)
506-
g.narrow_for_local(lhs.value)
507-
g.cur.local_set(idx)
530+
t := g.alloc_temp(w)
531+
g.cur.local_set(t)
532+
temps << t
533+
names << lhs.value
534+
ws << w
535+
unss << g.decl_is_unsigned(rhs_id, node.typ)
536+
widths << g.decl_width(rhs_id, node.typ)
508537
}
509538
i += 2
510539
}
540+
// Phase 2: bind each new local and store its buffered value.
541+
for k, name in names {
542+
idx := g.new_local(name, ws[k], unss[k], widths[k])
543+
g.cur.local_get(temps[k])
544+
g.narrow_for_local(name)
545+
g.cur.local_set(idx)
546+
}
511547
}
512548

513549
fn (mut g Gen) gen_assign(node flat.Node) {
@@ -786,7 +822,7 @@ fn (mut g Gen) gen_expr(id flat.NodeId) WType {
786822
return g.gen_infix(id, node)
787823
}
788824
.prefix {
789-
return g.gen_prefix(node)
825+
return g.gen_prefix(id, node)
790826
}
791827
.postfix {
792828
g.gen_postfix(node)
@@ -852,10 +888,17 @@ fn (mut g Gen) gen_infix(id flat.NodeId, node flat.Node) WType {
852888
}
853889
signed := !g.is_unsigned(lhs_id)
854890
g.gen_expr_as(lhs_id, value_w)
891+
if op == .right_shift_unsigned && value_w == .i32 {
892+
// A narrow signed lhs is stored sign-extended in i32; mask it to its
893+
// width so the upper bits don't feed into the logical shift.
894+
lw := narrow_width(g.tc.resolve_type(lhs_id))
895+
if lw != 32 {
896+
g.emit_narrow(lw, true)
897+
}
898+
}
855899
g.emit_shift_with_count(op, value_w, rhs_id, signed)
856900
if value_w == .i32 {
857-
rt := g.tc.resolve_type(id)
858-
g.emit_narrow(narrow_width(rt), type_is_unsigned(rt))
901+
g.narrow_result(id)
859902
}
860903
return value_w
861904
}
@@ -869,12 +912,18 @@ fn (mut g Gen) gen_infix(id flat.NodeId, node flat.Node) WType {
869912
g.emit_arith(op, ow, signed)
870913
// Sub-32-bit results (e.g. u8 + u8) wrap to their declared width in V.
871914
if ow == .i32 {
872-
rt := g.tc.resolve_type(id)
873-
g.emit_narrow(narrow_width(rt), type_is_unsigned(rt))
915+
g.narrow_result(id)
874916
}
875917
return ow
876918
}
877919

920+
// narrow_result masks/sign-extends the i32 on the stack to the resolved type's
921+
// sub-32-bit width (a no-op for 32/64-bit types).
922+
fn (mut g Gen) narrow_result(id flat.NodeId) {
923+
rt := g.tc.resolve_type(id)
924+
g.emit_narrow(narrow_width(rt), type_is_unsigned(rt))
925+
}
926+
878927
fn (mut g Gen) gen_logical(node flat.Node) WType {
879928
lhs_id := g.a.child(&node, 0)
880929
rhs_id := g.a.child(&node, 1)
@@ -896,7 +945,7 @@ fn (mut g Gen) gen_logical(node flat.Node) WType {
896945
return .i32
897946
}
898947

899-
fn (mut g Gen) gen_prefix(node flat.Node) WType {
948+
fn (mut g Gen) gen_prefix(id flat.NodeId, node flat.Node) WType {
900949
child_id := g.a.child(&node, 0)
901950
match node.op {
902951
.minus {
@@ -922,6 +971,7 @@ fn (mut g Gen) gen_prefix(node flat.Node) WType {
922971
g.cur.i32_const(0)
923972
g.gen_expr_as(child_id, .i32)
924973
g.cur.raw(0x6b) // i32.sub
974+
g.narrow_result(id) // negation of a narrow type wraps to its width
925975
return .i32
926976
}
927977
}
@@ -940,6 +990,7 @@ fn (mut g Gen) gen_prefix(node flat.Node) WType {
940990
}
941991
g.cur.i32_const(-1)
942992
g.cur.raw(0x73) // i32.xor
993+
g.narrow_result(id) // ~ of a narrow type keeps its width (e.g. ~u8(0)=255)
943994
return .i32
944995
}
945996
else {

vlib/v3/tests/wasm_codegen_test.v

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,36 @@ fn test_wasm_shift_keeps_lhs_width() {
380380
run_wasi_expect(wasm, ['0', '1099511627776', '0'])
381381
}
382382

383+
fn test_wasm_multi_decl_buffers_rhs() {
384+
v3_bin := v3_binary()
385+
// Both initializers must read the outer x before either new local is bound,
386+
// so y == 7 (outer x + 2), not 8 (rebound x + 2).
387+
src := 'fn main() {\n\tx := 5\n\t{\n\t\tx, y := x + 1, x + 2\n\t\tprintln(x)\n\t\tprintln(y)\n\t}\n}\n'
388+
wasm := compile_to_wasm(v3_bin, src, 'wasm_multidecl')
389+
assert_valid_wasm(wasm)
390+
run_wasi_expect(wasm, ['6', '7'])
391+
}
392+
393+
fn test_wasm_unsigned_right_shift_masks_narrow() {
394+
v3_bin := v3_binary()
395+
// `>>>` on a signed narrow operand works on the 8/16-bit pattern, so the
396+
// sign-extension bits must be masked off first.
397+
src := 'fn main() {\n\tprintln(int(i8(-5) >>> 1))\n\tprintln(int(i16(-5) >>> 1))\n\tprintln(int(u8(250) >>> 1))\n}\n'
398+
wasm := compile_to_wasm(v3_bin, src, 'wasm_urshift')
399+
assert_valid_wasm(wasm)
400+
run_wasi_expect(wasm, ['125', '32765', '125'])
401+
}
402+
403+
fn test_wasm_bit_not_narrows_result() {
404+
v3_bin := v3_binary()
405+
// ~ keeps the operand width: ~u8(0) is 255, ~u16(0) is 65535, and a u8
406+
// return carries the narrowed value.
407+
src := 'fn allset() u8 {\n\treturn ~u8(0)\n}\n\nfn main() {\n\tprintln(~u8(0))\n\tprintln(~u16(0))\n\tprintln(allset())\n}\n'
408+
wasm := compile_to_wasm(v3_bin, src, 'wasm_bitnot')
409+
assert_valid_wasm(wasm)
410+
run_wasi_expect(wasm, ['255', '65535', '255'])
411+
}
412+
383413
const wasi_runner_js = "import { WASI } from 'node:wasi';
384414
import { readFile } from 'node:fs/promises';
385415
const wasi = new WASI({ version: 'preview1', args: [], env: {} });

0 commit comments

Comments
 (0)