From 9695dbbd726126c223de994e22df7f8fce8f5cf1 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 28 Oct 2026 03:04:02 +0300 Subject: [PATCH] types: avoid interning and decoding type spellings in checker name filters (#31277) Sum/alias short-name filters, builtin-symbol probes, sum constructor callee checks, generic cast callee checks and receiver method suffix indexing now work on existing spelling ids or raw text instead of interning and re-parsing every name they see. A spelling that was never interned cannot be a collected struct, enum or sum type, so the probes return early without entering the interner. --- vlib/v/types/checker.v | 65 ++++++++++++++++++++------------- vlib/v/types/checker_comptime.v | 17 ++++++++- vlib/v/types/checker_tail.v | 29 ++++++++++++--- 3 files changed, 78 insertions(+), 33 deletions(-) diff --git a/vlib/v/types/checker.v b/vlib/v/types/checker.v index 6ea0f35871..280411d429 100644 --- a/vlib/v/types/checker.v +++ b/vlib/v/types/checker.v @@ -5970,13 +5970,13 @@ fn (mut tc TypeChecker) prepare_check_name_filters() { tc.global_short_names_count = tc.global_names.len mut sums := map[u32]bool{} for name_id, _ in tc.sum_types.ids { - sums[flat.source_type_syntax(generic_base_type_id(name_id)).unqualified()] = true + sums[scope_unqualified_type_name(generic_base_type_id(name_id))] = true } tc.sum_type_short_names = sums.move() tc.sum_type_short_names_count = tc.sum_types.ids.len mut aliases := map[u32]bool{} for name_id, _ in tc.type_aliases.ids { - aliases[flat.source_type_syntax(name_id).unqualified()] = true + aliases[scope_unqualified_type_name(name_id)] = true } tc.alias_short_names = aliases.move() tc.alias_short_names_count = tc.type_aliases.ids.len @@ -6177,7 +6177,7 @@ fn (tc &TypeChecker) selector_names_no_global(field string) bool { @[inline] fn (tc &TypeChecker) name_names_no_sum_type(name_id u32) bool { return tc.sum_type_short_names_count == tc.sum_types.ids.len - && flat.source_type_syntax(generic_base_type_id(name_id)).unqualified() !in tc.sum_type_short_names + && scope_unqualified_type_name(generic_base_type_id(name_id)) !in tc.sum_type_short_names } // const_key_for_suffix returns the const key matching `.${name}` as a suffix, @@ -7002,9 +7002,7 @@ fn (tc &TypeChecker) qualify_type_name_uncached(name_id u32) u32 { return name_id } if is_builtin_type_name_id(name_id) || name_id == qualify_source_type_uint { return name_id } - // Declaration and import tables still use symbol strings at their boundary. - name := flat.source_type_name(name_id) - if tc.unqualified_type_symbol_is_builtin(name) { return name_id } + if tc.unqualified_type_id_is_builtin(name_id) { return name_id } qualified_id := flat.source_type_composition_two(.qualified, flat.source_type_id(tc.cur_module), name_id) if qualified_id in tc.structs || qualified_id in tc.interface_names || tc.sum_types.contains(qualified_id) || qualified_id in tc.enum_names || qualified_id in tc.flag_enums || tc.type_aliases.contains(qualified_id) { @@ -8588,18 +8586,30 @@ fn (tc &TypeChecker) qualify_candidate_type_spelling_exists(name string) bool { } fn (tc &TypeChecker) unqualified_type_symbol_is_builtin(name string) bool { - name_id := flat.source_type_id(name) + // A spelling that was never interned names no collected struct or enum. + name_id := flat.source_type_existing_id(name) or { + if name != 'IError' { + return false + } + flat.source_type_id(name) + } + return tc.unqualified_type_id_is_builtin(name_id) +} + +fn (tc &TypeChecker) unqualified_type_id_is_builtin(name_id u32) bool { if mod_name := tc.struct_modules[name_id] { if mod_name != 'builtin' { return false } - } else if name != 'IError' && name_id !in tc.enum_names && name_id !in tc.flag_enums { + } else if name_id != ierror_source_interface_name && name_id !in tc.enum_names + && name_id !in tc.flag_enums { return false } // Builtin enum declarations use their unqualified source name, just like // builtin structs. Keep that name visible from imported modules unless a - // declaration in the active scope shadows it. - return !tc.unqualified_type_symbol_has_scoped_shadow(name) + // declaration in the active scope shadows it. The shadow probe still uses + // declaration and import tables keyed by symbol strings. + return !tc.unqualified_type_symbol_has_scoped_shadow(flat.source_type_name(name_id)) } fn (tc &TypeChecker) unqualified_type_symbol_has_scoped_shadow(name string) bool { @@ -8994,26 +9004,28 @@ fn (mut tc TypeChecker) add_receiver_method_suffix_index(name string) { // Probe the declaration name before entering the type interner; dotted // semantic methods already install both their full and short suffixes. if tc.method_suffix_prescreen && !name.contains('.') { return } - name_id := flat.source_type_id(name) - name_shape := flat.source_type_shape(name_id) - syntax := flat.source_type_syntax(name_id) - method_id := syntax.unqualified() - method := flat.source_type_name(method_id) + // Split the registered name at its last raw dot, exactly as the source-type + // syntax view does, instead of interning and parsing every declaration + // spelling. Map insertion owns these borrowed views of `name`. + last_dot := name.last_index_u8(`.`) + method := if last_dot < 0 { + name + } else { + unsafe { name.substr_unsafe(last_dot + 1, name.len) } + } + receiver := if last_dot < 0 { name } else { unsafe { name.substr_unsafe(0, last_dot) } } tc.receiver_method_short_names[method] = true if method.len > 1 && method[0] == `@` { - // Map insertion owns this borrowed suffix of the registered name. tc.receiver_method_short_names[unsafe { method.substr_unsafe(1, method.len) }] = true } - receiver_id := syntax.qualifier() - receiver := flat.source_type_name(receiver_id) if receiver.contains('[]') || receiver.contains('map[') { tc.collection_method_short_names[method] = true if method.len > 1 && method[0] == `@` { tc.collection_method_short_names[unsafe { method.substr_unsafe(1, method.len) }] = true } } - receiver_shape := flat.source_type_shape(receiver_id) - if receiver_shape.has(.bracketed) && flat.source_type_query(receiver_id).has(.bracket_suffix) { + receiver_bracket := receiver.index_u8(`[`) + if receiver_bracket >= 0 && receiver.ends_with(']') { mut indexed := tc.generic_receiver_method_index[method] or { []string{} } if name !in indexed { indexed << name @@ -9022,13 +9034,14 @@ fn (mut tc TypeChecker) add_receiver_method_suffix_index(name string) { } } tc.set_receiver_method_suffix_index(name, name) - if name_shape.has(.dotted) && receiver_shape.has(.bracketed) && !receiver_shape.has(.leading_bracket) { - base_id := flat.source_type_syntax(receiver_id).generic_base() - tc.set_receiver_method_suffix_index(flat.source_type_name(flat.source_type_composition_two(.qualified, base_id, method_id)), name) + if last_dot >= 0 && receiver_bracket > 0 { + // `Foo[T].method` is also reachable as `Foo.method`. + receiver_base := unsafe { receiver.substr_unsafe(0, receiver_bracket) } + tc.set_receiver_method_suffix_index('${receiver_base}.${method}', name) } - for suffix_id in syntax.qualifier_suffixes() { - if suffix_id != 0 { - tc.set_receiver_method_suffix_index(flat.source_type_name(suffix_id), name) + for i in 0 .. name.len - 1 { + if name[i] == `.` { + tc.set_receiver_method_suffix_index(unsafe { name.substr_unsafe(i + 1, name.len) }, name) } } } diff --git a/vlib/v/types/checker_comptime.v b/vlib/v/types/checker_comptime.v index 6429ef478c..f6d4409e4f 100644 --- a/vlib/v/types/checker_comptime.v +++ b/vlib/v/types/checker_comptime.v @@ -9724,6 +9724,21 @@ fn (tc &TypeChecker) shift_is_in_inactive_compact_else(id flat.NodeId) bool { return false } +// source_text_is_bare_generic is SourceTypeSyntax.bare_generic for raw text: one +// ASCII uppercase letter after trimming. Checking the text first keeps plain +// identifiers from being interned as type spellings. +fn source_text_is_bare_generic(text string) bool { + mut start := 0 + mut end := text.len + for start < end && text[start] in [` `, `\n`, `\t`, `\v`, `\f`, `\r`] { + start++ + } + for end > start && text[end - 1] in [` `, `\n`, `\t`, `\v`, `\f`, `\r`] { + end-- + } + return end - start == 1 && text[start] >= `A` && text[start] <= `Z` +} + fn (tc &TypeChecker) expr_has_unresolved_generic_name_ident(id flat.NodeId) bool { if !tc.valid_node_id(id) { return false @@ -9731,7 +9746,7 @@ fn (tc &TypeChecker) expr_has_unresolved_generic_name_ident(id flat.NodeId) bool node := tc.a.node(id) if node.kind == .ident { name := node.value_text() - if name !in ['C', 'JS'] && flat.source_type_syntax(node.source_value_id()).bare_generic() + if name !in ['C', 'JS'] && source_text_is_bare_generic(name) && !tc.type_name_known(node.source_value_id()) && name !in tc.fn_context.generic_params && !tc.active_generic_param(name) && !tc.node_has_enclosing_generic_param(id, name) diff --git a/vlib/v/types/checker_tail.v b/vlib/v/types/checker_tail.v index c8af9ef362..8770d5948b 100644 --- a/vlib/v/types/checker_tail.v +++ b/vlib/v/types/checker_tail.v @@ -7868,7 +7868,7 @@ fn (tc &TypeChecker) sum_constructor_call_name(node flat.Node) ?u32 { } } } - if tc.name_names_no_sum_type(callee.source_value_id()) { + if tc.callee_names_no_sum_type(callee) { return none } return tc.known_sum_constructor_name(callee.source_value_id()) @@ -7877,7 +7877,7 @@ fn (tc &TypeChecker) sum_constructor_call_name(node flat.Node) ?u32 { base := tc.a.child_node(callee, 0) if base.kind == .ident { // `mod.Name` has the last segment of `Name`. - if tc.name_names_no_sum_type(callee.source_value_id()) { + if tc.callee_names_no_sum_type(callee) { return none } mod_name := tc.resolve_import_alias(base.value) or { base.value } @@ -7896,16 +7896,33 @@ fn (tc &TypeChecker) sum_constructor_call_name(node flat.Node) ?u32 { return none } +// callee_names_no_sum_type is name_names_no_sum_type for a called name. A name +// without `.` or `[` is its own short generic base, and one that was never +// interned cannot be a sum type short name, so plain calls are not interned. +fn (tc &TypeChecker) callee_names_no_sum_type(callee flat.Node) bool { + if callee.value.len == 0 || tc.sum_type_short_names_count != tc.sum_types.ids.len { + return tc.name_names_no_sum_type(callee.source_value_id()) + } + name_id := flat.source_type_existing_id(callee.value) or { + if callee.value.index_u8(`.`) < 0 && callee.value.index_u8(`[`) < 0 { + return true + } + flat.source_type_id(callee.value) + } + return tc.name_names_no_sum_type(name_id) +} + fn (tc &TypeChecker) generic_named_type_cast_call_type(node flat.Node) ?Type { if node.children_count != 2 { return none } callee := tc.a.child_node(&node, 0) - if callee.kind == .selector && !flat.source_type_query(flat.source_type_id(callee.value)).has(.bracket_suffix) { + // `.bracket_suffix` is exactly a raw `]` suffix; test the text instead of + // interning every called selector and identifier. + if callee.kind == .selector && !callee.value.ends_with(']') { return none } - if callee.kind == .ident && !flat.source_type_query(flat.source_type_id(callee.value)).has(.bracket_suffix) - && tc.file_selective_imports.len == 0 { + if callee.kind == .ident && !callee.value.ends_with(']') && tc.file_selective_imports.len == 0 { return none } target_id := tc.type_expr_name(tc.a.child(&node, 0)) @@ -11826,7 +11843,7 @@ fn (tc &TypeChecker) alias_return_type_from_text(fn_name string) ?Type { } // Both aliases below, `clean` and `mod.clean`, end in the last segment of clean. if tc.alias_short_names_count == tc.type_aliases.ids.len - && flat.source_type_syntax(clean_id).unqualified() !in tc.alias_short_names { + && scope_unqualified_type_name(clean_id) !in tc.alias_short_names { return none } if tc.type_aliases.contains(clean_id) {