From 040dc31fab0141abfd846bb974fbf31d5e506045 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Mon, 2 Nov 2026 22:04:48 +0100 Subject: [PATCH] perf(resolver): look up TypeScript proof imports and Rust uses by name The TypeScript proof scanned a file's imports for each callee and annotation, and every narrowing of a path for each receiver on it. TypeScript's own checker.ts names over a thousand imports in 54,000 lines. Imports now use the file's rows, and a narrowing lookup is one binary search: entries sorted by path and start carry the furthest end so far, and one covers a position exactly when that end passes it. A Rust module's uses were walked for each local item found, each imported name and each trait a method call asked about. Long lists now answer from rows by local name and by last segment, in list order. Each of these went from about 4x to about 2x work when the input doubled. Answers are unchanged. Signed-off-by: Martin Vogel --- internal/cbm/lsp/ts_proof.c | 32 +++++---- internal/cbm/resolve/rust_flow.c | 6 +- internal/cbm/resolve/rust_names.c | 75 ++++++++++++++++++++-- internal/cbm/resolve/rust_names_internal.h | 10 +++ tests/test_resolver_rust.c | 68 ++++++++++++++++++++ tests/test_resolver_ts.c | 22 +++++++ 6 files changed, 191 insertions(+), 22 deletions(-) diff --git a/internal/cbm/lsp/ts_proof.c b/internal/cbm/lsp/ts_proof.c index 6d3a23e8e..174d26abf 100644 --- a/internal/cbm/lsp/ts_proof.c +++ b/internal/cbm/lsp/ts_proof.c @@ -28,7 +28,8 @@ typedef struct { typedef struct { const char *path; uint32_t start, end; - TSNode guard; /* The callee; null for instanceof. */ + TSNode guard; /* The callee; null for instanceof. */ + uint32_t reach; /* Once sorted: the furthest end among this path's entries up to this one. */ } ts_proof_narrowing_t; struct CBMTSProofAudit { @@ -1010,6 +1011,12 @@ static void settle_guards(TSLSPContext *ctx) { audit->narrowing_count = kept; if (kept > 1) qsort(audit->narrowings, kept, sizeof(*audit->narrowings), narrowing_compare); + for (size_t i = 0; i < kept; i++) { + ts_proof_narrowing_t *entry = &audit->narrowings[i]; + const ts_proof_narrowing_t *prior = i ? &audit->narrowings[i - 1] : NULL; + bool same_path = prior && !strcmp(prior->path, entry->path); + entry->reach = same_path && prior->reach > entry->end ? prior->reach : entry->end; + } } /* The checker selects on what the value was narrowed to, and this proof reads declarations, so @@ -1026,18 +1033,21 @@ static bool receiver_narrowed(TSLSPContext *ctx, TSNode receiver, uint32_t at) { const char *path = narrowing_path(ctx, receiver, 0); if (!path) return false; + /* This path's entries starting at or before `at` are a prefix of its sorted entries, so one of + * them covers `at` exactly when the furthest end among them passes it. */ + CBM_LSP_WORK(1); size_t low = 0, high = audit->narrowing_count; while (low < high) { size_t mid = low + (high - low) / 2; - if (strcmp(audit->narrowings[mid].path, path) < 0) + int order = strcmp(audit->narrowings[mid].path, path); + if (order < 0 || (order == 0 && audit->narrowings[mid].start <= at)) low = mid + 1; else high = mid; } - for (; low < audit->narrowing_count && !strcmp(audit->narrowings[low].path, path); low++) - if (at >= audit->narrowings[low].start && at < audit->narrowings[low].end) - return true; - return false; + if (!low || strcmp(audit->narrowings[low - 1].path, path)) + return false; + return at < audit->narrowings[low - 1].reach; } /* The registered function a call's callee names: a module function, an import, or a method of a @@ -1067,11 +1077,8 @@ static const CBMRegisteredFunc *callee_function(TSLSPContext *ctx, TSNode callee return NULL; qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, name); } else { - for (int i = 0; i < ctx->import_count; i++) { - const char *key = ctx->import_local_names[i]; + for (int i = ts_import_first(ctx, name); i >= 0; i = ts_import_next(ctx, name, i)) { const char *value = ctx->import_module_qns[i]; - if (!key || strcmp(key, name)) - continue; if (!value || !*value || (qn && strcmp(qn, value))) return NULL; qn = value; @@ -1123,10 +1130,7 @@ static bool annotation_names_local_type(TSLSPContext *ctx, TSNode annotation, ui (void)find_binding(ctx, name, at, &occupied); if (occupied) return true; - for (int i = 0; i < ctx->import_count; i++) - if (ctx->import_local_names[i] && !strcmp(ctx->import_local_names[i], name)) - return true; - return false; + return ts_import_first(ctx, name) >= 0; } /* The class the elements of this value are: `items[i]` and `for (const x of items)`. */ diff --git a/internal/cbm/resolve/rust_flow.c b/internal/cbm/resolve/rust_flow.c index 0a0a18fb6..7a1c6ac57 100644 --- a/internal/cbm/resolve/rust_flow.c +++ b/internal/cbm/resolve/rust_flow.c @@ -722,9 +722,9 @@ static bool rs_inst_binds_trait(rs_walk_t *w, rs_inst_t *inst, const rs_item_t * const rs_mod_t *syn = inst->syn; if (trait->owner == syn) return true; - for (const rs_use_t *use = syn->uses; use; use = use->next) { - if (!use->nseg || strcmp(use->segs[use->nseg - 1], trait->name) != 0) - continue; + int row = -1; + for (const rs_use_t *use = rs_use_ending(syn, NULL, trait->name, &row); use; + use = rs_use_ending(syn, use, trait->name, &row)) { int consumed = 0; rs_ent_t ent = rs_resolve_path(&w->q, inst, NULL, 0, use->segs, use->nseg, use->absolute, RS_NS_TYPE, true, NULL, &consumed); diff --git a/internal/cbm/resolve/rust_names.c b/internal/cbm/resolve/rust_names.c index 119d26f31..f6946fb7b 100644 --- a/internal/cbm/resolve/rust_names.c +++ b/internal/cbm/resolve/rust_names.c @@ -981,8 +981,71 @@ static void rs_mod_index_impls(CBMArena *arena, rs_mod_t *mod) { mod->nimpl_rows = count; } +static void rs_mod_index_uses(CBMArena *arena, rs_mod_t *mod) { + int count = 0; + for (const rs_use_t *use = mod->uses; use; use = use->next) + count++; + if (count < CBM_NAME_ROWS_MIN) + return; + const rs_use_t **by_name = cbm_arena_alloc(arena, (size_t)count * sizeof(*by_name)); + const rs_use_t **by_tail = cbm_arena_alloc(arena, (size_t)count * sizeof(*by_tail)); + if (!by_name || !by_tail) + return; /* the list still answers */ + int names = 0, tails = 0; + for (const rs_use_t *use = mod->uses; use; use = use->next) { + if (use->local) { + by_name[names] = use; + if (!cbm_name_rows_add(arena, &mod->use_name_rows, NULL, use->local, names++)) + return; + } + if (use->nseg) { + by_tail[tails] = use; + if (!cbm_name_rows_add(arena, &mod->use_tail_rows, NULL, use->segs[use->nseg - 1], + tails++)) + return; + } + } + mod->uses_by_name = by_name; + mod->uses_by_tail = by_tail; +} + +/* The first or next use binding this local name, in list order. */ +const rs_use_t *rs_use_named(const rs_mod_t *mod, const rs_use_t *after, const char *name, + int *row) { + cbm_resolve_stats_add(CBM_RESOLVE_STAT_LOOKUP_PROBES, 1); + if (mod->uses_by_name) { + *row = after ? mod->use_name_rows.next[*row] + : cbm_name_rows_lookup(&mod->use_name_rows, NULL, name); + return *row >= 0 ? mod->uses_by_name[*row] : NULL; + } + for (const rs_use_t *use = after ? after->next : mod->uses; use; use = use->next) { + cbm_resolve_stats_add(CBM_RESOLVE_STAT_LOOKUP_PROBES, 1); + if (use->local && !strcmp(use->local, name)) + return use; + } + return NULL; +} + +/* The first or next use whose path ends in this name, in list order. */ +const rs_use_t *rs_use_ending(const rs_mod_t *mod, const rs_use_t *after, const char *name, + int *row) { + cbm_resolve_stats_add(CBM_RESOLVE_STAT_LOOKUP_PROBES, 1); + if (mod->uses_by_tail) { + *row = after ? mod->use_tail_rows.next[*row] + : cbm_name_rows_lookup(&mod->use_tail_rows, NULL, name); + return *row >= 0 ? mod->uses_by_tail[*row] : NULL; + } + for (const rs_use_t *use = after ? after->next : mod->uses; use; use = use->next) { + cbm_resolve_stats_add(CBM_RESOLVE_STAT_LOOKUP_PROBES, 1); + if (use->nseg && !strcmp(use->segs[use->nseg - 1], name)) + return use; + } + return NULL; +} + void rs_mod_seal(CBMArena *arena, rs_mod_t *mod) { rs_mod_index_impls(arena, mod); + rs_mod_index_uses(arena, mod); for (rs_impl_t *impl = mod->impls; impl; impl = impl->next) rs_consts_seal(arena, impl->consts, &impl->sorted_consts, &impl->nconsts); for (rs_item_t *item = mod->item_list; item; item = item->next) @@ -2339,9 +2402,9 @@ static rs_ent_t rs_named_uses(rs_query_t *q, rs_inst_t *inst, const char *name, rs_inst_t *from) { rs_ent_t found = rs_ent(RS_ENT_NONE, RS_VIA_NONE); bool hidden = false, found_cfg = false; - for (const rs_use_t *use = inst->syn->uses; use; use = use->next) { - if (!use->local || strcmp(use->local, name) != 0) - continue; + int row = -1; + for (const rs_use_t *use = rs_use_named(inst->syn, NULL, name, &row); use; + use = rs_use_named(inst->syn, use, name, &row)) { if (!rs_visible(use->vis, inst->syn, from)) { hidden = true; continue; @@ -2484,8 +2547,10 @@ static rs_ent_t rs_lookup_steps(rs_query_t *q, rs_inst_t *inst, const char *name /* `#[cfg(a)] use x::f; #[cfg(not(a))] fn f() {}`: one name, a different declaration under * each configuration, and nothing here says which one applies. Without cfg this does * not compile, so only a conditional use can stand beside a local item. */ - for (const rs_use_t *use = inst->syn->uses; use; use = use->next) { - if (!use->has_cfg || !use->local || strcmp(use->local, name) != 0) + int row = -1; + for (const rs_use_t *use = rs_use_named(inst->syn, NULL, name, &row); use; + use = rs_use_named(inst->syn, use, name, &row)) { + if (!use->has_cfg) continue; rs_ent_t imported = rs_named_uses(q, inst, name, ns, from); if (rs_ent_found(&imported) && !rs_ent_same(&ent, &imported)) diff --git a/internal/cbm/resolve/rust_names_internal.h b/internal/cbm/resolve/rust_names_internal.h index 4cf0b2287..6f8d10671 100644 --- a/internal/cbm/resolve/rust_names_internal.h +++ b/internal/cbm/resolve/rust_names_internal.h @@ -216,6 +216,12 @@ typedef struct rs_use { struct rs_use *next; } rs_use_t; +/* The first or next use of a module binding a local name, and whose path ends in a name. */ +const rs_use_t *rs_use_named(const rs_mod_t *mod, const rs_use_t *after, const char *name, + int *row); +const rs_use_t *rs_use_ending(const rs_mod_t *mod, const rs_use_t *after, const char *name, + int *row); + /* One syntactic module: a file's top level, an inline `mod x { }`, or the * item scope of a block. The same file reached from two crate roots has one * syntactic module and two instances. */ @@ -230,6 +236,10 @@ struct rs_mod { rs_item_t **items; /* sorted by name after sealing the module */ int nitems; rs_use_t *uses; /* named leaves */ + /* A long uses list by local name and by last segment, in list order: every name looked up + * and every trait asked about would walk it. */ + const rs_use_t **uses_by_name, **uses_by_tail; + CBMNameRows use_name_rows, use_tail_rows; rs_use_t *globs; /* glob leaves */ rs_impl_t *impls; /* A long impls list by start byte, then list position: each impl walked looks itself up. */ diff --git a/tests/test_resolver_rust.c b/tests/test_resolver_rust.c index 7b712895e..38cc436b0 100644 --- a/tests/test_resolver_rust.c +++ b/tests/test_resolver_rust.c @@ -1615,6 +1615,73 @@ TEST(resolver_rust_trait_on_external_type_delegating_to_inherent) { PASS(); } +/* Enough impls on outside types to answer from rows by path and method: the same answers as the + * chains give, for a crate's Result alias, a delegating impl, two traits on one node, and two + * traits on two nodes. The first source stays under the rows' threshold, the second is over it. */ +#define RUST_OUTSIDE_IMPLS \ + "use std::process::Child as StdChild;\n" \ + "mod a { pub trait One { fn apart(&self) -> u32; }\n" \ + " impl One for String { fn apart(&self) -> u32 { 1 } } }\n" \ + "mod b { pub trait Two { fn apart(&self) -> u32; }\n" \ + " impl Two for String { fn apart(&self) -> u32 { 2 } } }\n" \ + "use a::One;\nuse b::Two;\n" \ + "trait WithIndex { fn with_index(self, i: usize) -> Option; }\n" \ + "impl WithIndex for Result { fn with_index(self, i: usize) -> Option { " \ + "self.ok() } }\n" \ + "trait Shout { fn shout(&self) -> String; }\n" \ + "impl Shout for String { fn shout(&self) -> String { self.clone() } }\n" \ + "trait Wait { fn id(&self) -> u32; fn extra(&self) -> u32; }\n" \ + "impl Wait for StdChild { fn id(&self) -> u32 { self.id() } fn extra(&self) -> u32 { 0 } }\n" \ + "trait Three { fn near(&self) -> u32; }\n" \ + "trait Four { fn near(&self) -> u32; }\n" \ + "impl Three for String { fn near(&self) -> u32 { 3 } }\n" \ + "impl Four for String { fn near(&self) -> u32 { 4 } }\n" \ + "fn from_alias(r: heed::Result) { r.with_index(1); }\n" \ + "fn on_string(s: String) { s.shout(); }\n" \ + "fn other_method(s: String) { s.len(); }\n" \ + "fn inherent(c: StdChild) { c.id(); }\n" \ + "fn trait_only(c: StdChild) { c.extra(); }\n" \ + "fn one_node(s: String) { s.near(); }\n" \ + "fn two_nodes(s: String) { s.apart(); }\n" \ + "fn main() {}\n" +#define RUST_OUTSIDE_IMPLS_MANIFEST \ + "[package]\nname = \"demo\"\nedition = \"2021\"\n[dependencies]\nheed = \"1\"\n" +#define RUST_OUTSIDE_IMPLS_ANSWERS(store) \ + do { \ + ASSERT_NOT_NULL(store); \ + ASSERT_EQ(rust_edge(store, "CALLS", "src.main.from_alias", "src.main.Result.with_index"), \ + 1); \ + ASSERT_EQ(rust_edge(store, "CALLS", "src.main.on_string", "src.main.String.shout"), 1); \ + ASSERT_EQ(rust_edges_from(store, "CALLS", "src.main.other_method"), 0); \ + ASSERT_EQ(rust_edges_from(store, "CALLS", "src.main.inherent"), 0); \ + ASSERT_EQ(rust_edge(store, "CALLS", "src.main.trait_only", "src.main.StdChild.extra"), 1); \ + ASSERT_EQ(rust_edges_from(store, "CALLS", "src.main.one_node"), 1); \ + ASSERT_EQ(rust_edges_from(store, "CALLS", "src.main.two_nodes"), 0); \ + ASSERT_TRUE( \ + rust_occurrence_reason(store, "CALLS", "c.id", "rust_external_receiver_method")); \ + ASSERT_TRUE( \ + rust_occurrence_reason(store, "CALLS", "s.apart", "rust_target_qn_collision")); \ + } while (0) +TEST(resolver_rust_many_outside_impls_answer_as_few_do) { + static const rust_proof_file_t few[] = {{"Cargo.toml", RUST_OUTSIDE_IMPLS_MANIFEST}, + {"src/main.rs", RUST_OUTSIDE_IMPLS}}; + static const rust_proof_file_t many[] = { + {"Cargo.toml", RUST_OUTSIDE_IMPLS_MANIFEST}, + {"src/main.rs", + "trait Pad { fn p0(&self); fn p1(&self); fn p2(&self); fn p3(&self); fn p4(&self);" + " fn p5(&self); fn p6(&self); fn p7(&self); fn p8(&self); }\n" + "impl Pad for u8 { fn p0(&self) {} fn p1(&self) {} fn p2(&self) {} fn p3(&self) {}" + " fn p4(&self) {} fn p5(&self) {} fn p6(&self) {} fn p7(&self) {} fn p8(&self) {} " + "}\n" RUST_OUTSIDE_IMPLS}}; + RUST_ROUTES(store, root, few) { + RUST_OUTSIDE_IMPLS_ANSWERS(store); + } + RUST_ROUTES(store, root, many) { + RUST_OUTSIDE_IMPLS_ANSWERS(store); + } + PASS(); +} + /* tokio's blocking.rs: `cfg_rt! { pub(crate) use crate::runtime::spawn_blocking; }` beside * `cfg_not_rt! { pub(crate) fn spawn_blocking() {} }`. Each configuration has a different declaration, * so neither is the answer. A plain #[cfg] twin is the same case. */ @@ -2629,6 +2696,7 @@ SUITE(resolver_rust) { RUN_TEST(resolver_rust_unrepaired_syntax_error_stays_unresolved); RUN_TEST(resolver_rust_spelled_external_type_takes_a_project_trait); RUN_TEST(resolver_rust_trait_on_external_type_delegating_to_inherent); + RUN_TEST(resolver_rust_many_outside_impls_answer_as_few_do); RUN_TEST(resolver_rust_conditional_use_beside_a_local_item_is_ambiguous); RUN_TEST(resolver_rust_wrapper_keeps_into_inner_for_itself); RUN_TEST(resolver_rust_pin_project_declares_its_struct); diff --git a/tests/test_resolver_ts.c b/tests/test_resolver_ts.c index 6abe037d9..0bbac68a1 100644 --- a/tests/test_resolver_ts.c +++ b/tests/test_resolver_ts.c @@ -665,6 +665,27 @@ TEST(resolver_ts_duplicate_method_declarations_abstain) { PASS(); } +/* Sorted by start, the nested function's narrowing is the last one before the final call, and it + * ended before that call. The outer one still covers it; nothing covers the function after them. */ +TEST(resolver_ts_narrowing_outlives_a_nested_one) { + static const char source[] = "class C { run() {} } class D extends C { run() {} }\n" + "function outer(a: C) {\n" + " if (a instanceof D) { a.run(); }\n" + " function inner(a: C) { if (a instanceof D) { a.run(); } }\n" + " a.run();\n" + "}\n" + "function after(a: C) { a.run(); }\n"; + CBMFileResult *file = cbm_extract_file(source, (int)strlen(source), CBM_LANG_TYPESCRIPT, "test", + "main.ts", 0, NULL, NULL); + ASSERT_NOT_NULL(file); + int declared = ts_proved_count(&file->resolved_calls, "test.main.C.run"); + int narrower = ts_proved_count(&file->resolved_calls, "test.main.D.run"); + cbm_free_result(file); + ASSERT_EQ(declared, 1); + ASSERT_EQ(narrower, 0); + PASS(); +} + SUITE(resolver_ts) { RUN_TEST(resolver_ts_declared_receiver_selects_exact_occurrence); RUN_TEST(resolver_ts_unknown_union_and_later_shadow_abstain); @@ -684,6 +705,7 @@ SUITE(resolver_ts) { RUN_TEST(resolver_ts_annotation_type_refs_are_exact_occurrences); RUN_TEST(resolver_ts_this_of_a_literal_method_is_not_the_class); RUN_TEST(resolver_ts_imported_type_requires_export_binding); + RUN_TEST(resolver_ts_narrowing_outlives_a_nested_one); RUN_TEST(resolver_ts_overlay_copies_coalesce_only_same_declaration); RUN_TEST(resolver_ts_duplicate_method_declarations_abstain); }