From 9a465e6b96d42f728e54f7aeeaffabb0d833bbfa Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Tue, 2 Jun 2026 20:36:17 +0300 Subject: [PATCH 1/2] checker: fix static method used as `map`/`filter` argument (#27328) A static method used as a first-class value, e.g. `arr.map(Type.from)`, is parsed as an `ast.EnumVal`, since `Type.from` is syntactically identical to an enum value (`Color.red`). The parser only rewrites it into a function `ast.Ident` when the static method is already registered in the function table, which is not the case for cross-module references (imported modules are parsed after the call site) nor for forward references within a file. In those cases the `EnumVal` reached the backend, which emitted the static method's name as a bare value without calling it (and markused did not keep the method, so it was never generated) -> `undeclared identifier` / `unknown enum`. Resolve the value in the checker: in `array_builtin_method_call`, once the callback argument of `map`/`filter`/`any`/`all`/`count` has been resolved to a function type, rewrite the `ast.EnumVal` into a function `ast.Ident`, so the backends call it and markused keeps the static method, exactly like any other function value. --- vlib/v/checker/fn.v | 26 ++++++++++++ .../tests/fns/static_method_as_map_arg_test.v | 40 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 vlib/v/tests/fns/static_method_as_map_arg_test.v diff --git a/vlib/v/checker/fn.v b/vlib/v/checker/fn.v index 7d87a00304b1a3..8b1ed0576d0729 100644 --- a/vlib/v/checker/fn.v +++ b/vlib/v/checker/fn.v @@ -4930,6 +4930,32 @@ fn (mut c Checker) array_builtin_method_call(mut node ast.CallExpr, left_type as } arg_type = c.check_expr_option_or_result_call(arg.expr, expr_type) } + // `arr.map(Type.from)`, `arr.filter(Type.ok)` etc.: a static method used as a + // first-class value is parsed as an `ast.EnumVal` (it is syntactically identical + // to an enum value, e.g. `Color.red`). The parser can only emit a function + // `ast.Ident` for it when the static method is already registered, which fails + // for cross-module (and forward) references, since imported modules are parsed + // after the call site. Once the checker has resolved the value to a function + // type, rewrite it into a function `ast.Ident`, so the backends call it and + // markused keeps the static method, exactly like any other function value. + if node.kind in [.map, .filter, .any, .all, .count] && node.args.len > 0 { + arg_expr := node.args[0].expr + if arg_expr is ast.EnumVal && c.table.sym(arg_expr.typ).kind == .function { + static_fn_name := '${arg_expr.enum_name}__static__${arg_expr.val}' + if func := c.table.find_fn(static_fn_name) { + node.args[0].expr = ast.Ident{ + name: static_fn_name + mod: c.mod + kind: .function + info: ast.IdentFn{ + typ: ast.new_type(c.table.find_or_register_fn_type(func, false, true)) + } + pos: arg_expr.pos + scope: node.scope + } + } + } + } if node.kind == .map { // eprintln('>>>>>>> map node.args[0].expr: ${node.args[0].expr}, left_type: ${left_type} | elem_typ: ${elem_typ} | arg_type: ${arg_type}') // check fn diff --git a/vlib/v/tests/fns/static_method_as_map_arg_test.v b/vlib/v/tests/fns/static_method_as_map_arg_test.v new file mode 100644 index 00000000000000..07f46fcc425b09 --- /dev/null +++ b/vlib/v/tests/fns/static_method_as_map_arg_test.v @@ -0,0 +1,40 @@ +// Regression test for a static method used as a first-class value in the +// builtin array methods (`map`/`filter`/...). It is parsed as an `ast.EnumVal` +// (same syntax as an enum value, e.g. `Color.red`); when the static method is +// not yet registered while parsing the call site (forward reference here, or a +// cross-module reference), the parser cannot emit a function `ast.Ident`, so the +// checker has to rewrite the resolved value into one. See issue #27328. + +fn test_static_method_as_map_arg() { + nums := [1, 2, 3] + foos := nums.map(Foo.from) + assert foos.map(it.x) == [1, 2, 3] +} + +fn test_static_method_as_map_arg_explicit_call() { + nums := [1, 2, 3] + foos := nums.map(Foo.from(it)) + assert foos.map(it.x) == [1, 2, 3] +} + +fn test_static_method_as_filter_arg() { + nums := [1, 2, 3, 4] + odds := nums.filter(is_odd) + assert odds == [1, 3] +} + +// Defined *after* the call sites on purpose, to exercise the forward-reference +// path (the function is not in the table yet when the calls above are parsed). +struct Foo { + x int +} + +fn Foo.from(n int) Foo { + return Foo{ + x: n + } +} + +fn is_odd(n int) bool { + return n % 2 == 1 +} From 035ae39bcdff45bfe3ae9273544015dd33f829d7 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 3 Jun 2026 21:06:46 +0300 Subject: [PATCH 2/2] checker: resolve aliased static methods in map/filter args (#27328) Address review feedback: when the callback is a static method reached through a type alias (`type Alias = Struct; arr.map(Alias.new)`), rebuilding the function name from the EnumVal's `enum_name` produced `Alias__static__new`, which is not a registered function, so the value was left as an EnumVal (markused dropped the method, cgen emitted an alias-named, non-existent function). Factor the alias-aware lookup used by `enum_val_as_static_fn` into a shared `static_method_of_enum_val` helper (it also checks the final/unaliased symbol) and reuse it at the rewrite site, taking the resolved function's real name. --- vlib/v/checker/checker.v | 24 +++++++++++++------ vlib/v/checker/fn.v | 8 ++++--- .../tests/fns/static_method_as_map_arg_test.v | 10 ++++++++ 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index 7fcceacb42a1e4..4533f2cab482f0 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -8372,17 +8372,27 @@ fn (mut c Checker) static_fn_value_from_enum_val(mut node ast.EnumVal, _ string, return node.typ } -fn (mut c Checker) enum_val_as_static_fn(mut node ast.EnumVal, typ_sym ast.TypeSymbol, fsym ast.TypeSymbol) ast.Type { - fn_name := '${typ_sym.name}__static__${node.val}' - if func := c.table.find_fn(fn_name) { - return c.static_fn_value_from_enum_val(mut node, fn_name, func) +// static_method_of_enum_val resolves the static method that an `ast.EnumVal`-shaped +// expression (`Type.method`, syntactically identical to an enum value `Color.red`) +// refers to. It also checks the final/unaliased symbol, so static methods reached +// through a supported type alias (`type Alias = Struct; Alias.new`) are found by +// their real fkey instead of the alias name. +fn (c &Checker) static_method_of_enum_val(node ast.EnumVal, typ_sym ast.TypeSymbol, fsym ast.TypeSymbol) ?ast.Fn { + if func := c.table.find_fn('${typ_sym.name}__static__${node.val}') { + return func } if fsym.name != typ_sym.name { - alias_fn_name := '${fsym.name}__static__${node.val}' - if func := c.table.find_fn(alias_fn_name) { - return c.static_fn_value_from_enum_val(mut node, alias_fn_name, func) + if func := c.table.find_fn('${fsym.name}__static__${node.val}') { + return func } } + return none +} + +fn (mut c Checker) enum_val_as_static_fn(mut node ast.EnumVal, typ_sym ast.TypeSymbol, fsym ast.TypeSymbol) ast.Type { + if func := c.static_method_of_enum_val(node, typ_sym, fsym) { + return c.static_fn_value_from_enum_val(mut node, func.name, func) + } return ast.void_type } diff --git a/vlib/v/checker/fn.v b/vlib/v/checker/fn.v index 8b1ed0576d0729..2dba6b4a8735c2 100644 --- a/vlib/v/checker/fn.v +++ b/vlib/v/checker/fn.v @@ -4941,10 +4941,12 @@ fn (mut c Checker) array_builtin_method_call(mut node ast.CallExpr, left_type as if node.kind in [.map, .filter, .any, .all, .count] && node.args.len > 0 { arg_expr := node.args[0].expr if arg_expr is ast.EnumVal && c.table.sym(arg_expr.typ).kind == .function { - static_fn_name := '${arg_expr.enum_name}__static__${arg_expr.val}' - if func := c.table.find_fn(static_fn_name) { + enum_typ := ast.new_type(c.table.find_type_idx(arg_expr.enum_name)) + typ_sym := c.table.sym(enum_typ) + fsym := c.table.final_sym(enum_typ) + if func := c.static_method_of_enum_val(arg_expr, typ_sym, fsym) { node.args[0].expr = ast.Ident{ - name: static_fn_name + name: func.name mod: c.mod kind: .function info: ast.IdentFn{ diff --git a/vlib/v/tests/fns/static_method_as_map_arg_test.v b/vlib/v/tests/fns/static_method_as_map_arg_test.v index 07f46fcc425b09..7fec1ce122cbcb 100644 --- a/vlib/v/tests/fns/static_method_as_map_arg_test.v +++ b/vlib/v/tests/fns/static_method_as_map_arg_test.v @@ -23,6 +23,16 @@ fn test_static_method_as_filter_arg() { assert odds == [1, 3] } +fn test_aliased_static_method_as_map_arg() { + // the static method is reached through a type alias, so it must be resolved + // by its real fkey (`Foo__static__from`), not the alias name + nums := [1, 2, 3] + foos := nums.map(FooAlias.from) + assert foos.map(it.x) == [1, 2, 3] +} + +type FooAlias = Foo + // Defined *after* the call sites on purpose, to exercise the forward-reference // path (the function is not in the table yet when the calls above are parsed). struct Foo {