From 3a2fbfe17ef6859959c98da81fdc512ba31b0679 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 25 Mar 2026 16:42:25 +0300 Subject: [PATCH] checker: fix generics relying on symbol declaration order (fixes #24678) --- vlib/v/ast/table.v | 62 ++++-- vlib/v/ast/types.v | 17 +- vlib/v/checker/checker.v | 3 +- vlib/v/checker/fn.v | 29 ++- vlib/v/checker/struct.v | 8 +- vlib/v/parser/parse_type.v | 42 +++- vlib/v/parser/parser.v | 197 +++++++++--------- ...eturn_later_declared_generic_struct_test.v | 17 ++ 8 files changed, 244 insertions(+), 131 deletions(-) create mode 100644 vlib/v/tests/fns/generic_fn_return_later_declared_generic_struct_test.v diff --git a/vlib/v/ast/table.v b/vlib/v/ast/table.v index c62ce1713..54de2382e 100644 --- a/vlib/v/ast/table.v +++ b/vlib/v/ast/table.v @@ -1041,6 +1041,25 @@ pub fn (mut t Table) update_sym_by_idx(existing_idx int, sym &TypeSymbol) { } } +fn (mut t Table) promote_placeholder_generic_children(parent_idx int, sym TypeSymbol) { + for i, child in t.type_symbols { + if child.kind != .placeholder || child.parent_idx != parent_idx + || child.generic_types.len == 0 { + continue + } + t.update_sym_by_idx(i, &TypeSymbol{ + ...sym + name: child.name + cname: child.cname + ngname: child.ngname + rname: if child.rname == '' { sym.name } else { child.rname } + parent_idx: parent_idx + methods: child.methods + generic_types: child.generic_types.clone() + }) + } +} + fn (mut t Table) rewrite_already_registered_symbol(typ TypeSymbol, existing_idx int) int { existing_symbol := t.type_symbols[existing_idx] $if trace_rewrite_already_registered_symbol ? { @@ -1056,6 +1075,7 @@ fn (mut t Table) rewrite_already_registered_symbol(typ TypeSymbol, existing_idx idx: existing_idx is_builtin: existing_symbol.is_builtin } + t.promote_placeholder_generic_children(existing_idx, typ) return existing_idx } // Allow overwriting a generic_inst with a more complete concrete type definition @@ -3235,7 +3255,8 @@ fn (mut t Table) unwrap_generic_type_ex_with_depth(typ Type, generic_names []str } } } - nrt = '${ts.name}[' + base_name := if ts.ngname == '' { ts.name } else { ts.ngname } + nrt = '${base_name}[' c_nrt = '${ts.cname}_T_' for i in 0 .. ts.info.generic_types.len { if ct := t.convert_generic_type(ts.info.generic_types[i], t_generic_names, @@ -3365,12 +3386,13 @@ fn (mut t Table) unwrap_generic_type_ex_with_depth(typ Type, generic_names []str info.parent_type = typ.set_flag(.generic) info.fields = fields new_idx := t.register_sym( - kind: .struct - name: nrt - cname: util.no_dots(c_nrt) - mod: ts.mod - info: info - is_pub: ts.is_pub + kind: .struct + name: nrt + cname: util.no_dots(c_nrt) + parent_idx: typ.idx() + mod: ts.mod + info: info + is_pub: ts.is_pub ) if final_concrete_types.len > 0 { t.unwrap_method_types(ts, generic_names, concrete_types, final_concrete_types) @@ -3404,12 +3426,13 @@ fn (mut t Table) unwrap_generic_type_ex_with_depth(typ Type, generic_names []str info.fields = fields info.variants = variants new_idx := t.register_sym( - kind: .sum_type - name: nrt - cname: util.no_dots(c_nrt) - mod: ts.mod - info: info - is_pub: ts.is_pub + kind: .sum_type + name: nrt + cname: util.no_dots(c_nrt) + parent_idx: typ.idx() + mod: ts.mod + info: info + is_pub: ts.is_pub ) if final_concrete_types.len > 0 { t.unwrap_method_types(ts, generic_names, concrete_types, final_concrete_types) @@ -3450,12 +3473,13 @@ fn (mut t Table) unwrap_generic_type_ex_with_depth(typ Type, generic_names []str info.fields = fields info.methods = imethods new_idx := t.register_sym( - kind: .interface - name: nrt - cname: util.no_dots(c_nrt) - mod: ts.mod - info: info - is_pub: ts.is_pub + kind: .interface + name: nrt + cname: util.no_dots(c_nrt) + parent_idx: typ.idx() + mod: ts.mod + info: info + is_pub: ts.is_pub ) mut ts_copy := t.sym(idx_to_type(new_idx)) for method in all_methods { diff --git a/vlib/v/ast/types.v b/vlib/v/ast/types.v index 6fe56a098..e122d4548 100644 --- a/vlib/v/ast/types.v +++ b/vlib/v/ast/types.v @@ -1717,10 +1717,21 @@ pub fn (t &Table) type_to_str_using_aliases(typ Type, import_aliases map[string] if typ.has_flag(.generic) { match sym.info { Struct, Interface, SumType { + base_name := if sym.ngname == '' { + strip_extra_struct_types(res) + } else { + sym.ngname + } + res = t.shorten_user_defined_typenames(base_name, import_aliases) + generic_types := if sym.generic_types.len > 0 { + sym.generic_types + } else { + sym.info.generic_types + } res += '[' - for i, gtyp in sym.info.generic_types { - res += t.sym(gtyp).name - if i != sym.info.generic_types.len - 1 { + for i, gtyp in generic_types { + res += t.type_to_str_using_aliases(gtyp, import_aliases) + if i != generic_types.len - 1 { res += ', ' } } diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index d87a0fb85..2483d1b05 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -1144,8 +1144,7 @@ and use a reference to the sum type instead: `var := &${node.name}(${variant_nam if typ !in node.generic_types { sumtype_type_names := node.generic_types.map(c.table.type_to_str(it)).join(', ') generic_sumtype_name := '${node.name}[${sumtype_type_names}]' - variant_type_names := sym.info.generic_types.map(c.table.type_to_str(it)).join(', ') - generic_variant_name := '${sym.name}[${variant_type_names}]' + generic_variant_name := c.table.type_to_str(variant.typ) c.error('generic type name `${c.table.sym(typ).name}` of generic struct `${generic_variant_name}` is not mentioned in sumtype `${generic_sumtype_name}`', variant.pos) } diff --git a/vlib/v/checker/fn.v b/vlib/v/checker/fn.v index 4b78222c6..b652fa322 100644 --- a/vlib/v/checker/fn.v +++ b/vlib/v/checker/fn.v @@ -2957,7 +2957,20 @@ fn (mut c Checker) method_call(mut node ast.CallExpr, mut continue_check &bool) if field := c.table.find_field(left_sym, method_name) { unknown_method_msg = 'unknown method `${field.name}` did you mean to access the field with the same name instead?' } else { - sname := left_sym.symbol_name_except_generic() + mut sname := left_sym.symbol_name_except_generic() + match left_sym.info { + ast.Struct, ast.Interface, ast.SumType { + if left_sym.info.concrete_types.len > 0 + && left_sym.info.parent_type.has_flag(.generic) { + sname = c.table.sym(left_sym.info.parent_type).symbol_name_except_generic() + } + } + else {} + } + if left_sym.generic_types.len > 0 { + generic_names := left_sym.generic_types.map(c.table.sym(it).name).join(', ') + sname = '${left_sym.ngname}<${generic_names}>' + } name := sname.replace_each(['<', '[', '>', ']']) unknown_method_msg = 'unknown method or field: `${name}.${method_name}`' } @@ -3256,6 +3269,11 @@ fn (mut c Checker) method_call(mut node ast.CallExpr, mut continue_check &bool) c.table.register_fn_concrete_types(method.fkey(), concrete_types) } } + method_concrete_types := if method_generic_names_len == rec_concrete_types.len { + rec_concrete_types + } else { + concrete_types + } if exp_arg_typ.has_flag(.generic) { has_unresolved_generic_param = c.check_unresolved_generic_param(node, arg) || has_unresolved_generic_param @@ -3335,7 +3353,14 @@ fn (mut c Checker) method_call(mut node ast.CallExpr, mut continue_check &bool) if c.is_optional_array_arg_compatible(got_arg_typ, exp_arg_typ) { continue } - c.error('${err.msg()} in argument ${i + 1} to `${left_sym.name}.${method_name}`', + receiver_name := if method.receiver_type.has_flag(.generic) + && method_concrete_types.len > 0 { + c.table.type_to_str(c.table.unwrap_generic_type(method.receiver_type.set_nr_muls(0), + method.generic_names, method_concrete_types)) + } else { + left_sym.name + } + c.error('${err.msg()} in argument ${i + 1} to `${receiver_name}.${method_name}`', arg.pos) } if mut arg.expr is ast.LambdaExpr { diff --git a/vlib/v/checker/struct.v b/vlib/v/checker/struct.v index c48dff31b..03fe649f1 100644 --- a/vlib/v/checker/struct.v +++ b/vlib/v/checker/struct.v @@ -419,13 +419,11 @@ fn (mut c Checker) struct_decl(mut node ast.StructDecl) { t_sym := c.table.sym(t.typ) if t_sym.info is ast.Interface { if t_sym.info.is_generic { - itype_name := c.table.type_to_str(t.typ) - if !itype_name.contains('[') { + if t_sym.generic_types.len == 0 { c.error('missing generic type on ${t_sym.name}', t.pos) - } - if itype_name.contains('<') { + } else { struct_generic_letters := node.generic_types.map(c.table.type_to_str(it)) - unknown_letters := itype_name.all_after('<').all_before('>').split(',').filter(it !in struct_generic_letters) + unknown_letters := t_sym.generic_types.filter(it.has_flag(.generic)).map(c.table.type_to_str(it)).filter(it !in struct_generic_letters) if unknown_letters.len > 0 { c.error('unknown generic type ${unknown_letters.first()}', t.pos) diff --git a/vlib/v/parser/parse_type.v b/vlib/v/parser/parse_type.v index a530f85ff..cb4927814 100644 --- a/vlib/v/parser/parse_type.v +++ b/vlib/v/parser/parse_type.v @@ -835,15 +835,45 @@ fn (mut p Parser) parse_any_type(language ast.Language, is_ptr bool, check_dot b } } +fn (mut p Parser) find_or_register_placeholder_generic_type(sym &ast.TypeSymbol) ast.Type { + generic_names := p.types_to_names(p.init_generic_types, p.tok.pos(), 'struct_init_generic_types') or { + return ast.no_type + } + mut sym_name := sym.name + '<' + for i, gt in generic_names { + sym_name += gt + if i != generic_names.len - 1 { + sym_name += ',' + } + } + sym_name += '>' + existing_idx := p.table.type_idxs[sym_name] + if existing_idx > 0 { + return ast.new_type(existing_idx) + } + idx := p.table.register_sym(ast.TypeSymbol{ + ...*sym + name: sym_name + rname: sym.name + parent_idx: sym.idx + generic_types: p.init_generic_types.clone() + }) + return ast.new_type(idx) +} + fn (mut p Parser) find_type_or_add_placeholder(name string, language ast.Language) ast.Type { // struct / enum / placeholder mut idx := p.table.find_type_idx_fn_scoped(name, p.cur_fn_scope) if idx > 0 { mut typ := ast.new_type(idx) sym := p.table.sym(typ) + if sym.kind == .placeholder && p.consume_init_generic_types && p.init_generic_types.len > 0 { + return p.find_or_register_placeholder_generic_type(sym) + } match sym.info { ast.Struct, ast.Interface, ast.SumType { - if p.init_generic_types.len > 0 && sym.info.generic_types.len > 0 + if p.consume_init_generic_types && p.init_generic_types.len > 0 + && sym.info.generic_types.len > 0 && p.init_generic_types != sym.info.generic_types { generic_names := p.types_to_names(p.init_generic_types, p.tok.pos(), 'struct_init_generic_types') or { return ast.no_type } @@ -883,7 +913,8 @@ fn (mut p Parser) find_type_or_add_placeholder(name string, language ast.Languag } } ast.FnType { - if p.init_generic_types.len > 0 && sym.info.func.generic_names.len > 0 { + if p.consume_init_generic_types && p.init_generic_types.len > 0 + && sym.info.func.generic_names.len > 0 { generic_names := p.types_to_names(p.init_generic_types, p.tok.pos(), 'struct_init_generic_types') or { return ast.no_type } if generic_names != sym.info.func.generic_names { @@ -930,6 +961,9 @@ fn (mut p Parser) find_type_or_add_placeholder(name string, language ast.Languag } // not found - add placeholder idx = p.table.add_placeholder_type(name, name, language) + if p.consume_init_generic_types && p.init_generic_types.len > 0 { + return p.find_or_register_placeholder_generic_type(p.table.sym(ast.new_type(idx))) + } return ast.new_type(idx) } @@ -1065,6 +1099,10 @@ fn (mut p Parser) parse_generic_inst_type(name string, name_pos token.Pos) ast.T }) return ast.new_type(idx) } + p.consume_init_generic_types = true + defer { + p.consume_init_generic_types = false + } return p.find_type_or_add_placeholder(name, .v).set_flag(.generic) } diff --git a/vlib/v/parser/parser.v b/vlib/v/parser/parser.v index d67f05432..604aaf592 100644 --- a/vlib/v/parser/parser.v +++ b/vlib/v/parser/parser.v @@ -25,104 +25,105 @@ mut: unique_prefix string // a hash of p.file_path, used for making anon fn generation unique file_backend_mode ast.Language // .c for .c.v|.c.vv|.c.vsh files; .js for .js.v files, .amd64/.rv32/other arches for .amd64.v/.rv32.v/etc. files, .v otherwise. // see comment in parse_file - tok token.Token - prev_tok token.Token - peek_tok token.Token - language ast.Language - fn_language ast.Language // .c for `fn C.abcd()` declarations - struct_language ast.Language // for `struct C.abcd{ embedded struct/union }` declarations - expr_level int // prevent too deep recursions for pathological programs - inside_vlib_file bool // true for all vlib/ files - inside_test_file bool // when inside _test.v or _test.vv file - inside_if bool - inside_comptime_if bool - inside_if_expr bool - inside_if_cond bool - inside_ct_if_expr bool - inside_or_expr bool - inside_for bool - inside_for_expr bool - inside_fn bool // true even with implicit main - inside_fn_return bool - inside_fn_param bool // true while parsing function parameter types - inside_fn_concrete_type bool // parsing fn_name[concrete_type]() call expr - inside_call_args bool // true inside f( .... ) - inside_unsafe_fn bool - inside_str_interp bool - inside_array_lit bool - inside_in_array bool - inside_infix bool - inside_assign_rhs bool // rhs assignment - inside_match bool // to separate `match A { }` from `Struct{}` - inside_select bool // to allow `ch <- Struct{} {` inside `select` - inside_match_case bool // to separate `match_expr { }` from `Struct{}` - inside_match_body bool // to fix eval not used TODO - inside_ct_match bool - inside_ct_match_case bool - inside_ct_match_body bool - inside_unsafe bool - inside_sum_type bool // to prevent parsing inline sum type again - inside_asm_template bool - inside_asm bool - inside_defer bool - defer_mode ast.DeferMode - inside_generic_params bool // indicates if parsing between `<` and `>` of a method/function - inside_receiver_param bool // indicates if parsing the receiver parameter inside the first `(` and `)` of a method - inside_struct_field_decl bool - inside_struct_attr_decl bool - inside_map_init bool - inside_orm bool - inside_chan_decl bool - inside_attr_decl bool - inside_lock_exprs bool - array_dim int // array dim parsing level - fixed_array_dim int // fixed array dim parsing level - or_is_handled bool // ignore `or` in this expression - builtin_mod bool // are we in the `builtin` module? - mod string // current module name - is_manualfree bool // true when `@[manualfree] module abc`, makes *all* fns in the current .v file, opt out of autofree - has_globals bool // `@[has_globals] module abc` - allow globals declarations, even without -enable-globals, in that single .v file __only__ - is_generated bool // `@[generated] module abc` - turn off compiler notices for that single .v file __only__. - is_translated bool // `@[translated] module abc` - mark a file as translated, to relax some compiler checks for translated code. - attrs []ast.Attr // attributes before next decl stmt - expr_mod string // for constructing full type names in parse_type() - last_enum_name string // saves the last enum name on an array initialization - last_enum_mod string // saves the last enum mod name on an array initialization - imports map[string]string // alias => mod_name - ast_imports []ast.Import // mod_names - used_imports []string - auto_imports []string // imports, the user does not need to specify - implied_imports []string // ​imports that the user's code uses but omitted to import explicitly, used by `vfmt` - imported_symbols map[string]string - imported_symbols_used map[string]bool - imported_symbols_trie token.KeywordsMatcherTrie - is_amp bool // for generating the right code for `&Foo{}` - returns bool - is_stmt_ident bool // true while the beginning of a statement is an ident/selector - expecting_type bool // `is Type`, expecting type - expecting_value bool = true // true where a node value will be used - cur_fn_name string - cur_fn_scope &ast.Scope = unsafe { nil } - label_names []string - name_error bool // indicates if the token is not a name or the name is on another line - n_asm int // controls assembly labels - global_labels []string - comptime_if_cond bool - defer_vars []ast.Ident - should_abort bool // when too many errors/warnings/notices are accumulated, should_abort becomes true, and the parser should stop - codegen_text string - anon_struct_decl ast.StructDecl - init_generic_types []ast.Type - if_cond_comments []ast.Comment - left_comments []ast.Comment - script_mode bool - script_mode_start_token token.Token - generic_type_level int // to avoid infinite recursion segfaults due to compiler bugs in ensure_type_exists - main_already_defined bool // TODO move to checker - is_vls bool - is_vls_skip_file bool // in `vls` mode, skip parse and check for unrelated files, such as `vlib` - inside_import_section bool - cur_comments []ast.Comment // comments between other stmts + tok token.Token + prev_tok token.Token + peek_tok token.Token + language ast.Language + fn_language ast.Language // .c for `fn C.abcd()` declarations + struct_language ast.Language // for `struct C.abcd{ embedded struct/union }` declarations + expr_level int // prevent too deep recursions for pathological programs + inside_vlib_file bool // true for all vlib/ files + inside_test_file bool // when inside _test.v or _test.vv file + inside_if bool + inside_comptime_if bool + inside_if_expr bool + inside_if_cond bool + inside_ct_if_expr bool + inside_or_expr bool + inside_for bool + inside_for_expr bool + inside_fn bool // true even with implicit main + inside_fn_return bool + inside_fn_param bool // true while parsing function parameter types + inside_fn_concrete_type bool // parsing fn_name[concrete_type]() call expr + inside_call_args bool // true inside f( .... ) + inside_unsafe_fn bool + inside_str_interp bool + inside_array_lit bool + inside_in_array bool + inside_infix bool + inside_assign_rhs bool // rhs assignment + inside_match bool // to separate `match A { }` from `Struct{}` + inside_select bool // to allow `ch <- Struct{} {` inside `select` + inside_match_case bool // to separate `match_expr { }` from `Struct{}` + inside_match_body bool // to fix eval not used TODO + inside_ct_match bool + inside_ct_match_case bool + inside_ct_match_body bool + inside_unsafe bool + inside_sum_type bool // to prevent parsing inline sum type again + inside_asm_template bool + inside_asm bool + inside_defer bool + defer_mode ast.DeferMode + inside_generic_params bool // indicates if parsing between `<` and `>` of a method/function + inside_receiver_param bool // indicates if parsing the receiver parameter inside the first `(` and `)` of a method + inside_struct_field_decl bool + inside_struct_attr_decl bool + inside_map_init bool + inside_orm bool + inside_chan_decl bool + inside_attr_decl bool + inside_lock_exprs bool + array_dim int // array dim parsing level + fixed_array_dim int // fixed array dim parsing level + or_is_handled bool // ignore `or` in this expression + builtin_mod bool // are we in the `builtin` module? + mod string // current module name + is_manualfree bool // true when `@[manualfree] module abc`, makes *all* fns in the current .v file, opt out of autofree + has_globals bool // `@[has_globals] module abc` - allow globals declarations, even without -enable-globals, in that single .v file __only__ + is_generated bool // `@[generated] module abc` - turn off compiler notices for that single .v file __only__. + is_translated bool // `@[translated] module abc` - mark a file as translated, to relax some compiler checks for translated code. + attrs []ast.Attr // attributes before next decl stmt + expr_mod string // for constructing full type names in parse_type() + last_enum_name string // saves the last enum name on an array initialization + last_enum_mod string // saves the last enum mod name on an array initialization + imports map[string]string // alias => mod_name + ast_imports []ast.Import // mod_names + used_imports []string + auto_imports []string // imports, the user does not need to specify + implied_imports []string // ​imports that the user's code uses but omitted to import explicitly, used by `vfmt` + imported_symbols map[string]string + imported_symbols_used map[string]bool + imported_symbols_trie token.KeywordsMatcherTrie + is_amp bool // for generating the right code for `&Foo{}` + returns bool + is_stmt_ident bool // true while the beginning of a statement is an ident/selector + expecting_type bool // `is Type`, expecting type + expecting_value bool = true // true where a node value will be used + cur_fn_name string + cur_fn_scope &ast.Scope = unsafe { nil } + label_names []string + name_error bool // indicates if the token is not a name or the name is on another line + n_asm int // controls assembly labels + global_labels []string + comptime_if_cond bool + defer_vars []ast.Ident + should_abort bool // when too many errors/warnings/notices are accumulated, should_abort becomes true, and the parser should stop + codegen_text string + anon_struct_decl ast.StructDecl + init_generic_types []ast.Type + consume_init_generic_types bool + if_cond_comments []ast.Comment + left_comments []ast.Comment + script_mode bool + script_mode_start_token token.Token + generic_type_level int // to avoid infinite recursion segfaults due to compiler bugs in ensure_type_exists + main_already_defined bool // TODO move to checker + is_vls bool + is_vls_skip_file bool // in `vls` mode, skip parse and check for unrelated files, such as `vlib` + inside_import_section bool + cur_comments []ast.Comment // comments between other stmts pub mut: scanner &scanner.Scanner = unsafe { nil } table &ast.Table = unsafe { nil } diff --git a/vlib/v/tests/fns/generic_fn_return_later_declared_generic_struct_test.v b/vlib/v/tests/fns/generic_fn_return_later_declared_generic_struct_test.v new file mode 100644 index 000000000..67d422db1 --- /dev/null +++ b/vlib/v/tests/fns/generic_fn_return_later_declared_generic_struct_test.v @@ -0,0 +1,17 @@ +fn foo_24678[A, B](name string, data A) Response24678[B] { + return Response24678[B]{ + response: B{} + err: 'none' + } +} + +struct Response24678[T] { + response T + err string +} + +fn test_generic_fn_return_can_reference_later_declared_generic_struct() { + x := foo_24678[string, int]('foo', '') + assert x.response == 0 + assert x.err == 'none' +} -- 2.39.5