From e614f08757587ebcb47b26dc87d36b2e56a595ce Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 24 May 2026 17:04:14 +0300 Subject: [PATCH] checker, cgen: allow consts referencing each other inside anon fn bodies (fix #27087) (#27224) --- vlib/v/checker/checker.v | 44 ++++++++++++++++++- vlib/v/gen/c/assign.v | 4 +- ...erencing_each_other_inside_anon_fns_test.v | 38 ++++++++++++++++ 3 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 vlib/v/tests/consts/const_referencing_each_other_inside_anon_fns_test.v diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index e55fcfd5a..a31f7c0a7 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -84,6 +84,7 @@ pub mut: strict_map_index_in_module bool // true if the current module has @[strict_map_index] attribute const_var &ast.ConstField = unsafe { nil } // the current constant, when checking const declarations const_deps []string + const_eval_stack []string // names of constants currently being recursively resolved (to break cycles via anon fn bodies) const_names []string global_names []string locked_names []string // vars that are currently locked @@ -3439,6 +3440,7 @@ fn (mut c Checker) const_decl(mut node ast.ConstDecl) { } for i, mut field in node.fields { c.const_deps << field.name + c.const_eval_stack << field.name prev_const_var := c.const_var c.const_var = unsafe { field } mut typ := c.check_expr_option_or_result_call(field.expr, c.expr(mut field.expr)) @@ -3476,6 +3478,7 @@ fn (mut c Checker) const_decl(mut node ast.ConstDecl) { } } c.const_deps = [] + c.const_eval_stack.pop() c.const_var = prev_const_var } } @@ -6350,7 +6353,9 @@ fn (mut c Checker) ident(mut node ast.Ident) ast.Type { // detect cycles, while allowing for references to the same constant, // used inside its initialisation like: `struct Abc { x &Abc } ... const a = [ Abc{0}, Abc{unsafe{&a[0]}} ]!` // see vlib/v/tests/const_fixed_array_containing_references_to_itself_test.v - if unsafe { c.const_var != 0 } && name == c.const_var.name { + // references inside anonymous function bodies are runtime-only, so they + // cannot create an initialisation-time cycle (fix #27087) + if unsafe { c.const_var != 0 } && name == c.const_var.name && !c.inside_anon_fn { if mut c.const_var.expr is ast.ArrayInit && c.const_var.expr.is_fixed && c.expected_type.nr_muls() > 0 { elem_typ := c.expected_type.deref() @@ -6366,7 +6371,9 @@ fn (mut c Checker) ident(mut node ast.Ident) ast.Type { c.error('cycle in constant `${c.const_var.name}`', node.pos) return ast.void_type } - c.const_deps << name + if !c.inside_anon_fn { + c.const_deps << name + } } if node.kind == .blank_ident { if node.tok_kind !in [.assign, .decl_assign] { @@ -6606,11 +6613,44 @@ fn (mut c Checker) ident(mut node ast.Ident) ast.Type { } mut typ := obj.typ if typ == 0 { + // a constant currently being recursively resolved is + // referenced again. Inside an anonymous function body the + // reference is runtime-only (fix #27087), so fall back to + // the expected type without caching it; otherwise this is + // a real multi-step initialisation cycle (e.g. a -> b -> a). + if obj.name in c.const_eval_stack { + if !c.inside_anon_fn { + c.error('cycle in constant `${obj.name}`', node.pos) + return ast.void_type + } + fallback_typ := if c.expected_type != ast.void_type { + c.expected_type + } else { + ast.void_type + } + node.name = name + node.kind = .constant + node.info = ast.IdentVar{ + typ: fallback_typ + } + node.obj = obj + c.mark_const_decl_as_referenced(obj.name) + return fallback_typ + } old_c_mod := c.mod c.mod = obj.mod inside_const := c.inside_const c.inside_const = true + // the constant's own initializer is evaluated at + // init-time, even if the caller is checking an + // anon function body; clear inside_anon_fn so a + // cycle hit during this recursion is reported. + inside_anon_fn := c.inside_anon_fn + c.inside_anon_fn = false + c.const_eval_stack << obj.name typ = c.expr(mut obj.expr) + c.const_eval_stack.pop() + c.inside_anon_fn = inside_anon_fn c.inside_const = inside_const c.mod = old_c_mod diff --git a/vlib/v/gen/c/assign.v b/vlib/v/gen/c/assign.v index f1d7aa50f..92ae7a12d 100644 --- a/vlib/v/gen/c/assign.v +++ b/vlib/v/gen/c/assign.v @@ -2133,10 +2133,12 @@ fn (mut g Gen) assign_stmt(node_ ast.AssignStmt) { g.inside_assign_fn_var = val is ast.PrefixExpr && val.op == .amp && is_fn_var mut nval := val + mut nval_handled := false if val is ast.PrefixExpr && val.right is ast.CallExpr { call_expr := val.right as ast.CallExpr if call_expr.name == 'new_array_from_c_array' { nval = call_expr + nval_handled = true if !var_type.has_flag(.shared_f) { g.write('HEAP(${g.styp(var_type.clear_ref())}, ') } @@ -2146,7 +2148,7 @@ fn (mut g Gen) assign_stmt(node_ ast.AssignStmt) { } } } - if nval == val { + if !nval_handled { if auto_heap_uses_existing_storage { g.write_auto_heap_assignment_expr(nval, val_type) } else { diff --git a/vlib/v/tests/consts/const_referencing_each_other_inside_anon_fns_test.v b/vlib/v/tests/consts/const_referencing_each_other_inside_anon_fns_test.v new file mode 100644 index 000000000..e41e757d2 --- /dev/null +++ b/vlib/v/tests/consts/const_referencing_each_other_inside_anon_fns_test.v @@ -0,0 +1,38 @@ +// Regression test for https://github.com/vlang/v/issues/27087 +// Constants that reference each other only inside anonymous function bodies +// are not a real initialisation-time cycle (the bodies run at runtime). + +type FnPtr = fn (voidptr) + +fn FnPtr.nop(_ voidptr) {} + +struct AppViewFn { +mut: + on_event FnPtr = FnPtr.nop + on_update FnPtr = FnPtr.nop + on_draw FnPtr = FnPtr.nop +} + +struct AppView { +mut: + on_fn AppViewFn +} + +const fn_idle = AppViewFn{ + on_event: fn (mut view AppView) { + view.on_fn = fn_track + } +} + +const fn_track = AppViewFn{ + on_event: fn (mut view AppView) { + view.on_fn = fn_idle + } +} + +fn test_mutual_const_references_inside_anon_fn_bodies_compile() { + v := AppView{ + on_fn: fn_idle + } + assert voidptr(v.on_fn.on_event) != unsafe { nil } +} -- 2.39.5