Skip to content

Commit b02a629

Browse files
authored
checker: fix static method used as map/filter argument (#27328) (#27331)
1 parent 93df383 commit b02a629

3 files changed

Lines changed: 95 additions & 7 deletions

File tree

vlib/v/checker/checker.v

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8372,17 +8372,27 @@ fn (mut c Checker) static_fn_value_from_enum_val(mut node ast.EnumVal, _ string,
83728372
return node.typ
83738373
}
83748374

8375-
fn (mut c Checker) enum_val_as_static_fn(mut node ast.EnumVal, typ_sym ast.TypeSymbol, fsym ast.TypeSymbol) ast.Type {
8376-
fn_name := '${typ_sym.name}__static__${node.val}'
8377-
if func := c.table.find_fn(fn_name) {
8378-
return c.static_fn_value_from_enum_val(mut node, fn_name, func)
8375+
// static_method_of_enum_val resolves the static method that an `ast.EnumVal`-shaped
8376+
// expression (`Type.method`, syntactically identical to an enum value `Color.red`)
8377+
// refers to. It also checks the final/unaliased symbol, so static methods reached
8378+
// through a supported type alias (`type Alias = Struct; Alias.new`) are found by
8379+
// their real fkey instead of the alias name.
8380+
fn (c &Checker) static_method_of_enum_val(node ast.EnumVal, typ_sym ast.TypeSymbol, fsym ast.TypeSymbol) ?ast.Fn {
8381+
if func := c.table.find_fn('${typ_sym.name}__static__${node.val}') {
8382+
return func
83798383
}
83808384
if fsym.name != typ_sym.name {
8381-
alias_fn_name := '${fsym.name}__static__${node.val}'
8382-
if func := c.table.find_fn(alias_fn_name) {
8383-
return c.static_fn_value_from_enum_val(mut node, alias_fn_name, func)
8385+
if func := c.table.find_fn('${fsym.name}__static__${node.val}') {
8386+
return func
83848387
}
83858388
}
8389+
return none
8390+
}
8391+
8392+
fn (mut c Checker) enum_val_as_static_fn(mut node ast.EnumVal, typ_sym ast.TypeSymbol, fsym ast.TypeSymbol) ast.Type {
8393+
if func := c.static_method_of_enum_val(node, typ_sym, fsym) {
8394+
return c.static_fn_value_from_enum_val(mut node, func.name, func)
8395+
}
83868396
return ast.void_type
83878397
}
83888398

vlib/v/checker/fn.v

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4930,6 +4930,34 @@ fn (mut c Checker) array_builtin_method_call(mut node ast.CallExpr, left_type as
49304930
}
49314931
arg_type = c.check_expr_option_or_result_call(arg.expr, expr_type)
49324932
}
4933+
// `arr.map(Type.from)`, `arr.filter(Type.ok)` etc.: a static method used as a
4934+
// first-class value is parsed as an `ast.EnumVal` (it is syntactically identical
4935+
// to an enum value, e.g. `Color.red`). The parser can only emit a function
4936+
// `ast.Ident` for it when the static method is already registered, which fails
4937+
// for cross-module (and forward) references, since imported modules are parsed
4938+
// after the call site. Once the checker has resolved the value to a function
4939+
// type, rewrite it into a function `ast.Ident`, so the backends call it and
4940+
// markused keeps the static method, exactly like any other function value.
4941+
if node.kind in [.map, .filter, .any, .all, .count] && node.args.len > 0 {
4942+
arg_expr := node.args[0].expr
4943+
if arg_expr is ast.EnumVal && c.table.sym(arg_expr.typ).kind == .function {
4944+
enum_typ := ast.new_type(c.table.find_type_idx(arg_expr.enum_name))
4945+
typ_sym := c.table.sym(enum_typ)
4946+
fsym := c.table.final_sym(enum_typ)
4947+
if func := c.static_method_of_enum_val(arg_expr, typ_sym, fsym) {
4948+
node.args[0].expr = ast.Ident{
4949+
name: func.name
4950+
mod: c.mod
4951+
kind: .function
4952+
info: ast.IdentFn{
4953+
typ: ast.new_type(c.table.find_or_register_fn_type(func, false, true))
4954+
}
4955+
pos: arg_expr.pos
4956+
scope: node.scope
4957+
}
4958+
}
4959+
}
4960+
}
49334961
if node.kind == .map {
49344962
// eprintln('>>>>>>> map node.args[0].expr: ${node.args[0].expr}, left_type: ${left_type} | elem_typ: ${elem_typ} | arg_type: ${arg_type}')
49354963
// check fn
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Regression test for a static method used as a first-class value in the
2+
// builtin array methods (`map`/`filter`/...). It is parsed as an `ast.EnumVal`
3+
// (same syntax as an enum value, e.g. `Color.red`); when the static method is
4+
// not yet registered while parsing the call site (forward reference here, or a
5+
// cross-module reference), the parser cannot emit a function `ast.Ident`, so the
6+
// checker has to rewrite the resolved value into one. See issue #27328.
7+
8+
fn test_static_method_as_map_arg() {
9+
nums := [1, 2, 3]
10+
foos := nums.map(Foo.from)
11+
assert foos.map(it.x) == [1, 2, 3]
12+
}
13+
14+
fn test_static_method_as_map_arg_explicit_call() {
15+
nums := [1, 2, 3]
16+
foos := nums.map(Foo.from(it))
17+
assert foos.map(it.x) == [1, 2, 3]
18+
}
19+
20+
fn test_static_method_as_filter_arg() {
21+
nums := [1, 2, 3, 4]
22+
odds := nums.filter(is_odd)
23+
assert odds == [1, 3]
24+
}
25+
26+
fn test_aliased_static_method_as_map_arg() {
27+
// the static method is reached through a type alias, so it must be resolved
28+
// by its real fkey (`Foo__static__from`), not the alias name
29+
nums := [1, 2, 3]
30+
foos := nums.map(FooAlias.from)
31+
assert foos.map(it.x) == [1, 2, 3]
32+
}
33+
34+
type FooAlias = Foo
35+
36+
// Defined *after* the call sites on purpose, to exercise the forward-reference
37+
// path (the function is not in the table yet when the calls above are parsed).
38+
struct Foo {
39+
x int
40+
}
41+
42+
fn Foo.from(n int) Foo {
43+
return Foo{
44+
x: n
45+
}
46+
}
47+
48+
fn is_odd(n int) bool {
49+
return n % 2 == 1
50+
}

0 commit comments

Comments
 (0)