From d3f9c67a9a7e2b86799329b234ec955891b8fdbd Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 24 May 2026 15:46:30 +0300 Subject: [PATCH] checker, cgen: fix `$for method in field.methods` dispatch (fix #27076) (#27226) --- vlib/v/checker/comptime.v | 7 ++ vlib/v/checker/used_features.v | 9 +++ vlib/v/gen/c/comptime.v | 6 +- .../comptime_for_field_methods_test.v | 66 +++++++++++++++++++ 4 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 vlib/v/tests/comptime/comptime_for_field_methods_test.v diff --git a/vlib/v/checker/comptime.v b/vlib/v/checker/comptime.v index 325237e70..1a8ae5228 100644 --- a/vlib/v/checker/comptime.v +++ b/vlib/v/checker/comptime.v @@ -777,6 +777,13 @@ fn (mut c Checker) comptime_for(mut node ast.ComptimeFor) { typ = c.unwrap_generic(node.typ) } } + // When the resolved type is FieldData, the expression refers to a comptime + // field variable (e.g. `$for method in field.methods` where `field` comes + // from an outer `$for field in T.fields`). In that case use the actual field + // type from the outer comptime loop instead of the FieldData descriptor type. + if node.typ == c.field_data_type { + typ = c.comptime.comptime_for_field_type + } sym := if node.typ != c.field_data_type { c.table.final_sym(typ) } else { diff --git a/vlib/v/checker/used_features.v b/vlib/v/checker/used_features.v index 9116673d8..22ef2304d 100644 --- a/vlib/v/checker/used_features.v +++ b/vlib/v/checker/used_features.v @@ -186,6 +186,15 @@ fn (mut c Checker) markused_method_call(mut node ast.CallExpr, mut left_expr ast c.table.used_features.comptime_calls['${int(left_type.ref())}.${node.name}'] = true } } + } else if !left_type.has_flag(.generic) && left_expr is ast.ComptimeSelector { + // `x.$(field.name).method()` — the concrete receiver type varies per + // outer `$for f in T.fields` iteration, and skip-unused has no way to + // see the runtime dispatch. Mark the per-iteration receiver type so + // the method survives -skip-unused. + c.table.used_features.comptime_calls['${int(left_type)}.${node.name}'] = true + if !left_type.is_ptr() { + c.table.used_features.comptime_calls['${int(left_type.ref())}.${node.name}'] = true + } } else if left_type.has_flag(.generic) { unwrapped_left := c.unwrap_generic(left_type) c.table.used_features.comptime_calls['${int(unwrapped_left)}.${node.name}'] = true diff --git a/vlib/v/gen/c/comptime.v b/vlib/v/gen/c/comptime.v index 85c1a409c..89c1315b9 100644 --- a/vlib/v/gen/c/comptime.v +++ b/vlib/v/gen/c/comptime.v @@ -329,9 +329,13 @@ fn (mut g Gen) resolve_comptime_selector_field(node ast.ComptimeSelector, left_t g.comptime.comptime_for_field_value.name } if node.field_expr is ast.SelectorExpr && g.comptime.comptime_for_field_var != '' - && g.comptime.comptime_for_method_var == '' && node.field_expr.field_name == 'name' { + && node.field_expr.field_name == 'name' { if node.field_expr.expr is ast.Ident && node.field_expr.expr.name == g.comptime.comptime_for_field_var { + // The checker stores typ_key per AST node, so the cached field name + // in `typ_key` is stale after iteration; always prefer the current + // outer-loop field value here (also necessary when this `$(field.name)` + // is used inside a nested `$for method in field.methods`). field_name = g.comptime.comptime_for_field_value.name } } diff --git a/vlib/v/tests/comptime/comptime_for_field_methods_test.v b/vlib/v/tests/comptime/comptime_for_field_methods_test.v new file mode 100644 index 000000000..8405705f5 --- /dev/null +++ b/vlib/v/tests/comptime/comptime_for_field_methods_test.v @@ -0,0 +1,66 @@ +// Regression test for https://github.com/vlang/v/issues/27076 — +// `$for method in field.methods` inside `$for field in T.fields` evaluated +// `$if method.name == ...` against an empty method name and dropped the body. + +struct Comp1 { +mut: + a int +} + +fn (mut c Comp1) on_init() { + c.a = 42 +} + +struct Comp2 { +mut: + b int +} + +fn (mut c Comp2) on_init() { + c.b = 42 +} + +struct Comp3 { +mut: + c int +} + +fn (mut c Comp3) on_init() { + c.c = 42 +} + +struct Container { +mut: + id string + cmp1 Comp1 + cmp2 Comp2 + cmp3 Comp3 + cmp4 Comp1 + cmp5 Comp2 + cmp6 Comp3 + cmp7 Comp1 + cmp8 Comp2 + cmp9 Comp3 +} + +fn test_comptime_method_dispatch_on_struct_fields() { + mut app := Container{} + $for field in Container.fields { + $if field.is_struct { + $for method in field.methods { + $if method.name == 'on_init' { + app.$(field.name).on_init() + } + } + } + } + assert app.cmp1.a == 42 + assert app.cmp2.b == 42 + assert app.cmp3.c == 42 + assert app.cmp4.a == 42 + assert app.cmp5.b == 42 + assert app.cmp6.c == 42 + assert app.cmp7.a == 42 + assert app.cmp8.b == 42 + assert app.cmp9.c == 42 +} -- 2.39.5