From d0637eb51bc2153560c5bd3b4795b0b5a4f425ba Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Thu, 4 Jun 2026 07:22:54 +0300 Subject: [PATCH] checker: fix cgen 'pointer expected' for loop-local var passed to variadic interface arg (#27327) --- vlib/v/ast/scope.v | 35 +++++++ vlib/v/checker/checker.v | 33 ++++++- .../interfaces/interface_variadic_test.v | 92 +++++++++++++++++++ 3 files changed, 158 insertions(+), 2 deletions(-) diff --git a/vlib/v/ast/scope.v b/vlib/v/ast/scope.v index 49c464a0d..3c4ad970d 100644 --- a/vlib/v/ast/scope.v +++ b/vlib/v/ast/scope.v @@ -112,6 +112,41 @@ pub fn (s &Scope) find_var(name string) ?&Var { return none } +// find_var_decl returns the nearest declaration variable named `name`, walking +// the parent scope chain and skipping synthetic smartcast vars introduced by +// `if x is T`/`match` branches. Use this when a promotion such as auto-heap must +// be recorded on the variable that codegen emits as the declaration, not on a +// smartcast copy living in an inner branch scope. +// +// Only true synthetic copies are skipped: those are registered as separate +// scope objects carrying both a non-empty `smartcasts` list and a non-zero +// `orig_type` (the pre-smartcast type). A real declaration that is smartcast in +// place keeps `orig_type == 0` (e.g. an option var unwrapped by +// `if x == none { continue }` via `update_smartcasts`), so it is returned +// rather than skipped. +pub fn (s &Scope) find_var_decl(name string) ?&Var { + if _unlikely_(s == unsafe { nil }) { + return none + } + for sc := unsafe { s }; true; sc = sc.parent { + pobj := unsafe { &sc.objects[name] or { nil } } + if pobj != unsafe { nil } { + match pobj { + Var { + if pobj.smartcasts.len == 0 || pobj.orig_type == 0 { + return &pobj + } + } + else {} + } + } + if sc.dont_lookup_parent() { + break + } + } + return none +} + pub fn (s &Scope) find_global(name string) ?&GlobalField { obj := s.find_ptr(name) if _likely_(obj != unsafe { nil }) { diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index 4533f2cab..f758f2f3e 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -7640,8 +7640,22 @@ fn (mut c Checker) mark_as_referenced(mut node ast.Expr, as_interface bool) { ast.Ident { if mut node.obj is ast.Var { mut obj := unsafe { &node.obj } - if c.fn_scope != unsafe { nil } { - obj = c.fn_scope.find_var(node.obj.name) or { obj } + // Resolve the canonical declaration variable so that the + // `is_auto_heap` promotion below is recorded on the variable + // that cgen will see when emitting its declaration. Walk the + // use-site scope chain (so variables declared in nested scopes, + // e.g. inside a `for` loop body, are found, unlike + // `c.fn_scope.find_var` which only locates function-level vars), + // while skipping synthetic smartcast vars from `if x is T`/`match` + // branches: those are copies with no declaration of their own, so + // promoting them would leave the real declaration emitted as a + // plain value and desync the pointer indirection. + obj = node.scope.find_var_decl(node.obj.name) or { + if c.fn_scope != unsafe { nil } { + c.fn_scope.find_var(node.obj.name) or { obj } + } else { + obj + } } if obj.typ == 0 { return @@ -7681,6 +7695,21 @@ fn (mut c Checker) mark_as_referenced(mut node ast.Expr, as_interface bool) { } } + if obj.is_auto_heap { + // `obj` is the canonical declaration, which cgen emits as a + // heap pointer. When the value is read through a branch-local + // smartcast/option-unwrap copy (`if x is T`, `if x != none`), + // cgen resolves that use-site copy (see + // `resolved_ident_is_auto_heap`) when emitting the read, so it + // must carry the same `is_auto_heap`. Otherwise the read (e.g. + // an unwrapped option's `.data`) is emitted without the pointer + // indirection the declaration was given, producing invalid C. + node.obj.is_auto_heap = true + if mut use_site := node.scope.find_var(node.obj.name) { + use_site.is_auto_heap = true + } + } + if as_interface { for method in type_sym.methods { c.mark_fn_decl_as_referenced(method.fkey()) diff --git a/vlib/v/tests/interfaces/interface_variadic_test.v b/vlib/v/tests/interfaces/interface_variadic_test.v index a7b5d3d12..717d7c25e 100644 --- a/vlib/v/tests/interfaces/interface_variadic_test.v +++ b/vlib/v/tests/interfaces/interface_variadic_test.v @@ -36,6 +36,98 @@ fn test_variadic_interface_fn_arg() { check_animals(c, d) } +// For issue 27326: passing a local variable declared inside a loop to a +// variadic interface parameter caused a `pointer expected` C error, because +// the value was promoted to auto-heap at the use site but not at its +// declaration, producing inconsistent pointer indirection in the generated C. +interface Value {} + +fn collect(params ...Value) int { + return params.len +} + +fn test_variadic_interface_arg_declared_in_loop() { + mut total := 0 + for i := 0; i < 3; i++ { + id := [u8(1), 2, 3] + code := 'hello ${i}' + total += collect(id, code) + } + assert total == 6 +} + +type ValueSum = Cat | Dog + +// For issue 27326: a variable smartcast via `if x is T` / `match` resolves to a +// synthetic smartcast scope var, not its real declaration. Promoting that +// synthetic var to auto-heap instead of the declaration must not desync the +// pointer indirection when the value is passed to a variadic interface param. +fn test_variadic_interface_arg_smartcast() { + mut total := 0 + s := ValueSum(Cat{}) + if s is Cat { + total += collect(s) + } + v := Value(Cat{}) + if v is Cat { + total += collect(v) + } + match v { + Cat { total += collect(v) } + else {} + } + + assert total == 3 +} + +// For issue 27326: a variable that is both declared in a nested scope (a `for` +// loop body) and smartcast via `if x is T` must still resolve to its real +// nested declaration, not the synthetic smartcast var nor only the function +// scope, otherwise the auto-heap promotion desyncs the pointer indirection. +fn test_variadic_interface_arg_smartcast_in_loop() { + mut total := 0 + for _ in 0 .. 3 { + s := ValueSum(Cat{}) + if s is Cat { + total += collect(s) + } + } + for _ in 0 .. 2 { + v := Value(Cat{}) + if v is Cat { + total += collect(v) + } + } + assert total == 5 +} + +fn maybe_value() ?Cat { + return Cat{} +} + +// For issue 27326: an option value unwrapped via an if-guard or `if x != none` +// and then passed to a variadic interface parameter is auto-heap promoted, so +// its declaration is emitted as an option pointer. The unwrapped `.data` read +// at the call site must use the matching pointer indirection (`->data`); +// previously cgen emitted value-style access and failed C compilation. +fn test_variadic_interface_arg_option_unwrap() { + mut total := 0 + if x := maybe_value() { + total += collect(x) + } + x := maybe_value() + if x != none { + total += collect(x) + } + for _ in 0 .. 2 { + y := maybe_value() + if y != none { + total += collect(y) + } + } + assert total == 4 +} + fn check_animals(animals ...Animal) { assert animals[0] is Cat assert animals[1] is Dog -- 2.39.5