fix(rust-lsp): a Rust call never binds another language's definition

Both Rust registries were built from the pipeline's mixed-language def
set. The shared Tier-2 build (cbm_rust_build_cross_registry) and the
per-file conversion (pxc_lspdefs_to_rust) copied every def, so a Perl
`sub new` or any C function became a Rust free function. The
lsp_short_name_unique fallback then bound `Store::new()` to the Perl
constructor at confidence 0.90 on both venues: its "crate scope" check
compares only the first module-QN segment, which is the project name.
(Found by the generic-engine lane on a 12-language fixture.)

Register Rust defs only, at both conversion points, exactly as the Go
builder filters to Go. Rust impl-relation records already carry
CBM_LANG_RUST. As a side effect, the shared Rust build no longer
registers the C defs of a mixed C+Rust tree.

Tests: pipeline_rust_call_never_binds_another_language (sequential and
parallel): no CALLS from the Rust caller into the Perl file, and the
Rust associated function still binds.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel
2026-10-17 23:22:56 +02:00
parent 9f85f1043a
commit 2ac9c33e18
3 changed files with 157 additions and 36 deletions
+25 -17
View File
@@ -6323,7 +6323,10 @@ static void rust_resolve_against_registry(CBMArena *arena, const char *source, i
* 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, including proven trait provenance).
* module_qn=NULL is byte-identical because all_defs always carry def_module_qn. */
* module_qn=NULL is byte-identical because all_defs always carry def_module_qn.
* all_defs is mixed-language: only Rust defs are registered, exactly like the
* per-file conversion, so a Perl `new` or a C function can never become the
* target of a Rust call (the Go builder filters the same way). */
CBMTypeRegistry *cbm_rust_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, int def_count) {
if (!arena)
return NULL;
@@ -6331,29 +6334,34 @@ CBMTypeRegistry *cbm_rust_build_cross_registry(CBMArena *arena, CBMLSPDef *defs,
if (!reg)
return NULL;
CBMRustLSPDef *rdefs = NULL;
int n = 0;
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].signature_param_types = defs[i].signature_param_types;
rdefs[i].signature_param_count = defs[i].signature_param_count;
rdefs[i].trait_qn = defs[i].trait_qn;
rdefs[i].is_interface = defs[i].is_interface;
rdefs[i].is_rust_impl_relation = defs[i].is_rust_impl_relation;
rdefs[i].is_abstract = defs[i].is_abstract;
if (defs[i].lang != CBM_LANG_RUST) {
continue;
}
rdefs[n].qualified_name = defs[i].qualified_name;
rdefs[n].short_name = defs[i].short_name;
rdefs[n].label = defs[i].label;
rdefs[n].receiver_type = defs[i].receiver_type;
rdefs[n].def_module_qn = defs[i].def_module_qn;
rdefs[n].return_types = defs[i].return_types;
rdefs[n].embedded_types = defs[i].embedded_types;
rdefs[n].field_defs = defs[i].field_defs;
rdefs[n].method_names_str = defs[i].method_names_str;
rdefs[n].signature_param_types = defs[i].signature_param_types;
rdefs[n].signature_param_count = defs[i].signature_param_count;
rdefs[n].trait_qn = defs[i].trait_qn;
rdefs[n].is_interface = defs[i].is_interface;
rdefs[n].is_rust_impl_relation = defs[i].is_rust_impl_relation;
rdefs[n].is_abstract = defs[i].is_abstract;
n++;
}
}
rust_populate_cross_registry(reg, arena, rdefs, def_count, /*module_qn=*/NULL);
rust_populate_cross_registry(reg, arena, n > 0 ? rdefs : NULL, n, /*module_qn=*/NULL);
reg->read_only = true; /* seal: shared Tier-2 registry is read-only during resolve */
return reg;
}
+31 -19
View File
@@ -1470,31 +1470,42 @@ const struct CBMCargoManifest *cbm_pxc_get_rust_manifest(void) {
/* 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
* have similar fields but different layouts; CBMLSPDef adds
* language/namespace metadata, so a memcpy is unsafe — copy field-by-field. */
static CBMRustLSPDef *pxc_lspdefs_to_rust(CBMArena *arena, const CBMLSPDef *defs, int def_count) {
* language/namespace metadata, so a memcpy is unsafe — copy field-by-field.
* Only Rust defs are kept: the def set is mixed-language, and a Perl or C
* function must never become a Rust call target (the Go registrar filters
* the same way). *out_count gets the number kept. */
static CBMRustLSPDef *pxc_lspdefs_to_rust(CBMArena *arena, const CBMLSPDef *defs, int def_count,
int *out_count) {
*out_count = 0;
if (!defs || def_count <= 0)
return NULL;
CBMRustLSPDef *out =
(CBMRustLSPDef *)cbm_arena_alloc(arena, (size_t)def_count * sizeof(CBMRustLSPDef));
if (!out)
return NULL;
int n = 0;
for (int i = 0; i < def_count; i++) {
out[i].qualified_name = defs[i].qualified_name;
out[i].short_name = defs[i].short_name;
out[i].label = defs[i].label;
out[i].receiver_type = defs[i].receiver_type;
out[i].def_module_qn = defs[i].def_module_qn;
out[i].return_types = defs[i].return_types;
out[i].embedded_types = defs[i].embedded_types;
out[i].field_defs = defs[i].field_defs;
out[i].method_names_str = defs[i].method_names_str;
out[i].signature_param_types = defs[i].signature_param_types;
out[i].signature_param_count = defs[i].signature_param_count;
out[i].trait_qn = defs[i].trait_qn;
out[i].is_interface = defs[i].is_interface;
out[i].is_rust_impl_relation = defs[i].is_rust_impl_relation;
out[i].is_abstract = defs[i].is_abstract;
if (defs[i].lang != CBM_LANG_RUST) {
continue;
}
out[n].qualified_name = defs[i].qualified_name;
out[n].short_name = defs[i].short_name;
out[n].label = defs[i].label;
out[n].receiver_type = defs[i].receiver_type;
out[n].def_module_qn = defs[i].def_module_qn;
out[n].return_types = defs[i].return_types;
out[n].embedded_types = defs[i].embedded_types;
out[n].field_defs = defs[i].field_defs;
out[n].method_names_str = defs[i].method_names_str;
out[n].signature_param_types = defs[i].signature_param_types;
out[n].signature_param_count = defs[i].signature_param_count;
out[n].trait_qn = defs[i].trait_qn;
out[n].is_interface = defs[i].is_interface;
out[n].is_rust_impl_relation = defs[i].is_rust_impl_relation;
out[n].is_abstract = defs[i].is_abstract;
n++;
}
*out_count = n;
return out;
}
@@ -1581,9 +1592,10 @@ void cbm_pxc_run_one_with_php_bindings(CBMLanguage lang, CBMFileResult *r, const
* but diverge after, so convert into the scratch arena. The
* workspace manifest (set once by the sequential driver) lets
* `crate_a::foo` route across the crate boundary (#56). */
CBMRustLSPDef *rdefs = pxc_lspdefs_to_rust(&scratch, defs, def_count);
int rust_def_count = 0;
CBMRustLSPDef *rdefs = pxc_lspdefs_to_rust(&scratch, defs, def_count, &rust_def_count);
cbm_run_rust_lsp_cross_with_manifest(&scratch, source, source_len, module_qn, rdefs,
def_count, imp_names, imp_qns, imp_count, tree,
rust_def_count, imp_names, imp_qns, imp_count, tree,
g_pxc_rust_manifest, &out, &synthetic_calls);
break;
}
+101
View File
@@ -8757,6 +8757,105 @@ TEST(pipeline_go_rw_usage_never_cross_into_c) {
PASS();
}
/* The Rust registries were built from the mixed-language def set: a Perl
* `sub new` became a Rust free function, and the project-wide
* lsp_short_name_unique fallback bound `Store::new()` to it at 0.90 (found by
* the generic-engine lane on its 12-language fixture). A Rust call never
* targets another language's definition. pad_files > 0 forces the parallel
* resolver, whose Rust registry is the shared all-defs build. */
static int rust_registry_rust_only_case(int pad_files) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_rust_only_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_temp_file(tmp, "Cargo.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n");
write_temp_file(tmp, "src/store.rs",
"pub struct Store {\n"
" n: u32,\n"
"}\n"
"\n"
"impl Store {\n"
" pub fn new() -> Store {\n"
" Store { n: 0 }\n"
" }\n"
"\n"
" pub fn get(&self, k: u32) -> u32 {\n"
" self.n + k\n"
" }\n"
"}\n");
write_temp_file(tmp, "src/main.rs",
"mod store;\n"
"\n"
"use crate::store::Store;\n"
"\n"
"fn run() -> u32 {\n"
" let s = Store::new();\n"
" s.get(1)\n"
"}\n"
"\n"
"fn main() {\n"
" run();\n"
"}\n");
write_temp_file(tmp, "perlsrc/Lib.pm",
"package Lib;\n"
"\n"
"sub new {\n"
" my ($class) = @_;\n"
" return bless {}, $class;\n"
"}\n"
"\n"
"1;\n");
for (int i = 0; i < pad_files; i++) {
char name[64];
char body[128];
snprintf(name, sizeof(name), "src/filler%d.rs", i);
snprintf(body, sizeof(body), "pub fn filler%d() -> u32 {\n %d\n}\n", i, i);
write_temp_file(tmp, name, body);
}
char *saved = NULL;
if (pad_files > 0) {
char *old_workers = getenv("CBM_WORKERS");
saved = old_workers ? strdup(old_workers) : NULL;
cbm_setenv("CBM_WORKERS", "4", 1);
}
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/rust_only.db", tmp);
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p);
ASSERT_EQ(cbm_pipeline_run(p), 0);
const char *project = cbm_pipeline_project_name(p);
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
/* Never onto the Perl constructor ... */
ASSERT_EQ(named_edge_to_file_count(s, project, "CALLS", "run", "new", "perlsrc/Lib.pm"), 0);
/* ... and the Rust call still binds the Rust associated function. */
ASSERT_GTE(named_edge_to_file_count(s, project, "CALLS", "run", "new", "src/store.rs"), 1);
cbm_store_close(s);
cbm_pipeline_free(p);
if (pad_files > 0) {
if (saved) {
cbm_setenv("CBM_WORKERS", saved, 1);
free(saved);
} else {
cbm_unsetenv("CBM_WORKERS");
}
}
th_rmtree(tmp);
PASS();
}
TEST(pipeline_rust_call_never_binds_another_language) {
return rust_registry_rust_only_case(0);
}
TEST(pipeline_rust_call_never_binds_another_language_parallel) {
return rust_registry_rust_only_case(52);
}
TEST(pipeline_go_rw_usage_never_cross_into_c_parallel) {
/* Parallel twin of the test above: USAGE and WRITES/READS each have a
* second, independent resolver in pass_parallel.c (resolve_file_usages /
@@ -23615,6 +23714,8 @@ SUITE(pipeline) {
RUN_TEST(pipeline_html_embedded_member_call_stays_unbound);
RUN_TEST(pipeline_go_rw_usage_never_cross_into_c);
RUN_TEST(pipeline_go_rw_usage_never_cross_into_c_parallel);
RUN_TEST(pipeline_rust_call_never_binds_another_language);
RUN_TEST(pipeline_rust_call_never_binds_another_language_parallel);
RUN_TEST(pipeline_rust_std_receiver_never_binds_project_method);
RUN_TEST(pipeline_rust_std_receiver_never_binds_project_method_parallel);
RUN_TEST(pipeline_go_bare_ref_never_binds_field);