Skip to content

Commit d0637eb

Browse files
authored
checker: fix cgen 'pointer expected' for loop-local var passed to variadic interface arg (#27327)
1 parent 27f3945 commit d0637eb

3 files changed

Lines changed: 158 additions & 2 deletions

File tree

vlib/v/ast/scope.v

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,41 @@ pub fn (s &Scope) find_var(name string) ?&Var {
112112
return none
113113
}
114114

115+
// find_var_decl returns the nearest declaration variable named `name`, walking
116+
// the parent scope chain and skipping synthetic smartcast vars introduced by
117+
// `if x is T`/`match` branches. Use this when a promotion such as auto-heap must
118+
// be recorded on the variable that codegen emits as the declaration, not on a
119+
// smartcast copy living in an inner branch scope.
120+
//
121+
// Only true synthetic copies are skipped: those are registered as separate
122+
// scope objects carrying both a non-empty `smartcasts` list and a non-zero
123+
// `orig_type` (the pre-smartcast type). A real declaration that is smartcast in
124+
// place keeps `orig_type == 0` (e.g. an option var unwrapped by
125+
// `if x == none { continue }` via `update_smartcasts`), so it is returned
126+
// rather than skipped.
127+
pub fn (s &Scope) find_var_decl(name string) ?&Var {
128+
if _unlikely_(s == unsafe { nil }) {
129+
return none
130+
}
131+
for sc := unsafe { s }; true; sc = sc.parent {
132+
pobj := unsafe { &sc.objects[name] or { nil } }
133+
if pobj != unsafe { nil } {
134+
match pobj {
135+
Var {
136+
if pobj.smartcasts.len == 0 || pobj.orig_type == 0 {
137+
return &pobj
138+
}
139+
}
140+
else {}
141+
}
142+
}
143+
if sc.dont_lookup_parent() {
144+
break
145+
}
146+
}
147+
return none
148+
}
149+
115150
pub fn (s &Scope) find_global(name string) ?&GlobalField {
116151
obj := s.find_ptr(name)
117152
if _likely_(obj != unsafe { nil }) {

vlib/v/checker/checker.v

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7640,8 +7640,22 @@ fn (mut c Checker) mark_as_referenced(mut node ast.Expr, as_interface bool) {
76407640
ast.Ident {
76417641
if mut node.obj is ast.Var {
76427642
mut obj := unsafe { &node.obj }
7643-
if c.fn_scope != unsafe { nil } {
7644-
obj = c.fn_scope.find_var(node.obj.name) or { obj }
7643+
// Resolve the canonical declaration variable so that the
7644+
// `is_auto_heap` promotion below is recorded on the variable
7645+
// that cgen will see when emitting its declaration. Walk the
7646+
// use-site scope chain (so variables declared in nested scopes,
7647+
// e.g. inside a `for` loop body, are found, unlike
7648+
// `c.fn_scope.find_var` which only locates function-level vars),
7649+
// while skipping synthetic smartcast vars from `if x is T`/`match`
7650+
// branches: those are copies with no declaration of their own, so
7651+
// promoting them would leave the real declaration emitted as a
7652+
// plain value and desync the pointer indirection.
7653+
obj = node.scope.find_var_decl(node.obj.name) or {
7654+
if c.fn_scope != unsafe { nil } {
7655+
c.fn_scope.find_var(node.obj.name) or { obj }
7656+
} else {
7657+
obj
7658+
}
76457659
}
76467660
if obj.typ == 0 {
76477661
return
@@ -7681,6 +7695,21 @@ fn (mut c Checker) mark_as_referenced(mut node ast.Expr, as_interface bool) {
76817695
}
76827696
}
76837697

7698+
if obj.is_auto_heap {
7699+
// `obj` is the canonical declaration, which cgen emits as a
7700+
// heap pointer. When the value is read through a branch-local
7701+
// smartcast/option-unwrap copy (`if x is T`, `if x != none`),
7702+
// cgen resolves that use-site copy (see
7703+
// `resolved_ident_is_auto_heap`) when emitting the read, so it
7704+
// must carry the same `is_auto_heap`. Otherwise the read (e.g.
7705+
// an unwrapped option's `.data`) is emitted without the pointer
7706+
// indirection the declaration was given, producing invalid C.
7707+
node.obj.is_auto_heap = true
7708+
if mut use_site := node.scope.find_var(node.obj.name) {
7709+
use_site.is_auto_heap = true
7710+
}
7711+
}
7712+
76847713
if as_interface {
76857714
for method in type_sym.methods {
76867715
c.mark_fn_decl_as_referenced(method.fkey())

vlib/v/tests/interfaces/interface_variadic_test.v

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,98 @@ fn test_variadic_interface_fn_arg() {
3636
check_animals(c, d)
3737
}
3838

39+
// For issue 27326: passing a local variable declared inside a loop to a
40+
// variadic interface parameter caused a `pointer expected` C error, because
41+
// the value was promoted to auto-heap at the use site but not at its
42+
// declaration, producing inconsistent pointer indirection in the generated C.
43+
interface Value {}
44+
45+
fn collect(params ...Value) int {
46+
return params.len
47+
}
48+
49+
fn test_variadic_interface_arg_declared_in_loop() {
50+
mut total := 0
51+
for i := 0; i < 3; i++ {
52+
id := [u8(1), 2, 3]
53+
code := 'hello ${i}'
54+
total += collect(id, code)
55+
}
56+
assert total == 6
57+
}
58+
59+
type ValueSum = Cat | Dog
60+
61+
// For issue 27326: a variable smartcast via `if x is T` / `match` resolves to a
62+
// synthetic smartcast scope var, not its real declaration. Promoting that
63+
// synthetic var to auto-heap instead of the declaration must not desync the
64+
// pointer indirection when the value is passed to a variadic interface param.
65+
fn test_variadic_interface_arg_smartcast() {
66+
mut total := 0
67+
s := ValueSum(Cat{})
68+
if s is Cat {
69+
total += collect(s)
70+
}
71+
v := Value(Cat{})
72+
if v is Cat {
73+
total += collect(v)
74+
}
75+
match v {
76+
Cat { total += collect(v) }
77+
else {}
78+
}
79+
80+
assert total == 3
81+
}
82+
83+
// For issue 27326: a variable that is both declared in a nested scope (a `for`
84+
// loop body) and smartcast via `if x is T` must still resolve to its real
85+
// nested declaration, not the synthetic smartcast var nor only the function
86+
// scope, otherwise the auto-heap promotion desyncs the pointer indirection.
87+
fn test_variadic_interface_arg_smartcast_in_loop() {
88+
mut total := 0
89+
for _ in 0 .. 3 {
90+
s := ValueSum(Cat{})
91+
if s is Cat {
92+
total += collect(s)
93+
}
94+
}
95+
for _ in 0 .. 2 {
96+
v := Value(Cat{})
97+
if v is Cat {
98+
total += collect(v)
99+
}
100+
}
101+
assert total == 5
102+
}
103+
104+
fn maybe_value() ?Cat {
105+
return Cat{}
106+
}
107+
108+
// For issue 27326: an option value unwrapped via an if-guard or `if x != none`
109+
// and then passed to a variadic interface parameter is auto-heap promoted, so
110+
// its declaration is emitted as an option pointer. The unwrapped `.data` read
111+
// at the call site must use the matching pointer indirection (`->data`);
112+
// previously cgen emitted value-style access and failed C compilation.
113+
fn test_variadic_interface_arg_option_unwrap() {
114+
mut total := 0
115+
if x := maybe_value() {
116+
total += collect(x)
117+
}
118+
x := maybe_value()
119+
if x != none {
120+
total += collect(x)
121+
}
122+
for _ in 0 .. 2 {
123+
y := maybe_value()
124+
if y != none {
125+
total += collect(y)
126+
}
127+
}
128+
assert total == 4
129+
}
130+
39131
fn check_animals(animals ...Animal) {
40132
assert animals[0] is Cat
41133
assert animals[1] is Dog

0 commit comments

Comments
 (0)