Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 61 additions & 25 deletions src/pipeline/registry.c
Original file line number Diff line number Diff line change
Expand Up @@ -858,9 +858,38 @@ void cbm_registry_free(cbm_registry_t *r) {

/* ── Registration ────────────────────────────────────────────────── */

/* Record `owned_qn` in the by-name bucket for `key`, keeping the bucket's
* is_test flags in step with its entries.
*
* No array dedup needed: cbm_registry_add's exact-map check guarantees the QN
* is new, and it calls this at most once per distinct key. */
static void index_under_name(cbm_registry_t *r, const char *key, const char *owned_qn) {
qn_array_t *arr = cbm_ht_get(r->by_name, key);
if (!arr) {
arr = calloc(CBM_ALLOC_ONE, sizeof(qn_array_t));
cbm_ht_set(r->by_name, strdup(key), arr);
}
int before = arr->count;
cbm_da_push(arr, (char *)owned_qn);
if (arr->count == before) {
return; /* the name could not be recorded: no verdict to cache */
}
if (arr->count > arr->is_test_cap) {
int want = arr->cap > 0 ? arr->cap : arr->count;
uint8_t *grown =
cbm_realloc(CBM_MEM_CLASS_DYN_ARRAY, arr->is_test, (size_t)want * sizeof(uint8_t));
if (grown) {
arr->is_test = grown;
arr->is_test_cap = want;
}
}
if (arr->count <= arr->is_test_cap) {
arr->is_test[arr->count - SKIP_ONE] = is_test_qn(owned_qn) ? 1 : 0;
}
}

void cbm_registry_add(cbm_registry_t *r, const char *name, const char *qualified_name,
const char *label) {
(void)name;
if (!r || !qualified_name || !label) {
return;
}
Expand Down Expand Up @@ -893,30 +922,37 @@ void cbm_registry_add(cbm_registry_t *r, const char *name, const char *qualified
cbm_ht_set(r->exact, strdup(qualified_name), (void *)interned);
const char *owned_qn = cbm_ht_get_key(r->exact, qualified_name);

/* Index by simple name.
* No array dedup needed: exact-map check above guarantees uniqueness. */
const char *simple = simple_name(qualified_name);
qn_array_t *arr = cbm_ht_get(r->by_name, simple);
if (!arr) {
arr = calloc(CBM_ALLOC_ONE, sizeof(qn_array_t));
cbm_ht_set(r->by_name, strdup(simple), arr);
}
int before = arr->count;
cbm_da_push(arr, (char *)owned_qn);
if (arr->count == before) {
return; /* the name could not be recorded: no verdict to cache */
}
if (arr->count > arr->is_test_cap) {
int want = arr->cap > 0 ? arr->cap : arr->count;
uint8_t *grown =
cbm_realloc(CBM_MEM_CLASS_DYN_ARRAY, arr->is_test, (size_t)want * sizeof(uint8_t));
if (grown) {
arr->is_test = grown;
arr->is_test_cap = want;
}
}
if (arr->count <= arr->is_test_cap) {
arr->is_test[arr->count - SKIP_ONE] = is_test_qn(owned_qn) ? 1 : 0;
/* Index the symbol under the QN's last dot segment, and additionally under
* the name its caller passed when that segment carries a '#' fence.
*
* The derived key is the one every language has always used and it stays
* unconditional, so a grammar whose QNs carry no '#' is byte-identical to
* before this commit. The second key exists for one shape: a fenced tail is
* a literal string no bare callee is ever written as. rust_cfg_qualified_name
* mints a `#[cfg(test)]` twin as "proj.lib.add#cfg(test)", simple_name() has
* no '#' handling, so the derived key filed that function under the whole
* string "add#cfg(test)" where no bare `add` callee could reach it.
*
* Gating on the fence rather than on the file's language is what makes
* "unchanged for every other grammar" checkable instead of asserted. Every
* by-name lookup keys through simple_name(), which splits on '.' and "::"
* only, so a passed name that differs from the derived key by anything
* OTHER than a fence is a key no lookup can reach: an HCL block's
* "resource.aws_instance.web" and a TOML table's "tool.poetry.dependencies"
* are both already indexed under the tail a reference spells, and adding
* their dotted form would only widen the bucket the scorer walks.
*
* `name` is NULL or empty only for callers that have no symbol name to
* give; those have the derived key and nothing else. */
const char *derived = simple_name(qualified_name);
index_under_name(r, derived, owned_qn);
/* '#' is a QN fence, and extract_defs.c's rust_cfg_qualified_name is the
* only thing in the tree that mints one today. A grammar that starts
* minting a '#' opts into this second key by doing so, whatever it means by
* the fence: its symbols become reachable under the passed name as well,
* and they share that name's bucket with everything else filed under it. */
if (name && name[0] && strchr(derived, '#') && strcmp(name, derived) != 0) {
index_under_name(r, name, owned_qn);
}
}

Expand Down
55 changes: 55 additions & 0 deletions tests/test_pipeline.c
Original file line number Diff line number Diff line change
Expand Up @@ -1147,6 +1147,60 @@ TEST(pipeline_nix_scoped_binding_calls_resolve) {
PASS();
}

/* Terraform reference resolution, end to end.
*
* An HCL block names itself with its labels appended -- find_hcl_block_name
* mints "resource.aws_instance.web" -- while its QN's last dot segment is
* bare "web", and a reference written `aws_instance.web.id` reaches the
* by-name index through that tail. HCL is therefore a language where the
* definition name and the QN tail are different strings, and an index keyed on
* either one alone drops every cross-resource reference in the file.
*
* This has to be a pipeline test: the registry is shared by every language
* and nothing in the HCL extractor mentions the index key, so the two halves
* can disagree with every extraction-level assertion still green.
*/
TEST(pipeline_hcl_block_reference_resolves_to_its_block) {
if (setup_test_repo() != 0) {
FAIL("failed to create temp dir");
}

char tf_path[512];
snprintf(tf_path, sizeof(tf_path), "%s/main.tf", g_tmpdir);
FILE *tf = fopen(tf_path, "w");
if (!tf) {
teardown_test_repo();
FAIL("failed to write terraform fixture");
}
fprintf(tf, "resource \"aws_instance\" \"web\" {\n"
" ami = \"ami-0c55b159cbfafe1f0\"\n"
" instance_type = \"t2.micro\"\n"
"}\n"
"\n"
"resource \"aws_eip\" \"ip\" {\n"
" instance = aws_instance.web.id\n"
"}\n");
fclose(tf);

char tf_db[512];
snprintf(tf_db, sizeof(tf_db), "%s/test_hcl_refs.db", g_tmpdir);

cbm_pipeline_t *tp = cbm_pipeline_new(g_tmpdir, tf_db, CBM_MODE_FULL);
ASSERT_NOT_NULL(tp);
ASSERT_EQ(cbm_pipeline_run(tp), 0);

cbm_store_t *ts = cbm_store_open_path(tf_db);
ASSERT_NOT_NULL(ts);
const char *tf_project = cbm_pipeline_project_name(tp);

ASSERT(cross_file_edge_exists(ts, tf_project, "main", "resource.aws_instance.web", "USAGE"));

cbm_store_close(ts);
cbm_pipeline_free(tp);
teardown_test_repo();
PASS();
}

/* Regression: incremental re-index of an edited file must NOT drop inbound
* cross-file CALLS edges whose source lives in an UNCHANGED file.
*
Expand Down Expand Up @@ -15129,6 +15183,7 @@ SUITE(pipeline) {
/* Calls pass */
RUN_TEST(pipeline_calls_resolution);
RUN_TEST(pipeline_nix_scoped_binding_calls_resolve);
RUN_TEST(pipeline_hcl_block_reference_resolves_to_its_block);
RUN_TEST(pipeline_incremental_preserves_cross_file_calls);
RUN_TEST(pipeline_objectscript_export_preserves_calls_sequential_parallel);
RUN_TEST(pipeline_objectscript_export_incremental_matches_full_relationships);
Expand Down
52 changes: 52 additions & 0 deletions tests/test_registry.c
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,56 @@ TEST(resolve_qualified_ambiguous_tail_falls_through) {
PASS();
}

/* cbm_registry_add used to discard its `name` argument and re-derive the
* lookup key from the QN's last dot segment. That made the QN's tail load
* bearing for the bare-name index every language shares: a Rust cfg twin,
* minted as "add#cfg(test)", was indexed under that literal string and no bare
* `add` callee could ever reach it. */
TEST(registry_indexes_by_passed_name_not_qn_tail) {
cbm_registry_t *r = cbm_registry_new();
cbm_registry_add(r, "add", "proj.lib.add#cfg(test)", "Function");

cbm_resolution_t res = cbm_registry_resolve(r, "add", "proj.lib.caller", NULL, NULL, 0);
ASSERT_STR_EQ(res.qualified_name, "proj.lib.add#cfg(test)");

cbm_registry_free(r);
PASS();
}

/* The derived key stays unconditional, and this pins that.
*
* A name may carry segments the QN's tail drops, and then the tail is the key
* callers actually spell. An HCL block is named "resource.aws_instance.web" by
* find_hcl_block_name while its QN tail is bare "web", which is how an
* `aws_instance.web.id` reference reaches it. Re-keying the index on the passed
* name would lose that lookup outright, so the fence gate above leaves these
* shapes exactly as they were.
*
* The Module row registers a shape production cannot produce: every caller of
* cbm_registry_add gates on cbm_label_is_registry_symbol, which does not admit
* "Module". It is here because the registry's own contract is per-label-string,
* not per-caller, and a future label change should not silently drop the stem
* lookup a bare module reference needs. */
TEST(registry_indexes_a_dotted_name_under_its_tail_too) {
cbm_registry_t *r = cbm_registry_new();
cbm_registry_add(r, "resource.aws_instance.web", "proj.main.resource.aws_instance.web",
"Class");
cbm_registry_add(r, "helper.py", "proj.pkg.helper", "Module");

cbm_resolution_t tail = cbm_registry_resolve(r, "web", "proj.main", NULL, NULL, 0);
ASSERT_STR_EQ(tail.qualified_name, "proj.main.resource.aws_instance.web");

cbm_resolution_t whole =
cbm_registry_resolve(r, "resource.aws_instance.web", "proj.main", NULL, NULL, 0);
ASSERT_STR_EQ(whole.qualified_name, "proj.main.resource.aws_instance.web");

cbm_resolution_t stem = cbm_registry_resolve(r, "helper", "proj.pkg.caller", NULL, NULL, 0);
ASSERT_STR_EQ(stem.qualified_name, "proj.pkg.helper");

cbm_registry_free(r);
PASS();
}

TEST(resolve_import_map) {
cbm_registry_t *r = cbm_registry_new();
cbm_registry_add(r, "Process", "proj.pkg.worker.Process", "Function");
Expand Down Expand Up @@ -1201,6 +1251,8 @@ SUITE(registry) {
RUN_TEST(resolve_same_module);
RUN_TEST(resolve_qualified_disambiguates_same_name);
RUN_TEST(resolve_qualified_ambiguous_tail_falls_through);
RUN_TEST(registry_indexes_by_passed_name_not_qn_tail);
RUN_TEST(registry_indexes_a_dotted_name_under_its_tail_too);
RUN_TEST(resolve_import_map);
RUN_TEST(resolve_import_map_bare_function);
RUN_TEST(resolve_import_map_bare_alias);
Expand Down
Loading