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 7d87a00304b1a3..2dba6b4a8735c2 100644 --- a/vlib/v/checker/fn.v +++ b/vlib/v/checker/fn.v @@ -4930,6 +4930,34 @@ 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 { + 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: func.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..7fec1ce122cbcb --- /dev/null +++ b/vlib/v/tests/fns/static_method_as_map_arg_test.v @@ -0,0 +1,50 @@ +// 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] +} + +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 { + x int +} + +fn Foo.from(n int) Foo { + return Foo{ + x: n + } +} + +fn is_odd(n int) bool { + return n % 2 == 1 +}