mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
fix(lsp): detach field arrays before overlay refinement
Copy inherited field-type pointer arrays into the writable registry arena before Python refines an existing field. Keep shared type objects intact and avoid retaining per-file scratch types in the sealed fallback. Add deterministic shared-state and later-overlay coverage, retaining the sequential Python field regression and adding a forced-parallel variant. Refs #1277 Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
1 parent
519c765e1f
commit
bf2daa3603
3 files changed
+203
-20
No files matched your search
@@ -677,8 +677,28 @@ CBMRegisteredType *cbm_registry_type_for_update(CBMTypeRegistry *head, const cha
|
||||
for (const CBMTypeRegistry *r = head->fallback; r; r = r->fallback) {
|
||||
const CBMRegisteredType *base = lookup_type_self(r, qualified_name);
|
||||
if (base) {
|
||||
CBMRegisteredType copy = *base;
|
||||
/* Field refinements overwrite entries in this array. The type
|
||||
* objects remain immutable, but the writable overlay must own
|
||||
* the pointer array rather than mutate its shared fallback. */
|
||||
if (base->field_types) {
|
||||
size_t count = 0;
|
||||
/* Types are parallel to names; a named field may have no
|
||||
* type, so an interior NULL does not terminate that array. */
|
||||
while (base->field_names ? base->field_names[count] != NULL
|
||||
: base->field_types[count] != NULL) {
|
||||
count++;
|
||||
}
|
||||
const CBMType **field_types = (const CBMType **)cbm_arena_alloc(
|
||||
head->arena, (count + 1) * sizeof(*field_types));
|
||||
if (!field_types) {
|
||||
return NULL;
|
||||
}
|
||||
memcpy(field_types, base->field_types, (count + 1) * sizeof(*field_types));
|
||||
copy.field_types = field_types;
|
||||
}
|
||||
int before = head->type_count;
|
||||
cbm_registry_add_type(head, *base);
|
||||
cbm_registry_add_type(head, copy);
|
||||
if (head->type_count == before + 1) {
|
||||
return &head->types[before];
|
||||
}
|
||||
|
||||
@@ -15350,6 +15350,10 @@ TEST(registry_overlay_chain_iterates_and_copies_on_write) {
|
||||
memset(&t, 0, sizeof(t));
|
||||
t.qualified_name = "pkg.T";
|
||||
t.short_name = "T";
|
||||
const char *field_names[] = {"untyped", "typed", NULL};
|
||||
const CBMType *field_types[] = {NULL, cbm_type_named(&arena, "pkg.Value"), NULL};
|
||||
t.field_names = field_names;
|
||||
t.field_types = field_types;
|
||||
cbm_registry_add_type(&base, t);
|
||||
cbm_registry_finalize(&base);
|
||||
base.read_only = true;
|
||||
@@ -15414,6 +15418,10 @@ TEST(registry_overlay_chain_iterates_and_copies_on_write) {
|
||||
CBMRegisteredType *wt = cbm_registry_type_for_update(&overlay, "pkg.T");
|
||||
ASSERT_NOT_NULL(wt);
|
||||
ASSERT_EQ(overlay.type_count, 1);
|
||||
/* A missing type for an earlier named field must not truncate the copy. */
|
||||
bool field_slots_preserved = wt->field_types && wt->field_types != field_types &&
|
||||
wt->field_types[0] == NULL &&
|
||||
wt->field_types[1] == field_types[1] && wt->field_types[2] == NULL;
|
||||
cbm_registry_all_types_chain(&overlay, &ti);
|
||||
seen = 0;
|
||||
while (cbm_type_short_iter_next(&ti) >= 0) {
|
||||
@@ -15430,6 +15438,7 @@ TEST(registry_overlay_chain_iterates_and_copies_on_write) {
|
||||
|
||||
cbm_arena_destroy(&scratch);
|
||||
cbm_arena_destroy(&arena);
|
||||
ASSERT_TRUE(field_slots_preserved);
|
||||
PASS();
|
||||
}
|
||||
|
||||
@@ -15804,6 +15813,111 @@ TEST(seal_py_shared_registry_readonly_fields) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #1277: the production resolver receives a writable overlay, so refining an
|
||||
* existing field must detach its pointer array from the sealed fallback. */
|
||||
TEST(seal_py_overlay_existing_field_keeps_shared_type) {
|
||||
CBMArena arena;
|
||||
cbm_arena_init(&arena);
|
||||
CBMLSPDef defs[4] = {0};
|
||||
defs[0].qualified_name = "test.mod.Foo";
|
||||
defs[0].short_name = "Foo";
|
||||
defs[0].label = "Class";
|
||||
defs[0].def_module_qn = "test.mod";
|
||||
defs[0].field_defs = "x:test.contracts.Contract";
|
||||
defs[1].qualified_name = "test.mod.Foo.m";
|
||||
defs[1].short_name = "m";
|
||||
defs[1].label = "Method";
|
||||
defs[1].receiver_type = "test.mod.Foo";
|
||||
defs[1].def_module_qn = "test.mod";
|
||||
defs[2].qualified_name = "test.contracts.Contract";
|
||||
defs[2].short_name = "Contract";
|
||||
defs[2].label = "Class";
|
||||
defs[2].def_module_qn = "test.contracts";
|
||||
defs[3].qualified_name = "test.contracts.Contract.process_batch";
|
||||
defs[3].short_name = "process_batch";
|
||||
defs[3].label = "Method";
|
||||
defs[3].receiver_type = "test.contracts.Contract";
|
||||
defs[3].def_module_qn = "test.contracts";
|
||||
for (int i = 0; i < 4; i++) {
|
||||
defs[i].lang = CBM_LANG_PYTHON;
|
||||
}
|
||||
CBMTypeRegistry *reg = cbm_py_build_cross_registry(&arena, defs, 4);
|
||||
ASSERT_NOT_NULL(reg);
|
||||
ASSERT_TRUE(reg->read_only);
|
||||
const CBMRegisteredType *base = cbm_registry_lookup_type(reg, "test.mod.Foo");
|
||||
ASSERT_NOT_NULL(base);
|
||||
ASSERT_NOT_NULL(base->field_names);
|
||||
ASSERT_NOT_NULL(base->field_types);
|
||||
const char **names_before = base->field_names;
|
||||
const CBMType **types_before = base->field_types;
|
||||
const CBMType *type_before = base->field_types[0];
|
||||
ASSERT_NOT_NULL(type_before);
|
||||
ASSERT_NULL(base->field_types[1]);
|
||||
|
||||
CBMArena scratch;
|
||||
cbm_arena_init(&scratch);
|
||||
CBMTypeRegistry overlay;
|
||||
cbm_registry_init(&overlay, &scratch);
|
||||
overlay.fallback = reg;
|
||||
const char *writer = "class Foo:\n"
|
||||
" def m(self):\n"
|
||||
" self.x = 1\n";
|
||||
CBMResolvedCallArray out = {0};
|
||||
cbm_run_py_lsp_cross_with_registry(&scratch, writer, (int)strlen(writer), "test.mod", &overlay,
|
||||
NULL, NULL, 0, NULL, &out, NULL);
|
||||
|
||||
/* Observe while scratch is live, then release both arenas before any
|
||||
* failure assertion so the regression also fails cleanly under LSan. */
|
||||
bool shared_names_unchanged = base->field_names == names_before;
|
||||
bool shared_types_unchanged = base->field_types == types_before;
|
||||
bool shared_element_unchanged = base->field_types[0] == type_before;
|
||||
bool shared_terminator_unchanged = base->field_types[1] == NULL;
|
||||
const CBMRegisteredType *refined = cbm_registry_lookup_type(&overlay, "test.mod.Foo");
|
||||
bool local_refined = refined && refined != base && refined->field_types &&
|
||||
refined->field_types != types_before && refined->field_types[0] &&
|
||||
refined->field_types[0] != type_before &&
|
||||
refined->field_types[0]->kind == CBM_TYPE_BUILTIN &&
|
||||
refined->field_types[1] == NULL;
|
||||
cbm_arena_destroy(&scratch);
|
||||
|
||||
/* Never read a corrupted shared pointer after scratch destruction. When
|
||||
* intact, a later file must still see the original Contract field. */
|
||||
int hits = 0;
|
||||
if (shared_names_unchanged && shared_types_unchanged && shared_element_unchanged &&
|
||||
shared_terminator_unchanged && local_refined) {
|
||||
cbm_arena_init(&scratch);
|
||||
cbm_registry_init(&overlay, &scratch);
|
||||
overlay.fallback = reg;
|
||||
const char *reader = "from mod import Foo\n\n"
|
||||
"def run(foo: Foo):\n"
|
||||
" foo.x.process_batch()\n";
|
||||
const char *import_names[] = {"Foo"};
|
||||
const char *import_qns[] = {"test.mod.Foo"};
|
||||
memset(&out, 0, sizeof(out));
|
||||
cbm_run_py_lsp_cross_with_registry(&scratch, reader, (int)strlen(reader), "test.reader",
|
||||
&overlay, import_names, import_qns, 1, NULL, &out, NULL);
|
||||
for (int i = 0; i < out.count; i++) {
|
||||
const CBMResolvedCall *call = &out.items[i];
|
||||
if (call->caller_qn && call->callee_qn &&
|
||||
strcmp(call->caller_qn, "test.reader.run") == 0 &&
|
||||
strcmp(call->callee_qn, "test.contracts.Contract.process_batch") == 0 &&
|
||||
call->kind == CBM_RESOLVED_INVOCATION &&
|
||||
call->confidence >= CBM_LSP_CONFIDENCE_FLOOR) {
|
||||
hits++;
|
||||
}
|
||||
}
|
||||
cbm_arena_destroy(&scratch);
|
||||
}
|
||||
cbm_arena_destroy(&arena);
|
||||
ASSERT_TRUE(shared_names_unchanged);
|
||||
ASSERT_TRUE(shared_types_unchanged);
|
||||
ASSERT_TRUE(shared_element_unchanged);
|
||||
ASSERT_TRUE(shared_terminator_unchanged);
|
||||
ASSERT_TRUE(local_refined);
|
||||
ASSERT_EQ(hits, 1);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(seal_cs_shared_registry_readonly) {
|
||||
CBMArena arena;
|
||||
cbm_arena_init(&arena);
|
||||
@@ -16402,6 +16516,7 @@ SUITE(c_lsp) {
|
||||
RUN_TEST(registry_overlay_chain_iterates_and_copies_on_write);
|
||||
RUN_TEST(clsp_method_return_refinement_is_copy_on_write);
|
||||
RUN_TEST(seal_py_shared_registry_readonly_fields);
|
||||
RUN_TEST(seal_py_overlay_existing_field_keeps_shared_type);
|
||||
RUN_TEST(seal_cs_shared_registry_readonly);
|
||||
RUN_TEST(seal_ts_shared_registry_readonly);
|
||||
RUN_TEST(seal_go_shared_registry_readonly);
|
||||
|
||||
+67
-19
@@ -8198,7 +8198,7 @@ static int count_lsp_calls(cbm_store_t *s, const char *project, const char *call
|
||||
return hits;
|
||||
}
|
||||
|
||||
TEST(python_crossfile_typed_field_calls_issue1277) {
|
||||
static int check_python_crossfile_typed_field_calls_issue1277(bool parallel) {
|
||||
const char *contracts = "class Contract:\n"
|
||||
" def process_batch(self) -> None:\n"
|
||||
" ...\n";
|
||||
@@ -8226,31 +8226,70 @@ TEST(python_crossfile_typed_field_calls_issue1277) {
|
||||
}
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "%s/loop.py", g_usages_tmpdir);
|
||||
FILE *f = fopen(path, "w");
|
||||
ASSERT_NOT_NULL(f);
|
||||
fputs(loop, f);
|
||||
fclose(f);
|
||||
if (th_write_file(path, loop) != 0) {
|
||||
teardown_usages_repo();
|
||||
FAIL("failed to write loop.py");
|
||||
}
|
||||
if (parallel) {
|
||||
/* Three semantic files plus 50 inert files exceed the production
|
||||
* threshold for the fused parallel pipeline. */
|
||||
for (int i = 0; i < 50; i++) {
|
||||
snprintf(path, sizeof(path), "%s/field_pad_%02d.py", g_usages_tmpdir, i);
|
||||
if (th_write_file(path, "field_padding = 0\n") != 0) {
|
||||
teardown_usages_repo();
|
||||
FAIL("failed to write parallel-selection fixture");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const char *old_workers = getenv("CBM_WORKERS");
|
||||
char *saved_workers = old_workers ? strdup(old_workers) : NULL;
|
||||
const char *old_single = getenv("CBM_INDEX_SINGLE_THREAD");
|
||||
char *saved_single = old_single ? strdup(old_single) : NULL;
|
||||
if ((old_workers && !saved_workers) || (old_single && !saved_single)) {
|
||||
free(saved_workers);
|
||||
free(saved_single);
|
||||
teardown_usages_repo();
|
||||
FAIL("failed to save pipeline environment");
|
||||
}
|
||||
int pin_workers_rc = parallel ? cbm_setenv("CBM_WORKERS", "4", 1) : 0;
|
||||
int pin_single_rc = parallel ? cbm_unsetenv("CBM_INDEX_SINGLE_THREAD") : 0;
|
||||
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/test_1277.db", g_usages_tmpdir);
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(g_usages_tmpdir, db_path, CBM_MODE_FULL);
|
||||
ASSERT_NOT_NULL(p);
|
||||
ASSERT_EQ(cbm_pipeline_run(p), 0);
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
const char *project = cbm_pipeline_project_name(p);
|
||||
cbm_pipeline_t *p = pin_workers_rc == 0 && pin_single_rc == 0
|
||||
? cbm_pipeline_new(g_usages_tmpdir, db_path, CBM_MODE_FULL)
|
||||
: NULL;
|
||||
int run_rc = p ? cbm_pipeline_run(p) : -1;
|
||||
cbm_store_t *s = run_rc == 0 ? cbm_store_open_path(db_path) : NULL;
|
||||
bool store_opened = s != NULL;
|
||||
int via_alias = -1, via_typed = -1, via_class = -1, via_untyped = -1;
|
||||
if (s) {
|
||||
const char *project = cbm_pipeline_project_name(p);
|
||||
via_alias = count_lsp_calls(s, project, "loop.run", "contracts.Contract.process_batch");
|
||||
via_typed =
|
||||
count_lsp_calls(s, project, "loop.run_typed", "contracts.Contract.process_batch");
|
||||
via_class =
|
||||
count_lsp_calls(s, project, "loop.run_classlevel", "contracts.Contract.process_batch");
|
||||
via_untyped = count_lsp_calls(s, project, "loop.run_untyped", "process_batch");
|
||||
cbm_store_close(s);
|
||||
}
|
||||
|
||||
int via_alias = count_lsp_calls(s, project, "loop.run", "contracts.Contract.process_batch");
|
||||
int via_typed =
|
||||
count_lsp_calls(s, project, "loop.run_typed", "contracts.Contract.process_batch");
|
||||
int via_class =
|
||||
count_lsp_calls(s, project, "loop.run_classlevel", "contracts.Contract.process_batch");
|
||||
int via_untyped = count_lsp_calls(s, project, "loop.run_untyped", "process_batch");
|
||||
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p);
|
||||
int restore_workers_rc =
|
||||
saved_workers ? cbm_setenv("CBM_WORKERS", saved_workers, 1) : cbm_unsetenv("CBM_WORKERS");
|
||||
int restore_single_rc = saved_single ? cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1)
|
||||
: cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
|
||||
free(saved_workers);
|
||||
free(saved_single);
|
||||
teardown_usages_repo();
|
||||
|
||||
ASSERT_EQ(pin_workers_rc, 0);
|
||||
ASSERT_EQ(pin_single_rc, 0);
|
||||
ASSERT_EQ(restore_workers_rc, 0);
|
||||
ASSERT_EQ(restore_single_rc, 0);
|
||||
ASSERT_EQ(run_rc, 0);
|
||||
ASSERT_TRUE(store_opened);
|
||||
ASSERT_EQ(via_alias, 1); /* self.x = <annotated __init__ parameter> */
|
||||
ASSERT_EQ(via_typed, 1); /* self.x: T = ... */
|
||||
ASSERT_EQ(via_class, 1); /* class-level x: T */
|
||||
@@ -8258,6 +8297,14 @@ TEST(python_crossfile_typed_field_calls_issue1277) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(python_crossfile_typed_field_calls_issue1277) {
|
||||
return check_python_crossfile_typed_field_calls_issue1277(false);
|
||||
}
|
||||
|
||||
TEST(python_crossfile_typed_field_calls_issue1277_parallel) {
|
||||
return check_python_crossfile_typed_field_calls_issue1277(true);
|
||||
}
|
||||
|
||||
TEST(usages_creates_edges) {
|
||||
/* Port of TestPassUsagesCreatesEdges.
|
||||
* Go source with callback reference → USAGE edge. */
|
||||
@@ -16471,6 +16518,7 @@ SUITE(pipeline) {
|
||||
RUN_TEST(implements_no_match);
|
||||
/* Usages pass (full pipeline integration) */
|
||||
RUN_TEST(python_crossfile_typed_field_calls_issue1277);
|
||||
RUN_TEST(python_crossfile_typed_field_calls_issue1277_parallel);
|
||||
RUN_TEST(usages_creates_edges);
|
||||
RUN_TEST(usages_no_duplicate_calls);
|
||||
RUN_TEST(calls_edge_carries_call_site_line);
|
||||
|
||||
Reference in new issue
Block a user