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.
This commit is contained in:
Alexander Medvednikov authored and GitHub committed 2026-10-28 03:04:02 +03:00
1 parent 064d83671c
commit 9695dbbd72
3 files changed
+78 -33

No files matched your search

+39 -26
View File
@@ -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)
}
}
}
+16 -1
View File
@@ -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)
+23 -6
View File
@@ -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) {