diff --git a/internal/cbm/lsp/rust_lsp.c b/internal/cbm/lsp/rust_lsp.c index dd0ace583..16e3aedce 100644 --- a/internal/cbm/lsp/rust_lsp.c +++ b/internal/cbm/lsp/rust_lsp.c @@ -5294,36 +5294,17 @@ void cbm_run_rust_lsp(CBMArena *arena, CBMFileResult *result, const char *source extern const TSLanguage *tree_sitter_rust(void); -void cbm_run_rust_lsp_cross_with_manifest(CBMArena *arena, const char *source, int source_len, - const char *module_qn, CBMRustLSPDef *defs, int def_count, - const char **import_names, const char **import_qns, - int import_count, TSTree *cached_tree, - const struct CBMCargoManifest *manifest, - CBMResolvedCallArray *out) { - if (!source || source_len <= 0 || !out) - return; - - TSParser *parser = NULL; - TSTree *tree = cached_tree; - bool owns_tree = false; - if (!tree) { - parser = ts_parser_new(); - if (!parser) - return; - ts_parser_set_language(parser, tree_sitter_rust()); - tree = ts_parser_parse_string(parser, NULL, source, source_len); - owns_tree = true; - if (!tree) { - ts_parser_delete(parser); - return; - } - } - TSNode root = ts_tree_root_node(tree); - - /* Build registry from cross-file defs + stdlib. */ - CBMTypeRegistry reg; - cbm_registry_init(®, arena); - cbm_rust_stdlib_register(®, arena); +/* Populate + finalize a Rust cross-file type registry from `defs`. Shared by the + * per-file resolver (cbm_run_rust_lsp_cross_with_manifest) and the build-once shared + * registry (cbm_rust_build_cross_registry) so both produce a byte-identical registry. + * `module_qn` is ONLY the fallback used to qualify a def's return type when that def + * carries no def_module_qn; pass NULL for the shared build (all_defs always carry + * def_module_qn — verified: 0 NULL across the C + Rust kernel corpora). */ +static void rust_populate_cross_registry(CBMTypeRegistry *reg, CBMArena *arena, + CBMRustLSPDef *defs, int def_count, + const char *module_qn) { + cbm_registry_init(reg, arena); + cbm_rust_stdlib_register(reg, arena); for (int i = 0; i < def_count; i++) { CBMRustLSPDef *d = &defs[i]; @@ -5340,7 +5321,7 @@ void cbm_run_rust_lsp_cross_with_manifest(CBMArena *arena, const char *source, i rt.short_name = cbm_arena_strdup(arena, d->short_name); rt.is_interface = d->is_interface || strcmp(d->label, "Trait") == 0 || strcmp(d->label, "Interface") == 0; - cbm_registry_add_type(®, rt); + cbm_registry_add_type(reg, rt); } if (strcmp(d->label, "Function") == 0 || strcmp(d->label, "Method") == 0) { @@ -5381,25 +5362,25 @@ void cbm_run_rust_lsp_cross_with_manifest(CBMArena *arena, const char *source, i if (strcmp(d->label, "Method") == 0 && d->receiver_type && d->receiver_type[0]) { rf.receiver_type = cbm_arena_strdup(arena, d->receiver_type); - if (!cbm_registry_lookup_type(®, rf.receiver_type)) { + if (!cbm_registry_lookup_type(reg, rf.receiver_type)) { CBMRegisteredType auto_t; memset(&auto_t, 0, sizeof(auto_t)); auto_t.qualified_name = rf.receiver_type; const char *dot = strrchr(d->receiver_type, '.'); auto_t.short_name = dot ? cbm_arena_strdup(arena, dot + 1) : rf.receiver_type; - cbm_registry_add_type(®, auto_t); + cbm_registry_add_type(reg, auto_t); } } - cbm_registry_add_func(®, rf); + cbm_registry_add_func(reg, rf); /* If trait_qn set: encode embedded_type linkage on receiver. */ if (rf.receiver_type && d->trait_qn && d->trait_qn[0]) { CBMRegisteredType *rt = NULL; - for (int ti = 0; ti < reg.type_count; ti++) { - if (reg.types[ti].qualified_name && - strcmp(reg.types[ti].qualified_name, rf.receiver_type) == 0) { - rt = ®.types[ti]; + for (int ti = 0; ti < reg->type_count; ti++) { + if (reg->types[ti].qualified_name && + strcmp(reg->types[ti].qualified_name, rf.receiver_type) == 0) { + rt = ®->types[ti]; break; } } @@ -5420,9 +5401,130 @@ void cbm_run_rust_lsp_cross_with_manifest(CBMArena *arena, const char *source, i } } - /* Run the per-file walker. */ /* Finalise the cross-file registry now that all defs are added. */ - cbm_registry_finalize(®); + cbm_registry_finalize(reg); +} + +/* Resolve one Rust file against an ALREADY-built (per-file or shared) registry. */ +static void rust_resolve_against_registry(CBMArena *arena, const char *source, int source_len, + const char *module_qn, const CBMTypeRegistry *reg, + const char **import_names, const char **import_qns, + int import_count, TSNode root, + const struct CBMCargoManifest *manifest, + CBMResolvedCallArray *out, CBMFileResult *result) { + RustLSPContext ctx; + rust_lsp_init(&ctx, arena, source, source_len, reg, module_qn, out); + ctx.cargo_manifest = manifest; + rust_collect_uses(&ctx, root); + for (int i = 0; i < import_count; i++) { + if (import_names[i] && import_qns[i]) { + rust_lsp_add_use(&ctx, import_names[i], import_qns[i]); + } + } + rust_lsp_process_file(&ctx, root); + if (result) + cbm_rust_synth_proc_macro_edges(arena, result); +} + +/* Tier-2: build the Rust cross registry ONCE from all project defs, sealed + * read-only, and shared across every Rust file's resolve (mirrors C/py/cs/ts). + * Converts the pipeline's CBMLSPDef into CBMRustLSPDef inline (same field copy as + * pass_lsp_cross.c's pxc_lspdefs_to_rust, incl. trait_qn=NULL). module_qn=NULL is + * byte-identical because all_defs always carry def_module_qn. */ +CBMTypeRegistry *cbm_rust_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, int def_count) { + if (!arena) + return NULL; + CBMTypeRegistry *reg = (CBMTypeRegistry *)cbm_arena_alloc(arena, sizeof(*reg)); + if (!reg) + return NULL; + CBMRustLSPDef *rdefs = NULL; + if (def_count > 0) { + rdefs = (CBMRustLSPDef *)cbm_arena_alloc(arena, (size_t)def_count * sizeof(CBMRustLSPDef)); + if (!rdefs) + return NULL; + for (int i = 0; i < def_count; i++) { + rdefs[i].qualified_name = defs[i].qualified_name; + rdefs[i].short_name = defs[i].short_name; + rdefs[i].label = defs[i].label; + rdefs[i].receiver_type = defs[i].receiver_type; + rdefs[i].def_module_qn = defs[i].def_module_qn; + rdefs[i].return_types = defs[i].return_types; + rdefs[i].embedded_types = defs[i].embedded_types; + rdefs[i].field_defs = defs[i].field_defs; + rdefs[i].method_names_str = defs[i].method_names_str; + rdefs[i].trait_qn = NULL; + rdefs[i].is_interface = defs[i].is_interface; + } + } + rust_populate_cross_registry(reg, arena, rdefs, def_count, /*module_qn=*/NULL); + reg->read_only = true; /* seal: shared Tier-2 registry is read-only during resolve */ + return reg; +} + +/* Cross-file Rust resolve using a pre-built shared registry (Tier-2). Skips the + * per-file registry build; just parse + resolve. Mirrors cbm_run_c_lsp_cross_with_registry. */ +void cbm_run_rust_lsp_cross_with_registry(CBMArena *arena, const char *source, int source_len, + const char *module_qn, const CBMTypeRegistry *reg, + const char **import_names, const char **import_qns, + int import_count, TSTree *cached_tree, + const struct CBMCargoManifest *manifest, + CBMResolvedCallArray *out, CBMFileResult *result) { + if (!source || source_len <= 0 || !out || !reg) + return; + TSParser *parser = NULL; + TSTree *tree = cached_tree; + bool owns_tree = false; + if (!tree) { + parser = ts_parser_new(); + if (!parser) + return; + ts_parser_set_language(parser, tree_sitter_rust()); + tree = ts_parser_parse_string(parser, NULL, source, source_len); + owns_tree = true; + if (!tree) { + ts_parser_delete(parser); + return; + } + } + TSNode root = ts_tree_root_node(tree); + rust_resolve_against_registry(arena, source, source_len, module_qn, reg, import_names, + import_qns, import_count, root, manifest, out, result); + if (owns_tree) { + ts_tree_delete(tree); + if (parser) + ts_parser_delete(parser); + } +} + +void cbm_run_rust_lsp_cross_with_manifest(CBMArena *arena, const char *source, int source_len, + const char *module_qn, CBMRustLSPDef *defs, int def_count, + const char **import_names, const char **import_qns, + int import_count, TSTree *cached_tree, + const struct CBMCargoManifest *manifest, + CBMResolvedCallArray *out) { + if (!source || source_len <= 0 || !out) + return; + + TSParser *parser = NULL; + TSTree *tree = cached_tree; + bool owns_tree = false; + if (!tree) { + parser = ts_parser_new(); + if (!parser) + return; + ts_parser_set_language(parser, tree_sitter_rust()); + tree = ts_parser_parse_string(parser, NULL, source, source_len); + owns_tree = true; + if (!tree) { + ts_parser_delete(parser); + return; + } + } + TSNode root = ts_tree_root_node(tree); + + /* Build registry from cross-file defs + stdlib (per-file). */ + CBMTypeRegistry reg; + rust_populate_cross_registry(®, arena, defs, def_count, module_qn); RustLSPContext ctx; rust_lsp_init(&ctx, arena, source, source_len, ®, module_qn, out); diff --git a/internal/cbm/lsp/rust_lsp.h b/internal/cbm/lsp/rust_lsp.h index 302565761..759e5b1d0 100644 --- a/internal/cbm/lsp/rust_lsp.h +++ b/internal/cbm/lsp/rust_lsp.h @@ -28,6 +28,7 @@ #include "scope.h" #include "type_registry.h" #include "../cbm.h" +#include "go_lsp.h" // for CBMLSPDef (pipeline def), used by the Tier-2 builder /* Forward declaration — defined in rust_cargo.h. We keep it forward * here to avoid pulling rust_cargo.h into every consumer that only @@ -296,6 +297,22 @@ void cbm_run_rust_lsp_cross_with_manifest(CBMArena *arena, const char *source, i const struct CBMCargoManifest *manifest, CBMResolvedCallArray *out); +/* Tier-2: build the project-wide Rust cross registry ONCE from all defs, finalize, + * and seal read-only. Shared across every Rust file's resolve (mirrors C/py/cs/ts). + * Def-driven → byte-identical entries to the per-file build. */ +CBMTypeRegistry *cbm_rust_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, int def_count); + +/* Cross-file Rust resolve using a pre-built shared registry (Tier-2). Skips the + * per-file registry build; just parse + resolve. `manifest` = the same Cargo manifest + * the per-file path uses (cross-crate #56). `result` may be NULL (the cross path does + * not synthesise proc-macro edges). Mirrors cbm_run_c_lsp_cross_with_registry. */ +void cbm_run_rust_lsp_cross_with_registry(CBMArena *arena, const char *source, int source_len, + const char *module_qn, const CBMTypeRegistry *reg, + const char **import_names, const char **import_qns, + int import_count, TSTree *cached_tree, + const struct CBMCargoManifest *manifest, + CBMResolvedCallArray *out, CBMFileResult *result); + /* Per-file input for batch cross-file Rust LSP processing. */ typedef struct { const char *source; diff --git a/src/pipeline/pass_lsp_cross.c b/src/pipeline/pass_lsp_cross.c index e06ef6a1e..40ec8e264 100644 --- a/src/pipeline/pass_lsp_cross.c +++ b/src/pipeline/pass_lsp_cross.c @@ -492,6 +492,10 @@ void cbm_pxc_set_rust_manifest(const CBMCargoManifest *m) { g_pxc_rust_manifest = m; } +const struct CBMCargoManifest *cbm_pxc_get_rust_manifest(void) { + return g_pxc_rust_manifest; +} + /* Convert a CBMLSPDef array (the pipeline's lingua franca, go_lsp.h:73) * into a CBMRustLSPDef array (rust_lsp.h) inside `arena`. The two structs * share their first 9 string fields; CBMRustLSPDef adds `trait_qn` before diff --git a/src/pipeline/pass_lsp_cross.h b/src/pipeline/pass_lsp_cross.h index e6d536198..1fae666b3 100644 --- a/src/pipeline/pass_lsp_cross.h +++ b/src/pipeline/pass_lsp_cross.h @@ -35,6 +35,7 @@ #include "lsp/c_lsp.h" /* cbm_c_build_cross_registry / cbm_run_c_lsp_cross_with_registry */ #include "lsp/cs_lsp.h" /* cbm_cs_build_cross_registry / cbm_run_cs_lsp_cross_with_registry */ #include "lsp/ts_lsp.h" /* cbm_ts_build_cross_registry / cbm_run_ts_lsp_cross_with_registry */ +#include "lsp/rust_lsp.h" /* cbm_rust_build_cross_registry / cbm_run_rust_lsp_cross_with_registry */ #include "pipeline/pipeline_internal.h" #include @@ -107,6 +108,7 @@ typedef struct { CBMTypeRegistry *ts; /* CBM_LANG_JAVASCRIPT, TYPESCRIPT, TSX */ CBMTypeRegistry *php; /* CBM_LANG_PHP */ CBMTypeRegistry *cs; /* CBM_LANG_CSHARP */ + CBMTypeRegistry *rust; /* CBM_LANG_RUST */ } CBMCrossLspRegistries; /* Return the appropriate pre-built registry for a language, or NULL @@ -132,11 +134,19 @@ static inline CBMTypeRegistry *cbm_pxc_registry_for_lang(const CBMCrossLspRegist return r->php; case CBM_LANG_CSHARP: return r->cs; + case CBM_LANG_RUST: + return r->rust; default: return NULL; } } +/* Borrow the (thread-local) Rust Cargo manifest the cross-file LSP pass set for + * cross-crate (#56) routing. The Tier-2 prebuilt Rust resolve reads it so it sees + * exactly what the per-file fallback (cbm_pxc_run_one) would on the same thread. */ +struct CBMCargoManifest; +const struct CBMCargoManifest *cbm_pxc_get_rust_manifest(void); + /* Run the cross-file LSP resolver for non-TS languages. Appends * resolved CALLS into r->resolved_calls (lives in r->arena). Caller * owns source, module_qn, all_defs, imp_keys, imp_vals. diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index 75db49881..3f3a1cfaa 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -2738,6 +2738,36 @@ static void resolve_worker(int worker_id, void *ctx_ptr) { result->cached_tree, &result->resolved_calls); used_prebuilt = true; break; + case CBM_LANG_RUST: { + /* Byte-identity-preserving Tier-2. The rust per-file build uses + * all_defs ONLY when the filter returns NULL (the ~90s amplifier + * files, e.g. rust kernel modules); those share the once-built all_defs + * registry. Files that filter to a SUBSET (the majority) resolve + * against that subset today, so they keep their per-file build — + * feeding them the shared all_defs registry would be a SUPERSET and + * could change the graph. (See report cc799e17.) Both paths read the + * same worker-thread manifest as the fallback. */ + CBMLSPDef *r_filtered = NULL; + int r_fc = 0; + if (rc->module_def_index) { + r_filtered = cbm_pxc_filter_defs_for_file( + rc->module_def_index, rc->all_defs, lang, result->namespace_name, + def_module, imp_vals, imp_count, &r_fc); + } + if (!r_filtered) { + cbm_run_rust_lsp_cross_with_registry( + &result->arena, lsp_source, lsp_source_len, def_module, prebuilt, + imp_keys, imp_vals, imp_count, result->cached_tree, + cbm_pxc_get_rust_manifest(), &result->resolved_calls, + /*result=*/NULL); + } else { + cbm_pxc_run_one(lang, result, lsp_source, lsp_source_len, def_module, + r_filtered, r_fc, imp_keys, imp_vals, imp_count); + free(r_filtered); + } + used_prebuilt = true; + break; + } case CBM_LANG_CSHARP: cbm_run_cs_lsp_cross_with_registry( &result->arena, lsp_source, lsp_source_len, def_module, prebuilt, diff --git a/src/pipeline/pipeline.c b/src/pipeline/pipeline.c index c3b7d572e..9e8d44b3a 100644 --- a/src/pipeline/pipeline.c +++ b/src/pipeline/pipeline.c @@ -903,6 +903,13 @@ static int run_parallel_pipeline(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx, cross_registries.c = cbm_c_build_cross_registry(&cross_lsp_arena, all_defs, def_count); cross_registries.cs = cbm_cs_build_cross_registry(&cross_lsp_arena, all_defs, def_count); cross_registries.ts = cbm_ts_build_cross_registry(&cross_lsp_arena, all_defs, def_count); + cross_registries.rust = cbm_rust_build_cross_registry(&cross_lsp_arena, all_defs, def_count); + if (cross_registries.rust) { + char _rsb[96]; + snprintf(_rsb, sizeof(_rsb), "types=%d funcs=%d", cross_registries.rust->type_count, + cross_registries.rust->func_count); + cbm_log_info("cross_lsp.rust_registry", "scale", _rsb); + } } cbm_log_info("pass.timing", "pass", "lsp_cross_prepare", "elapsed_ms", itoa_buf((int)elapsed_ms(*t))); diff --git a/tests/test_rust_lsp.c b/tests/test_rust_lsp.c index 10371e5cf..fbdd7b681 100644 --- a/tests/test_rust_lsp.c +++ b/tests/test_rust_lsp.c @@ -518,6 +518,47 @@ TEST(rustlsp_crossfile_method_dispatch) { PASS(); } +/* F4 Tier-2: the build-once shared registry path (cbm_rust_build_cross_registry + + * cbm_run_rust_lsp_cross_with_registry) must resolve IDENTICALLY to the per-file + * cbm_run_rust_lsp_cross — same fixture as rustlsp_crossfile_method_dispatch. Also + * asserts the registry is sealed read-only. */ +TEST(rustlsp_shared_registry_resolves_like_per_file) { + const char *caller = "fn run(d: &demo::Database) { d.query(\"select\"); }\n"; + CBMArena a; + cbm_arena_init(&a); + + CBMLSPDef defs[2]; + memset(defs, 0, sizeof(defs)); + defs[0].qualified_name = "test.demo.Database"; + defs[0].short_name = "Database"; + defs[0].label = "Type"; + defs[0].def_module_qn = "test.demo"; + defs[1].qualified_name = "test.demo.Database.query"; + defs[1].short_name = "query"; + defs[1].label = "Method"; + defs[1].receiver_type = "test.demo.Database"; + defs[1].def_module_qn = "test.demo"; + defs[1].return_types = "alloc.string.String"; + + const char *imp_names[] = {"demo"}; + const char *imp_qns[] = {"test::demo"}; + + CBMTypeRegistry *reg = cbm_rust_build_cross_registry(&a, defs, 2); + ASSERT_NOT_NULL(reg); + ASSERT_TRUE(reg->read_only); + + CBMResolvedCallArray out; + memset(&out, 0, sizeof(out)); + cbm_run_rust_lsp_cross_with_registry(&a, caller, (int)strlen(caller), "test.caller", reg, + imp_names, imp_qns, 1, NULL, /*manifest=*/NULL, &out, + /*result=*/NULL); + + ASSERT_GTE(find_confident(&out, "run", "Database.query"), 0); + + cbm_arena_destroy(&a); + PASS(); +} + TEST(rustlsp_crossfile_free_function) { const char *caller = "fn main() { utils::greet(\"x\"); }\n"; CBMArena a; @@ -5973,6 +6014,7 @@ void suite_rust_lsp(void) { /* Cross-file */ RUN_TEST(rustlsp_crossfile_method_dispatch); + RUN_TEST(rustlsp_shared_registry_resolves_like_per_file); RUN_TEST(rustlsp_crossfile_free_function); /* Robustness */