fix: preserve semantic graph relationships

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel
2026-07-18 15:52:50 +02:00
parent 7d6cdb23ef
commit 87a0e3f74f
122 changed files with 39342 additions and 3083 deletions
+55 -13
View File
@@ -66,13 +66,19 @@ SANITIZE = -fsanitize=address,undefined -fno-omit-frame-pointer
EDITOR_TEST_DEFINES = -DCBM_JSON_LIKE_ENABLE_TEST_API=1 \
-DCBM_TOML_EDIT_ENABLE_TEST_API=1 -DCBM_YAML_ENABLE_TEST_API=1 \
-DCBM_TEXT_EDIT_ENABLE_TEST_API=1 -DCBM_CLI_ENABLE_TEST_API=1
CFLAGS_TEST = $(CFLAGS_COMMON) $(EDITOR_TEST_DEFINES) -g -O1 $(SANITIZE)
KOTLIN_DEDUP_TEST_DEFINE = -DCBM_KOTLIN_DEDUP_TEST_API=1
CALL_REFERENCE_LOOKUP_TEST_DEFINE = -DCBM_CALL_REFERENCE_LOOKUP_TEST_API=1
INCREMENTAL_TEST_DEFINE = -DCBM_INCREMENTAL_TEST_API=1
CFLAGS_TEST = $(CFLAGS_COMMON) $(EDITOR_TEST_DEFINES) $(KOTLIN_DEDUP_TEST_DEFINE) \
$(CALL_REFERENCE_LOOKUP_TEST_DEFINE) $(INCREMENTAL_TEST_DEFINE) \
-g -O1 $(SANITIZE)
CXXFLAGS_TEST = $(CXXFLAGS_COMMON) -g -O1 $(SANITIZE)
# TSan (can't combine with ASan)
TSAN_SANITIZE = -fsanitize=thread -fno-omit-frame-pointer
CFLAGS_TSAN = $(CFLAGS_COMMON) $(EDITOR_TEST_DEFINES) -g -O1 \
$(TSAN_SANITIZE)
CFLAGS_TSAN = $(CFLAGS_COMMON) $(EDITOR_TEST_DEFINES) $(KOTLIN_DEDUP_TEST_DEFINE) \
$(CALL_REFERENCE_LOOKUP_TEST_DEFINE) $(INCREMENTAL_TEST_DEFINE) \
-g -O1 $(TSAN_SANITIZE)
CXXFLAGS_TSAN = $(CXXFLAGS_COMMON) -g -O1 \
$(TSAN_SANITIZE)
@@ -156,6 +162,14 @@ LSP_UNITY_DEPS = $(CBM_DIR)/lsp_all.c \
# Tree-sitter runtime
TS_RUNTIME_SRC = $(CBM_DIR)/ts_runtime.c
# ts_runtime.c is a unity wrapper that textually includes the vendored runtime.
# Track those included sources and headers explicitly: without compiler-generated
# dependencies, changing stack.c (including its GLR recursion cap) otherwise
# leaves every runtime object stale while the final binary is merely relinked.
TS_RUNTIME_DEPS = $(TS_RUNTIME_SRC) \
$(wildcard $(TS_SRC)/*.c $(TS_SRC)/*.h \
$(TS_SRC)/portable/*.h $(TS_SRC)/unicode/*.h \
$(TS_INCLUDE)/tree_sitter/*.h)
# All 64 grammar shim files
GRAMMAR_SRCS = $(wildcard $(CBM_DIR)/grammar_*.c)
@@ -357,7 +371,7 @@ TEST_DISCOVER_SRCS = \
TEST_GRAPH_BUFFER_SRCS = tests/test_graph_buffer.c
TEST_PIPELINE_SRCS = tests/test_registry.c tests/test_pipeline.c tests/test_fqn.c tests/test_route_canon.c tests/test_path_alias.c tests/test_configlink.c tests/test_infrascan.c tests/test_worker_pool.c tests/test_parallel.c tests/test_index_resilience.c
TEST_PIPELINE_SRCS = tests/test_registry.c tests/test_pipeline.c tests/test_fqn.c tests/test_route_canon.c tests/test_path_alias.c tests/test_configlink.c tests/test_infrascan.c tests/test_worker_pool.c tests/test_parallel.c tests/test_index_resilience.c tests/test_call_reference_contract.c tests/repro/repro_call_scope_usages.c tests/repro/repro_call_argument_usages.c tests/repro/repro_reference_precision.c tests/repro/repro_lexical_binding_precision.c tests/repro/repro_call_argument_matrix_a.c tests/repro/repro_call_argument_matrix_b.c tests/repro/repro_call_node_behaviors.c tests/repro/repro_language_registry.c tests/repro/repro_call_node_manifest.c tests/repro/repro_lsp_ordered_signatures.c tests/repro/repro_lsp_ordered_local.c tests/repro/repro_ts_overload_return_chains.c tests/repro/repro_harness_cleanup.c tests/repro/repro_runner_filter.c
TEST_WATCHER_SRCS = tests/test_watcher.c
@@ -423,12 +437,26 @@ TEST_SIMHASH_SRCS = tests/test_simhash.c
TEST_STACK_OVERFLOW_SRCS = tests/test_stack_overflow.c
# Cumulative BUG-REPRODUCTION suite (separate runner, NOT in ALL_TEST_SRCS).
# These cases are RED by design (one open bug each) — see tests/repro/repro_main.c.
# Contains RED reproductions plus GREEN controls — see tests/repro/repro_main.c.
# Kept out of the gating `make test` so `ci-ok` stays green; run via `make test-repro`.
TEST_REPRO_SRCS = \
tests/repro/repro_main.c \
tests/repro/repro_parallel_determinism.c \
tests/repro/repro_extraction.c \
tests/repro/repro_runner_filter.c \
tests/repro/repro_harness_cleanup.c \
tests/repro/repro_language_registry.c \
tests/repro/repro_call_node_manifest.c \
tests/repro/repro_call_scope_usages.c \
tests/repro/repro_call_argument_usages.c \
tests/repro/repro_lsp_ordered_signatures.c \
tests/repro/repro_lsp_ordered_local.c \
tests/repro/repro_ts_overload_return_chains.c \
tests/repro/repro_reference_precision.c \
tests/repro/repro_lexical_binding_precision.c \
tests/repro/repro_call_argument_matrix_a.c \
tests/repro/repro_call_argument_matrix_b.c \
tests/repro/repro_call_node_behaviors.c \
tests/repro/repro_issue495.c \
tests/repro/repro_issue521.c \
tests/repro/repro_issue382.c \
@@ -485,6 +513,11 @@ TEST_REPRO_SRCS = \
tests/repro/repro_lsp_java_cs.c \
tests/repro/repro_lsp_kt_php_rust.c
TEST_SHARED_HEADERS = tests/test_framework.h tests/test_helpers.h tests/grammar_cases.h \
tests/repro/repro_harness.h tests/repro/repro_runner.h
TEST_REPRO_HEADERS = $(wildcard tests/repro/*.h) tests/test_framework.h tests/test_helpers.h
PROD_HEADERS = $(wildcard src/*.h src/*/*.h $(CBM_DIR)/*.h $(CBM_DIR)/lsp/*.h)
ALL_TEST_SRCS =$(TEST_FOUNDATION_SRCS) $(TEST_EXTRACTION_SRCS) $(TEST_STORE_SRCS) $(TEST_CYPHER_SRCS) $(TEST_MCP_SRCS) $(TEST_DISCOVER_SRCS) $(TEST_GRAPH_BUFFER_SRCS) $(TEST_PIPELINE_SRCS) $(TEST_WATCHER_SRCS) $(TEST_LZ4_SRCS) $(TEST_ZSTD_SRCS) $(TEST_ARTIFACT_SRCS) $(TEST_SQLITE_WRITER_SRCS) $(TEST_GO_LSP_SRCS) $(TEST_C_LSP_SRCS) $(TEST_PHP_LSP_SRCS) $(TEST_CS_LSP_SRCS) $(TEST_CS_LSP_BENCH_SRCS) $(TEST_PERL_LSP_SRCS) $(TEST_SCOPE_SRCS) $(TEST_TYPE_REP_SRCS) $(TEST_PY_LSP_SRCS) $(TEST_PY_LSP_BENCH_SRCS) $(TEST_PY_LSP_STRESS_SRCS) $(TEST_PY_LSP_SCALE_SRCS) $(TEST_TS_LSP_SRCS) $(TEST_JAVA_LSP_SRCS) $(TEST_KOTLIN_LSP_SRCS) $(TEST_RUST_LSP_SRCS) $(TEST_TRACES_SRCS) $(TEST_CLI_SRCS) $(TEST_MEM_SRCS) $(TEST_UI_SRCS) $(TEST_HTTPD_SRCS) $(TEST_SECURITY_SRCS) $(TEST_YAML_SRCS) $(TEST_SEMANTIC_SRCS) $(TEST_AST_PROFILE_SRCS) $(TEST_SLAB_ALLOC_SRCS) $(TEST_SIMHASH_SRCS) $(TEST_STACK_OVERFLOW_SRCS) $(TEST_INTEGRATION_SRCS)
@@ -531,7 +564,7 @@ test-foundation: $(BUILD_DIR)/test-foundation
$(BUILD_DIR)/%.o: $(CBM_DIR)/%.c | $(BUILD_DIR)
$(CC) $(GRAMMAR_CFLAGS_TEST) -c -o $@ $<
$(BUILD_DIR)/ts_runtime.o: $(CBM_DIR)/ts_runtime.c | $(BUILD_DIR)
$(BUILD_DIR)/ts_runtime.o: $(TS_RUNTIME_DEPS) | $(BUILD_DIR)
$(CC) $(GRAMMAR_CFLAGS_TEST) -c -o $@ $<
$(BUILD_DIR)/lsp_all.o: $(LSP_UNITY_DEPS) | $(BUILD_DIR)
@@ -543,7 +576,7 @@ $(BUILD_DIR)/preprocessor.o: $(CBM_DIR)/preprocessor.cpp | $(BUILD_DIR)
$(BUILD_DIR)/tsan_%.o: $(CBM_DIR)/%.c | $(BUILD_DIR)
$(CC) $(GRAMMAR_CFLAGS_TSAN) -c -o $@ $<
$(BUILD_DIR)/tsan_ts_runtime.o: $(CBM_DIR)/ts_runtime.c | $(BUILD_DIR)
$(BUILD_DIR)/tsan_ts_runtime.o: $(TS_RUNTIME_DEPS) | $(BUILD_DIR)
$(CC) $(GRAMMAR_CFLAGS_TSAN) -c -o $@ $<
$(BUILD_DIR)/tsan_lsp_all.o: $(LSP_UNITY_DEPS) | $(BUILD_DIR)
@@ -629,7 +662,7 @@ $(UNIXCODER_OBJ): $(UNIXCODER_BLOB_SRC) vendored/nomic/code_vectors.bin | $(BUIL
OBJS_VENDORED_TEST = $(MIMALLOC_OBJ_TEST) $(SQLITE3_OBJ_TEST) $(TRE_OBJ_TEST) $(GRAMMAR_OBJS_TEST) $(TS_RUNTIME_OBJ_TEST) $(LSP_OBJ_TEST) $(PP_OBJ_TEST) $(LZ4_OBJ_TEST) $(ZSTD_OBJ_TEST) $(UNIXCODER_OBJ)
OBJS_VENDORED_TSAN = $(MIMALLOC_OBJ_TSAN) $(SQLITE3_OBJ_TSAN) $(TRE_OBJ_TSAN) $(GRAMMAR_OBJS_TSAN) $(TS_RUNTIME_OBJ_TSAN) $(LSP_OBJ_TSAN) $(PP_OBJ_TSAN) $(LZ4_OBJ_TSAN) $(ZSTD_OBJ_TSAN) $(UNIXCODER_OBJ)
$(BUILD_DIR)/test-runner: $(ALL_TEST_SRCS) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TEST) | $(BUILD_DIR)
$(BUILD_DIR)/test-runner: $(ALL_TEST_SRCS) $(TEST_SHARED_HEADERS) $(PROD_HEADERS) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TEST) | $(BUILD_DIR)
$(CC) $(CFLAGS_TEST) -Itests -Itests/repro -o $@ \
$(ALL_TEST_SRCS) $(PROD_SRCS) \
$(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \
@@ -653,11 +686,11 @@ test-focused: $(BUILD_DIR)/test-runner
(echo "TEST_SUITES is required for test-focused"; exit 2)
cd $(CURDIR) && $(BUILD_DIR)/test-runner $(TEST_SUITES)
# ── Cumulative bug-reproduction runner (RED by design, non-gating) ──
# ── Cumulative bug-reproduction runner (RED board by design, non-gating) ──
# Mirrors test-runner's link line but uses repro_main.c (own main + counters)
# and TEST_REPRO_SRCS instead of ALL_TEST_SRCS. Exits non-zero while any bug is
# still reproduced (the expected state); bug-repro.yml surfaces it as a board.
$(BUILD_DIR)/test-repro-runner: $(TEST_REPRO_SRCS) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TEST) | $(BUILD_DIR)
$(BUILD_DIR)/test-repro-runner: $(TEST_REPRO_SRCS) $(TEST_REPRO_HEADERS) $(PROD_HEADERS) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TEST) | $(BUILD_DIR)
$(CC) $(CFLAGS_TEST) -Itests -o $@ \
$(TEST_REPRO_SRCS) $(PROD_SRCS) \
$(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \
@@ -665,6 +698,15 @@ $(BUILD_DIR)/test-repro-runner: $(TEST_REPRO_SRCS) $(PROD_SRCS) $(EXTRACTION_SRC
$(LDFLAGS_TEST)
test-repro: $(BUILD_DIR)/test-repro-runner
cd $(CURDIR) && bash tests/repro/repro_script_summary.sh
cd $(CURDIR) && bash tests/repro/repro_script_zero_selection.sh
cd $(CURDIR) && bash tests/repro/repro_script_missing_summary.sh
cd $(CURDIR) && bash tests/repro/repro_script_skipped_summary.sh
cd $(CURDIR) && bash tests/repro/repro_runner_zero_selection.sh
cd $(CURDIR) && bash tests/repro/repro_runner_passing_semantics.sh
cd $(CURDIR) && bash tests/repro/repro_script_no_workspace_artifact.sh
cd $(CURDIR) && bash tests/repro/repro_script_stderr_summary.sh
cd $(CURDIR) && bash tests/repro/repro_make_tracks_headers.sh
cd $(CURDIR) && $(BUILD_DIR)/test-repro-runner
# ── TSan full test ───────────────────────────────────────────────
@@ -672,7 +714,7 @@ test-repro: $(BUILD_DIR)/test-repro-runner
TEST_TSAN_SUITES ?= mem slab_alloc parallel
TSAN_OPTIONS ?= halt_on_error=1
$(BUILD_DIR)/test-runner-tsan: $(ALL_TEST_SRCS) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TSAN) | $(BUILD_DIR)
$(BUILD_DIR)/test-runner-tsan: $(ALL_TEST_SRCS) $(TEST_SHARED_HEADERS) $(PROD_HEADERS) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TSAN) | $(BUILD_DIR)
$(CC) $(CFLAGS_TSAN) -Itests -Itests/repro -o $@ \
$(ALL_TEST_SRCS) $(PROD_SRCS) \
$(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \
@@ -693,7 +735,7 @@ PP_OBJ_PROD = $(BUILD_DIR)/prod_preprocessor.o
$(BUILD_DIR)/prod_%.o: $(CBM_DIR)/%.c | $(BUILD_DIR)
$(CC) $(GRAMMAR_CFLAGS) -c -o $@ $<
$(BUILD_DIR)/prod_ts_runtime.o: $(CBM_DIR)/ts_runtime.c | $(BUILD_DIR)
$(BUILD_DIR)/prod_ts_runtime.o: $(TS_RUNTIME_DEPS) | $(BUILD_DIR)
$(CC) $(GRAMMAR_CFLAGS) -c -o $@ $<
$(BUILD_DIR)/prod_lsp_all.o: $(LSP_UNITY_DEPS) | $(BUILD_DIR)
@@ -718,7 +760,7 @@ OBJS_VENDORED_PROD = $(MIMALLOC_OBJ_PROD) $(SQLITE3_OBJ_PROD) $(TRE_OBJ_PROD) $(
MAIN_SRC = src/main.c
$(BUILD_DIR)/codebase-memory-mcp: $(MAIN_SRC) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_PROD) | $(BUILD_DIR)
$(BUILD_DIR)/codebase-memory-mcp: $(MAIN_SRC) $(PROD_HEADERS) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_PROD) | $(BUILD_DIR)
$(CC) $(CFLAGS_PROD) -o $@ \
$(MAIN_SRC) $(PROD_SRCS) \
$(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \
+6 -3
View File
@@ -175,7 +175,10 @@ Removes owned agent config entries, skills, hooks, instructions, and the install
- **Cross-repo architecture summary** combining services, routes, and dependencies across the indexed fleet
### Edge types (selected)
- `CALLS`, `IMPORTS`, `DEFINES`, `IMPLEMENTS`, `INHERITS`
- `CALLS` — a callable is invoked at the source site
- `CALL_REFERENCE` — a callable is used at a supported reference site (for example, a direct value argument) and resolves to one exact target
- `USAGE` — an identifier is used, but a unique callable target is not proven (including ambiguous or complex expressions)
- `IMPORTS`, `DEFINES`, `IMPLEMENTS`, `INHERITS`
- `HTTP_CALLS`, `ASYNC_CALLS` (cross-service)
- `EMITS`, `LISTENS_ON` (channels)
- `DATA_FLOWS` with arg-to-param mapping + field access chains
@@ -568,7 +571,7 @@ codebase-memory-mcp cli --raw search_graph '{"project": "my-project", "label": "
### Edge Types
`CONTAINS_PACKAGE`, `CONTAINS_FOLDER`, `CONTAINS_FILE`, `DEFINES`, `DEFINES_METHOD`, `IMPORTS`, `CALLS`, `HTTP_CALLS`, `ASYNC_CALLS`, `IMPLEMENTS`, `HANDLES`, `USAGE`, `CONFIGURES`, `WRITES`, `MEMBER_OF`, `TESTS`, `USES_TYPE`, `FILE_CHANGES_WITH`
`CONTAINS_PACKAGE`, `CONTAINS_FOLDER`, `CONTAINS_FILE`, `DEFINES`, `DEFINES_METHOD`, `IMPORTS`, `CALLS`, `CALL_REFERENCE`, `HTTP_CALLS`, `ASYNC_CALLS`, `IMPLEMENTS`, `HANDLES`, `USAGE`, `CONFIGURES`, `WRITES`, `MEMBER_OF`, `TESTS`, `USES_TYPE`, `FILE_CHANGES_WITH`
### Qualified Names
@@ -665,7 +668,7 @@ SQLite databases stored at `~/.cache/codebase-memory-mcp/`. Persists across rest
Tree-sitter alone gives a syntactic AST. That handles naming, structure, and call sites well, but it can't tell you that `user.profile.display_name()` resolves to `Profile.display_name` declared three modules away — tree-sitter doesn't track imports, generics, inheritance, or stdlib types.
codebase-memory-mcp ships a **lightweight C implementation of language type-resolution algorithms, structurally inspired by and compatible with major language servers** (tsserver / typescript-go, pyright, gopls, Roslyn, Eclipse JDT, rust-analyzer), embedded directly into the static binary. No language server process, no per-project setup, no API key. We call this layer **Hybrid LSP**: it runs alongside tree-sitter on every parse and refines `CALLS`, `USAGE`, and `RESOLVED_CALLS` edges with type information, so the resulting graph mirrors what an IDE "Go to Definition" would resolve.
codebase-memory-mcp ships a **lightweight C implementation of language type-resolution algorithms, structurally inspired by and compatible with major language servers** (tsserver / typescript-go, pyright, gopls, Roslyn, Eclipse JDT, rust-analyzer), embedded directly into the static binary. No language server process, no per-project setup, no API key. We call this layer **Hybrid LSP**: it runs alongside tree-sitter on every parse and refines invocation resolution (`CALLS` / `RESOLVED_CALLS`) and callable-value resolution (`CALL_REFERENCE`, with ambiguous values retained as `USAGE`) using type information, so the resulting graph mirrors what an IDE "Go to Definition" would resolve.
**Languages with full Hybrid LSP:**
+32 -3
View File
@@ -1229,7 +1229,8 @@ CBMFileResult *cbm_extract_file_ex(const char *source, int source_len, CBMLangua
cbm_run_go_lsp(a, result, source, source_len, root);
}
if (language == CBM_LANG_C || language == CBM_LANG_CPP || language == CBM_LANG_CUDA) {
cbm_run_c_lsp(a, result, source, source_len, root, language != CBM_LANG_C);
cbm_run_c_lsp(a, result, source, source_len, root, language != CBM_LANG_C,
CBM_SOURCE_ORIGIN_RAW);
}
if (language == CBM_LANG_PHP) {
cbm_run_php_lsp(a, result, source, source_len, root);
@@ -1289,8 +1290,11 @@ CBMFileResult *cbm_extract_file_ex(const char *source, int source_len, CBMLangua
if (preprocessed && preprocessed->source) {
char *expanded = preprocessed->source;
int expanded_len = (int)strlen(expanded);
// Record calls count before second pass
// Record every site-bearing array boundary before the second pass.
// Numeric byte spans in `expanded` are not raw-source coordinates.
int calls_before = result->calls.count;
int usages_before = result->usages.count;
int resolved_before = result->resolved_calls.count;
// Parse expanded source with fresh tree
TSParser *pp_parser = get_thread_parser(ts_lang, language);
@@ -1326,11 +1330,30 @@ CBMFileResult *cbm_extract_file_ex(const char *source, int source_len, CBMLangua
// harmless (pipeline deduplicates by caller+callee).
cbm_extract_unified(&pp_ctx);
/* Stamp parser carriers before C-LSP performs any
* origin-sensitive rewrite. Numeric spans in the expanded
* buffer may collide with unrelated raw-source spans. */
for (int i = calls_before; i < result->calls.count; i++) {
result->calls.items[i].source_origin = CBM_SOURCE_ORIGIN_PREPROCESSED;
}
for (int i = usages_before; i < result->usages.count; i++) {
result->usages.items[i].source_origin = CBM_SOURCE_ORIGIN_PREPROCESSED;
}
// Also run LSP on expanded source for additional type-resolved
// calls (language is already C/C++/CUDA — checked in enclosing
// block). Runs in every mode.
cbm_run_c_lsp(a, result, expanded, expanded_len, pp_root,
language != CBM_LANG_C);
language != CBM_LANG_C, CBM_SOURCE_ORIGIN_PREPROCESSED);
/* All C-LSP emitters stamp origin directly so rewrite-time
* comparisons are already safe. Keep this boundary sweep as
* a defensive invariant for any future emitter added to the
* C resolver. */
for (int i = resolved_before; i < result->resolved_calls.count; i++) {
result->resolved_calls.items[i].source_origin =
CBM_SOURCE_ORIGIN_PREPROCESSED;
}
/* #961: a def whose body braces are split across
* #ifdef/#else branches parses as an ERROR region on the
@@ -1537,6 +1560,12 @@ void cbm_free_result(CBMFileResult *result) {
ts_tree_delete(result->cached_tree);
result->cached_tree = NULL;
}
for (int i = 0; i < result->owned_result_count; i++) {
cbm_free_result(result->owned_results[i]);
}
free(result->owned_results);
result->owned_results = NULL;
result->owned_result_count = 0;
cbm_arena_destroy(&result->arena);
free(result);
}
+104 -37
View File
@@ -186,38 +186,44 @@ typedef struct {
const char *file_path; // relative path
uint32_t start_line;
uint32_t end_line;
const char *signature; // parameter text (NULL if none)
const char *return_type; // return type text (NULL if none)
const char *receiver; // Go method receiver (NULL if none)
const char *docstring; // leading doc comment (NULL if none)
const char *parent_class; // enclosing class QN for methods (NULL if none)
const char **decorators; // NULL-terminated array (NULL if none)
const char **base_classes; // NULL-terminated array (NULL if none)
const char **param_names; // NULL-terminated array (NULL if none)
const char **param_types; // NULL-terminated array (NULL if none)
const char **return_types; // NULL-terminated array (NULL if none)
const char *route_path; // HTTP route path from decorator (e.g., "/api/users") or NULL
const char *route_method; // HTTP method from decorator (e.g., "POST") or NULL
int complexity; // cyclomatic complexity
int cognitive; // cognitive complexity (nesting-weighted)
int loop_count; // number of loop constructs in the body
int loop_depth; // max nested-loop depth (bottleneck proxy)
bool is_recursive; // body contains a direct self-call (seed for "recursive")
int param_count; // number of parameters (large = complexity smell)
int max_access_depth; // deepest chained member/subscript access (a.b.c.d)
int linear_scan_in_loop; // count of linear-scan calls (find/contains/indexOf) inside loops
int alloc_in_loop; // count of allocation/append calls inside loops
bool recursion_in_loop; // a self-call occurs inside a loop body
bool unguarded_recursion; // recursive with no self-call guarded by a conditional
int lines; // body line count
uint32_t *fingerprint; // MinHash fingerprint (arena-allocated, K values) or NULL
int fingerprint_k; // number of hash values (CBM_MINHASH_K or 0)
const char *signature; // parameter text (NULL if none)
const char *return_type; // return type text (NULL if none)
const char *receiver; // Go method receiver (NULL if none)
const char *docstring; // leading doc comment (NULL if none)
const char *parent_class; // enclosing class QN for methods (NULL if none)
const char **decorators; // NULL-terminated array (NULL if none)
const char **base_classes; // NULL-terminated array (NULL if none)
const char **param_names; // NULL-terminated array (NULL if none)
const char **param_types; // NULL-terminated array (NULL if none)
const char **signature_param_types; // ordered internal signature types; "?" means unknown
int signature_param_count; // number of entries in signature_param_types
const char **return_types; // NULL-terminated array (NULL if none)
const char *route_path; // HTTP route path from decorator (e.g., "/api/users") or NULL
const char *route_method; // HTTP method from decorator (e.g., "POST") or NULL
int complexity; // cyclomatic complexity
int cognitive; // cognitive complexity (nesting-weighted)
int loop_count; // number of loop constructs in the body
int loop_depth; // max nested-loop depth (bottleneck proxy)
bool is_recursive; // body contains a direct self-call (seed for "recursive")
int param_count; // number of parameters (large = complexity smell)
int max_access_depth; // deepest chained member/subscript access (a.b.c.d)
int linear_scan_in_loop; // count of linear-scan calls (find/contains/indexOf) inside loops
int alloc_in_loop; // count of allocation/append calls inside loops
bool recursion_in_loop; // a self-call occurs inside a loop body
bool unguarded_recursion; // recursive with no self-call guarded by a conditional
int lines; // body line count
uint32_t *fingerprint; // MinHash fingerprint (arena-allocated, K values) or NULL
int fingerprint_k; // number of hash values (CBM_MINHASH_K or 0)
bool is_exported;
bool is_abstract;
bool is_test;
bool is_entry_point;
const char *structural_profile; // AST structural profile (arena-allocated) or NULL
const char *body_tokens; // space-separated raw identifier tokens from body (arena) or NULL
/* Rust only: raw trait path from the exact `impl Trait for Type` block
* that declared this method. Kept at the tail so zero-initialised
* callers in every other language remain ABI/source compatible. */
const char *impl_trait;
} CBMDefinition;
/* Argument captured from a call expression */
@@ -230,6 +236,14 @@ typedef struct {
#define CBM_MAX_CALL_ARGS 8
/* Byte offsets are meaningful only within the source buffer that produced
* them. C/C++/CUDA run both raw and preprocessed extraction passes, and those
* buffers can contain unrelated occurrences at the same numeric span. */
typedef enum {
CBM_SOURCE_ORIGIN_RAW = 0,
CBM_SOURCE_ORIGIN_PREPROCESSED,
} CBMSourceOrigin;
typedef struct {
const char *callee_name; // raw callee text ("pkg.Func", "foo")
const char *enclosing_func_qn; // QN of enclosing function (or module QN)
@@ -240,9 +254,14 @@ typedef struct {
int loop_depth; // enclosing loop nesting at the call site
int branch_depth; // enclosing branch nesting at the call site
int start_line; // 1-based source line of the call (for def range-match)
uint32_t site_start_byte; // exact AST occurrence span; end > start when present
uint32_t site_end_byte; // exclusive byte offset in the source file
CBMSourceOrigin source_origin; // raw source or C-family preprocessed buffer
bool is_method; // method/member call with a non-self receiver. Perl:
// arrow/method call ($obj->m). TS/JS/TSX: member call
// x.foo() whose receiver is not this/super. Default false.
bool requires_lsp_resolution; // synthetic semantic candidate (for example an implicit
// C++ operator). Never fall back to textual resolution.
} CBMCall;
typedef struct {
@@ -250,9 +269,22 @@ typedef struct {
const char *module_path; // resolved module path / QN
} CBMImport;
typedef enum {
CBM_USAGE_VALUE = 0,
CBM_USAGE_CALL_REFERENCE,
} CBMUsageKind;
typedef struct {
const char *ref_name; // referenced identifier
const char *enclosing_func_qn; // QN of enclosing function (or module QN)
const char *ref_name; // referenced identifier
const char *enclosing_func_qn; // QN of enclosing function (or module QN)
CBMUsageKind kind; // ordinary USAGE or explicit callable reference
bool may_be_call_reference; // syntactic candidate; exact LSP proof may upgrade its edge
bool semantic_reference_blocked; // lexical evidence blocks only unproven textual fallback
bool semantic_reference_local_shadow; // blocker belongs to a non-module lexical scope
uint32_t lexical_scope_id; // extraction-local scope instance; never graph identity
uint32_t site_start_byte; // exact reference-token span; end > start when present
uint32_t site_end_byte; // exclusive byte offset in the source file
CBMSourceOrigin source_origin; // raw source or C-family preprocessed buffer
} CBMUsage;
typedef struct {
@@ -327,15 +359,28 @@ typedef struct {
typedef struct {
const char *trait_name; // trait name (raw text)
const char *struct_name; // struct/type name (raw text)
/* Exact extracted QN of the implementing type. Unlike struct_name this
* does not need a later leaf-name guess, and the relation exists even for
* an empty `impl Trait for Type {}` block. */
const char *struct_qn;
} CBMImplTrait;
// LSP-resolved call: high-confidence type-aware call resolution
typedef enum {
CBM_RESOLVED_INVOCATION = 0,
CBM_RESOLVED_CALL_REFERENCE,
} CBMResolvedKind;
// LSP-resolved invocation/reference: high-confidence type-aware resolution.
typedef struct {
const char *caller_qn; // enclosing function QN
const char *callee_qn; // resolved target QN (fully qualified)
const char *strategy; // "lsp_type_dispatch", "lsp_direct", etc.
float confidence; // 0.90-0.95
const char *reason; // diagnostic label for unresolved calls (NULL if resolved)
const char *caller_qn; // enclosing function QN
const char *callee_qn; // resolved target QN (fully qualified)
const char *strategy; // "lsp_type_dispatch", "lsp_direct", etc.
float confidence; // 0.90-0.95
const char *reason; // diagnostic label for unresolved calls (NULL if resolved)
CBMResolvedKind kind; // invocation (CALLS) or explicit callable reference
uint32_t site_start_byte; // exact source occurrence; end > start when present
uint32_t site_end_byte; // exclusive byte offset in the source file
CBMSourceOrigin source_origin; // raw source or C-family preprocessed buffer
} CBMResolvedCall;
typedef struct {
@@ -424,8 +469,8 @@ typedef struct {
} CBMChannelArray;
// Full extraction result for one file.
typedef struct {
CBMArena arena; // owns all string memory
typedef struct CBMFileResult {
CBMArena arena; // owns local memory; composites may also retain child arenas below
CBMDefArray defs;
CBMCallArray calls;
@@ -437,7 +482,7 @@ typedef struct {
CBMEnvAccessArray env_accesses;
CBMTypeAssignArray type_assigns;
CBMImplTraitArray impl_traits; // Rust: impl Trait for Struct pairs
CBMResolvedCallArray resolved_calls; // LSP-resolved calls (high confidence)
CBMResolvedCallArray resolved_calls; // LSP-resolved invocations/references (high confidence)
CBMStringRefArray string_refs; // URL/config string literals from AST
CBMInfraBindingArray infra_bindings; // topic→URL pairs from IaC configs
CBMChannelArray channels; // Socket.IO / EventEmitter pub/sub participation
@@ -474,6 +519,13 @@ typedef struct {
// file (defs/calls already extracted are unaffected).
const char *source;
int source_len;
// Composite extraction results (currently ObjectScript Studio Export)
// retain their per-unit results so shallow-copied carrier strings remain
// valid for the composite's full lifetime. Owned and recursively released
// by cbm_free_result(); ordinary single-file results leave these zeroed.
struct CBMFileResult **owned_results;
int owned_result_count;
} CBMFileResult;
// --- Enclosing function cache ---
@@ -628,6 +680,21 @@ uint64_t cbm_get_preprocess_ns(void);
uint64_t cbm_get_files_preprocessed(void);
void cbm_reset_profile(void);
#if defined(CBM_KOTLIN_DEDUP_TEST_API) && CBM_KOTLIN_DEDUP_TEST_API
// Test-build-only operation counter for Kotlin operator-carrier deduplication.
// Production builds do not expose or retain this instrumentation.
void cbm_kotlin_operator_dedup_test_reset(void);
uint64_t cbm_kotlin_operator_dedup_test_comparisons(void);
#endif
#if defined(CBM_CALL_REFERENCE_LOOKUP_TEST_API) && CBM_CALL_REFERENCE_LOOKUP_TEST_API
// Test-build-only work counter for resolving a node's field role while
// classifying value references. Production builds retain no instrumentation.
void cbm_usage_field_lookup_test_reset(void);
uint64_t cbm_usage_field_lookup_test_work(void);
uint64_t cbm_usage_slow_parent_fallback_test_count(void);
#endif
// Toggle C/C++ preprocessor Macro-node extraction (#375). The pipeline enables
// it only for full/advanced index modes (it dominates extraction on macro-dense
// codebases). Default ON. Set before extraction; read-only during.
+1213 -189
View File
File diff suppressed because it is too large Load Diff
+399 -50
View File
@@ -684,9 +684,12 @@ TSNode cbm_resolve_func_name(TSNode node, CBMLanguage lang) {
return null_node;
}
// ObjectScript routine tag is its own name node.
if (lang == CBM_LANG_OBJECTSCRIPT_ROUTINE && strcmp(kind, "tag") == 0) {
return node;
// A parameterized ObjectScript routine wraps its tag and body in a
// procedure node. Use the direct tag as the callable name so the
// definition spans the complete procedure instead of only its label.
if (lang == CBM_LANG_OBJECTSCRIPT_ROUTINE &&
(strcmp(kind, "tag") == 0 || strcmp(kind, "procedure") == 0)) {
return strcmp(kind, "tag") == 0 ? node : cbm_find_child_by_kind(node, "tag");
}
// ObjectScript method/classmethod: name lives under method_definition ->
// method_name -> first named child.
@@ -702,6 +705,9 @@ TSNode cbm_resolve_func_name(TSNode node, CBMLanguage lang) {
TSNode null_node = {0};
return null_node;
}
if (lang == CBM_LANG_OBJECTSCRIPT_UDL && strcmp(kind, "query") == 0) {
return cbm_find_child_by_kind(node, "query_name");
}
TSNode name = func_name_node(node);
@@ -2821,6 +2827,14 @@ static char *clean_type_name(CBMArena *a, const char *raw) {
static char *resolve_param_name(CBMArena *a, TSNode param, const char *source) {
const char *pk = ts_node_type(param);
if (strcmp(pk, "argument") == 0 || strcmp(pk, "tag_parameter") == 0) {
TSNode name = find_first_descendant_by_kind(param, "objectscript_identifier", 6);
if (ts_node_is_null(name)) {
name = find_first_descendant_by_kind(param, "objectscript_identifier_special", 6);
}
return ts_node_is_null(name) ? NULL : cbm_node_text(a, name, source);
}
if (strcmp(pk, "parameter_declaration") == 0) {
TSNode nm = ts_node_child_by_field_name(param, TS_FIELD("name"));
if (!ts_node_is_null(nm)) {
@@ -3024,6 +3038,15 @@ static char *resolve_param_type_text(CBMArena *a, TSNode param, const char *sour
CBMLanguage lang) {
const char *pk = ts_node_type(param);
if ((lang == CBM_LANG_OBJECTSCRIPT_UDL || lang == CBM_LANG_OBJECTSCRIPT_ROUTINE) &&
(strcmp(pk, "argument") == 0 || strcmp(pk, "tag_parameter") == 0)) {
TSNode return_type = cbm_find_child_by_kind(param, "return_type");
TSNode typename = ts_node_is_null(return_type)
? (TSNode){0}
: cbm_find_child_by_kind(return_type, "typename");
return ts_node_is_null(typename) ? NULL : cbm_node_text(a, typename, source);
}
if (lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX) {
if (strcmp(pk, "required_parameter") == 0 || strcmp(pk, "optional_parameter") == 0) {
return extract_ts_param_type(a, param, source);
@@ -3094,6 +3117,226 @@ static const char **extract_param_types(CBMArena *a, TSNode params, const char *
return result;
}
/* Internal, occurrence-preserving signature extraction. `param_types` above is
* the legacy USES_TYPE projection: it intentionally drops builtins and
* duplicate types. Semantic call resolution needs a different contract — one
* type entry per source-level call argument, in declaration order. */
static const char signature_unknown_type[] = "?";
static char *signature_receiver_name(CBMArena *a, TSNode param, const char *source,
bool unwrap_wrappers) {
char *name = resolve_param_name(a, param, source);
if (name && name[0]) {
return name;
}
TSNode candidate = ts_node_child_by_field_name(param, TS_FIELD("pattern"));
if (ts_node_is_null(candidate)) {
candidate = ts_node_child_by_field_name(param, TS_FIELD("name"));
}
if (ts_node_is_null(candidate)) {
uint32_t nc = ts_node_named_child_count(param);
for (uint32_t i = 0; i < nc; i++) {
TSNode child = ts_node_named_child(param, i);
const char *ck = ts_node_type(child);
if (strcmp(ck, "type_annotation") == 0 || strcmp(ck, "decorator") == 0 ||
strcmp(ck, "attribute_item") == 0) {
continue;
}
candidate = child;
break;
}
}
/* Python typed/splat wrappers and TS pattern wrappers place the actual
* receiver identifier at the first named leaf. */
for (int depth = 0; depth < 4 && !ts_node_is_null(candidate); depth++) {
if (ts_node_named_child_count(candidate) == 0) {
return cbm_node_text(a, candidate, source);
}
if (!unwrap_wrappers) {
return NULL;
}
TSNode next = ts_node_child_by_field_name(candidate, TS_FIELD("pattern"));
if (ts_node_is_null(next)) {
next = ts_node_child_by_field_name(candidate, TS_FIELD("name"));
}
if (ts_node_is_null(next)) {
next = ts_node_named_child(candidate, 0);
}
candidate = next;
}
return NULL;
}
static bool is_signature_implicit_receiver(CBMArena *a, TSNode param, const char *source,
CBMLanguage lang, bool method, bool first_parameter) {
const char *pk = ts_node_type(param);
/* These grammar nodes describe a receiver supplied by member-call syntax,
* not an argument at the call site. */
if (strcmp(pk, "self_parameter") == 0 || strcmp(pk, "receiver_parameter") == 0 ||
strcmp(pk, "explicit_object_parameter_declaration") == 0) {
return true;
}
if (lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX) {
char *name = signature_receiver_name(a, param, source, false);
return name && strcmp(name, "this") == 0;
}
/* Python instance/class receivers are conventional identifiers rather than
* distinct AST nodes. Only exclude the leading slot on a method path. */
if (method && first_parameter && lang == CBM_LANG_PYTHON) {
char *name = signature_receiver_name(a, param, source, true);
return name && (strcmp(name, "self") == 0 || strcmp(name, "cls") == 0);
}
return false;
}
static bool is_signature_non_parameter(TSNode param, const char *source, CBMLanguage lang) {
const char *pk = ts_node_type(param);
if (strcmp(pk, "comment") == 0 || strcmp(pk, "line_comment") == 0 ||
strcmp(pk, "block_comment") == 0 || strcmp(pk, "attribute_item") == 0 ||
strcmp(pk, "inner_attribute_item") == 0 || strcmp(pk, "attribute_specifier") == 0 ||
strcmp(pk, "attribute_declaration") == 0 || strcmp(pk, "annotation") == 0 ||
strcmp(pk, "decorator") == 0 || strcmp(pk, "modifiers") == 0 ||
strcmp(pk, "positional_separator") == 0 || strcmp(pk, "keyword_separator") == 0 ||
strcmp(pk, "type_parameters") == 0 || strcmp(pk, "type_parameter_declaration") == 0) {
return true;
}
/* In C-family declarations `(void)` means zero parameters. The grammar
* exposes `void` as a named primitive_type child rather than a parameter. */
if ((lang == CBM_LANG_C || lang == CBM_LANG_CPP || lang == CBM_LANG_CUDA ||
lang == CBM_LANG_GLSL) &&
strcmp(pk, "primitive_type") == 0) {
uint32_t start = ts_node_start_byte(param);
uint32_t end = ts_node_end_byte(param);
return end - start == 4 && strncmp(source + start, "void", 4) == 0;
}
return false;
}
/* Go permits one declaration node to introduce several positional parameters:
* `func f(a, b T)`. Count its direct name identifiers; unnamed parameter
* declarations still represent one position. */
static int signature_param_multiplicity(TSNode param, CBMLanguage lang) {
if (lang != CBM_LANG_GO) {
return 1;
}
const char *pk = ts_node_type(param);
if (strcmp(pk, "parameter_declaration") != 0 &&
strcmp(pk, "variadic_parameter_declaration") != 0) {
return 1;
}
int field_names = 0;
int identifier_names = 0;
uint32_t nc = ts_node_child_count(param);
for (uint32_t i = 0; i < nc; i++) {
TSNode child = ts_node_child(param, i);
if (ts_node_is_null(child) || !ts_node_is_named(child)) {
continue;
}
const char *field = ts_node_field_name_for_child(param, i);
if (field && strcmp(field, "name") == 0) {
field_names++;
}
if (strcmp(ts_node_type(child), "identifier") == 0) {
identifier_names++;
}
}
int names = field_names > 0 ? field_names : identifier_names;
return names > 0 ? names : 1;
}
/* Resolve the full source spelling where the legacy resolver has no supported
* case (defaulted C++ parameters, Python annotations, complex TS types, etc.).
* This deliberately does not clean or deduplicate the text. */
static char *resolve_signature_param_type_text(CBMArena *a, TSNode param, const char *source,
CBMLanguage lang) {
TSNode type_node = ts_node_child_by_field_name(param, TS_FIELD("type"));
if (!ts_node_is_null(type_node)) {
/* TypeScript-family grammars may expose the `type_annotation` wrapper
* (including its colon) through the field. Store the actual type node. */
if (strcmp(ts_node_type(type_node), "type_annotation") == 0 &&
ts_node_named_child_count(type_node) > 0) {
type_node = ts_node_named_child(type_node, 0);
}
char *type_text = cbm_node_text(a, type_node, source);
if (type_text && type_text[0]) {
return type_text;
}
}
TSNode annotation = cbm_find_child_by_kind(param, "type_annotation");
if (!ts_node_is_null(annotation)) {
uint32_t nc = ts_node_named_child_count(annotation);
if (nc > 0) {
char *type_text = cbm_node_text(a, ts_node_named_child(annotation, 0), source);
if (type_text && type_text[0]) {
return type_text;
}
}
}
TSNode typed_pattern = cbm_find_child_by_kind(param, "typed_pattern");
if (!ts_node_is_null(typed_pattern)) {
type_node = ts_node_child_by_field_name(typed_pattern, TS_FIELD("type"));
if (!ts_node_is_null(type_node)) {
char *type_text = cbm_node_text(a, type_node, source);
if (type_text && type_text[0]) {
return type_text;
}
}
}
return resolve_param_type_text(a, param, source, lang);
}
static const char **extract_signature_param_types(CBMArena *a, TSNode params, const char *source,
CBMLanguage lang, bool method, int *out_count) {
*out_count = 0;
if (ts_node_is_null(params)) {
return NULL;
}
const char *types[MAX_PARAMS];
uint32_t nc = ts_node_child_count(params);
for (uint32_t i = 0; i < nc && *out_count < MAX_PARAMS_MINUS_1; i++) {
TSNode param = ts_node_child(params, i);
if (ts_node_is_null(param) || !ts_node_is_named(param) ||
is_signature_non_parameter(param, source, lang)) {
continue;
}
if (is_signature_implicit_receiver(a, param, source, lang, method, *out_count == 0)) {
continue;
}
char *type_text = resolve_signature_param_type_text(a, param, source, lang);
const char *entry = type_text && type_text[0] ? type_text : signature_unknown_type;
int multiplicity = signature_param_multiplicity(param, lang);
for (int n = 0; n < multiplicity && *out_count < MAX_PARAMS_MINUS_1; n++) {
types[(*out_count)++] = entry;
}
}
if (*out_count == 0) {
return NULL;
}
const char **result =
(const char **)cbm_arena_alloc(a, (size_t)(*out_count + NULL_TERM) * sizeof(const char *));
if (!result) {
*out_count = 0;
return NULL;
}
for (int i = 0; i < *out_count; i++) {
result[i] = types[i];
}
result[*out_count] = NULL;
return result;
}
// --- Function definition extraction ---
// For C++/CUDA template_declaration, find the inner function_definition or declaration.
@@ -3108,17 +3351,57 @@ static TSNode unwrap_template_inner(TSNode node, CBMLanguage lang) {
// C/C++/CUDA/GLSL: parameters live on function_declarator inside declarator chain.
static TSNode find_c_params(TSNode func_node) {
TSNode decl = ts_node_child_by_field_name(func_node, TS_FIELD("declarator"));
for (int d = 0; d < C_RETURN_WALK_DEPTH && !ts_node_is_null(decl); d++) {
for (int d = 0; d < DECLARATOR_DEPTH_LIMIT && !ts_node_is_null(decl); d++) {
TSNode params = ts_node_child_by_field_name(decl, TS_FIELD("parameters"));
if (!ts_node_is_null(params)) {
return params;
}
decl = ts_node_child_by_field_name(decl, TS_FIELD("declarator"));
TSNode nested = ts_node_child_by_field_name(decl, TS_FIELD("declarator"));
/* tree-sitter-cpp and tree-sitter-cuda do not assign the nested
* function declarator a `declarator` field when a reference return
* wraps it (`Item& operator[](int)`). That wrapper has one named child;
* descend only for this reproduced grammar shape. */
if (ts_node_is_null(nested) && strcmp(ts_node_type(decl), "reference_declarator") == 0 &&
ts_node_named_child_count(decl) > 0) {
nested = ts_node_named_child(decl, 0);
}
decl = nested;
}
TSNode null_node = {0};
return null_node;
}
/* Resolve the parameter-list node across the grammars used by the generic
* definition extractor. Newer tree-sitter-kotlin exposes
* `function_value_parameters` as a direct child rather than a `parameters`
* field. Keep the legacy and ordered signature extractors on the same node so
* adding positional metadata does not replace or reinterpret `param_types`. */
static TSNode find_function_params(TSNode func_node, CBMLanguage lang) {
TSNode params = ts_node_child_by_field_name(func_node, TS_FIELD("parameters"));
// ObjectScript exposes the parameter list under a `parameter_list` field.
if (ts_node_is_null(params) &&
(lang == CBM_LANG_OBJECTSCRIPT_UDL || lang == CBM_LANG_OBJECTSCRIPT_ROUTINE)) {
params = ts_node_child_by_field_name(func_node, TS_FIELD("parameter_list"));
}
if (ts_node_is_null(params) && lang == CBM_LANG_OBJECTSCRIPT_ROUTINE) {
params = cbm_find_child_by_kind(func_node, "parameter_list");
}
if (ts_node_is_null(params) && lang == CBM_LANG_OBJECTSCRIPT_UDL) {
TSNode method_definition = cbm_find_child_by_kind(func_node, "method_definition");
if (!ts_node_is_null(method_definition)) {
params = cbm_find_child_by_kind(method_definition, "arguments");
}
}
if (ts_node_is_null(params) && lang == CBM_LANG_KOTLIN) {
params = cbm_find_child_by_kind(func_node, "function_value_parameters");
}
if (ts_node_is_null(params) && (lang == CBM_LANG_C || lang == CBM_LANG_CPP ||
lang == CBM_LANG_CUDA || lang == CBM_LANG_GLSL)) {
params = find_c_params(func_node);
}
return params;
}
// C++: resolve trailing return type (auto f() -> Type) on a declarator node.
// Updates def->return_type and def->return_types if trailing type found.
static void resolve_cpp_trailing_return(CBMArena *a, TSNode func_node, const char *source,
@@ -3237,7 +3520,8 @@ static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
* enclosing scope here is always a namespace. The out-of-line method path
* below overrides this for `Ns::Cls::method` definitions. */
if (ctx->enclosing_class_qn &&
(ctx->language == CBM_LANG_CPP || ctx->language == CBM_LANG_CUDA)) {
(ctx->language == CBM_LANG_CPP || ctx->language == CBM_LANG_CUDA ||
ctx->language == CBM_LANG_TYPESCRIPT || ctx->language == CBM_LANG_TSX)) {
def.qualified_name = cbm_arena_sprintf(a, "%s.%s", ctx->enclosing_class_qn, name);
}
def.label = "Function";
@@ -3246,18 +3530,19 @@ static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
def.end_line = ts_node_end_point(node).row + TS_LINE_OFFSET;
def.lines = (int)(def.end_line - def.start_line + TS_LINE_OFFSET);
def.is_exported = cbm_is_exported(name, ctx->language);
if (ctx->language == CBM_LANG_RUST &&
strcmp(ts_node_type(node), "function_signature_item") == 0) {
def.is_abstract = true;
}
// Parameters — use func_node (inner function for templates)
TSNode params = ts_node_child_by_field_name(func_node, TS_FIELD("parameters"));
if (ts_node_is_null(params) &&
(ctx->language == CBM_LANG_C || ctx->language == CBM_LANG_CPP ||
ctx->language == CBM_LANG_CUDA || ctx->language == CBM_LANG_GLSL)) {
params = find_c_params(func_node);
}
TSNode params = find_function_params(func_node, ctx->language);
if (!ts_node_is_null(params)) {
def.signature = cbm_node_text(a, params, ctx->source);
def.param_names = extract_param_names(a, params, ctx->source, ctx->language);
def.param_types = extract_param_types(a, params, ctx->source, ctx->language);
def.signature_param_types = extract_signature_param_types(
a, params, ctx->source, ctx->language, false, &def.signature_param_count);
}
// Return type — use func_node (inner function for templates)
@@ -4110,6 +4395,22 @@ static TSNode find_class_body(TSNode class_node, CBMLanguage lang) {
return null_node;
}
/* Java keeps enum constants directly in enum_body, but declarations that
* follow the semicolon (methods, fields, and nested types) inside a dedicated
* enum_body_declarations child. Keep find_class_body() returning the raw body
* so enum-member extraction still sees the constants, and normalize only the
* consumers that walk declaration members. */
static TSNode find_class_member_body(TSNode class_node, CBMLanguage lang) {
TSNode body = find_class_body(class_node, lang);
if (ts_node_is_null(body) || lang != CBM_LANG_JAVA ||
strcmp(ts_node_type(body), "enum_body") != 0) {
return body;
}
TSNode declarations = cbm_find_child_by_kind(body, "enum_body_declarations");
return ts_node_is_null(declarations) ? body : declarations;
}
// Dart: resolve method name from method_signature/function_signature.
static TSNode resolve_dart_method_name(TSNode child, const char *ck) {
if (strcmp(ck, "method_signature") == 0) {
@@ -4247,16 +4548,17 @@ static void push_method_def(CBMExtractCtx *ctx, TSNode child, TSNode class_node,
def.end_line = ts_node_end_point(child).row + TS_LINE_OFFSET;
def.lines = (int)(def.end_line - def.start_line + TS_LINE_OFFSET);
def.is_exported = cbm_is_exported(name, ctx->language);
TSNode params = ts_node_child_by_field_name(child, TS_FIELD("parameters"));
// ObjectScript exposes the parameter list under a `parameter_list` field.
if (ts_node_is_null(params) && (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL ||
ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE)) {
params = ts_node_child_by_field_name(child, TS_FIELD("parameter_list"));
if (ctx->language == CBM_LANG_RUST &&
strcmp(ts_node_type(child), "function_signature_item") == 0) {
def.is_abstract = true;
}
TSNode params = find_function_params(child, ctx->language);
if (!ts_node_is_null(params)) {
def.signature = cbm_node_text(a, params, ctx->source);
def.param_types = extract_param_types(a, params, ctx->source, ctx->language);
def.signature_param_types = extract_signature_param_types(
a, params, ctx->source, ctx->language, true, &def.signature_param_count);
}
// Return type (same fields as extract_func_def)
@@ -4332,7 +4634,7 @@ static void extract_objc_impl_methods(CBMExtractCtx *ctx, TSNode impl_node, cons
// Extract methods inside a class body
static void extract_class_methods(CBMExtractCtx *ctx, TSNode class_node, const char *class_qn,
const CBMLangSpec *spec) {
TSNode body = find_class_body(class_node, ctx->language);
TSNode body = find_class_member_body(class_node, ctx->language);
if (ts_node_is_null(body)) {
return;
}
@@ -4407,6 +4709,23 @@ static void extract_class_methods(CBMExtractCtx *ctx, TSNode class_node, const c
continue;
}
/* Kotlin secondary constructors have no source-level name node. They
* are nevertheless concrete callable declarations, so materialize one
* under the owning class using the class name (`Explicit.Explicit`).
* Classes with only an implicit constructor do not enter this branch
* and therefore do not gain a fabricated callable target. */
if (ctx->language == CBM_LANG_KOTLIN &&
strcmp(ts_node_type(method_node), "secondary_constructor") == 0) {
TSNode constructor_name = ts_node_child_by_field_name(class_node, TS_FIELD("name"));
if (ts_node_is_null(constructor_name)) {
constructor_name = cbm_find_child_by_kind(class_node, "type_identifier");
}
if (!ts_node_is_null(constructor_name)) {
push_method_def(ctx, method_node, class_node, class_qn, spec, constructor_name);
}
continue;
}
TSNode name_node = resolve_method_name(method_node, ctx->language);
if (ts_node_is_null(name_node)) {
continue;
@@ -4439,7 +4758,10 @@ static void extract_rust_impl(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
}
}
const char *type_qn = cbm_fqn_compute(a, ctx->project, ctx->rel_path, type_name);
// Check for "impl Trait for Struct" pattern
const char *impl_trait = NULL;
TSNode trait_node = ts_node_child_by_field_name(node, TS_FIELD("trait"));
if (!ts_node_is_null(trait_node)) {
char *trait_name = cbm_node_text(a, trait_node, ctx->source);
@@ -4453,15 +4775,15 @@ static void extract_rust_impl(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
}
}
if (trait_name && trait_name[0]) {
CBMImplTrait it;
CBMImplTrait it = {0};
it.trait_name = trait_name;
it.struct_name = type_name;
it.struct_qn = type_qn;
cbm_impltrait_push(&ctx->result->impl_traits, a, it);
impl_trait = trait_name;
}
}
const char *type_qn = cbm_fqn_compute(a, ctx->project, ctx->rel_path, type_name);
// Extract methods inside impl body
TSNode body = ts_node_child_by_field_name(node, TS_FIELD("body"));
if (ts_node_is_null(body)) {
@@ -4497,6 +4819,7 @@ static void extract_rust_impl(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
def.label = "Method";
def.file_path = ctx->rel_path;
def.parent_class = type_qn;
def.impl_trait = impl_trait;
def.start_line = ts_node_start_point(child).row + TS_LINE_OFFSET;
def.end_line = ts_node_end_point(child).row + TS_LINE_OFFSET;
def.is_exported = cbm_is_exported(name, ctx->language);
@@ -4505,6 +4828,8 @@ static void extract_rust_impl(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
if (!ts_node_is_null(params)) {
def.signature = cbm_node_text(a, params, ctx->source);
def.param_types = extract_param_types(a, params, ctx->source, ctx->language);
def.signature_param_types = extract_signature_param_types(
a, params, ctx->source, ctx->language, true, &def.signature_param_count);
}
if (spec->branching_node_types && spec->branching_node_types[0]) {
@@ -5723,7 +6048,7 @@ static void extract_class_fields(CBMExtractCtx *ctx, TSNode class_node, const ch
return;
}
TSNode body = find_class_body(class_node, ctx->language);
TSNode body = find_class_member_body(class_node, ctx->language);
if (ts_node_is_null(body)) {
return;
}
@@ -6038,7 +6363,7 @@ static void extract_class_variables(CBMExtractCtx *ctx, TSNode class_node,
return;
}
TSNode body = find_class_body(class_node, ctx->language);
TSNode body = find_class_member_body(class_node, ctx->language);
if (ts_node_is_null(body)) {
return;
}
@@ -6218,20 +6543,14 @@ static bool is_template_class_node(TSNode node, CBMLanguage lang) {
return false;
}
// Compute the enclosing class QN for a class node (for nested class context).
// Compute the enclosing class/namespace QN for a scoped node.
/* A namespace contributes a QN segment so a symbol declared in `namespace ns`
* is `proj.file.ns.sym`, not a top-level `proj.file.sym`. Without the namespace
* in the QN, namespace-aware resolution (C++ ADL) is starved: a bare call
* collapses to the file scope and resolves directly instead. Unlike a class, a
* namespace emits no def of its own — it only extends the enclosing scope for
* its members. C#/PHP need the same treatment paired with their LSP resolvers
* (a def-only change breaks their existing namespace handling), done separately. */
static bool is_namespace_scope_kind(CBMLanguage lang, const char *kind) {
if (lang == CBM_LANG_CPP || lang == CBM_LANG_CUDA) {
return strcmp(kind, "namespace_definition") == 0;
}
return false;
}
* namespace extends the enclosing scope for its members. C#/PHP need the same
* treatment paired with their LSP resolvers (a def-only change breaks their
* existing namespace handling), done separately. */
static const char *compute_class_qn(CBMExtractCtx *ctx, TSNode node, const char *saved_enclosing) {
TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("name"));
@@ -6259,26 +6578,53 @@ static const char *compute_class_qn(CBMExtractCtx *ctx, TSNode node, const char
return saved_enclosing;
}
static void extract_typescript_namespace_def(CBMExtractCtx *ctx, TSNode node,
const char *saved_enclosing) {
TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("name"));
if (ts_node_is_null(name_node)) {
return;
}
char *name = cbm_node_text(ctx->arena, name_node, ctx->source);
const char *namespace_qn = compute_class_qn(ctx, node, saved_enclosing);
if (!name || !name[0] || !namespace_qn || !namespace_qn[0]) {
return;
}
CBMDefinition def = {0};
def.name = name;
def.qualified_name = namespace_qn;
def.label = "Module";
def.file_path = ctx->rel_path;
def.start_line = ts_node_start_point(node).row + TS_LINE_OFFSET;
def.end_line = ts_node_end_point(node).row + TS_LINE_OFFSET;
def.lines = (int)(def.end_line - def.start_line + TS_LINE_OFFSET);
def.is_exported = true;
cbm_defs_push(&ctx->result->defs, ctx->arena, def);
}
// Push nested class children from a class body container onto the walk stack.
static void push_class_body_children(TSNode node, const CBMLangSpec *spec, wd_stack_t *s,
const char *new_enclosing, CBMArena *arena) {
uint32_t nc = ts_node_child_count(node);
for (uint32_t ci = 0; ci < nc; ci++) {
TSNode child = ts_node_child(node, ci);
const char *ck = ts_node_type(child);
if (strcmp(ck, "field_declaration_list") == 0 || strcmp(ck, "class_body") == 0 ||
strcmp(ck, "declaration_list") == 0 || strcmp(ck, "body") == 0 ||
strcmp(ck, "block") == 0 || strcmp(ck, "suite") == 0 ||
// Groovy class bodies are a `closure` node; routing through the
// nested-class path keeps methods from being re-walked (and thus
// double-extracted) as top-level functions. Gated to Groovy so other
// grammars that also name a node "closure" are unaffected.
(strcmp(ck, "closure") == 0 && spec->language == CBM_LANG_GROOVY)) {
push_nested_class_nodes(child, spec, s, new_enclosing, arena);
return;
}
/* Use the same language-aware body selection as method extraction. The old
* independent spelling list omitted valid containers such as Scala's
* `template_body` and Solidity's contract body. Methods were extracted
* once by extract_class_methods(), then the generic walk descended through
* the unrecognized body and extracted them again as free functions. */
/* Smali's class node is itself the member container. Method extraction
* currently preserves Smali's established file-level Function contract,
* so the generic walk must still visit those direct method_definition
* children. Treating the class itself as a completed body here would only
* scan it for nested classes and silently drop every method. */
TSNode body = {0};
if (spec->language != CBM_LANG_SMALI) {
body = find_class_member_body(node, spec->language);
}
if (!ts_node_is_null(body)) {
push_nested_class_nodes(body, spec, s, new_enclosing, arena);
return;
}
// No body found — push all children directly
uint32_t nc = ts_node_child_count(node);
for (int ci = (int)nc - SKIP_CHAR; ci >= 0; ci--) {
wd_push(s, ts_node_child(node, (uint32_t)ci), new_enclosing);
}
@@ -6779,8 +7125,11 @@ static void walk_defs(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec,
* is walked normally — functions AND classes, unlike a class body which
* routes methods through extract_class_methods. Do NOT emit a def or run
* the class/func paths on the namespace node itself. */
if (is_namespace_scope_kind(ctx->language, kind)) {
if (cbm_is_namespace_scope_kind(ctx->language, kind)) {
const char *new_enclosing = compute_class_qn(ctx, node, frame.enclosing_class_qn);
if (ctx->language == CBM_LANG_TYPESCRIPT || ctx->language == CBM_LANG_TSX) {
extract_typescript_namespace_def(ctx, node, frame.enclosing_class_qn);
}
wd_push_children_reverse(&s, node, new_enclosing);
continue;
}
+13 -2
View File
@@ -107,6 +107,17 @@ static const char *path_last(CBMArena *a, const char *path) {
return path;
}
/* An unaliased dotted Python import binds its first component, not its last:
* `import xml.etree` introduces `xml`. Keep module_path intact for dependency
* resolution while recording the language-correct local spelling. */
static const char *python_import_root(CBMArena *a, const char *path) {
if (!path) {
return NULL;
}
const char *dot = strchr(path, '.');
return dot ? cbm_arena_strndup(a, path, (size_t)(dot - path)) : path;
}
// --- Go imports ---
// import_declaration -> import_spec_list -> import_spec -> (name, path)
@@ -197,7 +208,7 @@ static void process_py_import_stmt(CBMExtractCtx *ctx, TSNode node) {
if (strcmp(ck, "dotted_name") == 0 || strcmp(ck, "identifier") == 0) {
char *mod = cbm_node_text(a, child, ctx->source);
if (mod && mod[0]) {
CBMImport imp = {.local_name = path_last(a, mod), .module_path = mod};
CBMImport imp = {.local_name = python_import_root(a, mod), .module_path = mod};
cbm_imports_push(&ctx->result->imports, a, imp);
}
} else if (strcmp(ck, "aliased_import") == 0) {
@@ -211,7 +222,7 @@ static void process_py_import_stmt(CBMExtractCtx *ctx, TSNode node) {
} else {
char *mod = cbm_node_text(a, name_node, ctx->source);
if (mod && mod[0]) {
CBMImport imp = {.local_name = path_last(a, mod), .module_path = mod};
CBMImport imp = {.local_name = python_import_root(a, mod), .module_path = mod};
cbm_imports_push(&ctx->result->imports, a, imp);
}
}
+607 -36
View File
@@ -14,14 +14,195 @@ enum { MAX_INFRA_BINDINGS = 8 };
// --- Scope stack management ---
static void push_scope(WalkState *state, uint8_t kind, uint32_t depth, const char *qn) {
if (state->scope_top >= MAX_SCOPES) {
return;
static bool ensure_scope_capacity(WalkState *state) {
if (state->scope_top < state->scope_capacity) {
return true;
}
int new_capacity = state->scope_capacity > 0 ? state->scope_capacity * 2 : MAX_SCOPES;
CBMWalkScope *grown =
(CBMWalkScope *)cbm_arena_alloc(state->arena, (size_t)new_capacity * sizeof(*grown));
if (!grown) {
return false;
}
memcpy(grown, state->scopes, (size_t)state->scope_top * sizeof(*grown));
state->scopes = grown;
state->scope_capacity = new_capacity;
return true;
}
static const CBMLexicalScope *lexical_scope_by_id(const WalkState *state, uint32_t id) {
return state && id > 0 && id <= (uint32_t)state->lexical_scope_count
? &state->lexical_scopes[id - 1U]
: NULL;
}
static CBMLexicalScope *mutable_lexical_scope_by_id(WalkState *state, uint32_t id) {
return state && id > 0 && id <= (uint32_t)state->lexical_scope_count
? &state->lexical_scopes[id - 1U]
: NULL;
}
static uint32_t current_lexical_scope_id(const WalkState *state) {
if (!state) {
return 0;
}
for (int i = state->scope_top - 1; i >= 0; i--) {
if (state->scopes[i].lexical_scope_id != 0) {
return state->scopes[i].lexical_scope_id;
}
}
return state->root_lexical_scope_id;
}
static bool ensure_lexical_scope_capacity(WalkState *state) {
if (state->lexical_scope_count < state->lexical_scope_capacity) {
return true;
}
if (state->lexical_scope_capacity > INT32_MAX / PAIR_LEN) {
state->lexical_binding_tracking_failed = true;
return false;
}
int new_capacity = state->lexical_scope_capacity * PAIR_LEN;
CBMLexicalScope *grown =
(CBMLexicalScope *)cbm_arena_alloc(state->arena, (size_t)new_capacity * sizeof(*grown));
if (!grown) {
state->lexical_binding_tracking_failed = true;
return false;
}
memcpy(grown, state->lexical_scopes, (size_t)state->lexical_scope_count * sizeof(*grown));
state->lexical_scopes = grown;
state->lexical_scope_capacity = new_capacity;
return true;
}
static uint32_t python_function_lookup_parent(const WalkState *state,
uint32_t structural_parent_id) {
uint32_t id = structural_parent_id;
int remaining = state ? state->lexical_scope_count : 0;
while (id != 0 && remaining-- > 0) {
const CBMLexicalScope *scope = lexical_scope_by_id(state, id);
if (!scope) {
return structural_parent_id;
}
if (scope->kind == CBM_LEXICAL_SCOPE_CLASS) {
return scope->lookup_parent_id;
}
if (scope->kind == CBM_LEXICAL_SCOPE_FUNCTION ||
scope->kind == CBM_LEXICAL_SCOPE_COMPREHENSION ||
scope->kind == CBM_LEXICAL_SCOPE_MODULE) {
return structural_parent_id;
}
id = scope->parent_id;
}
return structural_parent_id;
}
static uint32_t add_lexical_scope(WalkState *state, TSNode node, CBMLexicalScopeKind kind) {
if (!state || ts_node_is_null(node) || !ensure_lexical_scope_capacity(state)) {
return 0;
}
uint32_t parent_id = current_lexical_scope_id(state);
uint32_t lookup_parent_id = parent_id;
/* Python method/function lookup bypasses the containing class namespace;
* class attributes require qualification (`self.x`/`Cls.x`). */
if (kind == CBM_LEXICAL_SCOPE_FUNCTION && state->language == CBM_LANG_PYTHON) {
lookup_parent_id = python_function_lookup_parent(state, parent_id);
}
CBMLexicalScope *scope = &state->lexical_scopes[state->lexical_scope_count];
scope->id = (uint32_t)state->lexical_scope_count + 1U;
scope->parent_id = parent_id;
scope->lookup_parent_id = lookup_parent_id;
scope->start_byte = ts_node_start_byte(node);
scope->end_byte = ts_node_end_byte(node);
scope->kind = (uint8_t)kind;
state->lexical_scope_count++;
return scope->id;
}
static bool push_scope(WalkState *state, uint8_t kind, uint32_t depth, const char *qn) {
if (!ensure_scope_capacity(state)) {
return false;
}
state->scopes[state->scope_top].kind = kind;
state->scopes[state->scope_top].depth = depth;
state->scopes[state->scope_top].qn = qn;
state->scopes[state->scope_top].lexical_scope_id = 0;
state->scopes[state->scope_top].invocation_kind = CBM_INVOCATION_NONE;
state->scopes[state->scope_top].callee_expr = (TSNode){0};
state->scopes[state->scope_top].callee_leaf = (TSNode){0};
state->scope_top++;
return true;
}
static bool push_lexical_scope(WalkState *state, uint8_t walk_kind, uint32_t depth, const char *qn,
TSNode node, CBMLexicalScopeKind lexical_kind) {
if (!push_scope(state, walk_kind, depth, qn)) {
state->lexical_binding_tracking_failed = true;
return false;
}
state->scopes[state->scope_top - SKIP_ONE].lexical_scope_id =
add_lexical_scope(state, node, lexical_kind);
return true;
}
static bool push_existing_lexical_scope(WalkState *state, uint8_t walk_kind, uint32_t depth,
const char *qn, uint32_t lexical_scope_id, TSNode node) {
CBMLexicalScope *scope = mutable_lexical_scope_by_id(state, lexical_scope_id);
if (!scope || !push_scope(state, walk_kind, depth, qn)) {
state->lexical_binding_tracking_failed = true;
return false;
}
state->scopes[state->scope_top - SKIP_ONE].lexical_scope_id = lexical_scope_id;
uint32_t end_byte = ts_node_end_byte(node);
if (end_byte > scope->end_byte) {
scope->end_byte = end_byte;
}
return true;
}
static uint32_t active_same_function_scope_id(const WalkState *state, const char *function_qn,
TSNode node) {
if (!state || !function_qn) {
return 0;
}
uint32_t start = ts_node_start_byte(node);
uint32_t end = ts_node_end_byte(node);
for (int i = state->scope_top - 1; i >= 0; i--) {
if (state->scopes[i].kind != SCOPE_FUNC || !state->scopes[i].qn ||
strcmp(state->scopes[i].qn, function_qn) != 0) {
continue;
}
const CBMLexicalScope *scope =
lexical_scope_by_id(state, state->scopes[i].lexical_scope_id);
if (scope && scope->start_byte <= start && end <= scope->end_byte) {
return scope->id;
}
}
return 0;
}
static bool push_function_scope(WalkState *state, uint32_t depth, const char *function_qn,
TSNode node) {
uint32_t existing_id = active_same_function_scope_id(state, function_qn, node);
return existing_id ? push_existing_lexical_scope(state, SCOPE_FUNC, depth, function_qn,
existing_id, node)
: push_lexical_scope(state, SCOPE_FUNC, depth, function_qn, node,
CBM_LEXICAL_SCOPE_FUNCTION);
}
static void push_call_scope(WalkState *state, uint32_t depth,
const CBMInvocationDescriptor *invocation) {
if (!invocation ||
(invocation->kind != CBM_INVOCATION_CALLABLE_REFERENCE && !invocation->raw_call_emitted) ||
(ts_node_is_null(invocation->callee_expr) && ts_node_is_null(invocation->callee_leaf))) {
return;
}
if (!push_scope(state, SCOPE_CALL, depth, NULL)) {
return;
}
state->scopes[state->scope_top - SKIP_ONE].invocation_kind = invocation->kind;
state->scopes[state->scope_top - SKIP_ONE].callee_expr = invocation->callee_expr;
state->scopes[state->scope_top - SKIP_ONE].callee_leaf = invocation->callee_leaf;
}
// Pop scopes that we've ascended out of (depth >= current cursor depth).
@@ -35,7 +216,9 @@ static void pop_expired_scopes(WalkState *state, uint32_t cur_depth) {
static void recompute_state(WalkState *state, const char *module_qn) {
state->enclosing_func_qn = module_qn;
state->enclosing_class_qn = NULL;
state->inside_call = false;
state->invocation_kind = CBM_INVOCATION_NONE;
state->callee_expr = (TSNode){0};
state->callee_leaf = (TSNode){0};
state->inside_import = false;
state->loop_depth = 0;
state->branch_depth = 0;
@@ -46,10 +229,13 @@ static void recompute_state(WalkState *state, const char *module_qn) {
state->enclosing_func_qn = state->scopes[i].qn;
break;
case SCOPE_CLASS:
case SCOPE_NAMESPACE:
state->enclosing_class_qn = state->scopes[i].qn;
break;
case SCOPE_CALL:
state->inside_call = true;
state->invocation_kind = state->scopes[i].invocation_kind;
state->callee_expr = state->scopes[i].callee_expr;
state->callee_leaf = state->scopes[i].callee_leaf;
break;
case SCOPE_IMPORT:
state->inside_import = true;
@@ -453,10 +639,22 @@ static const char *objectscript_get_class_name(CBMExtractCtx *ctx, TSNode node)
return NULL;
}
// Resolve the QN of an ObjectScript method/classmethod node for scope tracking.
// Resolve the QN of an ObjectScript method/classmethod/query node for scope tracking.
static const char *objectscript_get_method_qn(CBMExtractCtx *ctx, TSNode node,
const char *enclosing_class_qn) {
const char *nk = ts_node_type(node);
if (strcmp(nk, "query") == 0) {
TSNode query_name = cbm_find_child_by_kind(node, "query_name");
if (ts_node_is_null(query_name)) {
return NULL;
}
char *name = cbm_node_text(ctx->arena, query_name, ctx->source);
if (!name || !name[0]) {
return NULL;
}
return enclosing_class_qn ? cbm_arena_sprintf(ctx->arena, "%s.%s", enclosing_class_qn, name)
: cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, name);
}
if (strcmp(nk, "method") != 0 && strcmp(nk, "classmethod") != 0) {
return NULL;
}
@@ -495,8 +693,20 @@ static const char *compute_func_qn(CBMExtractCtx *ctx, TSNode node, const CBMLan
return objectscript_get_method_qn(ctx, node, state->enclosing_class_qn);
}
if (ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE) {
if (strcmp(ts_node_type(node), "tag") == 0) {
char *name = cbm_node_text(ctx->arena, node, ctx->source);
const char *kind = ts_node_type(node);
TSNode tag = {0};
if (strcmp(kind, "procedure") == 0) {
tag = cbm_find_child_by_kind(node, "tag");
} else if (strcmp(kind, "tag") == 0) {
/* The enclosing procedure owns the full callable range and already
* pushed this QN. Do not create a nested label-only function scope. */
TSNode parent = ts_node_parent(node);
if (ts_node_is_null(parent) || strcmp(ts_node_type(parent), "procedure") != 0) {
tag = node;
}
}
if (!ts_node_is_null(tag)) {
char *name = cbm_node_text(ctx->arena, tag, ctx->source);
if (name && name[0]) {
return cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, name);
}
@@ -1315,6 +1525,15 @@ static void scan_infra_bindings(CBMExtractCtx *ctx, TSNode node) {
}
}
/* Extra nodes are grammar-declared trivia (most commonly comments). They still
* participate in cursor traversal and scope expiry, but none of the unified
* semantic handlers consumes their contents; documentation is collected by
* the definition/LSP passes. The grammar predicate covers every language
* without confusing structured syntax such as SQL COMMENT statements. */
static bool is_unified_trivia_node(TSNode node) {
return ts_node_is_extra(node);
}
// JS/TS `export_statement` appears in import_node_types so re-exports
// (`export { X } from './m'`) are treated as an import boundary. But it also
// wraps exported *declarations* (`export function f(cfg: Config) {}`), and
@@ -1341,9 +1560,280 @@ static bool is_export_of_declaration(TSNode node) {
return false;
}
// Some languages encode imports with the same generic AST node used by every
// ordinary invocation. Static node-kind membership is therefore not enough to
// make the whole subtree an import scope.
static bool is_actual_import_boundary(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) {
bool direct_import = spec->import_node_types && cbm_kind_in_set(node, spec->import_node_types);
bool from_import = spec->import_from_types && cbm_kind_in_set(node, spec->import_from_types);
if (!direct_import && !from_import) {
return false;
}
const char *kind = ts_node_type(node);
TSNode head = ts_node_child_by_field_name(node, TS_FIELD("function"));
if (ts_node_is_null(head)) {
head = ts_node_child_by_field_name(node, TS_FIELD("method"));
}
if (ts_node_is_null(head)) {
head = ts_node_child_by_field_name(node, TS_FIELD("command_name"));
}
if (ts_node_is_null(head)) {
head = ts_node_child_by_field_name(node, TS_FIELD("name"));
}
if (ts_node_is_null(head) && ts_node_named_child_count(node) > 0) {
head = ts_node_named_child(node, 0);
}
char *name = ts_node_is_null(head) ? NULL : cbm_node_text(ctx->arena, head, ctx->source);
switch (ctx->language) {
case CBM_LANG_ELIXIR:
return strcmp(kind, "call") != 0 ||
(name && (strcmp(name, "import") == 0 || strcmp(name, "alias") == 0 ||
strcmp(name, "require") == 0 || strcmp(name, "use") == 0));
case CBM_LANG_LUA:
return strcmp(kind, "function_call") != 0 || (name && strcmp(name, "require") == 0);
case CBM_LANG_RUBY:
return strcmp(kind, "call") != 0 ||
(name && (strcmp(name, "require") == 0 || strcmp(name, "require_relative") == 0));
case CBM_LANG_BASH:
return strcmp(kind, "command") != 0 ||
(name && (strcmp(name, "source") == 0 || strcmp(name, ".") == 0));
case CBM_LANG_R:
if (strcmp(kind, "call") != 0 || !name) {
return strcmp(kind, "call") != 0;
}
return strcmp(name, "library") == 0 || strcmp(name, "require") == 0 ||
strcmp(name, "requireNamespace") == 0 || strcmp(name, "loadNamespace") == 0 ||
strcmp(name, "source") == 0 || strcmp(name, "box::use") == 0;
case CBM_LANG_ZIG: {
if (strcmp(kind, "builtin_function") != 0) {
return true;
}
char *text = cbm_node_text(ctx->arena, node, ctx->source);
return text && (strncmp(text, "@import", sizeof("@import") - 1) == 0 ||
strncmp(text, "@cImport", sizeof("@cImport") - 1) == 0);
}
case CBM_LANG_SCSS:
/* `@include` is an executable mixin invocation, not a module import. */
return strcmp(kind, "include_statement") != 0;
default:
return true;
}
}
/* NASM labels and instructions are flat siblings. For an actual instruction,
* find the nearest preceding label at the first ancestor level that has one.
* This is structural scope recovery; the instruction mnemonic is not used. */
static TSNode nasm_wrapped_label(TSNode node, int remaining_depth) {
if (ts_node_is_null(node) || remaining_depth < 0) {
return (TSNode){0};
}
if (strcmp(ts_node_type(node), "label") == 0) {
return node;
}
const char *kind = ts_node_type(node);
if (strcmp(kind, "instruction") != 0 && strcmp(kind, "source_line") != 0) {
return (TSNode){0};
}
uint32_t count = ts_node_named_child_count(node);
for (uint32_t i = 0; i < count; i++) {
TSNode label = nasm_wrapped_label(ts_node_named_child(node, i), remaining_depth - 1);
if (!ts_node_is_null(label)) {
return label;
}
}
return (TSNode){0};
}
static TSNode nasm_preceding_label(TSNode node) {
TSNode current = node;
for (int level = 0; level < 4 && !ts_node_is_null(current); level++) {
TSNode previous = ts_node_prev_named_sibling(current);
while (!ts_node_is_null(previous)) {
TSNode label = nasm_wrapped_label(previous, 2);
if (!ts_node_is_null(label)) {
return label;
}
previous = ts_node_prev_named_sibling(previous);
}
TSNode parent = ts_node_parent(current);
if (ts_node_is_null(parent) || strcmp(ts_node_type(current), "source_file") == 0) {
break;
}
current = parent;
}
return (TSNode){0};
}
/* Traditional ObjectScript routines are flat: a tag statement is followed by
* sibling command statements until the next tag. Re-anchor each top-level
* command statement to its nearest preceding tag, just as NASM instructions
* are re-anchored to flat labels. Braced `procedure` nodes already provide a
* real lexical boundary and deliberately bypass this recovery path. */
static TSNode objectscript_routine_statement_tag(TSNode statement) {
TSNode tag_statement = cbm_find_child_by_kind(statement, "tag_statement");
return ts_node_is_null(tag_statement) ? (TSNode){0}
: cbm_find_child_by_kind(tag_statement, "tag");
}
static TSNode objectscript_routine_preceding_tag(TSNode node) {
if (strcmp(ts_node_type(node), "statement") != 0) {
return (TSNode){0};
}
TSNode parent = ts_node_parent(node);
if (ts_node_is_null(parent) || strcmp(ts_node_type(parent), "source_file") != 0 ||
!ts_node_is_null(cbm_find_child_by_kind(node, "tag_statement")) ||
!ts_node_is_null(cbm_find_child_by_kind(node, "procedure"))) {
return (TSNode){0};
}
for (TSNode previous = ts_node_prev_named_sibling(node); !ts_node_is_null(previous);
previous = ts_node_prev_named_sibling(previous)) {
TSNode tag = objectscript_routine_statement_tag(previous);
if (!ts_node_is_null(tag)) {
return tag;
}
}
return (TSNode){0};
}
static bool push_pre_node_scope(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec,
WalkState *state, uint32_t depth) {
for (int i = 0; i < state->scope_top; i++) {
if (state->scopes[i].kind == SCOPE_FUNC) {
return false;
}
}
TSNode label = {0};
if (ctx->language == CBM_LANG_NASM && strcmp(ts_node_type(node), "actual_instruction") == 0) {
label = nasm_preceding_label(node);
} else if (ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE) {
if (strcmp(ts_node_type(node), "tag_statement") == 0) {
/* A parameterized flat tag owns its header as well as the following
* sibling command statements. Anchor before descending so its
* parameter_list records lexical bindings under the same tag QN. */
label = cbm_find_child_by_kind(node, "tag");
} else {
label = objectscript_routine_preceding_tag(node);
}
}
if (ts_node_is_null(label)) {
return false;
}
const char *fqn = compute_func_qn(ctx, label, spec, state);
if (!fqn) {
return false;
}
uint32_t anchor_start = ts_node_start_byte(label);
uint32_t anchor_end = ts_node_end_byte(label);
bool same_flat_callable = state->flat_function_scope_id != 0 &&
state->flat_anchor_start_byte == anchor_start &&
state->flat_anchor_end_byte == anchor_end &&
state->flat_function_qn && strcmp(state->flat_function_qn, fqn) == 0;
bool pushed = same_flat_callable
? push_existing_lexical_scope(state, SCOPE_FUNC, depth, fqn,
state->flat_function_scope_id, node)
: push_function_scope(state, depth, fqn, node);
if (pushed && !same_flat_callable) {
state->flat_function_scope_id = state->scopes[state->scope_top - SKIP_ONE].lexical_scope_id;
state->flat_anchor_start_byte = anchor_start;
state->flat_anchor_end_byte = anchor_end;
state->flat_function_qn = fqn;
}
return pushed;
}
static bool unified_kind_in_set(const char *kind, const char *const *set) {
for (int i = 0; kind && set && set[i]; i++) {
if (strcmp(kind, set[i]) == 0) {
return true;
}
}
return false;
}
static bool lexical_boundary_kind(TSNode node, CBMLexicalScopeKind *out_kind) {
const char *kind = ts_node_type(node);
static const char *const block_kinds[] = {"block",
"statement_block",
"compound_statement",
"switch_body",
"catch_clause",
"except_clause",
"finally_clause",
"for_statement",
"for_in_statement",
"for_of_statement",
"enhanced_for_statement",
"foreach_statement",
"foreach_clause",
"loop_expression",
"match_block",
"case_block",
"script_block",
"do_block",
NULL};
static const char *const comprehension_kinds[] = {
"list_comprehension", "set_comprehension", "dictionary_comprehension",
"generator_expression", "comprehension_expression", NULL};
static const char *const anonymous_function_kinds[] = {"lambda",
"lambda_expression",
"anonymous_function",
"anonymous_function_creation_expression",
"anonymous_method_expression",
"arrow_function",
"closure_expression",
"func_literal",
"function_expression",
"function_literal",
NULL};
/* Rust inline modules own independent item/import namespaces. Without a
* concrete module scope, `mod inner { use ... as x; }` leaks x into every
* sibling function in the file. */
if (strcmp(kind, "mod_item") == 0) {
*out_kind = CBM_LEXICAL_SCOPE_MODULE;
return true;
}
if (unified_kind_in_set(kind, comprehension_kinds)) {
*out_kind = CBM_LEXICAL_SCOPE_COMPREHENSION;
return true;
}
if (unified_kind_in_set(kind, anonymous_function_kinds)) {
*out_kind = CBM_LEXICAL_SCOPE_FUNCTION;
return true;
}
if (unified_kind_in_set(kind, block_kinds)) {
*out_kind = CBM_LEXICAL_SCOPE_BLOCK;
return true;
}
return false;
}
static bool node_already_has_lexical_scope(const WalkState *state, TSNode node) {
uint32_t start = ts_node_start_byte(node);
uint32_t end = ts_node_end_byte(node);
for (int i = state->scope_top - 1; i >= 0; i--) {
const CBMLexicalScope *scope =
lexical_scope_by_id(state, state->scopes[i].lexical_scope_id);
if (scope && scope->start_byte == start && scope->end_byte == end) {
return true;
}
}
return false;
}
static void push_lexical_boundary(TSNode node, WalkState *state, uint32_t depth) {
CBMLexicalScopeKind kind;
if (!node_already_has_lexical_scope(state, node) && lexical_boundary_kind(node, &kind)) {
(void)push_lexical_scope(state, SCOPE_LEXICAL, depth, NULL, node, kind);
}
}
// Push scope markers for function, class, call, and import boundary nodes.
static void push_boundary_scopes(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec,
WalkState *state, uint32_t depth) {
WalkState *state, uint32_t depth,
const CBMInvocationDescriptor *invocation) {
if (spec->function_node_types && cbm_kind_in_set(node, spec->function_node_types)) {
/* OCaml: a nested local `let x = e in ...` is itself a value_definition,
* but the def walk does not descend into function bodies, so it emits no
@@ -1362,8 +1852,20 @@ static void push_boundary_scopes(CBMExtractCtx *ctx, TSNode node, const CBMLangS
}
if (!skip_nested) {
const char *fqn = compute_func_qn(ctx, node, spec, state);
if (fqn) {
push_scope(state, SCOPE_FUNC, depth, fqn);
if (fqn && push_function_scope(state, depth, fqn, node)) {
const char *node_kind = ts_node_type(node);
bool split_signature = (ctx->language == CBM_LANG_DART &&
(strcmp(node_kind, "function_signature") == 0 ||
strcmp(node_kind, "method_signature") == 0)) ||
(ctx->language == CBM_LANG_LLVM_IR &&
strcmp(node_kind, "function_header") == 0);
if (split_signature) {
state->split_function_scope_id =
state->scopes[state->scope_top - SKIP_ONE].lexical_scope_id;
state->split_signature_start_byte = ts_node_start_byte(node);
state->split_signature_end_byte = ts_node_end_byte(node);
state->split_function_qn = fqn;
}
// ObjectScript: entering a method resets local var types (keeping
// class-level property types) and seeds the declared parameter types.
if (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL ||
@@ -1407,10 +1909,16 @@ static void push_boundary_scopes(CBMExtractCtx *ctx, TSNode node, const CBMLangS
}
}
}
} else if (cbm_is_namespace_scope_kind(ctx->language, ts_node_type(node))) {
const char *namespace_qn = compute_class_qn(ctx, node, state);
if (namespace_qn) {
push_lexical_scope(state, SCOPE_NAMESPACE, depth, namespace_qn, node,
CBM_LEXICAL_SCOPE_MODULE);
}
} else if (spec->class_node_types && cbm_kind_in_set(node, spec->class_node_types)) {
const char *cqn = compute_class_qn(ctx, node, state);
if (cqn) {
push_scope(state, SCOPE_CLASS, depth, cqn);
push_lexical_scope(state, SCOPE_CLASS, depth, cqn, node, CBM_LEXICAL_SCOPE_CLASS);
// ObjectScript: a new class clears the type map entirely.
if (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL ||
ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE) {
@@ -1425,7 +1933,7 @@ static void push_boundary_scopes(CBMExtractCtx *ctx, TSNode node, const CBMLangS
if (type_name && type_name[0]) {
const char *tqn =
cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, type_name);
push_scope(state, SCOPE_CLASS, depth, tqn);
push_lexical_scope(state, SCOPE_CLASS, depth, tqn, node, CBM_LEXICAL_SCOPE_CLASS);
}
}
} else if (ctx->language == CBM_LANG_DART && strcmp(ts_node_type(node), "function_body") == 0) {
@@ -1442,16 +1950,54 @@ static void push_boundary_scopes(CBMExtractCtx *ctx, TSNode node, const CBMLangS
if (!ts_node_is_null(prev)) {
const char *fqn = compute_func_qn(ctx, prev, spec, state);
if (fqn) {
push_scope(state, SCOPE_FUNC, depth, fqn);
bool exact_pending =
state->split_function_scope_id != 0 && state->split_function_qn &&
strcmp(state->split_function_qn, fqn) == 0 &&
state->split_signature_start_byte == ts_node_start_byte(prev) &&
state->split_signature_end_byte == ts_node_end_byte(prev);
if (exact_pending) {
push_existing_lexical_scope(state, SCOPE_FUNC, depth, fqn,
state->split_function_scope_id, node);
} else {
push_function_scope(state, depth, fqn, node);
}
state->split_function_scope_id = 0;
state->split_function_qn = NULL;
}
}
} else if (ctx->language == CBM_LANG_LLVM_IR &&
strcmp(ts_node_type(node), "function_body") == 0) {
/* LLVM's function_header and function_body are siblings under
* fn_define. Re-anchor the already-extracted header definition across
* its body so instruction_call nodes inherit that routine. */
TSNode previous = ts_node_prev_sibling(node);
while (!ts_node_is_null(previous) &&
strcmp(ts_node_type(previous), "function_header") != 0) {
previous = ts_node_prev_sibling(previous);
}
if (!ts_node_is_null(previous)) {
const char *fqn = compute_func_qn(ctx, previous, spec, state);
if (fqn) {
bool exact_pending =
state->split_function_scope_id != 0 && state->split_function_qn &&
strcmp(state->split_function_qn, fqn) == 0 &&
state->split_signature_start_byte == ts_node_start_byte(previous) &&
state->split_signature_end_byte == ts_node_end_byte(previous);
if (exact_pending) {
push_existing_lexical_scope(state, SCOPE_FUNC, depth, fqn,
state->split_function_scope_id, node);
} else {
push_function_scope(state, depth, fqn, node);
}
state->split_function_scope_id = 0;
state->split_function_qn = NULL;
}
}
}
if (spec->call_node_types && cbm_kind_in_set(node, spec->call_node_types)) {
push_scope(state, SCOPE_CALL, depth, NULL);
}
if (spec->import_node_types && cbm_kind_in_set(node, spec->import_node_types) &&
!is_export_of_declaration(node)) {
push_lexical_boundary(node, state, depth);
push_call_scope(state, depth, invocation);
if (is_actual_import_boundary(ctx, node, spec) && !is_export_of_declaration(node)) {
push_scope(state, SCOPE_IMPORT, depth, NULL);
}
/* Loop / branch nesting for bottleneck metrics. Loops are gated on named
@@ -1472,33 +2018,56 @@ void cbm_extract_unified(CBMExtractCtx *ctx) {
}
TSTreeCursor cursor = ts_tree_cursor_new(ctx->root);
TSTreeCursor occurrence_cursor = ts_tree_cursor_copy(&cursor);
WalkState state;
memset(&state, 0, sizeof(state));
state.current_cursor = &cursor;
state.occurrence_cursor = &occurrence_cursor;
state.arena = ctx->arena;
state.language = ctx->language;
state.scopes = state.inline_scopes;
state.scope_capacity = MAX_SCOPES;
state.lexical_scopes = state.inline_lexical_scopes;
state.lexical_scope_capacity = INLINE_LEXICAL_SCOPES;
state.usage_start_index = ctx->result->usages.count;
state.root_lexical_scope_id = add_lexical_scope(&state, ctx->root, CBM_LEXICAL_SCOPE_MODULE);
uint32_t depth = 0;
for (;;) {
TSNode node = ts_tree_cursor_current_node(&cursor);
bool trivia = is_unified_trivia_node(node);
if (!trivia) {
/* Trivia consumes no semantic state. Scope expiry may be deferred
* until the next code-bearing node, which recomputes before use. */
pop_expired_scopes(&state, depth);
recompute_state(&state, ctx->module_qn);
if (push_pre_node_scope(ctx, node, spec, &state, depth)) {
recompute_state(&state, ctx->module_qn);
}
pop_expired_scopes(&state, depth);
recompute_state(&state, ctx->module_qn);
handle_string_constants(ctx, node, &state);
handle_objectscript_type_map(ctx, node, &state);
CBMInvocationDescriptor invocation = handle_calls(ctx, node, spec, &state);
handle_usages(ctx, node, spec, &state);
handle_throws(ctx, node, spec, &state);
handle_readwrites(ctx, node, spec, &state);
handle_type_refs(ctx, node, spec, &state);
handle_env_accesses(ctx, node, spec, &state);
handle_type_assigns(ctx, node, spec, &state);
handle_string_refs(ctx, node, &state);
handle_yaml_nested(ctx, node);
scan_infra_bindings(ctx, node);
handle_string_constants(ctx, node, &state);
handle_objectscript_type_map(ctx, node, &state);
handle_calls(ctx, node, spec, &state);
handle_usages(ctx, node, spec, &state);
handle_throws(ctx, node, spec, &state);
handle_readwrites(ctx, node, spec, &state);
handle_type_refs(ctx, node, spec, &state);
handle_env_accesses(ctx, node, spec, &state);
handle_type_assigns(ctx, node, spec, &state);
handle_string_refs(ctx, node, &state);
handle_yaml_nested(ctx, node);
scan_infra_bindings(ctx, node);
push_boundary_scopes(ctx, node, spec, &state, depth, &invocation);
}
push_boundary_scopes(ctx, node, spec, &state, depth);
if (ts_tree_cursor_goto_first_child(&cursor)) {
/* Lexer-terminal trivia has no semantic work and no descendants. Avoid
* asking the cursor to construct a child iterator for every one of
* hundreds of thousands of flat comment siblings. Structured extras
* still descend normally. */
if ((!trivia || ts_node_child_count(node) > 0) &&
ts_tree_cursor_goto_first_child(&cursor)) {
depth++;
continue;
}
@@ -1518,5 +2087,7 @@ void cbm_extract_unified(CBMExtractCtx *ctx) {
}
}
cbm_finalize_lexical_usages(ctx, &state);
ts_tree_cursor_delete(&occurrence_cursor);
ts_tree_cursor_delete(&cursor);
}
+118 -15
View File
@@ -11,8 +11,13 @@
#define SCOPE_IMPORT 4
#define SCOPE_LOOP 5
#define SCOPE_BRANCH 6
#define SCOPE_LEXICAL 7
#define SCOPE_NAMESPACE 8
#define MAX_SCOPES 64
#define INLINE_LEXICAL_SCOPES 64
#define INLINE_LEXICAL_BINDINGS 64
#define INLINE_PYTHON_DIRECTIVES 16
// ObjectScript type map: variable name → class name (for instance_method_call
// resolution). Stack-allocated, per-method scope. Overflow is silent (no crash).
@@ -28,30 +33,128 @@ typedef struct {
int class_base_count; // entries [0,class_base_count) survive method-scope resets
} os_type_map_t;
// WalkState tracks scope context during the unified cursor walk.
// Replaces parent-chain walks for enclosing_func_qn, inside_call, etc.
typedef struct {
const char *enclosing_func_qn; // current function QN (module_qn at top level)
const char *enclosing_class_qn; // current class QN (NULL outside class)
bool inside_call; // within a call_node_types subtree
bool inside_import; // within an import_node_types subtree
int loop_depth; // count of enclosing loop scopes (for bottleneck metrics)
int branch_depth; // count of enclosing branch scopes
// A call consumes only the exact AST occurrence that denotes its callee. The
// rest of the call subtree (receiver, computed key, arguments, callback body)
// remains ordinary expression input and is eligible for USAGE extraction.
typedef enum {
CBM_INVOCATION_NONE = 0,
CBM_INVOCATION_PRIMARY,
CBM_INVOCATION_CALLABLE_REFERENCE,
} CBMInvocationKind;
struct {
const char *qn;
uint32_t depth;
uint8_t kind;
} scopes[MAX_SCOPES];
typedef struct {
CBMInvocationKind kind;
TSNode site;
TSNode callee_expr;
TSNode callee_leaf;
const char *callee_name;
bool raw_call_emitted;
} CBMInvocationDescriptor;
typedef struct {
const char *qn;
uint32_t depth;
uint32_t lexical_scope_id;
uint8_t kind;
CBMInvocationKind invocation_kind;
TSNode callee_expr;
TSNode callee_leaf;
} CBMWalkScope;
typedef enum {
CBM_LEXICAL_SCOPE_MODULE = 0,
CBM_LEXICAL_SCOPE_CLASS,
CBM_LEXICAL_SCOPE_FUNCTION,
CBM_LEXICAL_SCOPE_BLOCK,
CBM_LEXICAL_SCOPE_COMPREHENSION,
} CBMLexicalScopeKind;
/* Concrete AST scope identity. QNs remain graph-attribution metadata only;
* overloads, lambdas and sibling blocks therefore never share binding facts. */
typedef struct {
uint32_t id;
uint32_t parent_id;
uint32_t lookup_parent_id;
uint32_t start_byte;
uint32_t end_byte;
uint8_t kind;
} CBMLexicalScope;
/* Deferred binding event. Applying these after the walk represents hoisted
* and whole-scope rules without depending on traversal order. */
typedef struct {
uint32_t scope_id;
uint32_t active_start;
uint32_t active_end;
const char *name;
} CBMLexicalBinding;
typedef enum {
CBM_PYTHON_DIRECTIVE_GLOBAL = 1,
CBM_PYTHON_DIRECTIVE_NONLOCAL,
} CBMPythonDirectiveKind;
typedef struct {
uint32_t function_scope_id;
const char *name;
uint8_t kind;
} CBMPythonDirective;
// WalkState tracks scope context during the unified cursor walk.
// Replaces parent-chain walks for enclosing_func_qn, import context, etc.
typedef struct {
const char *enclosing_func_qn; // current function QN (module_qn at top level)
const char *enclosing_class_qn; // current class QN (NULL outside class)
const TSTreeCursor *current_cursor; // unified walk cursor at the current node
TSTreeCursor *occurrence_cursor; // reusable parent-preserving classifier cursor
CBMInvocationKind invocation_kind; // exact active invocation/reference role
TSNode callee_expr; // exact active callee expression, if any
TSNode callee_leaf; // exact active terminal callee, if any
bool inside_import; // within an import_node_types subtree
int loop_depth; // count of enclosing loop scopes (for bottleneck metrics)
int branch_depth; // count of enclosing branch scopes
CBMArena *arena;
CBMWalkScope *scopes;
CBMWalkScope inline_scopes[MAX_SCOPES];
int scope_capacity;
int scope_top;
CBMLexicalScope *lexical_scopes;
CBMLexicalScope inline_lexical_scopes[INLINE_LEXICAL_SCOPES];
int lexical_scope_capacity;
int lexical_scope_count;
uint32_t root_lexical_scope_id;
uint32_t split_function_scope_id;
uint32_t split_signature_start_byte;
uint32_t split_signature_end_byte;
const char *split_function_qn;
uint32_t flat_function_scope_id;
uint32_t flat_anchor_start_byte;
uint32_t flat_anchor_end_byte;
const char *flat_function_qn;
CBMLexicalBinding *lexical_bindings;
CBMLexicalBinding inline_lexical_bindings[INLINE_LEXICAL_BINDINGS];
int lexical_binding_capacity;
int lexical_binding_count;
int usage_start_index;
bool lexical_binding_tracking_failed;
CBMPythonDirective *python_directives;
CBMPythonDirective inline_python_directives[INLINE_PYTHON_DIRECTIVES];
int python_directive_capacity;
int python_directive_count;
CBMLanguage language;
os_type_map_t os_type_map; // ObjectScript variable → type mapping
} WalkState;
// Per-node handler prototypes. Each is called once per node during the
// unified cursor walk, replacing the old recursive walk_* functions.
void handle_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state);
CBMInvocationDescriptor handle_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec,
WalkState *state);
void handle_usages(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state);
void cbm_finalize_lexical_usages(CBMExtractCtx *ctx, WalkState *state);
void handle_throws(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state);
void handle_readwrites(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state);
void handle_type_refs(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state);
File diff suppressed because it is too large Load Diff
+15 -1
View File
@@ -460,6 +460,19 @@ bool cbm_kind_in_set(TSNode node, const char **types) {
return kind_in_set_strcmp(node, (const char *const *)types);
}
bool cbm_is_namespace_scope_kind(CBMLanguage lang, const char *kind) {
if (!kind) {
return false;
}
if (lang == CBM_LANG_CPP || lang == CBM_LANG_CUDA) {
return strcmp(kind, "namespace_definition") == 0;
}
if (lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX) {
return strcmp(kind, "internal_module") == 0;
}
return false;
}
/* Free the calling thread's node-type bitset cache (the calloc'd `bits` arrays
* that cbm_kind_in_set builds lazily). The cache is thread-local, so each worker
* thread and the main thread must call this at teardown (worker exit / process
@@ -917,7 +930,8 @@ const char *cbm_enclosing_func_qn(CBMArena *a, TSNode node, CBMLanguage lang, co
const char *class_chain = NULL;
for (TSNode cur = ts_node_parent(func_node); !ts_node_is_null(cur);
cur = ts_node_parent(cur)) {
if (!cbm_kind_in_set(cur, spec->class_node_types)) {
if (!cbm_kind_in_set(cur, spec->class_node_types) &&
!cbm_is_namespace_scope_kind(lang, ts_node_type(cur))) {
continue;
}
TSNode class_name = ts_node_child_by_field_name(cur, TS_FIELD("name"));
+5
View File
@@ -86,6 +86,11 @@ TSNode cbm_find_child_by_kind(TSNode parent, const char *kind);
// Check if node kind matches a set of types (NULL-terminated array of strings).
bool cbm_kind_in_set(TSNode node, const char **types);
/* Namespace/module declarations that extend a qualified-name scope without
* turning their children into class methods. Shared by definition and unified
* walks so TS/TSX scope attribution cannot drift. */
bool cbm_is_namespace_scope_kind(CBMLanguage lang, const char *kind);
// Free the calling thread's cbm_kind_in_set bitset cache (call at thread/process
// teardown so the thread-local cache is not reported as a leak).
void cbm_kind_in_set_free_cache(void);
+31 -36
View File
@@ -198,9 +198,10 @@ static const char *go_assign_types[] = {"assignment_statement", "short_var_decla
static const char *py_func_types[] = {"function_definition", NULL};
static const char *py_class_types[] = {"class_definition", NULL};
static const char *py_module_types[] = {"module", NULL};
static const char *py_call_types[] = {"call", "with_statement", NULL};
static const char *py_call_types[] = {"call", NULL};
static const char *py_import_types[] = {"import_statement", "import", NULL};
static const char *py_import_from_types[] = {"import_from_statement", NULL};
static const char *py_import_from_types[] = {"import_from_statement", "future_import_statement",
NULL};
static const char *py_branch_types[] = {
"if_statement", "for_statement", "while_statement", "try_statement",
"except_clause", "with_statement", "elif_clause", NULL};
@@ -215,13 +216,8 @@ static const char *js_func_types[] = {"function_declaration", "generator_functio
static const char *js_class_types[] = {"class_declaration", "class", NULL};
static const char *js_module_types[] = {"program", NULL};
static const char *js_call_types[] = {"call_expression", "new_expression", NULL};
static const char *js_import_types[] = {"import_statement",
"lexical_declaration",
"export_statement",
"import",
"extends",
"require",
NULL};
static const char *js_import_types[] = {"import_statement", "export_statement", "import",
"extends", "require", NULL};
static const char *js_branch_types[] = {"if_statement",
"for_statement",
"for_in_statement",
@@ -343,12 +339,8 @@ static const char *cpp_class_types[] = {"class_specifier", "struct_specifier", "
static const char *cpp_field_types[] = {"field_declaration", NULL};
static const char *cpp_module_types[] = {"translation_unit", "namespace_definition",
"linkage_specification", "declaration", NULL};
static const char *cpp_call_types[] = {
"call_expression", "field_expression", "subscript_expression",
"new_expression", "delete_expression", "binary_expression",
"unary_expression", "update_expression", NULL};
static const char *cpp_import_types[] = {"preproc_include", "template_function", "declaration",
NULL};
static const char *cpp_call_types[] = {"call_expression", "new_expression", NULL};
static const char *cpp_import_types[] = {"preproc_include", NULL};
static const char *cpp_branch_types[] = {"if_statement", "for_statement", "for_range_loop",
"while_statement", "switch_statement", "case_statement",
"try_statement", "catch_clause", NULL};
@@ -422,8 +414,7 @@ static const char *scala_class_types[] = {"class_definition", "object_definition
"trait_definition", "enum_definition",
"type_definition", NULL};
static const char *scala_module_types[] = {"compilation_unit", NULL};
static const char *scala_call_types[] = {"call_expression", "generic_function",
"field_expression", "infix_expression",
static const char *scala_call_types[] = {"call_expression", "infix_expression",
"instance_expression", NULL};
static const char *scala_import_types[] = {"import_declaration", "extends", "import",
"using_directive", NULL};
@@ -442,7 +433,7 @@ static const char *kotlin_func_types[] = {"function_declaration", "secondary_con
static const char *kotlin_class_types[] = {"class_declaration", "object_declaration",
"companion_object", "type_alias", NULL};
static const char *kotlin_module_types[] = {"source_file", NULL};
static const char *kotlin_call_types[] = {"call_expression", "navigation_expression", NULL};
static const char *kotlin_call_types[] = {"call_expression", NULL};
static const char *kotlin_import_types[] = {"import", NULL};
static const char *kotlin_branch_types[] = {
"if_expression", "for_statement", "while_statement", "when_expression",
@@ -456,7 +447,7 @@ static const char *kotlin_decorator_types[] = {"annotation", NULL};
static const char *ruby_func_types[] = {"method", "singleton_method", NULL};
static const char *ruby_class_types[] = {"class", "module", NULL};
static const char *ruby_module_types[] = {"program", NULL};
static const char *ruby_call_types[] = {"call", "command_call", NULL};
static const char *ruby_call_types[] = {"call", NULL};
static const char *ruby_import_types[] = {"call", NULL};
static const char *ruby_branch_types[] = {"if", "unless", "while", "until", "for",
"case", "when", "rescue", "elsif", NULL};
@@ -507,7 +498,7 @@ static const char *zig_assign_types[] = {"assignment_expression", NULL};
// ==================== ELIXIR ====================
static const char *elixir_func_types[] = {"call", "anonymous_function", NULL};
static const char *elixir_module_types[] = {"source", NULL};
static const char *elixir_call_types[] = {"call", "dot", "binary_operator", NULL};
static const char *elixir_call_types[] = {"call", "binary_operator", NULL};
static const char *elixir_import_types[] = {"call", NULL};
static const char *elixir_branch_types[] = {"call", NULL};
static const char *elixir_var_types[] = {"binary_operator", NULL};
@@ -562,7 +553,7 @@ static const char *swift_class_types[] = {"class_declaration", "protocol_declara
static const char *swift_field_types[] = {"property_declaration", NULL};
static const char *swift_module_types[] = {"source_file", NULL};
static const char *swift_call_types[] = {"call_expression", "constructor_expression",
"macro_invocation", "navigation_expression", NULL};
"macro_invocation", NULL};
static const char *swift_import_types[] = {"import_declaration", "import", NULL};
static const char *swift_branch_types[] = {"if_statement", "guard_statement", "for_statement",
"while_statement", "switch_statement", NULL};
@@ -669,7 +660,7 @@ static const char *sql_func_types[] = {"create_function", "function_declaration"
static const char *sql_field_types[] = {"column_definition", NULL};
static const char *sql_class_types[] = {"custom_type", NULL};
static const char *sql_module_types[] = {"program", NULL};
static const char *sql_call_types[] = {"function_call", "invocation", "command", NULL};
static const char *sql_call_types[] = {"invocation", NULL};
static const char *sql_branch_types[] = {"if_statement", "case_expression", NULL};
static const char *sql_var_types[] = {"create_table", "create_view", NULL};
@@ -717,7 +708,7 @@ static const char *fsharp_func_types[] = {"function_declaration", "value_declara
"function_or_value_defn", NULL};
static const char *fsharp_class_types[] = {"type_definition", "exception_definition", NULL};
static const char *fsharp_module_types[] = {"file", NULL};
static const char *fsharp_call_types[] = {"application_expression", "dot_expression", NULL};
static const char *fsharp_call_types[] = {"application_expression", NULL};
static const char *fsharp_import_types[] = {"import_decl", "open_expression", "instance", NULL};
static const char *fsharp_branch_types[] = {"if_expression", "for_expression",
"while_expression", "match_expression",
@@ -770,7 +761,7 @@ static const char *elm_func_types[] = {"value_declaration", "function_declaratio
static const char *elm_class_types[] = {"type_declaration", "type_alias_declaration",
"module_declaration", NULL};
static const char *elm_module_types[] = {"file", NULL};
static const char *elm_call_types[] = {"function_call", "function_call_expr", NULL};
static const char *elm_call_types[] = {"function_call_expr", NULL};
static const char *elm_import_types[] = {"import", NULL};
static const char *elm_branch_types[] = {"case_of_expr", "if_else_expr", NULL};
@@ -781,7 +772,7 @@ static const char *fortran_func_types[] = {"function", "subroutine", "function_s
"subroutine_statement", NULL};
static const char *fortran_class_types[] = {"derived_type_definition", "enum_statement", NULL};
static const char *fortran_module_types[] = {"translation_unit", NULL};
static const char *fortran_call_types[] = {"call_expression", "keyword_argument", "call", NULL};
static const char *fortran_call_types[] = {"call_expression", "subroutine_call", NULL};
static const char *fortran_import_types[] = {"use_statement", "include_statement", "extends",
"import", "import_statement", "include",
"open_statement", "preproc_include", NULL};
@@ -926,7 +917,7 @@ static const char *matlab_var_types[] = {"assignment", NULL};
static const char *lean_func_types[] = {"def", "theorem", "instance", "abbrev", NULL};
static const char *lean_class_types[] = {"structure", "class_inductive", "inductive", NULL};
static const char *lean_module_types[] = {"module", NULL};
static const char *lean_call_types[] = {"apply", "command", NULL};
static const char *lean_call_types[] = {"apply", NULL};
static const char *lean_import_types[] = {"import", "extends", "instance", NULL};
static const char *lean_branch_types[] = {"if", "match", "do", NULL};
@@ -973,8 +964,7 @@ static const char *d_class_types[] = {"class_declaration",
"module_def",
NULL};
static const char *d_field_types[] = {"variable_declaration", NULL};
static const char *d_call_types[] = {"call_expression", "function_call_expression",
"new_expression", NULL};
static const char *d_call_types[] = {"call_expression", "new_expression", NULL};
static const char *d_import_types[] = {"import_declaration", "import", NULL};
static const char *d_branch_types[] = {"if_statement", "for_statement", "foreach_statement",
"while_statement", "do_statement", "switch_statement",
@@ -1094,7 +1084,7 @@ static const char *fish_module_types[] = {"program", NULL};
* called by name, so a call inside a rule is legitimately Module-sourced, and a
* rule must NOT be treated as a function boundary. */
static const char *awk_func_types[] = {"func_def", NULL};
static const char *awk_call_types[] = {"func_call", "command", NULL};
static const char *awk_call_types[] = {"func_call", NULL};
static const char *awk_branch_types[] = {"if_statement",
"for_statement",
"for_in_statement",
@@ -1309,7 +1299,7 @@ static const char *prisma_call_types[] = {"call_expression", NULL};
static const char *prisma_module_types[] = {"program", NULL};
static const char *hyprlang_module_types[] = {"source_file", NULL};
static const char *dotenv_module_types[] = {"source_file", NULL};
static const char *diff_call_types[] = {"command", NULL};
static const char *diff_call_types[] = {NULL};
static const char *diff_module_types[] = {"source", NULL};
static const char *wgsl_func_types[] = {"function_declaration", NULL};
static const char *wgsl_class_types[] = {"struct_declaration", "type_alias_declaration",
@@ -1352,7 +1342,7 @@ static const char *properties_var_types[] = {"property", NULL};
/* tree-sitter-properties roots the tree at `file`, not `source_file`. */
static const char *properties_module_types[] = {"file", "source_file", NULL};
static const char *sshconfig_module_types[] = {"source_file", NULL};
static const char *bibtex_call_types[] = {"command", NULL};
static const char *bibtex_call_types[] = {NULL};
static const char *bibtex_module_types[] = {"document", NULL};
static const char *starlark_func_types[] = {"function_definition", "lambda", NULL};
static const char *starlark_call_types[] = {"call", NULL};
@@ -1423,7 +1413,7 @@ static const char *devicetree_call_types[] = {"call_expression", NULL};
static const char *devicetree_import_types[] = {"dtsi_include", "preproc_include", NULL};
static const char *devicetree_module_types[] = {"document", NULL};
static const char *linkerscript_call_types[] = {"call_expression", NULL};
static const char *linkerscript_module_types[] = {"source_file", NULL};
static const char *linkerscript_module_types[] = {"linkerscript", NULL};
static const char *gn_call_types[] = {"call_expression", NULL};
static const char *gn_import_types[] = {"import_statement", "import", NULL};
static const char *gn_branch_types[] = {"if_statement", "foreach_statement", NULL};
@@ -1510,8 +1500,7 @@ static const char *mermaid_module_types[] = {"source_file", NULL};
static const char *puppet_func_types[] = {"function_declaration", "lambda", NULL};
static const char *puppet_class_types[] = {"class_definition", "node_definition",
"resource_declaration", "type_declaration", NULL};
static const char *puppet_call_types[] = {"function_call", "resource_declaration",
"include_statement", NULL};
static const char *puppet_call_types[] = {"function_call", "include_statement", NULL};
static const char *puppet_import_types[] = {"include_statement", "require_statement", "include",
"require", NULL};
static const char *puppet_branch_types[] = {"if_statement", "unless_statement", "case_statement",
@@ -1628,7 +1617,9 @@ static const char *objectscript_udl_module_types[] = {"source_file", NULL};
static const char *objectscript_udl_branch_types[] = {
"command_if", "command_for", "command_while", "elseif_block", "catch_block", NULL};
static const char *objectscript_routine_func_types[] = {"tag", NULL};
/* Parameterized entries are `procedure` nodes whose direct `tag` child names
* the callable; bare labels remain standalone `tag` nodes. */
static const char *objectscript_routine_func_types[] = {"procedure", "tag", NULL};
static const char *objectscript_routine_call_types[] = {"extrinsic_function", "routine_tag_call",
NULL};
static const char *objectscript_routine_module_types[] = {"source_file", NULL};
@@ -2645,7 +2636,11 @@ const CBMLangSpec *cbm_lang_spec(CBMLanguage lang) {
if (lang < 0 || lang >= CBM_LANG_COUNT) {
return NULL;
}
return &lang_specs[lang];
const CBMLangSpec *spec = &lang_specs[lang];
if (spec->language != lang || !spec->function_node_types) {
return NULL;
}
return spec;
}
const TSLanguage *cbm_ts_language(CBMLanguage lang) {
+669 -153
View File
File diff suppressed because it is too large Load Diff
+7 -2
View File
@@ -60,10 +60,14 @@ typedef struct {
// Output
CBMResolvedCallArray *resolved_calls;
CBMSourceOrigin source_origin; // source buffer represented by emitted occurrence spans
// Function pointer targets: var_name -> target function QN
// Function pointer targets: lexical binding -> exact target function QN.
// A NULL target is an explicitly unknown/ambiguous binding and must shadow
// any outer exact target of the same name.
const char **fp_var_names;
const char **fp_target_qns;
const CBMScope **fp_binding_scopes;
int fp_count;
int fp_cap;
@@ -93,6 +97,7 @@ typedef struct {
int eval_depth; // recursion depth for c_eval_expr_type (crash guard)
int eval_steps; // total expression eval calls for current file (hang guard)
int walk_depth; // c_resolve_calls_in_node self-recursion (AST nesting)
int control_flow_depth; // if/loop/switch/catch nesting; assignments merge fail-closed
} CLSPContext;
// --- API ---
@@ -120,7 +125,7 @@ const CBMType *c_simplify_type(CLSPContext *ctx, const CBMType *t, bool unwrap_p
// Single-file LSP: build registry from file defs + stdlib, run resolution.
void cbm_run_c_lsp(CBMArena *arena, CBMFileResult *result, const char *source, int source_len,
TSNode root, bool cpp_mode);
TSNode root, bool cpp_mode, CBMSourceOrigin source_origin);
// Cross-file LSP: build registry from defs + stdlib, re-parse and resolve.
void cbm_run_c_lsp_cross(CBMArena *arena, const char *source, int source_len, const char *module_qn,
+469 -56
View File
@@ -73,6 +73,7 @@ static void cs_register_type_decls(CSLSPContext *ctx, CBMTypeRegistry *reg, TSNo
static char *cs_node_text_cached(CSLSPContext *ctx, TSNode node);
static const CBMType *cs_unwrap_task(CSLSPContext *ctx, const CBMType *t);
static const CBMType *cs_unwrap_nullable(const CBMType *t);
static const char *cs_resolve_callable_value(CSLSPContext *ctx, TSNode node, TSNode *callable_leaf);
/* ── small helpers ──────────────────────────────────────────────── */
@@ -499,6 +500,38 @@ const CBMRegisteredFunc *cs_lookup_method(CSLSPContext *ctx, const char *type_qn
return NULL;
}
/* A static import is precise only when every matching directive reaches the
* same method. Repeated imports of one type are harmless, but selecting the
* first of two distinct targets would make resolution declaration-order
* dependent. */
static const CBMRegisteredFunc *cs_lookup_using_static_method(CSLSPContext *ctx,
const char *method_name,
bool *ambiguous) {
if (ambiguous)
*ambiguous = false;
if (!ctx || !method_name)
return NULL;
const CBMRegisteredFunc *only = NULL;
for (int i = 0; i < ctx->using_count; i++) {
const CBMCSUsing *u = &ctx->usings[i];
if (u->kind != CBM_CS_USING_STATIC)
continue;
const char *host = cs_resolve_type_name(ctx, u->target_qn);
const CBMRegisteredFunc *candidate =
cs_lookup_method(ctx, host ? host : u->target_qn, method_name);
if (!candidate || !candidate->qualified_name)
continue;
if (only && strcmp(only->qualified_name, candidate->qualified_name) != 0) {
if (ambiguous)
*ambiguous = true;
return NULL;
}
only = candidate;
}
return only;
}
/* ── extension methods ──────────────────────────────────────────── */
/* Search for a static extension method `M(this U self, ...)` accessible
@@ -1041,15 +1074,13 @@ static const CBMType *cs_eval_invocation_type(CSLSPContext *ctx, TSNode call) {
return f->signature->data.func.return_types[0];
}
}
for (int i = 0; i < ctx->using_count; i++) {
const CBMCSUsing *u = &ctx->usings[i];
if (u->kind != CBM_CS_USING_STATIC) continue;
const char *qn = cbm_arena_sprintf(ctx->arena, "%s.%s", u->target_qn, bare);
const CBMRegisteredFunc *f = cbm_registry_lookup_func(ctx->registry, qn);
if (f && f->signature && f->signature->data.func.return_types &&
f->signature->data.func.return_types[0]) {
return f->signature->data.func.return_types[0];
}
bool ambiguous = false;
const CBMRegisteredFunc *f = cs_lookup_using_static_method(ctx, bare, &ambiguous);
if (ambiguous)
return cbm_type_unknown();
if (f && f->signature && f->signature->data.func.return_types &&
f->signature->data.func.return_types[0]) {
return f->signature->data.func.return_types[0];
}
}
@@ -1255,6 +1286,166 @@ static void cs_bind_parameters(CSLSPContext *ctx, TSNode params_node, bool is_ex
/* ── statement processing ───────────────────────────────────────── */
/* Resolve a source expression only when it denotes one exact callable value.
* This deliberately accepts just direct method groups (bare or member access):
* conditional/switch/lambda expressions remain ordinary usages because they
* do not prove one target at this occurrence. */
static const char *cs_resolve_callable_name(CSLSPContext *ctx, const char *name) {
if (!ctx || !name || !name[0])
return NULL;
/* A lexical binding always wins, including an UNKNOWN ordinary binding:
* a nearer non-callable value must suppress a same-named class method. */
if (cbm_scope_contains(ctx->current_scope, name)) {
return cbm_scope_lookup_callable(ctx->current_scope, name);
}
if (ctx->enclosing_class_qn) {
const CBMRegisteredFunc *f = cs_lookup_method(ctx, ctx->enclosing_class_qn, name);
if (f && f->qualified_name)
return f->qualified_name;
}
if (ctx->enclosing_base_qn) {
const CBMRegisteredFunc *f = cs_lookup_method(ctx, ctx->enclosing_base_qn, name);
if (f && f->qualified_name)
return f->qualified_name;
}
bool using_static_ambiguous = false;
const CBMRegisteredFunc *using_static =
cs_lookup_using_static_method(ctx, name, &using_static_ambiguous);
if (using_static_ambiguous)
return NULL;
if (using_static)
return using_static->qualified_name;
const char *ns = cs_namespace_qn(ctx);
if (ns && ns[0]) {
const char *qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ns, name);
const CBMRegisteredFunc *f = cbm_registry_lookup_func(ctx->registry, qn);
if (f && f->qualified_name)
return f->qualified_name;
}
/* C# has no true namespace-level functions, but error-tolerant parses and
* generated fixtures can register receiverless callables. Only accept a
* unique short-name match; ambiguity is intentionally not guessed. */
const CBMRegisteredFunc *only = NULL;
for (int i = 0; ctx->registry && i < ctx->registry->func_count; i++) {
const CBMRegisteredFunc *candidate = &ctx->registry->funcs[i];
if (candidate->receiver_type || !candidate->short_name ||
strcmp(candidate->short_name, name) != 0) {
continue;
}
if (only && strcmp(only->qualified_name, candidate->qualified_name) != 0) {
return NULL;
}
only = candidate;
}
return only ? only->qualified_name : NULL;
}
static TSNode cs_callable_name_leaf(TSNode node) {
if (ts_node_is_null(node) || strcmp(ts_node_type(node), "generic_name") != 0) {
return node;
}
TSNode leaf = ts_node_child_by_field_name(node, "name", 4);
if (!ts_node_is_null(leaf))
return leaf;
uint32_t nc = ts_node_child_count(node);
for (uint32_t i = 0; i < nc; i++) {
TSNode child = ts_node_child(node, i);
if (!ts_node_is_null(child) && ts_node_is_named(child) &&
strcmp(ts_node_type(child), "identifier") == 0) {
return child;
}
}
return node;
}
static const char *cs_resolve_callable_value(CSLSPContext *ctx, TSNode node,
TSNode *callable_leaf) {
if (callable_leaf)
memset(callable_leaf, 0, sizeof(*callable_leaf));
if (!ctx || ts_node_is_null(node))
return NULL;
const char *kind = ts_node_type(node);
if (strcmp(kind, "argument") == 0 || strcmp(kind, "parenthesized_expression") == 0 ||
strcmp(kind, "equals_value_clause") == 0) {
TSNode inner = ts_node_child_by_field_name(node, "expression", 10);
if (ts_node_is_null(inner))
inner = ts_node_child_by_field_name(node, "value", 5);
if (ts_node_is_null(inner))
inner = cs_first_named_child(node);
return ts_node_is_null(inner) ? NULL : cs_resolve_callable_value(ctx, inner, callable_leaf);
}
if (strcmp(kind, "identifier") == 0 || strcmp(kind, "generic_name") == 0) {
char *raw = cs_node_text(ctx, node);
char *bare = raw ? cs_strip_generic_args(ctx->arena, raw) : NULL;
const char *target = bare ? cs_resolve_callable_name(ctx, bare) : NULL;
if (target && callable_leaf)
*callable_leaf = cs_callable_name_leaf(node);
return target;
}
if (strcmp(kind, "member_access_expression") != 0)
return NULL;
TSNode receiver = ts_node_child_by_field_name(node, "expression", 10);
TSNode member = ts_node_child_by_field_name(node, "name", 4);
if (ts_node_is_null(receiver))
receiver = cs_first_named_child(node);
if (ts_node_is_null(member)) {
uint32_t nc = ts_node_child_count(node);
for (uint32_t i = 0; i < nc; i++) {
TSNode child = ts_node_child(node, i);
if (ts_node_is_null(child) || !ts_node_is_named(child) || ts_node_eq(child, receiver)) {
continue;
}
const char *child_kind = ts_node_type(child);
if (strcmp(child_kind, "identifier") == 0 || strcmp(child_kind, "generic_name") == 0) {
member = child;
}
}
}
if (ts_node_is_null(receiver) || ts_node_is_null(member))
return NULL;
char *member_raw = cs_node_text(ctx, member);
char *member_name = member_raw ? cs_strip_generic_args(ctx->arena, member_raw) : NULL;
if (!member_name)
return NULL;
char *receiver_text = cs_node_text(ctx, receiver);
if (receiver_text) {
const char *type_qn = cs_resolve_type_name(ctx, receiver_text);
if (type_qn && cs_lookup_type_qn(ctx, type_qn)) {
const CBMRegisteredFunc *f = cs_lookup_method(ctx, type_qn, member_name);
if (f && callable_leaf)
*callable_leaf = cs_callable_name_leaf(member);
return f ? f->qualified_name : NULL;
}
}
const CBMType *receiver_type = cs_unwrap_nullable(cs_eval_expr_type(ctx, receiver));
const char *type_qn = NULL;
if (receiver_type && receiver_type->kind == CBM_TYPE_NAMED) {
type_qn = receiver_type->data.named.qualified_name;
} else if (receiver_type && receiver_type->kind == CBM_TYPE_TEMPLATE) {
type_qn = receiver_type->data.template_type.template_name;
}
if (!type_qn)
return NULL;
const CBMRegisteredFunc *f = cs_lookup_method(ctx, type_qn, member_name);
if (!f)
f = cs_lookup_extension(ctx, type_qn, member_name);
if (f && callable_leaf)
*callable_leaf = cs_callable_name_leaf(member);
return f ? f->qualified_name : NULL;
}
static void cs_process_local_decl(CSLSPContext *ctx, TSNode node) {
/* local_declaration_statement -> variable_declaration */
TSNode vd = cs_child_named_kind(node, "variable_declaration");
@@ -1291,12 +1482,18 @@ static void cs_process_local_decl(CSLSPContext *ctx, TSNode node) {
* variants. Try the field-name path first, then fall back to
* walking named children for any expression after the identifier. */
TSNode init = ts_node_child_by_field_name(c, "value", 5);
TSNode rhs_node;
memset(&rhs_node, 0, sizeof(rhs_node));
const CBMType *rhs_t = NULL;
if (!ts_node_is_null(init)) {
if (strcmp(ts_node_type(init), "equals_value_clause") == 0) {
TSNode rhs = cs_first_named_child(init);
if (!ts_node_is_null(rhs)) rhs_t = cs_eval_expr_type(ctx, rhs);
if (!ts_node_is_null(rhs)) {
rhs_node = rhs;
rhs_t = cs_eval_expr_type(ctx, rhs);
}
} else {
rhs_node = init;
rhs_t = cs_eval_expr_type(ctx, init);
}
}
@@ -1313,6 +1510,7 @@ static void cs_process_local_decl(CSLSPContext *ctx, TSNode node) {
if (strcmp(ck, "equals_value_clause") == 0) {
TSNode rhs = cs_first_named_child(cn);
if (!ts_node_is_null(rhs)) {
rhs_node = rhs;
const CBMType *t = cs_eval_expr_type(ctx, rhs);
if (t && t->kind != CBM_TYPE_UNKNOWN) rhs_t = t;
}
@@ -1323,6 +1521,7 @@ static void cs_process_local_decl(CSLSPContext *ctx, TSNode node) {
continue; /* the identifier name */
}
if (seen_named >= 1) {
rhs_node = cn;
const CBMType *t = cs_eval_expr_type(ctx, cn);
if (t && t->kind != CBM_TYPE_UNKNOWN) {
rhs_t = t;
@@ -1333,7 +1532,13 @@ static void cs_process_local_decl(CSLSPContext *ctx, TSNode node) {
}
const CBMType *bind = is_var ? (rhs_t ? rhs_t : declared_type) : declared_type;
if (!bind) bind = cbm_type_unknown();
cbm_scope_bind(ctx->current_scope, vname, bind);
const char *callable_qn =
ts_node_is_null(rhs_node) ? NULL : cs_resolve_callable_value(ctx, rhs_node, NULL);
if (callable_qn) {
cbm_scope_bind_callable(ctx->current_scope, vname, bind, callable_qn);
} else {
cbm_scope_bind(ctx->current_scope, vname, bind);
}
}
}
@@ -1398,28 +1603,163 @@ static void cs_process_assignment(CSLSPContext *ctx, TSNode node) {
if (!cs_node_is(lhs, "identifier")) return;
char *vname = cs_node_text(ctx, lhs);
if (!vname) return;
const CBMType *t = cs_eval_expr_type(ctx, rhs);
if (!t || t->kind == CBM_TYPE_UNKNOWN) return;
/* Don't override more-specific bindings with weaker ones. */
const CBMType *existing = cbm_scope_lookup(ctx->current_scope, vname);
if (existing && existing->kind == CBM_TYPE_NAMED && t->kind == CBM_TYPE_UNKNOWN) return;
cbm_scope_bind(ctx->current_scope, vname, t);
const char *callable_qn = cs_resolve_callable_value(ctx, rhs, NULL);
(void)cs_eval_expr_type(ctx, rhs);
if (callable_qn) {
cbm_scope_update_callable(ctx->current_scope, vname, callable_qn);
} else {
/* An ordinary or ambiguous reassignment clears stale callable identity
* while retaining the best type already known for the variable. */
cbm_scope_update_callable(ctx->current_scope, vname, NULL);
}
}
/* ── call resolution + emit ─────────────────────────────────────── */
static void cs_emit_resolved(CSLSPContext *ctx, const char *callee_qn, const char *strategy,
float confidence) {
static void cs_invalidate_conditionally_assigned_callables(CSLSPContext *ctx, TSNode node) {
if (!ctx || ts_node_is_null(node))
return;
const char *kind = ts_node_type(node);
if (strcmp(kind, "method_declaration") == 0 || strcmp(kind, "local_function_statement") == 0 ||
strcmp(kind, "lambda_expression") == 0 ||
strcmp(kind, "anonymous_method_expression") == 0 ||
strcmp(kind, "class_declaration") == 0 || strcmp(kind, "struct_declaration") == 0) {
return;
}
if (strcmp(kind, "assignment_expression") == 0) {
TSNode lhs = ts_node_child_by_field_name(node, "left", 4);
if (ts_node_is_null(lhs))
lhs = cs_first_named_child(node);
if (cs_node_is(lhs, "identifier")) {
char *name = cs_node_text(ctx, lhs);
if (name)
cbm_scope_update_callable(ctx->current_scope, name, NULL);
}
return;
}
uint32_t nc = ts_node_child_count(node);
for (uint32_t i = 0; i < nc; i++) {
TSNode child = ts_node_child(node, i);
if (!ts_node_is_null(child) && ts_node_is_named(child)) {
cs_invalidate_conditionally_assigned_callables(ctx, child);
}
}
}
static void cs_emit_resolved_kind_reason(CSLSPContext *ctx, const char *callee_qn,
const char *strategy, float confidence,
CBMResolvedKind kind, const char *reason) {
if (!ctx->resolved_calls || !callee_qn || !ctx->enclosing_func_qn) return;
CBMResolvedCall rc;
CBMResolvedCall rc = {0};
rc.caller_qn = ctx->enclosing_func_qn;
rc.callee_qn = callee_qn;
rc.strategy = strategy;
rc.confidence = confidence;
rc.reason = NULL;
rc.reason = reason;
rc.kind = kind;
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc);
}
static void cs_emit_resolved_kind(CSLSPContext *ctx, const char *callee_qn, const char *strategy,
float confidence, CBMResolvedKind kind) {
cs_emit_resolved_kind_reason(ctx, callee_qn, strategy, confidence, kind, NULL);
}
static void cs_emit_resolved(CSLSPContext *ctx, const char *callee_qn, const char *strategy,
float confidence) {
cs_emit_resolved_kind(ctx, callee_qn, strategy, confidence, CBM_RESOLVED_INVOCATION);
}
static void cs_emit_resolved_reason(CSLSPContext *ctx, const char *callee_qn, const char *strategy,
float confidence, const char *reason) {
cs_emit_resolved_kind_reason(ctx, callee_qn, strategy, confidence, CBM_RESOLVED_INVOCATION,
reason);
}
static void cs_stamp_resolved_site(CSLSPContext *ctx, int first, TSNode site) {
if (!ctx->resolved_calls || ts_node_is_null(site) || first < 0)
return;
for (int i = first; i < ctx->resolved_calls->count; i++) {
ctx->resolved_calls->items[i].site_start_byte = ts_node_start_byte(site);
ctx->resolved_calls->items[i].site_end_byte = ts_node_end_byte(site);
}
}
static void cs_emit_resolved_kind_reason_at(CSLSPContext *ctx, const char *callee_qn,
const char *strategy, float confidence,
CBMResolvedKind kind, const char *reason, TSNode site) {
int first = ctx->resolved_calls ? ctx->resolved_calls->count : -1;
cs_emit_resolved_kind_reason(ctx, callee_qn, strategy, confidence, kind, reason);
cs_stamp_resolved_site(ctx, first, site);
}
static void cs_retarget_callable_carrier(CSLSPContext *ctx, TSNode site, const char *source_name,
const char *target_qn) {
if (!ctx || !ctx->call_carriers || ts_node_is_null(site) || !source_name || !target_qn ||
!ctx->enclosing_func_qn) {
return;
}
const char *target_name = cs_short_name(target_qn);
uint32_t start = ts_node_start_byte(site);
uint32_t end = ts_node_end_byte(site);
for (int i = 0; i < ctx->call_carriers->count; i++) {
CBMCall *call = &ctx->call_carriers->items[i];
if (!call->callee_name || !call->enclosing_func_qn ||
strcmp(call->callee_name, source_name) != 0 ||
strcmp(call->enclosing_func_qn, ctx->enclosing_func_qn) != 0 ||
call->site_start_byte < start || call->site_end_byte > end) {
continue;
}
call->callee_name = cbm_arena_strdup(ctx->arena, target_name);
call->requires_lsp_resolution = true;
}
}
/* Promote only direct argument expressions whose value resolves to one exact
* method group. Wrappers are peeled, but conditionals/switches/lambdas are not
* traversed, so complex values retain parser-produced USAGE edges. */
static void cs_resolve_callable_arguments(CSLSPContext *ctx, TSNode call) {
TSNode args = ts_node_child_by_field_name(call, "arguments", 9);
if (ts_node_is_null(args))
args = cs_child_named_kind(call, "argument_list");
if (ts_node_is_null(args))
return;
uint32_t nc = ts_node_child_count(args);
for (uint32_t i = 0; i < nc; i++) {
TSNode argument = ts_node_child(args, i);
if (ts_node_is_null(argument) || !ts_node_is_named(argument) ||
strcmp(ts_node_type(argument), "argument") != 0) {
continue;
}
TSNode value = ts_node_child_by_field_name(argument, "expression", 10);
if (ts_node_is_null(value))
value = ts_node_child_by_field_name(argument, "value", 5);
if (ts_node_is_null(value)) {
uint32_t ac = ts_node_child_count(argument);
for (uint32_t j = ac; j > 0; j--) {
TSNode child = ts_node_child(argument, j - 1);
if (!ts_node_is_null(child) && ts_node_is_named(child)) {
value = child;
break;
}
}
}
if (ts_node_is_null(value))
continue;
TSNode callable_leaf;
memset(&callable_leaf, 0, sizeof(callable_leaf));
const char *target = cs_resolve_callable_value(ctx, value, &callable_leaf);
if (!target)
continue;
if (ts_node_is_null(callable_leaf))
continue;
char *source_name = cs_node_text(ctx, callable_leaf);
cs_emit_resolved_kind_reason_at(ctx, target, "lsp_callable_value_reference", 0.95f,
CBM_RESOLVED_CALL_REFERENCE, source_name, callable_leaf);
}
}
static void cs_resolve_invocation(CSLSPContext *ctx, TSNode call) {
TSNode fn = ts_node_child_by_field_name(call, "function", 8);
if (ts_node_is_null(fn)) {
@@ -1516,6 +1856,18 @@ static void cs_resolve_invocation(CSLSPContext *ctx, TSNode call) {
if (!fname) return;
char *bare = cs_strip_generic_args(ctx->arena, fname);
/* Callable-valued locals and parameters shadow class/static methods.
* An exact alias carries its real target; an ordinary lexical value
* suppresses all same-name fallbacks so we never fabricate a call. */
if (cbm_scope_contains(ctx->current_scope, bare)) {
const char *target = cbm_scope_lookup_callable(ctx->current_scope, bare);
if (target) {
cs_emit_resolved_reason(ctx, target, "lsp_callable_alias", 0.98f, bare);
cs_retarget_callable_carrier(ctx, call, bare, target);
}
return;
}
/* Try enclosing class member. cs_lookup_method walks the base chain, so
* a bare call may resolve to an INHERITED method. Distinguish, exactly
* as the instance-call path does: a method actually declared on the
@@ -1547,15 +1899,14 @@ static void cs_resolve_invocation(CSLSPContext *ctx, TSNode call) {
* the file-stem QN ("proj.Client.MathUtil"); resolve the target through
* the type-name resolver (its short-name fallback bridges the two)
* before the method lookup. */
for (int i = 0; i < ctx->using_count; i++) {
const CBMCSUsing *u = &ctx->usings[i];
if (u->kind != CBM_CS_USING_STATIC) continue;
const char *host = cs_resolve_type_name(ctx, u->target_qn);
const CBMRegisteredFunc *f = cs_lookup_method(ctx, host ? host : u->target_qn, bare);
if (f) {
cs_emit_resolved(ctx, f->qualified_name, "cs_using_static", 0.90f);
return;
}
bool using_static_ambiguous = false;
const CBMRegisteredFunc *using_static =
cs_lookup_using_static_method(ctx, bare, &using_static_ambiguous);
if (using_static_ambiguous)
return;
if (using_static) {
cs_emit_resolved(ctx, using_static->qualified_name, "cs_using_static", 0.90f);
return;
}
/* Free function in current namespace. */
@@ -1656,9 +2007,14 @@ static void cs_resolve_calls_in_node(CSLSPContext *ctx, TSNode node) {
} else if (strcmp(kind, "assignment_expression") == 0) {
cs_process_assignment(ctx, node);
} else if (strcmp(kind, "invocation_expression") == 0) {
int first = ctx->resolved_calls ? ctx->resolved_calls->count : -1;
cs_resolve_invocation(ctx, node);
cs_stamp_resolved_site(ctx, first, node);
cs_resolve_callable_arguments(ctx, node);
} else if (strcmp(kind, "object_creation_expression") == 0) {
int first = ctx->resolved_calls ? ctx->resolved_calls->count : -1;
cs_resolve_object_creation(ctx, node);
cs_stamp_resolved_site(ctx, first, node);
}
/* Recurse into children. We do NOT pre-bind anything that would only be
@@ -1692,6 +2048,13 @@ static void cs_resolve_calls_in_node(CSLSPContext *ctx, TSNode node) {
cs_resolve_calls_in_node(ctx, c);
} while (ts_tree_cursor_goto_next_sibling(&cursor));
ts_tree_cursor_delete(&cursor);
if (strcmp(kind, "if_statement") == 0 || strcmp(kind, "switch_statement") == 0 ||
strcmp(kind, "switch_expression") == 0 || strcmp(kind, "for_statement") == 0 ||
strcmp(kind, "for_each_statement") == 0 || strcmp(kind, "foreach_statement") == 0 ||
strcmp(kind, "while_statement") == 0 || strcmp(kind, "do_statement") == 0 ||
strcmp(kind, "try_statement") == 0) {
cs_invalidate_conditionally_assigned_callables(ctx, node);
}
}
/* ── method/constructor processing ──────────────────────────────── */
@@ -2690,6 +3053,34 @@ static void cs_collect_method_return_types(CSLSPContext *ctx, TSNode root,
}
}
void cbm_cs_refine_ast_return_types(CSLSPContext *ctx, CBMTypeRegistry *reg, TSNode root) {
if (!ctx || !reg || ts_node_is_null(root))
return;
cs_method_rt_table_t mtab = {0};
cs_collect_method_return_types(ctx, root, &mtab);
for (int i = 0; i < mtab.count; i++) {
const char *qn = mtab.items[i].qn;
const CBMType *rt = mtab.items[i].rt;
bool patched = false;
for (int j = 0; j < reg->func_count; j++) {
if (strcmp(reg->funcs[j].qualified_name, qn) == 0) {
const CBMType **rets =
(const CBMType **)cbm_arena_alloc(ctx->arena, 2 * sizeof(const CBMType *));
if (rets) {
rets[0] = rt;
rets[1] = NULL;
reg->funcs[j].signature =
cbm_type_func_replace_returns(ctx->arena, reg->funcs[j].signature, rets);
}
patched = true;
break;
}
}
(void)patched;
}
}
/* ── parse return type from CBMDefinition.return_type ──────────── */
static const CBMType *cs_parse_return_type_text(CSLSPContext *ctx, const char *text) {
@@ -2709,6 +3100,13 @@ static const CBMType *cs_parse_return_type_text(CSLSPContext *ctx, const char *t
return cbm_type_named(ctx->arena, resolved);
}
static const CBMType *cs_signature_param_type_adapter(CBMArena *arena, const char *text,
void *parser_ctx) {
(void)arena;
CSLSPContext *ctx = (CSLSPContext *)parser_ctx;
return ctx ? cs_parse_return_type_text(ctx, text) : cbm_type_unknown();
}
/* ── single-file entry: cbm_run_cs_lsp ──────────────────────────── */
void cbm_run_cs_lsp(CBMArena *arena, CBMFileResult *result, const char *source, int source_len,
@@ -2785,10 +3183,37 @@ void cbm_run_cs_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
/* Phase C: build context for type resolution + run the resolver. */
CSLSPContext ctx;
cs_lsp_init(&ctx, arena, source, source_len, &reg, module_qn, &result->resolved_calls);
ctx.call_carriers = &result->calls;
/* Pass 1: collect imports + namespaces (for type-resolution context). */
cs_collect_imports(&ctx, root);
/* The counted carrier is authoritative even when count is zero. Install it
* only after imports are available so textual types use the same local
* resolution context as declared returns. A truly absent carrier keeps the
* legacy zero-parameter behavior. */
for (int i = 0; i < result->defs.count; i++) {
CBMDefinition *d = &result->defs.items[i];
if (!d->qualified_name || !d->label ||
(strcmp(d->label, "Function") != 0 && strcmp(d->label, "Method") != 0) ||
(!d->signature_param_types && d->signature_param_count <= 0)) {
continue;
}
const CBMType **param_types = cbm_type_materialize_signature_params(
arena, d->signature_param_types, d->signature_param_count,
cs_signature_param_type_adapter, &ctx);
for (int j = 0; j < reg.func_count; j++) {
if (strcmp(reg.funcs[j].qualified_name, d->qualified_name) != 0)
continue;
const CBMType **return_types = NULL;
if (reg.funcs[j].signature && reg.funcs[j].signature->kind == CBM_TYPE_FUNC) {
return_types = reg.funcs[j].signature->data.func.return_types;
}
reg.funcs[j].signature = cbm_type_func(arena, NULL, param_types, return_types);
break;
}
}
/* Now refine return types of registered funcs using ctx (which has using
* directives). Also refine field types via collect_class_fields. */
for (int i = 0; i < result->defs.count; i++) {
@@ -2804,7 +3229,8 @@ void cbm_run_cs_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
if (rets) {
rets[0] = rt;
rets[1] = NULL;
reg.funcs[j].signature = cbm_type_func(arena, NULL, NULL, rets);
reg.funcs[j].signature =
cbm_type_func_replace_returns(arena, reg.funcs[j].signature, rets);
}
break;
}
@@ -2814,29 +3240,7 @@ void cbm_run_cs_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
/* Phase B.1.5: AST-driven return-type patch. The unified extractor
* leaves CBMDefinition.return_type NULL for C# methods, so we walk the
* AST ourselves to recover declared return types. */
{
cs_method_rt_table_t mtab = {0};
cs_collect_method_return_types(&ctx, root, &mtab);
for (int i = 0; i < mtab.count; i++) {
const char *qn = mtab.items[i].qn;
const CBMType *rt = mtab.items[i].rt;
bool patched = false;
for (int j = 0; j < reg.func_count; j++) {
if (strcmp(reg.funcs[j].qualified_name, qn) == 0) {
const CBMType **rets =
(const CBMType **)cbm_arena_alloc(arena, 2 * sizeof(const CBMType *));
if (rets) {
rets[0] = rt;
rets[1] = NULL;
reg.funcs[j].signature = cbm_type_func(arena, NULL, NULL, rets);
}
patched = true;
break;
}
}
(void)patched;
}
}
cbm_cs_refine_ast_return_types(&ctx, &reg, root);
/* Phase B.1: collect typed properties + fields. */
{
@@ -2928,13 +3332,22 @@ static void cs_register_lsp_defs(CBMArena *arena, CBMTypeRegistry *reg,
if (strcmp(d->label, "Method") == 0 && d->receiver_type) {
rf.receiver_type = d->receiver_type;
}
CSLSPContext parser_ctx = {0};
parser_ctx.arena = arena;
parser_ctx.registry = reg;
parser_ctx.module_qn = d->def_module_qn;
const CBMType **rets =
(const CBMType **)cbm_arena_alloc(arena, 2 * sizeof(const CBMType *));
if (rets) {
rets[0] = cbm_type_unknown();
rets[0] = d->return_types && d->return_types[0]
? cs_parse_return_type_text(&parser_ctx, d->return_types)
: cbm_type_unknown();
rets[1] = NULL;
}
rf.signature = cbm_type_func(arena, NULL, NULL, rets);
const CBMType **param_types = cbm_type_materialize_signature_params(
arena, d->signature_param_types, d->signature_param_count,
cs_signature_param_type_adapter, &parser_ctx);
rf.signature = cbm_type_func(arena, NULL, param_types, rets);
cbm_registry_add_func(reg, rf);
}
}
+9
View File
@@ -97,6 +97,10 @@ typedef struct {
/* Output: resolved calls accumulate here. */
CBMResolvedCallArray *resolved_calls;
/* Existing parser call carriers for this file. Exact callable aliases
* retarget their occurrence and require the semantic result, preventing a
* same-named definition from winning through textual fallback. */
CBMCallArray *call_carriers;
/* Active type-parameter substitution map (for generic methods/types).
* Parallel arrays. NULL-terminated. */
@@ -138,6 +142,11 @@ const char *cs_resolve_type_name(CSLSPContext *ctx, const char *name);
const CBMRegisteredFunc *cs_lookup_method(CSLSPContext *ctx, const char *type_qn,
const char *method_name);
/* Apply the local AST-declared return-type refinement to an already populated
* registry. Kept as a production-used seam so post-registration signature
* preservation can be regression-tested directly. */
void cbm_cs_refine_ast_return_types(CSLSPContext *ctx, CBMTypeRegistry *reg, TSNode root);
/* Single-file entry: build registry from file defs + stdlib, run resolution. */
void cbm_run_cs_lsp(CBMArena *arena, CBMFileResult *result, const char *source, int source_len,
TSNode root);
+493 -92
View File
@@ -22,7 +22,9 @@ static void resolve_calls_in_node(GoLSPContext* ctx, TSNode node) {
resolve_calls_in_node_inner(ctx, node);
ctx->walk_depth--;
}
static void emit_resolved_call(GoLSPContext* ctx, const char* callee_qn, const char* strategy, float confidence);
static void emit_resolved_call(GoLSPContext *ctx, const char *callee_qn, const char *strategy,
float confidence, TSNode site);
static const char *go_exact_callable_target(GoLSPContext *ctx, TSNode node);
static const CBMType* go_lookup_field(GoLSPContext* ctx, const char* type_qn, const char* field_name, int depth);
static void extract_type_params_from_ast(CBMArena* arena, CBMTypeRegistry* reg,
TSNode root, const char* source, const char* module_qn);
@@ -336,7 +338,8 @@ const CBMType* go_eval_expr_type(GoLSPContext* ctx, TSNode node) {
// Check scope first
const CBMType* t = cbm_scope_lookup(ctx->current_scope, name);
if (!cbm_type_is_unknown(t)) return t;
if (cbm_scope_contains(ctx->current_scope, name))
return t ? t : cbm_type_unknown();
// Check if it's a package-level function
const CBMRegisteredFunc* f = cbm_registry_lookup_symbol(ctx->registry, ctx->package_qn, name);
@@ -929,6 +932,76 @@ const CBMRegisteredFunc* go_lookup_field_or_method(GoLSPContext* ctx,
return go_lookup_field_or_method_depth(ctx, type_qn, member_name, 0);
}
/* Resolve only source forms that prove one callable identity. This is
* intentionally narrower than go_eval_expr_type: conditional expressions,
* indexed dispatch tables, interfaces with several implementers, and unknown
* locals remain ordinary values. */
static const char *go_exact_callable_target(GoLSPContext *ctx, TSNode node) {
if (!ctx || ts_node_is_null(node))
return NULL;
const char *kind = ts_node_type(node);
if (strcmp(kind, "parenthesized_expression") == 0 && ts_node_named_child_count(node) == 1) {
return go_exact_callable_target(ctx, ts_node_named_child(node, 0));
}
if (strcmp(kind, "identifier") == 0) {
char *name = lsp_node_text(ctx, node);
if (!name)
return NULL;
/* A local value shadows the package declaration even when its type is
* UNKNOWN. Only an explicitly tracked callable alias may pass. */
if (cbm_scope_contains(ctx->current_scope, name)) {
return cbm_scope_lookup_callable(ctx->current_scope, name);
}
const CBMRegisteredFunc *f =
cbm_registry_lookup_symbol(ctx->registry, ctx->package_qn, name);
return f ? f->qualified_name : NULL;
}
if (strcmp(kind, "selector_expression") == 0) {
TSNode operand = ts_node_child_by_field_name(node, "operand", 7);
TSNode field = ts_node_child_by_field_name(node, "field", 5);
if (ts_node_is_null(operand) || ts_node_is_null(field))
return NULL;
char *member = lsp_node_text(ctx, field);
if (!member)
return NULL;
if (strcmp(ts_node_type(operand), "identifier") == 0) {
char *local = lsp_node_text(ctx, operand);
const char *package_qn = local ? resolve_import(ctx, local) : NULL;
if (package_qn) {
const CBMRegisteredFunc *f =
cbm_registry_lookup_symbol(ctx->registry, package_qn, member);
return f ? f->qualified_name : NULL;
}
}
const CBMType *recv = go_eval_expr_type(ctx, operand);
if (recv && recv->kind == CBM_TYPE_POINTER)
recv = cbm_type_deref(recv);
if (recv && recv->kind == CBM_TYPE_NAMED) {
/* A direct method name is unique in a valid Go method set.
* Promoted lookup through embedded types can be ambiguous; without
* full selector-depth resolution, fail closed and retain USAGE. */
const CBMRegisteredFunc *method =
cbm_registry_lookup_method(ctx->registry, recv->data.named.qualified_name, member);
return method ? method->qualified_name : NULL;
}
}
return NULL;
}
static void go_scope_bind_value(GoLSPContext *ctx, const char *name, const CBMType *type,
const char *callable_qn) {
if (callable_qn) {
cbm_scope_bind_callable(ctx->current_scope, name, type, callable_qn);
} else {
cbm_scope_bind(ctx->current_scope, name, type);
}
}
// --- go_process_statement: bind variables from statements ---
void go_process_statement(GoLSPContext* ctx, TSNode node) {
@@ -942,17 +1015,22 @@ void go_process_statement(GoLSPContext* ctx, TSNode node) {
if (ts_node_is_null(left) || ts_node_is_null(right)) return;
const CBMType* rhs_type = NULL;
TSNode single_rhs = {0};
// Check if RHS is an expression_list (multiple values)
if (strcmp(ts_node_type(right), "expression_list") == 0) {
uint32_t rhs_count = ts_node_named_child_count(right);
if (rhs_count == 1) {
// Single expression that might return a tuple (multi-return)
rhs_type = go_eval_expr_type(ctx, ts_node_named_child(right, 0));
single_rhs = ts_node_named_child(right, 0);
rhs_type = go_eval_expr_type(ctx, single_rhs);
}
} else {
single_rhs = right;
rhs_type = go_eval_expr_type(ctx, right);
}
const char *rhs_callable =
ts_node_is_null(single_rhs) ? NULL : go_exact_callable_target(ctx, single_rhs);
// Bind left-hand side variables
if (strcmp(ts_node_type(left), "expression_list") == 0) {
@@ -971,23 +1049,67 @@ void go_process_statement(GoLSPContext* ctx, TSNode node) {
var_type = rhs_type;
}
}
cbm_scope_bind(ctx->current_scope, var_name, var_type);
go_scope_bind_value(ctx, var_name, var_type, i == 0 ? rhs_callable : NULL);
}
} else if (strcmp(ts_node_type(left), "identifier") == 0) {
char* var_name = lsp_node_text(ctx, left);
if (var_name && strcmp(var_name, "_") != 0 && rhs_type) {
cbm_scope_bind(ctx->current_scope, var_name, rhs_type);
go_scope_bind_value(ctx, var_name, rhs_type, rhs_callable);
}
}
return;
}
/* Reassignment must update (or clear) callable identity just like :=.
* Without this, `alias := target; alias = dynamic; alias()` would retain a
* stale exact target and fabricate a call. */
if (strcmp(kind, "assignment_statement") == 0) {
TSNode left = ts_node_child_by_field_name(node, "left", 4);
TSNode right = ts_node_child_by_field_name(node, "right", 5);
if (ts_node_is_null(left) || ts_node_is_null(right))
return;
uint32_t lhs_count = strcmp(ts_node_type(left), "expression_list") == 0
? ts_node_named_child_count(left)
: 1;
uint32_t rhs_count = strcmp(ts_node_type(right), "expression_list") == 0
? ts_node_named_child_count(right)
: 1;
for (uint32_t i = 0; i < lhs_count; i++) {
TSNode lhs = lhs_count == 1 && strcmp(ts_node_type(left), "expression_list") != 0
? left
: ts_node_named_child(left, i);
if (ts_node_is_null(lhs) || strcmp(ts_node_type(lhs), "identifier") != 0)
continue;
char *name = lsp_node_text(ctx, lhs);
if (!name || strcmp(name, "_") == 0)
continue;
TSNode rhs = {0};
if (rhs_count == lhs_count) {
rhs = rhs_count == 1 && strcmp(ts_node_type(right), "expression_list") != 0
? right
: ts_node_named_child(right, i);
}
const CBMType *type =
ts_node_is_null(rhs) ? cbm_type_unknown() : go_eval_expr_type(ctx, rhs);
const char *callable = ts_node_is_null(rhs) ? NULL : go_exact_callable_target(ctx, rhs);
go_scope_bind_value(ctx, name, type, callable);
}
return;
}
// var_spec: var x Type = expr OR var a, b, c Type = expr
if (strcmp(kind, "var_spec") == 0) {
TSNode type_node = ts_node_child_by_field_name(node, "type", 4);
TSNode value_node = ts_node_child_by_field_name(node, "value", 5);
const CBMType* var_type = cbm_type_unknown();
TSNode callable_value = value_node;
if (!ts_node_is_null(value_node) &&
strcmp(ts_node_type(value_node), "expression_list") == 0) {
callable_value = ts_node_named_child_count(value_node) == 1
? ts_node_named_child(value_node, 0)
: (TSNode){0};
}
if (!ts_node_is_null(type_node)) {
var_type = go_parse_type_node(ctx, type_node);
} else if (!ts_node_is_null(value_node)) {
@@ -998,6 +1120,8 @@ void go_process_statement(GoLSPContext* ctx, TSNode node) {
var_type = go_eval_expr_type(ctx, value_node);
}
}
const char *callable_qn =
ts_node_is_null(callable_value) ? NULL : go_exact_callable_target(ctx, callable_value);
// Bind all name identifiers (handles: var a, b, c int)
uint32_t vnc = ts_node_child_count(node);
@@ -1007,7 +1131,7 @@ void go_process_statement(GoLSPContext* ctx, TSNode node) {
if (strcmp(ts_node_type(ch), "identifier") == 0) {
char* var_name = lsp_node_text(ctx, ch);
if (var_name && strcmp(var_name, "_") != 0) {
cbm_scope_bind(ctx->current_scope, var_name, var_type);
go_scope_bind_value(ctx, var_name, var_type, callable_qn);
}
}
}
@@ -1022,6 +1146,13 @@ void go_process_statement(GoLSPContext* ctx, TSNode node) {
if (ts_node_is_null(name_node)) return;
const CBMType* const_type = cbm_type_unknown();
TSNode callable_value = value_node;
if (!ts_node_is_null(value_node) &&
strcmp(ts_node_type(value_node), "expression_list") == 0) {
callable_value = ts_node_named_child_count(value_node) == 1
? ts_node_named_child(value_node, 0)
: (TSNode){0};
}
if (!ts_node_is_null(type_node)) {
const_type = go_parse_type_node(ctx, type_node);
} else if (!ts_node_is_null(value_node)) {
@@ -1033,11 +1164,13 @@ void go_process_statement(GoLSPContext* ctx, TSNode node) {
const_type = go_eval_expr_type(ctx, value_node);
}
}
const char *callable_qn =
ts_node_is_null(callable_value) ? NULL : go_exact_callable_target(ctx, callable_value);
if (strcmp(ts_node_type(name_node), "identifier") == 0) {
char* name = lsp_node_text(ctx, name_node);
if (name && strcmp(name, "_") != 0)
cbm_scope_bind(ctx->current_scope, name, const_type);
go_scope_bind_value(ctx, name, const_type, callable_qn);
}
return;
}
@@ -1094,33 +1227,151 @@ void go_process_statement(GoLSPContext* ctx, TSNode node) {
// (needs per-case scope narrowing, not just variable binding)
}
// --- Emit a resolved call ---
// --- Emit a resolved call/reference ---
static void emit_resolved_call(GoLSPContext* ctx, const char* callee_qn, const char* strategy, float confidence) {
static void go_emit_resolved_kind(GoLSPContext *ctx, const char *callee_qn, const char *strategy,
float confidence, const char *source_name, CBMResolvedKind kind,
TSNode site) {
if (!ctx->resolved_calls || !callee_qn || !ctx->enclosing_func_qn) return;
CBMResolvedCall rc;
CBMResolvedCall rc = {0};
rc.caller_qn = ctx->enclosing_func_qn;
rc.callee_qn = callee_qn;
rc.strategy = strategy;
rc.confidence = confidence;
rc.reason = NULL;
/* When a callable alias has a different textual leaf, retain that source
* name for the exact-site join. */
rc.reason = source_name;
rc.kind = kind;
if (!ts_node_is_null(site)) {
rc.site_start_byte = ts_node_start_byte(site);
rc.site_end_byte = ts_node_end_byte(site);
}
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc);
}
static void emit_resolved_call(GoLSPContext *ctx, const char *callee_qn, const char *strategy,
float confidence, TSNode site) {
go_emit_resolved_kind(ctx, callee_qn, strategy, confidence, NULL, CBM_RESOLVED_INVOCATION,
site);
}
static void emit_resolved_alias_call(GoLSPContext *ctx, const char *callee_qn,
const char *source_name, TSNode site) {
go_emit_resolved_kind(ctx, callee_qn, "lsp_callable_alias", 0.97f, source_name,
CBM_RESOLVED_INVOCATION, site);
}
static void emit_resolved_reference(GoLSPContext *ctx, const char *callee_qn,
const char *source_name, TSNode site) {
const char *leaf = strrchr(callee_qn, '.');
leaf = leaf ? leaf + 1 : callee_qn;
const char *join_name = source_name && strcmp(source_name, leaf) != 0 ? source_name : NULL;
go_emit_resolved_kind(ctx, callee_qn, "lsp_callable_value_reference", 0.97f, join_name,
CBM_RESOLVED_CALL_REFERENCE, site);
}
static void emit_unresolved_reference(GoLSPContext *ctx, const char *source_name, TSNode site) {
if (!ctx || !ctx->resolved_calls || !ctx->enclosing_func_qn || !source_name ||
ts_node_is_null(site)) {
return;
}
CBMResolvedCall rc = {0};
rc.caller_qn = ctx->enclosing_func_qn;
rc.callee_qn = source_name;
rc.strategy = "lsp_unresolved";
rc.confidence = 0.0f;
rc.reason = "callable_value_not_in_registry";
rc.kind = CBM_RESOLVED_CALL_REFERENCE;
rc.site_start_byte = ts_node_start_byte(site);
rc.site_end_byte = ts_node_end_byte(site);
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc);
}
// Emit a diagnostic for an unresolved call (confidence 0.0).
static void emit_unresolved_call(GoLSPContext* ctx, const char* expr_text, const char* reason) {
static void emit_unresolved_call(GoLSPContext *ctx, const char *expr_text, const char *reason,
TSNode site) {
if (!ctx->resolved_calls || !ctx->enclosing_func_qn) return;
CBMResolvedCall rc;
CBMResolvedCall rc = {0};
rc.caller_qn = ctx->enclosing_func_qn;
rc.callee_qn = expr_text ? expr_text : "?";
rc.strategy = "lsp_unresolved";
rc.confidence = 0.0f;
rc.reason = reason;
rc.kind = CBM_RESOLVED_INVOCATION;
if (!ts_node_is_null(site)) {
rc.site_start_byte = ts_node_start_byte(site);
rc.site_end_byte = ts_node_end_byte(site);
}
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc);
}
/* Only direct argument expressions are candidates. Descending into a
* conditional/map/index expression would promote its constituent names even
* though the runtime callable is not statically unique. */
static void go_resolve_value_references_at(GoLSPContext *ctx, TSNode call) {
TSNode args = ts_node_child_by_field_name(call, "arguments", 9);
if (ts_node_is_null(args))
return;
uint32_t count = ts_node_named_child_count(args);
for (uint32_t i = 0; i < count; i++) {
TSNode arg = ts_node_named_child(args, i);
const char *kind = ts_node_type(arg);
if (strcmp(kind, "identifier") != 0 && strcmp(kind, "selector_expression") != 0 &&
strcmp(kind, "parenthesized_expression") != 0)
continue;
const char *target = go_exact_callable_target(ctx, arg);
char *source_name = lsp_node_text(ctx, arg);
if (target) {
emit_resolved_reference(ctx, target, source_name, arg);
} else if (strcmp(kind, "identifier") == 0 && source_name &&
!cbm_scope_contains(ctx->current_scope, source_name)) {
/* The per-file registry cannot see an unqualified function from
* another file in the same Go package. Preserve the exact source
* occurrence for the metadata-only project pass. */
emit_unresolved_reference(ctx, source_name, arg);
}
}
}
/* Any assignment that is guarded by a branch/loop makes the post-construct
* callable identity a join of at least two paths. Clear the outer exact
* identity before walking the construct; assignments inside its temporary
* scope may still be exact locally, but cannot leak past the join. */
static void go_invalidate_control_flow_aliases(GoLSPContext *ctx, TSNode node, int depth) {
if (!ctx || ts_node_is_null(node) || depth > 64)
return;
const char *kind = ts_node_type(node);
if (strcmp(kind, "func_literal") == 0)
return;
if (strcmp(kind, "assignment_statement") == 0) {
TSNode left = ts_node_child_by_field_name(node, "left", 4);
if (!ts_node_is_null(left)) {
uint32_t count = strcmp(ts_node_type(left), "expression_list") == 0
? ts_node_named_child_count(left)
: 1;
for (uint32_t i = 0; i < count; i++) {
TSNode target = count == 1 && strcmp(ts_node_type(left), "expression_list") != 0
? left
: ts_node_named_child(left, i);
if (ts_node_is_null(target) || strcmp(ts_node_type(target), "identifier") != 0) {
continue;
}
char *name = lsp_node_text(ctx, target);
if (name && cbm_scope_lookup_callable(ctx->current_scope, name)) {
cbm_scope_update_callable(ctx->current_scope, name, NULL);
}
}
}
return;
}
uint32_t count = ts_node_named_child_count(node);
for (uint32_t i = 0; i < count; i++) {
go_invalidate_control_flow_aliases(ctx, ts_node_named_child(node, i), depth + 1);
}
}
// --- Walk call expressions and resolve them ---
static void resolve_calls_in_node_inner(GoLSPContext* ctx, TSNode node) {
@@ -1130,8 +1381,15 @@ static void resolve_calls_in_node_inner(GoLSPContext* ctx, TSNode node) {
// Process statements to build scope
go_process_statement(ctx, node);
if (strcmp(kind, "if_statement") == 0 || strcmp(kind, "for_statement") == 0 ||
strcmp(kind, "expression_switch_statement") == 0 ||
strcmp(kind, "type_switch_statement") == 0 || strcmp(kind, "select_statement") == 0) {
go_invalidate_control_flow_aliases(ctx, node, 0);
}
// Resolve call expressions
if (strcmp(kind, "call_expression") == 0) {
go_resolve_value_references_at(ctx, node);
TSNode func_node = ts_node_child_by_field_name(node, "function", 8);
if (!ts_node_is_null(func_node)) {
const char* fk = ts_node_type(func_node);
@@ -1151,13 +1409,15 @@ static void resolve_calls_in_node_inner(GoLSPContext* ctx, TSNode node) {
if (pkg_qn && field_name) {
const CBMRegisteredFunc* f = cbm_registry_lookup_symbol(ctx->registry, pkg_qn, field_name);
if (f) {
emit_resolved_call(ctx, f->qualified_name, "lsp_direct", 0.95f);
emit_resolved_call(ctx, f->qualified_name, "lsp_direct", 0.95f,
node);
goto recurse;
}
// Package found but symbol not in registry
emit_unresolved_call(ctx,
emit_unresolved_call(
ctx,
cbm_arena_sprintf(ctx->arena, "%s.%s", pkg_name, field_name),
"symbol_not_in_registry");
"symbol_not_in_registry", node);
goto recurse;
}
}
@@ -1170,16 +1430,26 @@ static void resolve_calls_in_node_inner(GoLSPContext* ctx, TSNode node) {
if (base && base->kind == CBM_TYPE_POINTER) base = cbm_type_deref(base);
if (base && base->kind == CBM_TYPE_NAMED) {
const CBMRegisteredFunc* method = go_lookup_field_or_method(ctx,
base->data.named.qualified_name, field_name);
if (method) {
const char* strategy = "lsp_type_dispatch";
if (method->receiver_type &&
strcmp(method->receiver_type, base->data.named.qualified_name) != 0) {
strategy = "lsp_embed_dispatch";
const CBMRegisteredType *receiver_type = cbm_registry_lookup_type(
ctx->registry, base->data.named.qualified_name);
/* Registered interface receivers must reach the
* interface-resolution branch below. Their semantic
* method registrations are signatures, not concrete
* dispatch targets. */
if (!receiver_type || !receiver_type->is_interface) {
const CBMRegisteredFunc *method = go_lookup_field_or_method(
ctx, base->data.named.qualified_name, field_name);
if (method) {
const char *strategy = "lsp_type_dispatch";
if (method->receiver_type &&
strcmp(method->receiver_type,
base->data.named.qualified_name) != 0) {
strategy = "lsp_embed_dispatch";
}
emit_resolved_call(ctx, method->qualified_name, strategy, 0.95f,
node);
goto recurse;
}
emit_resolved_call(ctx, method->qualified_name, strategy, 0.95f);
goto recurse;
}
}
@@ -1243,17 +1513,20 @@ static void resolve_calls_in_node_inner(GoLSPContext* ctx, TSNode node) {
// `Type.method` wins over the interface-method type_dispatch
// for the same call site.
emit_resolved_call(ctx, concrete_method->qualified_name,
"lsp_interface_resolve", 0.95f);
"lsp_interface_resolve", 0.95f,
node);
goto recurse;
}
}
}
// Fallback: generic interface dispatch
emit_resolved_call(ctx,
emit_resolved_call(
ctx,
cbm_arena_sprintf(ctx->arena, "%s.%s",
iface_qn ? iface_qn : "interface", field_name),
"lsp_interface_dispatch", 0.85f);
iface_qn ? iface_qn : "interface",
field_name),
"lsp_interface_dispatch", 0.85f, node);
goto recurse;
}
}
@@ -1261,15 +1534,17 @@ static void resolve_calls_in_node_inner(GoLSPContext* ctx, TSNode node) {
// Type resolved to NAMED but neither method nor interface matched
if (base && base->kind == CBM_TYPE_NAMED) {
emit_unresolved_call(ctx,
cbm_arena_sprintf(ctx->arena, "%s.%s",
base->data.named.qualified_name, field_name),
"method_not_found");
cbm_arena_sprintf(ctx->arena, "%s.%s",
base->data.named.qualified_name,
field_name),
"method_not_found", node);
} else if (cbm_type_is_unknown(recv_type)) {
char* operand_text = lsp_node_text(ctx, operand);
emit_unresolved_call(ctx,
emit_unresolved_call(
ctx,
cbm_arena_sprintf(ctx->arena, "%s.%s",
operand_text ? operand_text : "?", field_name),
"unknown_receiver_type");
operand_text ? operand_text : "?", field_name),
"unknown_receiver_type", node);
}
}
}
@@ -1279,12 +1554,23 @@ static void resolve_calls_in_node_inner(GoLSPContext* ctx, TSNode node) {
if (strcmp(fk, "identifier") == 0) {
char* name = lsp_node_text(ctx, func_node);
if (name && !is_go_builtin_func(name)) {
if (cbm_scope_contains(ctx->current_scope, name)) {
const char *alias_target =
cbm_scope_lookup_callable(ctx->current_scope, name);
if (alias_target) {
emit_resolved_alias_call(ctx, alias_target, name, node);
} else {
emit_unresolved_call(ctx, name, "lexical_value_not_exact_callable",
node);
}
goto recurse;
}
// Package-local function
const CBMRegisteredFunc* f = cbm_registry_lookup_symbol(ctx->registry, ctx->package_qn, name);
if (f) {
emit_resolved_call(ctx, f->qualified_name, "lsp_direct", 0.95f);
emit_resolved_call(ctx, f->qualified_name, "lsp_direct", 0.95f, node);
} else {
emit_unresolved_call(ctx, name, "function_not_in_registry");
emit_unresolved_call(ctx, name, "function_not_in_registry", node);
}
}
}
@@ -1715,6 +2001,17 @@ const CBMType* cbm_parse_return_type_text(CBMArena* a, const char* text, const c
return cbm_type_named(a, cbm_arena_sprintf(a, "%s.%s", module_qn, text));
}
typedef struct {
const char *module_qn;
} CBMGoLSPSignatureParserContext;
static const CBMType *cbm_go_lsp_parse_signature_param_text(CBMArena *arena, const char *text,
void *parser_ctx) {
const CBMGoLSPSignatureParserContext *ctx = (const CBMGoLSPSignatureParserContext *)parser_ctx;
const char *module_qn = ctx && ctx->module_qn ? ctx->module_qn : "";
return cbm_parse_return_type_text(arena, text, module_qn);
}
// --- Entry point: build registry from file defs + run LSP ---
void cbm_run_go_lsp(CBMArena* arena, CBMFileResult* result,
@@ -1790,9 +2087,21 @@ void cbm_run_go_lsp(CBMArena* arena, CBMFileResult* result,
ret_types[1] = NULL;
}
// Build param types from param_types array
/* The occurrence-preserving carrier is authoritative whenever it
* is present, including a non-NULL carrier with an explicit zero
* count. Its slots deliberately have no names because legacy name
* extraction is not guaranteed to have the same multiplicity. */
const CBMType** param_types_arr = NULL;
if (d->param_types) {
const char **param_names_arr = NULL;
bool has_ordered_params =
d->signature_param_types != NULL || d->signature_param_count > 0;
CBMGoLSPSignatureParserContext param_parser_ctx = {module_qn};
if (has_ordered_params) {
param_types_arr = cbm_type_materialize_signature_params(
arena, d->signature_param_types, d->signature_param_count,
cbm_go_lsp_parse_signature_param_text, &param_parser_ctx);
} else if (d->param_types) {
param_names_arr = d->param_names;
int count = 0;
while (d->param_types[count]) count++;
if (count > 0) {
@@ -1802,37 +2111,44 @@ void cbm_run_go_lsp(CBMArena* arena, CBMFileResult* result,
}
param_types_arr[count] = NULL;
}
} else {
param_names_arr = d->param_names;
}
rf.signature = cbm_type_func(arena, d->param_names, param_types_arr, ret_types);
rf.signature = cbm_type_func(arena, param_names_arr, param_types_arr, ret_types);
// For methods, extract receiver type from receiver text
if (strcmp(d->label, "Method") == 0 && d->receiver && d->receiver[0]) {
// Receiver format: "(name *Type)" or "(name Type)"
// Extract type name: skip parens, skip first identifier, strip *
const char* r = d->receiver;
while (*r == '(' || *r == ' ') r++;
// Skip receiver name
while (*r && *r != ' ' && *r != '*') r++;
while (*r == ' ' || *r == '*') r++;
// Now r points to the type name
const char* end = r;
while (*end && *end != ')' && *end != ' ') end++;
if (end > r) {
char* type_name = cbm_arena_strndup(arena, r, end - r);
rf.receiver_type = cbm_arena_sprintf(arena, "%s.%s", module_qn, type_name);
// Also register this method under the type
const CBMRegisteredType* existing = cbm_registry_lookup_type(&reg, rf.receiver_type);
if (!existing) {
// Auto-create the type entry
CBMRegisteredType auto_type;
memset(&auto_type, 0, sizeof(auto_type));
auto_type.qualified_name = rf.receiver_type;
auto_type.short_name = type_name;
cbm_registry_add_type(&reg, auto_type);
// Prefer the extractor's semantic receiver QN. Raw receiver text is
// retained only as a compatibility fallback for hand-built defs;
// reparsing it alone loses valid unnamed receivers such as `(Right)`.
if (strcmp(d->label, "Method") == 0) {
if (d->parent_class && d->parent_class[0]) {
rf.receiver_type = d->parent_class;
} else if (d->receiver && d->receiver[0]) {
// Legacy named receiver formats: "(name *Type)" / "(name Type)".
const char *r = d->receiver;
while (*r == '(' || *r == ' ')
r++;
while (*r && *r != ' ' && *r != '*')
r++;
while (*r == ' ' || *r == '*')
r++;
const char *end = r;
while (*end && *end != ')' && *end != ' ')
end++;
if (end > r) {
char *type_name = cbm_arena_strndup(arena, r, (size_t)(end - r));
rf.receiver_type = cbm_arena_sprintf(arena, "%s.%s", module_qn, type_name);
}
}
if (rf.receiver_type && !cbm_registry_lookup_type(&reg, rf.receiver_type)) {
const char *dot = strrchr(rf.receiver_type, '.');
CBMRegisteredType auto_type;
memset(&auto_type, 0, sizeof(auto_type));
auto_type.qualified_name = rf.receiver_type;
auto_type.short_name = dot ? dot + 1 : rf.receiver_type;
cbm_registry_add_type(&reg, auto_type);
}
}
cbm_registry_add_func(&reg, rf);
@@ -2343,6 +2659,42 @@ static const CBMType* parse_type_node_with_params(CBMArena* arena, TSNode node,
return cbm_type_unknown();
}
/* Go permits one AST parameter declaration to introduce several call
* positions: `first, second T`. Count direct names without exposing or
* rewriting the legacy public param_names projection. */
static int cbm_go_lsp_signature_param_multiplicity(TSNode param) {
const char *kind = ts_node_type(param);
if (strcmp(kind, "parameter_declaration") != 0 &&
strcmp(kind, "variadic_parameter_declaration") != 0) {
return 1;
}
int field_names = 0;
int identifier_names = 0;
uint32_t child_count = ts_node_child_count(param);
for (uint32_t i = 0; i < child_count; i++) {
TSNode child = ts_node_child(param, i);
if (ts_node_is_null(child) || !ts_node_is_named(child))
continue;
const char *field = ts_node_field_name_for_child(param, i);
if (field && strcmp(field, "name") == 0)
field_names++;
if (strcmp(ts_node_type(child), "identifier") == 0)
identifier_names++;
}
int names = field_names > 0 ? field_names : identifier_names;
return names > 0 ? names : 1;
}
static const CBMType *cbm_go_lsp_keep_known_refinement(const CBMType *candidate,
const CBMType *existing) {
if (cbm_type_is_unknown(candidate) && !cbm_type_is_unknown(existing)) {
return existing;
}
return candidate ? candidate : cbm_type_unknown();
}
// Scan AST for generic function declarations and set type_param_names + re-parse
// return types on matching registered functions.
static void extract_type_params_from_ast(CBMArena* arena, CBMTypeRegistry* reg,
@@ -2450,20 +2802,32 @@ static void extract_type_params_from_ast(CBMArena* arena, CBMTypeRegistry* reg,
if (ts_node_is_null(rchild) || !ts_node_is_named(rchild)) continue;
TSNode rtype = ts_node_child_by_field_name(rchild, "type", 4);
if (ts_node_is_null(rtype)) rtype = rchild;
new_rets[idx++] = parse_type_node_with_params(arena,
rtype, source, module_qn, tp_names);
const CBMType *parsed = parse_type_node_with_params(
arena, rtype, source, module_qn, tp_names);
new_rets[idx] =
cbm_go_lsp_keep_known_refinement(parsed, old_rets[idx]);
idx++;
}
new_rets[idx] = NULL;
while (idx < ret_count) {
new_rets[idx] = old_rets[idx];
idx++;
}
new_rets[ret_count] = NULL;
} else {
new_rets[0] = parse_type_node_with_params(arena,
result_node, source, module_qn, tp_names);
new_rets[1] = NULL;
const CBMType *parsed = parse_type_node_with_params(
arena, result_node, source, module_qn, tp_names);
new_rets[0] = cbm_go_lsp_keep_known_refinement(parsed, old_rets[0]);
for (int idx = 1; idx < ret_count; idx++) {
new_rets[idx] = old_rets[idx];
}
new_rets[ret_count] = NULL;
}
CBMType* new_sig = (CBMType*)cbm_arena_alloc(arena, sizeof(CBMType));
*new_sig = *(CBMType*)reg->funcs[fi].signature;
new_sig->data.func.return_types = new_rets;
reg->funcs[fi].signature = new_sig;
const CBMType *new_sig = cbm_type_func_replace_returns(
arena, reg->funcs[fi].signature, new_rets);
if (new_sig && new_sig->kind == CBM_TYPE_FUNC) {
reg->funcs[fi].signature = new_sig;
}
}
}
}
@@ -2480,26 +2844,40 @@ static void extract_type_params_from_ast(CBMArena* arena, CBMTypeRegistry* reg,
const CBMType** new_params = (const CBMType**)cbm_arena_alloc(arena,
(pc + 1) * sizeof(const CBMType*));
int idx = 0;
bool overflow = false;
uint32_t pnc = ts_node_child_count(params_node);
for (uint32_t pi = 0; pi < pnc && idx < pc; pi++) {
for (uint32_t pi = 0; pi < pnc && !overflow; pi++) {
TSNode pchild = ts_node_child(params_node, pi);
if (ts_node_is_null(pchild) || !ts_node_is_named(pchild)) continue;
if (strcmp(ts_node_type(pchild), "parameter_declaration") != 0) continue;
const char *param_kind = ts_node_type(pchild);
if (strcmp(param_kind, "parameter_declaration") != 0 &&
strcmp(param_kind, "variadic_parameter_declaration") != 0) {
continue;
}
TSNode ptype = ts_node_child_by_field_name(pchild, "type", 4);
if (ts_node_is_null(ptype)) continue;
new_params[idx++] = parse_type_node_with_params(arena,
ptype, source, module_qn, tp_names);
const CBMType *parsed =
ts_node_is_null(ptype)
? cbm_type_unknown()
: parse_type_node_with_params(arena, ptype, source, module_qn,
tp_names);
int multiplicity = cbm_go_lsp_signature_param_multiplicity(pchild);
for (int occurrence = 0; occurrence < multiplicity; occurrence++) {
if (idx >= pc) {
overflow = true;
break;
}
new_params[idx] =
cbm_go_lsp_keep_known_refinement(parsed, old_params[idx]);
idx++;
}
}
new_params[idx] = NULL;
if (idx > 0) {
CBMType* sig = (CBMType*)reg->funcs[fi].signature;
if (sig != reg->funcs[fi].signature) {
// Already rebuilt — update in place
((CBMType*)reg->funcs[fi].signature)->data.func.param_types = new_params;
} else {
CBMType* new_sig = (CBMType*)cbm_arena_alloc(arena, sizeof(CBMType));
*new_sig = *sig;
new_sig->data.func.param_types = new_params;
if (!overflow && idx == pc) {
const CBMType *old_sig = reg->funcs[fi].signature;
const CBMType *new_sig =
cbm_type_func(arena, old_sig->data.func.param_names, new_params,
old_sig->data.func.return_types);
if (new_sig && new_sig->kind == CBM_TYPE_FUNC) {
reg->funcs[fi].signature = new_sig;
}
}
@@ -2587,7 +2965,11 @@ void cbm_run_go_lsp_cross(
// Build FUNC type from return_types text
const CBMType** ret_types = split_pipe_types(arena, d->return_types, def_mod);
rf.signature = cbm_type_func(arena, NULL, NULL, ret_types);
CBMGoLSPSignatureParserContext param_parser_ctx = {def_mod};
const CBMType **param_types = cbm_type_materialize_signature_params(
arena, d->signature_param_types, d->signature_param_count,
cbm_go_lsp_parse_signature_param_text, &param_parser_ctx);
rf.signature = cbm_type_func(arena, NULL, param_types, ret_types);
// Method receiver
if (strcmp(d->label, "Method") == 0 && d->receiver_type && d->receiver_type[0]) {
@@ -2833,7 +3215,11 @@ CBMTypeRegistry* cbm_go_build_cross_registry(
rf.qualified_name = d->qualified_name; /* borrowed */
rf.short_name = d->short_name;
const CBMType** ret_types = split_pipe_types(arena, d->return_types, def_mod);
rf.signature = cbm_type_func(arena, NULL, NULL, ret_types);
CBMGoLSPSignatureParserContext param_parser_ctx = {def_mod};
const CBMType **param_types = cbm_type_materialize_signature_params(
arena, d->signature_param_types, d->signature_param_count,
cbm_go_lsp_parse_signature_param_text, &param_parser_ctx);
rf.signature = cbm_type_func(arena, NULL, param_types, ret_types);
if (strcmp(d->label, "Method") == 0 && d->receiver_type && d->receiver_type[0]) {
rf.receiver_type = d->receiver_type;
if (!cbm_registry_lookup_type(reg, rf.receiver_type)) {
@@ -2972,17 +3358,28 @@ int cbm_go_fast_resolve_qualified_calls(
}
}
/* Case 3: an unqualified function value from another source file in
* the same Go package. Go module QNs are directory-based, so this is
* an exact package-local lookup rather than a project-wide short-name
* guess. */
if (!f && !strchr(uc->callee_qn, '.') && result->module_qn) {
f = cbm_registry_lookup_symbol(reg, result->module_qn, uc->callee_qn);
}
if (!f) continue;
/* Emit a resolved entry. cbm_pipeline_find_lsp_resolution
* picks the highest-confidence match, so the unresolved entry
* stays (harmless duplicate) but our resolved entry wins. */
CBMResolvedCall rc;
CBMResolvedCall rc = {0};
rc.caller_qn = uc->caller_qn;
rc.callee_qn = f->qualified_name; /* borrowed from pipeline arena */
rc.strategy = "lsp_strategy_cross_file";
rc.confidence = 0.92f;
rc.reason = NULL;
rc.kind = uc->kind;
rc.site_start_byte = uc->site_start_byte;
rc.site_end_byte = uc->site_end_byte;
cbm_resolvedcall_push(&result->resolved_calls, &result->arena, rc);
newly_resolved++;
}
@@ -3034,11 +3431,15 @@ void cbm_batch_go_lsp_cross(
for (int j = 0; j < file_out.count; j++) {
CBMResolvedCall* src = &file_out.items[j];
CBMResolvedCall* dst = &out[f].items[j];
memset(dst, 0, sizeof(*dst));
dst->caller_qn = src->caller_qn ? cbm_arena_strdup(arena, src->caller_qn) : NULL;
dst->callee_qn = src->callee_qn ? cbm_arena_strdup(arena, src->callee_qn) : NULL;
dst->strategy = src->strategy ? cbm_arena_strdup(arena, src->strategy) : NULL;
dst->confidence = src->confidence;
dst->reason = src->reason ? cbm_arena_strdup(arena, src->reason) : NULL;
dst->kind = src->kind;
dst->site_start_byte = src->site_start_byte;
dst->site_end_byte = src->site_end_byte;
}
}
+12
View File
@@ -84,9 +84,21 @@ typedef struct {
const char* embedded_types; // "|"-separated embedded type QNs (for struct embedding)
const char* field_defs; // "|"-separated "name:type" pairs (for struct fields, e.g. "Binder:Binder|Name:string")
const char* method_names_str; // "|"-separated method names for interfaces (e.g. "Get|Put|Delete")
const char **signature_param_types; // borrowed ordered parameter type texts; "?" means unknown
int signature_param_count; // number of positional entries above
bool is_interface;
CBMLanguage lang; // language of the file that defined this — used by Tier 2 per-language registry build to filter all_defs
const char* namespace_name; // declared namespace/package for source-root-independent JVM filtering
/* Rust-only raw trait spelling. The Rust registry canonicalizes this
* only when it has one exact/unique trait target. */
const char *trait_qn;
bool is_rust_impl_relation; // independent type-level impl record (empty impls survive)
bool is_abstract; // Rust required trait method; false for defaults
/* Python-only raw decorator syntax, borrowed from CBMDefinition. The
* resolver must retain it across fused/cross-file registry construction:
* a decorator rebinds the function name, so the undecorated definition is
* not automatically an exact callable-value target. */
const char **decorators;
} CBMLSPDef;
// Parse source, build registry from defs + stdlib, run LSP.
+217 -72
View File
@@ -1632,6 +1632,24 @@ static const char *class_super_qn(JavaLSPContext *ctx, TSNode class_node) {
return NULL;
}
/* Java enum constants are direct enum_body children, while declarations after
* the semicolon live in enum_body_declarations. LSP class processing only
* wants declarations, so expose the nested container for enums and the normal
* body for every other type declaration. */
static TSNode java_type_declaration_body(TSNode node) {
TSNode body = ts_node_child_by_field_name(node, "body", 4);
if (ts_node_is_null(body) || strcmp(ts_node_type(body), "enum_body") != 0)
return body;
uint32_t count = ts_node_named_child_count(body);
for (uint32_t i = 0; i < count; i++) {
TSNode child = ts_node_named_child(body, i);
if (strcmp(ts_node_type(child), "enum_body_declarations") == 0)
return child;
}
return body;
}
static void java_process_class_decl(JavaLSPContext *ctx, TSNode node) {
TSNode name_node = ts_node_child_by_field_name(node, "name", 4);
if (ts_node_is_null(name_node))
@@ -1664,7 +1682,7 @@ static void java_process_class_decl(JavaLSPContext *ctx, TSNode node) {
ctx->enclosing_super_qn = super_qn;
ctx->enclosing_class_short = cname;
TSNode body = ts_node_child_by_field_name(node, "body", 4);
TSNode body = java_type_declaration_body(node);
if (!ts_node_is_null(body)) {
CBMScope *saved_scope = ctx->current_scope;
ctx->current_scope = cbm_scope_push(ctx->arena, saved_scope);
@@ -1779,40 +1797,87 @@ void java_lsp_process_file(JavaLSPContext *ctx, TSNode root) {
/* ── Call-edge resolution ─────────────────────────────────────────── */
static void java_emit_resolved_orig(JavaLSPContext *ctx, const char *callee_qn, const char *orig,
const char *strategy, float confidence) {
static void java_emit_resolved_orig_kind(JavaLSPContext *ctx, const char *callee_qn,
const char *orig, const char *strategy, float confidence,
CBMResolvedKind kind) {
if (!ctx->resolved_calls || !ctx->enclosing_method_qn || !callee_qn)
return;
CBMResolvedCall rc;
CBMResolvedCall rc = {0};
rc.caller_qn = ctx->enclosing_method_qn;
rc.callee_qn = callee_qn;
rc.strategy = strategy;
rc.confidence = confidence;
// For a data-flow resolution (constructor reference `Lhs::new` resolved to
// the Lhs class), `reason` carries the ORIGINAL textual callee (`new`) so the
// pipeline join can match the textual call site even though the resolved
// callee_qn's short name differs. NULL/unread for normal resolved calls.
rc.kind = kind;
// For data-flow resolutions, `reason` may carry the original textual
// callee when it differs from the resolved target. NULL for normal calls.
rc.reason = orig;
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc);
}
static void java_emit_resolved_orig(JavaLSPContext *ctx, const char *callee_qn, const char *orig,
const char *strategy, float confidence) {
java_emit_resolved_orig_kind(ctx, callee_qn, orig, strategy, confidence,
CBM_RESOLVED_INVOCATION);
}
static void java_emit_resolved(JavaLSPContext *ctx, const char *callee_qn, const char *strategy,
float confidence) {
java_emit_resolved_orig(ctx, callee_qn, NULL, strategy, confidence);
}
static void java_emit_unresolved(JavaLSPContext *ctx, const char *expr_text, const char *reason) {
static void java_emit_resolved_reference(JavaLSPContext *ctx, const char *callee_qn,
const char *strategy, float confidence) {
java_emit_resolved_orig_kind(ctx, callee_qn, NULL, strategy, confidence,
CBM_RESOLVED_CALL_REFERENCE);
}
static void java_stamp_resolved_site(JavaLSPContext *ctx, int first, TSNode site) {
if (!ctx->resolved_calls || ts_node_is_null(site) || first < 0) {
return;
}
for (int i = first; i < ctx->resolved_calls->count; i++) {
ctx->resolved_calls->items[i].site_start_byte = ts_node_start_byte(site);
ctx->resolved_calls->items[i].site_end_byte = ts_node_end_byte(site);
}
}
static void java_emit_resolved_reference_at(JavaLSPContext *ctx, const char *callee_qn,
const char *strategy, float confidence, TSNode site) {
int first = ctx->resolved_calls ? ctx->resolved_calls->count : -1;
java_emit_resolved_reference(ctx, callee_qn, strategy, confidence);
java_stamp_resolved_site(ctx, first, site);
}
static void java_emit_unresolved_kind(JavaLSPContext *ctx, const char *expr_text,
const char *reason, CBMResolvedKind kind) {
if (!ctx->resolved_calls || !ctx->enclosing_method_qn)
return;
CBMResolvedCall rc;
CBMResolvedCall rc = {0};
rc.caller_qn = ctx->enclosing_method_qn;
rc.callee_qn = expr_text ? expr_text : "?";
rc.strategy = "lsp_unresolved";
rc.confidence = 0.0f;
rc.reason = reason;
rc.kind = kind;
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc);
}
static void java_emit_unresolved(JavaLSPContext *ctx, const char *expr_text, const char *reason) {
java_emit_unresolved_kind(ctx, expr_text, reason, CBM_RESOLVED_INVOCATION);
}
static void java_emit_unresolved_reference(JavaLSPContext *ctx, const char *expr_text,
const char *reason) {
java_emit_unresolved_kind(ctx, expr_text, reason, CBM_RESOLVED_CALL_REFERENCE);
}
static void java_emit_unresolved_reference_at(JavaLSPContext *ctx, const char *expr_text,
const char *reason, TSNode site) {
int first = ctx->resolved_calls ? ctx->resolved_calls->count : -1;
java_emit_unresolved_reference(ctx, expr_text, reason);
java_stamp_resolved_site(ctx, first, site);
}
/* Find a sole concrete in-project implementer of interface `iface_qn` that
* declares method `mname`. Returns the implementer's QN when exactly ONE
* exists (else NULL), and sets *out_count to the number found (capped at 2,
@@ -2590,8 +2655,9 @@ static void resolve_method_reference(JavaLSPContext *ctx, TSNode mref,
/* Try the last named child first; if it's the same as the LHS (only one
* named child total), we have a constructor ref where `new` is unnamed. */
char *mname = NULL;
TSNode name_node = {0};
if (nc_named >= 2) {
TSNode name_node = ts_node_named_child(mref, nc_named - 1);
name_node = ts_node_named_child(mref, nc_named - 1);
mname = java_node_text(ctx, name_node);
}
if (!mname || !mname[0]) {
@@ -2603,6 +2669,10 @@ static void resolve_method_reference(JavaLSPContext *ctx, TSNode mref,
const char *ck = ts_node_type(c);
if (strcmp(ck, "new") == 0) {
mname = "new";
/* Usage extraction represents `Type::new` by its type leaf,
* because `new` is an unnamed grammar token. Keep the LSP
* occurrence on that same leaf so the two passes join. */
name_node = lhs;
break;
}
if (strcmp(ck, "identifier") == 0) {
@@ -2610,8 +2680,10 @@ static void resolve_method_reference(JavaLSPContext *ctx, TSNode mref,
if (ts_node_eq(c, lhs))
continue;
mname = java_node_text(ctx, c);
if (mname && mname[0])
if (mname && mname[0]) {
name_node = c;
break;
}
}
}
}
@@ -2667,7 +2739,7 @@ static void resolve_method_reference(JavaLSPContext *ctx, TSNode mref,
type_qn = lhs_t->data.template_type.template_name;
}
if (!type_qn) {
java_emit_unresolved(ctx, mname, "method_reference_unknown_lhs");
java_emit_unresolved_reference_at(ctx, mname, "method_reference_unknown_lhs", name_node);
return;
}
@@ -2677,15 +2749,17 @@ static void resolve_method_reference(JavaLSPContext *ctx, TSNode mref,
short_name = short_name ? short_name + 1 : type_qn;
const CBMRegisteredFunc *cf =
cbm_registry_lookup_method(ctx->registry, type_qn, short_name);
// A `ClassName::new` reference constructs ClassName: resolve to the
// ClassName CLASS node (which the textual extractor stored), not the
// synthetic constructor QN that has no graph node. orig=mname ("new")
// lets the join match the textual `new` call site (the constructor
// reference is extracted as a call to `new`). cf distinguishes an
// indexed constructor (higher confidence) from a synthesized one.
java_emit_resolved_orig(ctx, type_qn, mname,
cf ? "lsp_method_ref_ctor" : "lsp_method_ref_ctor_synth",
cf ? 0.90f : 0.80f);
/* A constructor reference is precise only when a concrete constructor
* callable exists in the registry. Never relabel the class node itself
* as a callable target; if the constructor is absent/unmaterialized,
* the usage pass will retain the type occurrence as ordinary USAGE. */
if (cf && cf->qualified_name) {
java_emit_resolved_reference_at(ctx, cf->qualified_name, "lsp_method_ref_ctor", 0.90f,
name_node);
} else {
java_emit_unresolved_reference_at(
ctx, type_qn, "method_reference_constructor_not_materialized", name_node);
}
return;
}
@@ -2704,10 +2778,11 @@ static void resolve_method_reference(JavaLSPContext *ctx, TSNode mref,
if (!m)
m = java_lookup_method(ctx, type_qn, mname, 0);
if (m) {
java_emit_resolved(ctx, m->qualified_name, "lsp_method_ref", 0.90f);
java_emit_resolved_reference_at(ctx, m->qualified_name, "lsp_method_ref", 0.90f, name_node);
} else {
java_emit_unresolved(ctx, cbm_arena_sprintf(ctx->arena, "%s.%s", type_qn, mname),
"method_reference_no_match");
java_emit_unresolved_reference_at(ctx,
cbm_arena_sprintf(ctx->arena, "%s.%s", type_qn, mname),
"method_reference_no_match", name_node);
}
}
@@ -2825,7 +2900,13 @@ static void java_resolve_calls_in_node_inner(JavaLSPContext *ctx, TSNode node) {
}
if (strcmp(kind, "method_invocation") == 0) {
int first_resolution = ctx->resolved_calls ? ctx->resolved_calls->count : -1;
resolve_method_call(ctx, node);
/* Raw CALL extraction records the full method_invocation span. Stamp
* the semantic record with that identical occurrence so two calls to
* the same method name in one caller retain receiver identity at the
* pipeline join (for example alpha.handle(); beta.handle();). */
java_stamp_resolved_site(ctx, first_resolution, node);
/* Lambda / method-reference args: re-walk with the SAM-bound scope.
* This is the central piece that turns chained streams + forEach
@@ -2859,6 +2940,7 @@ static void java_resolve_calls_in_node_inner(JavaLSPContext *ctx, TSNode node) {
}
} else if (strcmp(kind, "object_creation_expression") == 0) {
/* Resolve constructor as a method. */
int first_resolution = ctx->resolved_calls ? ctx->resolved_calls->count : -1;
TSNode type_node = ts_node_child_by_field_name(node, "type", 4);
if (!ts_node_is_null(type_node)) {
const CBMType *t = java_parse_type_node(ctx, type_node);
@@ -2889,6 +2971,7 @@ static void java_resolve_calls_in_node_inner(JavaLSPContext *ctx, TSNode node) {
}
}
}
java_stamp_resolved_site(ctx, first_resolution, node);
}
/* catch_clause: push a fresh scope so the bound exception variable is
@@ -3095,6 +3178,19 @@ static const CBMType *parse_param_text(CBMArena *a, const char *text, const char
return parse_param_text_full(a, text, NULL, module_qn, NULL);
}
typedef struct {
const char *parent_class;
const char *module_qn;
const CBMTypeRegistry *registry;
} JavaSignatureParseContext;
static const CBMType *java_signature_parse_type(CBMArena *arena, const char *text,
void *parser_ctx) {
const JavaSignatureParseContext *ctx = (const JavaSignatureParseContext *)parser_ctx;
return parse_param_text_full(arena, text, ctx ? ctx->parent_class : NULL,
ctx ? ctx->module_qn : NULL, ctx ? ctx->registry : NULL);
}
static void register_local_func_or_type_from_file(JavaLSPContext *ctx, CBMTypeRegistry *reg,
CBMFileResult *result) {
CBMArena *a = ctx->arena;
@@ -3259,17 +3355,35 @@ static void register_local_func_or_type_from_file(JavaLSPContext *ctx, CBMTypeRe
* not "test.Main.Box". */
const char *parent = d->parent_class;
const CBMType **ptypes = NULL;
if (d->param_types) {
int pc = 0;
while (d->param_types[pc])
pc++;
if (pc > 0) {
ptypes = (const CBMType **)cbm_arena_alloc(a, (size_t)(pc + 1) * sizeof(*ptypes));
for (int j = 0; j < pc; j++) {
ptypes[j] = parse_param_text_full(a, d->param_types[j], parent, module_qn, reg);
const char **param_names = NULL;
if (d->signature_param_types || d->signature_param_count > 0) {
JavaSignatureParseContext parse_ctx = {
.parent_class = parent,
.module_qn = module_qn,
.registry = reg,
};
ptypes = cbm_type_materialize_signature_params(a, d->signature_param_types,
d->signature_param_count,
java_signature_parse_type, &parse_ctx);
} else if (!d->signature_param_types && d->signature_param_count == 0) {
/* Compatibility for callers that still construct CBMDefinition by
* hand. The legacy projection is deduplicated/non-positional, so
* it is consulted only when the counted carrier is wholly absent. */
if (d->param_types) {
int pc = 0;
while (d->param_types[pc])
pc++;
if (pc > 0) {
ptypes =
(const CBMType **)cbm_arena_alloc(a, (size_t)(pc + 1) * sizeof(*ptypes));
for (int j = 0; j < pc; j++) {
ptypes[j] =
parse_param_text_full(a, d->param_types[j], parent, module_qn, reg);
}
ptypes[pc] = NULL;
}
ptypes[pc] = NULL;
}
param_names = d->param_names;
}
/* Build return types — single return only in Java. */
const CBMType **rtypes = NULL;
@@ -3282,7 +3396,7 @@ static void register_local_func_or_type_from_file(JavaLSPContext *ctx, CBMTypeRe
rtypes[0] = parse_param_text_full(a, d->return_types[0], parent, module_qn, reg);
rtypes[1] = NULL;
}
rf.signature = cbm_type_func(a, d->param_names, ptypes, rtypes);
rf.signature = cbm_type_func(a, param_names, ptypes, rtypes);
if (d->parent_class) {
rf.receiver_type = d->parent_class;
}
@@ -3316,29 +3430,17 @@ static void patch_one_method(JavaLSPContext *ctx, CBMTypeRegistry *reg, TSNode m
return;
char *method_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", class_qn, mname);
/* Find the registered func by exact QN. */
CBMRegisteredFunc *slot = NULL;
for (int fi = 0; fi < reg->func_count; fi++) {
if (reg->funcs[fi].qualified_name &&
strcmp(reg->funcs[fi].qualified_name, method_qn) == 0) {
slot = &reg->funcs[fi];
break;
}
}
if (!slot)
return;
/* Re-extract param types from the AST. */
TSNode params = ts_node_child_by_field_name(method_node, "parameters", 10);
if (ts_node_is_null(params))
return;
uint32_t pn = ts_node_named_child_count(params);
if (pn == 0)
return;
const CBMType **ptypes =
(const CBMType **)cbm_arena_alloc(ctx->arena, (size_t)(pn + 1) * sizeof(*ptypes));
if (!ptypes)
return;
const CBMType **ptypes = NULL;
if (pn > 0) {
ptypes = (const CBMType **)cbm_arena_alloc(ctx->arena, (size_t)(pn + 1) * sizeof(*ptypes));
if (!ptypes)
return;
}
int idx = 0;
for (uint32_t i = 0; i < pn; i++) {
TSNode p = ts_node_named_child(params, i);
@@ -3355,7 +3457,37 @@ static void patch_one_method(JavaLSPContext *ctx, CBMTypeRegistry *reg, TSNode m
}
ptypes[idx++] = pt;
}
ptypes[idx] = NULL;
if (ptypes)
ptypes[idx] = NULL;
/* Java overloads share a graph QN. Select the registry slot whose counted
* signature has the same arity as this AST declaration; falling back to the
* first QN match keeps the legacy no-signature path best-effort. */
CBMRegisteredFunc *slot = NULL;
CBMRegisteredFunc *first_qn_match = NULL;
for (int fi = 0; fi < reg->func_count; fi++) {
CBMRegisteredFunc *candidate = &reg->funcs[fi];
if (!candidate->qualified_name || strcmp(candidate->qualified_name, method_qn) != 0)
continue;
if (!first_qn_match)
first_qn_match = candidate;
if (!candidate->signature || candidate->signature->kind != CBM_TYPE_FUNC)
continue;
int candidate_arity = 0;
const CBMType *const *candidate_params = candidate->signature->data.func.param_types;
if (candidate_params) {
while (candidate_params[candidate_arity])
candidate_arity++;
}
if (candidate_arity == idx) {
slot = candidate;
break;
}
}
if (!slot)
slot = first_qn_match;
if (!slot)
return;
/* Re-extract return type from AST (only for methods, not constructors). */
const CBMType **rtypes = NULL;
@@ -3401,7 +3533,7 @@ static void patch_method_signatures_from_ast(JavaLSPContext *ctx, CBMTypeRegistr
class_qn = cbm_arena_strdup(ctx->arena, cname);
}
TSNode body = ts_node_child_by_field_name(class_node, "body", 4);
TSNode body = java_type_declaration_body(class_node);
if (ts_node_is_null(body))
return;
@@ -3495,7 +3627,7 @@ static void populate_class_fields_from_ast(JavaLSPContext *ctx, CBMTypeRegistry
class_qn = cbm_arena_strdup(ctx->arena, cname);
}
TSNode body = ts_node_child_by_field_name(class_node, "body", 4);
TSNode body = java_type_declaration_body(class_node);
if (ts_node_is_null(body))
return;
/* Snapshot enclosing-class stack so java_parse_type_node sees inner-class
@@ -3595,20 +3727,10 @@ void cbm_run_java_lsp(CBMArena *arena, CBMFileResult *result, const char *source
/* ── Cross-file LSP ───────────────────────────────────────────────── */
void cbm_run_java_lsp_cross(CBMArena *arena, const char *source, int source_len,
const char *module_qn, CBMLSPDef *defs, int def_count,
const char **import_names, const char **import_qns, int import_count,
TSTree *cached_tree, CBMResolvedCallArray *out) {
if (!arena || !source)
return;
CBMTypeRegistry reg;
cbm_registry_init(&reg, arena);
cbm_java_stdlib_register(&reg, arena);
/* Register cross-file defs. */
void cbm_java_register_lsp_defs(CBMArena *arena, CBMTypeRegistry *reg, const CBMLSPDef *defs,
int def_count) {
for (int i = 0; i < def_count; i++) {
CBMLSPDef *d = &defs[i];
const CBMLSPDef *d = &defs[i];
if (!d->qualified_name || !d->short_name || !d->label)
continue;
if (strcmp(d->label, "Class") == 0 || strcmp(d->label, "Interface") == 0 ||
@@ -3642,7 +3764,7 @@ void cbm_run_java_lsp_cross(CBMArena *arena, const char *source, int source_len,
emb[idx] = NULL;
rt.embedded_types = emb;
}
cbm_registry_add_type(&reg, rt);
cbm_registry_add_type(reg, rt);
} else if (strcmp(d->label, "Method") == 0 || strcmp(d->label, "Function") == 0 ||
strcmp(d->label, "Constructor") == 0) {
CBMRegisteredFunc rf;
@@ -3651,6 +3773,14 @@ void cbm_run_java_lsp_cross(CBMArena *arena, const char *source, int source_len,
rf.short_name = d->short_name;
rf.min_params = -1;
rf.receiver_type = d->receiver_type;
JavaSignatureParseContext parse_ctx = {
.parent_class = d->receiver_type,
.module_qn = d->def_module_qn,
.registry = reg,
};
const CBMType **ptypes = cbm_type_materialize_signature_params(
arena, d->signature_param_types, d->signature_param_count,
java_signature_parse_type, &parse_ctx);
/* Build single-return signature from return_types text. */
const CBMType *ret = NULL;
if (d->return_types && d->return_types[0]) {
@@ -3670,10 +3800,25 @@ void cbm_run_java_lsp_cross(CBMArena *arena, const char *source, int source_len,
rtypes[0] = ret;
rtypes[1] = NULL;
}
rf.signature = cbm_type_func(arena, NULL, NULL, rtypes);
cbm_registry_add_func(&reg, rf);
rf.signature = cbm_type_func(arena, NULL, ptypes, rtypes);
cbm_registry_add_func(reg, rf);
}
}
}
void cbm_run_java_lsp_cross(CBMArena *arena, const char *source, int source_len,
const char *module_qn, CBMLSPDef *defs, int def_count,
const char **import_names, const char **import_qns, int import_count,
TSTree *cached_tree, CBMResolvedCallArray *out) {
if (!arena || !source)
return;
CBMTypeRegistry reg;
cbm_registry_init(&reg, arena);
cbm_java_stdlib_register(&reg, arena);
/* Register cross-file defs. */
cbm_java_register_lsp_defs(arena, &reg, defs, def_count);
/* Build the hash indexes: without this every lookup_type/func/method in
* the walk below is a LINEAR scan over the whole cross registry —
+3
View File
@@ -139,6 +139,9 @@ void cbm_run_java_lsp(CBMArena *arena, CBMFileResult *result, const char *source
/* Cross-file LSP: build registry from defs + stdlib, re-parse if needed,
* walk and resolve. defs include both local + cross-file definitions. */
void cbm_java_register_lsp_defs(CBMArena *arena, CBMTypeRegistry *reg, const CBMLSPDef *defs,
int def_count);
void cbm_run_java_lsp_cross(CBMArena *arena, const char *source, int source_len,
const char *module_qn, CBMLSPDef *defs, int def_count,
const char **import_names, const char **import_qns, int import_count,
File diff suppressed because it is too large Load Diff
+19
View File
@@ -65,6 +65,14 @@ typedef enum {
CBM_KT_USE_WILDCARD, /* import a.b.* — local_name is a.b prefix */
} CBMKotlinUseKind;
typedef struct {
const CBMScope *scope; /* exact lexical declaration frame, NULL for members */
const char *owner_qn; /* class/object QN for a member, NULL for lexical values */
const char *name;
const char *getter_qn;
const char *setter_qn;
} CBMKotlinDelegateBinding;
/* KotlinLSPContext — per-file state for Kotlin call resolution. */
typedef struct KotlinLSPContext {
CBMArena *arena;
@@ -107,6 +115,14 @@ typedef struct KotlinLSPContext {
/* Output: resolved calls accumulate here. */
CBMResolvedCallArray *resolved_calls;
/* Access-level delegated-property semantics. The declaration records the
* proven getValue/setValue targets; actual reads/writes inject exact call
* carriers and resolutions at their source occurrences. */
CBMCallArray *synthetic_calls;
CBMKotlinDelegateBinding *delegate_bindings;
int delegate_count;
int delegate_cap;
/* Recursion guard for kotlin_eval_expr_type. */
int eval_depth;
@@ -167,6 +183,9 @@ void cbm_run_kotlin_lsp(CBMArena *arena, CBMFileResult *result, const char *sour
* Mirrors cbm_run_java_lsp_cross. `defs` carries the graph QNs of every
* project definition so a bare top-level call in file B resolves to the
* definition node living in file A. Output is appended to `out`. */
void cbm_kotlin_register_lsp_defs(CBMArena *arena, CBMTypeRegistry *reg, const CBMLSPDef *defs,
int def_count);
void cbm_run_kotlin_lsp_cross(CBMArena *arena, const char *source, int source_len,
const char *module_qn, CBMLSPDef *defs, int def_count,
const char **import_names, const char **import_qns, int import_count,
+160 -8
View File
@@ -85,7 +85,8 @@ static const CBMType *perl_eval_function_call_type(PerlLSPContext *ctx, TSNode n
static const CBMType *perl_eval_method_call_type(PerlLSPContext *ctx, TSNode node);
static const CBMType *perl_eval_new_type(PerlLSPContext *ctx, TSNode node);
static void perl_emit_resolved(PerlLSPContext *ctx, const char *callee_qn, const char *strategy,
float confidence);
float confidence, TSNode site);
static void perl_resolve_direct_coderef_arguments(PerlLSPContext *ctx, TSNode call);
/* ── helpers ────────────────────────────────────────────────────── */
@@ -625,23 +626,168 @@ static const CBMType *perl_eval_method_call_type(PerlLSPContext *ctx, TSNode nod
/* ── emit ───────────────────────────────────────────────────────── */
static void perl_emit_resolved(PerlLSPContext *ctx, const char *callee_qn, const char *strategy,
float confidence) {
float confidence, TSNode site) {
if (!ctx->resolved_calls || !callee_qn || !ctx->enclosing_func_qn)
return;
CBMResolvedCall rc;
CBMResolvedCall rc = {0};
rc.caller_qn = ctx->enclosing_func_qn;
rc.callee_qn = callee_qn;
rc.strategy = strategy;
rc.confidence = confidence;
rc.reason = NULL;
rc.kind = CBM_RESOLVED_INVOCATION;
if (!ts_node_is_null(site)) {
rc.site_start_byte = ts_node_start_byte(site);
rc.site_end_byte = ts_node_end_byte(site);
}
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc);
}
static void perl_emit_reference(PerlLSPContext *ctx, const char *callee_qn, TSNode site) {
if (!ctx || !ctx->resolved_calls || !callee_qn || !ctx->enclosing_func_qn ||
ts_node_is_null(site)) {
return;
}
CBMResolvedCall rc = {0};
rc.caller_qn = ctx->enclosing_func_qn;
rc.callee_qn = callee_qn;
rc.strategy = "perl_coderef";
rc.confidence = PERL_CONF_LITERAL;
rc.kind = CBM_RESOLVED_CALL_REFERENCE;
rc.site_start_byte = ts_node_start_byte(site);
rc.site_end_byte = ts_node_end_byte(site);
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc);
}
/* Accept only the exact coderef spelling represented by one direct
* refgen_expression. Conditional/list/map expressions are not examined, so
* their constituent \&names remain ordinary usages. */
static char *perl_exact_coderef_name(PerlLSPContext *ctx, TSNode refgen) {
if (ts_node_is_null(refgen) || strcmp(ts_node_type(refgen), "refgen_expression") != 0)
return NULL;
char *text = perl_node_text(ctx, refgen);
if (!text)
return NULL;
const char *p = text;
while (isspace((unsigned char)*p))
p++;
if (p[0] != '\\' || p[1] != '&')
return NULL;
p += 2;
const char *start = p;
if (!(isalpha((unsigned char)*p) || *p == '_'))
return NULL;
while (isalnum((unsigned char)*p) || *p == '_' || *p == ':')
p++;
const char *end = p;
while (isspace((unsigned char)*p))
p++;
if (*p != '\0' || end == start)
return NULL;
return cbm_arena_strndup(ctx->arena, start, (size_t)(end - start));
}
static const CBMRegisteredFunc *perl_resolve_coderef_target(PerlLSPContext *ctx, char *name) {
if (!ctx || !name || !name[0])
return NULL;
char *colons = NULL;
for (char *p = strstr(name, "::"); p; p = strstr(p + 2, "::"))
colons = p;
if (colons) {
char *pkg = cbm_arena_strndup(ctx->arena, name, (size_t)(colons - name));
const char *short_name = colons + 2;
const CBMRegisteredFunc *f = perl_lookup_method(ctx, pkg, short_name);
return f ? f : cbm_registry_lookup_symbol(ctx->registry, pkg, short_name);
}
const char *import_qn = perl_find_import(ctx, name);
if (import_qn) {
const CBMRegisteredFunc *f = cbm_registry_lookup_func(ctx->registry, import_qn);
if (f)
return f;
}
return cbm_registry_lookup_symbol(ctx->registry, ctx->module_qn, name);
}
static void perl_resolve_one_coderef(PerlLSPContext *ctx, TSNode refgen) {
char *name = perl_exact_coderef_name(ctx, refgen);
const CBMRegisteredFunc *target = name ? perl_resolve_coderef_target(ctx, name) : NULL;
if (target && target->qualified_name)
perl_emit_reference(ctx, target->qualified_name, refgen);
}
static void perl_resolve_direct_coderef_arguments(PerlLSPContext *ctx, TSNode call) {
TSNode args = ts_node_child_by_field_name(call, "arguments", 9);
if (ts_node_is_null(args))
return;
if (strcmp(ts_node_type(args), "refgen_expression") == 0) {
perl_resolve_one_coderef(ctx, args);
return;
}
/* Multiple arguments are exposed through a flat list_expression. Do not
* recurse into any other wrapper: in particular, a conditional argument
* may contain refgen descendants but does not select one exact target. */
if (strcmp(ts_node_type(args), "list_expression") != 0)
return;
uint32_t count = ts_node_named_child_count(args);
for (uint32_t i = 0; i < count; i++) {
TSNode arg = ts_node_named_child(args, i);
if (strcmp(ts_node_type(arg), "refgen_expression") == 0)
perl_resolve_one_coderef(ctx, arg);
}
}
static bool perl_is_direct_coderef_argument(TSNode refgen) {
TSNode container = ts_node_parent(refgen);
if (ts_node_is_null(container))
return false;
if (strcmp(ts_node_type(container), "list_expression") != 0)
container = refgen;
TSNode call = ts_node_parent(container);
if (ts_node_is_null(call))
return false;
const char *kind = ts_node_type(call);
if (strcmp(kind, "function_call_expression") != 0 &&
strcmp(kind, "ambiguous_function_call_expression") != 0) {
return false;
}
TSNode args = ts_node_child_by_field_name(call, "arguments", 9);
return !ts_node_is_null(args) && ts_node_eq(args, container);
}
static void perl_emit_non_direct_coderef_usage(PerlLSPContext *ctx, TSNode refgen) {
if (!ctx || !ctx->usages || !ctx->enclosing_func_qn ||
perl_is_direct_coderef_argument(refgen)) {
return;
}
char *name = perl_exact_coderef_name(ctx, refgen);
if (!name)
return;
uint32_t start = ts_node_start_byte(refgen);
uint32_t end = ts_node_end_byte(refgen);
for (int i = 0; i < ctx->usages->count; i++) {
const CBMUsage *existing = &ctx->usages->items[i];
if (existing->kind == CBM_USAGE_VALUE && existing->ref_name &&
existing->enclosing_func_qn && strcmp(existing->ref_name, name) == 0 &&
strcmp(existing->enclosing_func_qn, ctx->enclosing_func_qn) == 0 &&
existing->site_start_byte == start && existing->site_end_byte == end) {
return;
}
}
CBMUsage usage = {0};
usage.ref_name = name;
usage.enclosing_func_qn = ctx->enclosing_func_qn;
usage.kind = CBM_USAGE_VALUE;
usage.site_start_byte = start;
usage.site_end_byte = end;
cbm_usages_push(ctx->usages, ctx->arena, usage);
}
/* ── call/method dispatch (emit edges) ──────────────────────────── */
/* Resolve a function/static call and emit an edge if it lands on a registered
* sub. Bare func(), Exporter func(), and Package::func() static calls. */
static void perl_resolve_function_call(PerlLSPContext *ctx, TSNode call) {
perl_resolve_direct_coderef_arguments(ctx, call);
TSNode fn = ts_node_child_by_field_name(call, "function", 8);
if (ts_node_is_null(fn))
return;
@@ -669,7 +815,7 @@ static void perl_resolve_function_call(PerlLSPContext *ctx, TSNode call) {
if (!f)
f = cbm_registry_lookup_symbol(ctx->registry, pkg, shortn);
if (f) {
perl_emit_resolved(ctx, f->qualified_name, "perl_static_call", PERL_CONF_LITERAL);
perl_emit_resolved(ctx, f->qualified_name, "perl_static_call", PERL_CONF_LITERAL, call);
return;
}
} else {
@@ -678,13 +824,14 @@ static void perl_resolve_function_call(PerlLSPContext *ctx, TSNode call) {
f = cbm_registry_lookup_func(ctx->registry, imp);
if (f) {
perl_emit_resolved(ctx, f->qualified_name, "perl_imported_function",
PERL_CONF_LITERAL);
PERL_CONF_LITERAL, call);
return;
}
}
f = cbm_registry_lookup_symbol(ctx->registry, ctx->module_qn, name);
if (f) {
perl_emit_resolved(ctx, f->qualified_name, "perl_function_local", PERL_CONF_LITERAL);
perl_emit_resolved(ctx, f->qualified_name, "perl_function_local", PERL_CONF_LITERAL,
call);
return;
}
}
@@ -718,7 +865,8 @@ static void perl_resolve_method_call(PerlLSPContext *ctx, TSNode call) {
return;
const CBMRegisteredFunc *sf = perl_lookup_method(ctx, parent_qn, super_method);
if (sf)
perl_emit_resolved(ctx, sf->qualified_name, "perl_method_super", PERL_CONF_LITERAL);
perl_emit_resolved(ctx, sf->qualified_name, "perl_method_super", PERL_CONF_LITERAL,
call);
return;
}
@@ -747,7 +895,7 @@ static void perl_resolve_method_call(PerlLSPContext *ctx, TSNode call) {
const char *strat = (f->receiver_type && strcmp(f->receiver_type, class_qn) == 0)
? strategy
: "perl_method_inherited";
perl_emit_resolved(ctx, f->qualified_name, strat, PERL_CONF_LITERAL);
perl_emit_resolved(ctx, f->qualified_name, strat, PERL_CONF_LITERAL, call);
return;
}
/* Receiver typed but method not found in the indexed inheritance chain.
@@ -823,6 +971,9 @@ static void perl_resolve_calls_in_node_inner(PerlLSPContext *ctx, TSNode node) {
perl_process_assignment(ctx, assign);
}
if (strcmp(k, "refgen_expression") == 0)
perl_emit_non_direct_coderef_usage(ctx, node);
/* Call-resolution dispatch. */
if (strcmp(k, "function_call_expression") == 0 ||
strcmp(k, "ambiguous_function_call_expression") == 0) {
@@ -1529,6 +1680,7 @@ void cbm_run_perl_lsp(CBMArena *arena, CBMFileResult *result, const char *source
* finished (const) registry. */
PerlLSPContext ctx;
perl_lsp_init(&ctx, arena, source, source_len, &reg, module_qn, &result->resolved_calls);
ctx.usages = &result->usages;
ctx.current_package_qn = "";
ctx.enclosing_package_qn = "";
+1
View File
@@ -61,6 +61,7 @@ typedef struct {
/* Output: resolved calls accumulate here. */
CBMResolvedCallArray *resolved_calls;
CBMUsageArray *usages;
/* Recursion guard for perl_eval_expr_type. */
int eval_depth;
+326 -74
View File
@@ -825,6 +825,33 @@ static char *fetch_leading_phpdoc(PHPLSPContext *ctx, TSNode node) {
return NULL;
}
/* PHP 8.1 first-class callable syntax reuses call-expression nodes but its
* argument list contains only a `variadic_placeholder`: `target(...)`,
* `$obj->method(...)`, or `Type::method(...)`. It creates a Closure; it does
* not invoke the target. */
static bool php_is_first_class_callable(TSNode node) {
TSNode args = ts_node_child_by_field_name(node, "arguments", 9);
if (ts_node_is_null(args))
return false;
bool saw_placeholder = false;
int semantic_children = 0;
uint32_t child_count = ts_node_child_count(args);
for (uint32_t i = 0; i < child_count; i++) {
TSNode child = ts_node_child(args, i);
if (ts_node_is_null(child) || !ts_node_is_named(child))
continue;
const char *kind = ts_node_type(child);
if (strcmp(kind, "comment") == 0)
continue;
semantic_children++;
if (strcmp(kind, "variadic_placeholder") != 0)
return false;
saw_placeholder = true;
}
return saw_placeholder && semantic_children == 1;
}
/* ── expression evaluation ──────────────────────────────────────── */
/* Returns the type of a tree-sitter PHP expression node.
@@ -868,6 +895,12 @@ const CBMType *php_eval_expr_type(PHPLSPContext *ctx, TSNode node) {
}
} else if (strcmp(kind, "object_creation_expression") == 0) {
result = eval_object_creation_type(ctx, node);
} else if ((strcmp(kind, "function_call_expression") == 0 ||
strcmp(kind, "member_call_expression") == 0 ||
strcmp(kind, "nullsafe_member_call_expression") == 0 ||
strcmp(kind, "scoped_call_expression") == 0) &&
php_is_first_class_callable(node)) {
result = cbm_type_named(ctx->arena, "Closure");
} else if (strcmp(kind, "member_call_expression") == 0 ||
strcmp(kind, "nullsafe_member_call_expression") == 0 ||
strcmp(kind, "scoped_call_expression") == 0) {
@@ -1250,28 +1283,137 @@ static const CBMType *eval_member_call_type(PHPLSPContext *ctx, TSNode call_node
/* ── emit ───────────────────────────────────────────────────────── */
static void emit_resolved_reason(PHPLSPContext *ctx, const char *callee_qn, const char *strategy,
float confidence, const char *reason) {
static void emit_resolved_reason_kind(PHPLSPContext *ctx, const char *callee_qn,
const char *strategy, float confidence, const char *reason,
CBMResolvedKind kind) {
if (!ctx->resolved_calls || !callee_qn || !ctx->enclosing_func_qn)
return;
CBMResolvedCall rc;
CBMResolvedCall rc = {0};
rc.caller_qn = ctx->enclosing_func_qn;
rc.callee_qn = callee_qn;
rc.strategy = strategy;
rc.confidence = confidence;
rc.reason = reason;
rc.kind = kind;
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc);
}
static void emit_resolved(PHPLSPContext *ctx, const char *callee_qn, const char *strategy,
float confidence) {
emit_resolved_reason(ctx, callee_qn, strategy, confidence, NULL);
static void emit_resolved_kind(PHPLSPContext *ctx, const char *callee_qn, const char *strategy,
float confidence, CBMResolvedKind kind) {
emit_resolved_reason_kind(ctx, callee_qn, strategy, confidence, NULL, kind);
}
static bool php_dynamic_reference_expr(PHPLSPContext *ctx, TSNode expr) {
if (ts_node_is_null(expr)) {
return false;
}
const char *kind = ts_node_type(expr);
if (strstr(kind, "subscript") || strstr(kind, "element_access") ||
strstr(kind, "index_expression") || strstr(kind, "computed")) {
return true;
}
static const char *const terminal_fields[] = {"property", "field", "attribute",
"name", "method", NULL};
for (int i = 0; terminal_fields[i]; i++) {
TSNode terminal = ts_node_child_by_field_name(expr, terminal_fields[i],
(uint32_t)strlen(terminal_fields[i]));
if (ts_node_is_null(terminal)) {
continue;
}
uint32_t start = ts_node_start_byte(terminal);
uint32_t expr_start = ts_node_start_byte(expr);
while (start > expr_start && isspace((unsigned char)ctx->source[start - 1])) {
start--;
}
return start > expr_start && ctx->source[start - 1] == '[';
}
return false;
}
static bool php_callable_reference_has_dynamic_member_name(PHPLSPContext *ctx, TSNode call) {
const char *call_kind = ts_node_type(call);
if (strcmp(call_kind, "member_call_expression") != 0 &&
strcmp(call_kind, "nullsafe_member_call_expression") != 0 &&
strcmp(call_kind, "scoped_call_expression") != 0) {
return false;
}
TSNode name = ts_node_child_by_field_name(call, "name", 4);
if (ts_node_is_null(name)) {
return false;
}
const char *name_kind = ts_node_type(name);
return strcmp(name_kind, "variable_name") == 0 ||
strcmp(name_kind, "dynamic_variable_name") == 0 || php_dynamic_reference_expr(ctx, name);
}
/* Mirror extract_calls.c's terminal_callee_leaf policy so the PHP LSP and
* USAGE passes attach the same exact token occurrence to a first-class
* callable, including qualified free-function references. */
static TSNode php_reference_callee_leaf(PHPLSPContext *ctx, TSNode expr) {
if (ts_node_is_null(expr) || php_dynamic_reference_expr(ctx, expr)) {
return (TSNode){0};
}
static const char *const terminal_fields[] = {"property", "field", "attribute",
"method", "name", NULL};
for (int i = 0; terminal_fields[i]; i++) {
TSNode terminal = ts_node_child_by_field_name(expr, terminal_fields[i],
(uint32_t)strlen(terminal_fields[i]));
if (!ts_node_is_null(terminal)) {
return php_reference_callee_leaf(ctx, terminal);
}
}
const char *kind = ts_node_type(expr);
if (strcmp(kind, "identifier") == 0 || strcmp(kind, "simple_identifier") == 0 ||
strcmp(kind, "type_identifier") == 0 || strcmp(kind, "field_identifier") == 0 ||
strcmp(kind, "property_identifier") == 0 || strcmp(kind, "name") == 0 ||
strcmp(kind, "variable_name") == 0 || strcmp(kind, "attribute") == 0 ||
strcmp(kind, "symbol") == 0 || strcmp(kind, "sym_lit") == 0 ||
strcmp(kind, "user_symbol") == 0) {
return expr;
}
uint32_t count = ts_node_named_child_count(expr);
if (count == 0) {
return (TSNode){0};
}
bool terminal_is_last = strstr(kind, "qualified") || strstr(kind, "scoped") ||
strstr(kind, "member") || strstr(kind, "selector") ||
strstr(kind, "navigation") || strstr(kind, "field_expression");
return php_reference_callee_leaf(ctx,
ts_node_named_child(expr, terminal_is_last ? count - 1 : 0));
}
static TSNode php_callable_reference_leaf(PHPLSPContext *ctx, TSNode call, const char *kind) {
if (php_callable_reference_has_dynamic_member_name(ctx, call)) {
return (TSNode){0};
}
TSNode expr = {0};
if (strcmp(kind, "function_call_expression") == 0) {
expr = ts_node_child_by_field_name(call, "function", 8);
} else {
expr = ts_node_child_by_field_name(call, "name", 4);
}
return php_reference_callee_leaf(ctx, expr);
}
static void php_stamp_resolved_site(PHPLSPContext *ctx, int first, TSNode site) {
if (!ctx->resolved_calls || ts_node_is_null(site) || first < 0) {
return;
}
for (int i = first; i < ctx->resolved_calls->count; i++) {
ctx->resolved_calls->items[i].site_start_byte = ts_node_start_byte(site);
ctx->resolved_calls->items[i].site_end_byte = ts_node_end_byte(site);
}
}
static void emit_unresolved(PHPLSPContext *ctx, const char *expr_text, const char *reason) {
if (!ctx->resolved_calls || !ctx->enclosing_func_qn)
return;
CBMResolvedCall rc;
CBMResolvedCall rc = {0};
rc.caller_qn = ctx->enclosing_func_qn;
rc.callee_qn = expr_text ? expr_text : "?";
rc.strategy = "lsp_unresolved";
@@ -1449,7 +1591,7 @@ static void process_catch(PHPLSPContext *ctx, TSNode node) {
/* ── call resolution ────────────────────────────────────────────── */
static void resolve_function_call(PHPLSPContext *ctx, TSNode call) {
static void resolve_function_call(PHPLSPContext *ctx, TSNode call, CBMResolvedKind kind) {
TSNode fn = ts_node_child_by_field_name(call, "function", 8);
if (ts_node_is_null(fn))
return;
@@ -1457,12 +1599,34 @@ static void resolve_function_call(PHPLSPContext *ctx, TSNode call) {
if (!name)
return;
/* In PHP, `$handler(...)` is syntactically a function-call expression but
* a typed object value dispatches to its materialized `__invoke` method.
* This is precise only for a first-class callable and a receiver type whose
* exact method exists in the registry; otherwise leave it unresolved so
* the usage pipeline can retain ordinary value semantics. */
if (kind == CBM_RESOLVED_CALL_REFERENCE && strcmp(ts_node_type(fn), "variable_name") == 0) {
const CBMType *receiver = php_eval_expr_type(ctx, fn);
const char *class_qn = NULL;
if (receiver && receiver->kind == CBM_TYPE_NAMED) {
class_qn = receiver->data.named.qualified_name;
} else if (receiver && receiver->kind == CBM_TYPE_TEMPLATE) {
class_qn = receiver->data.template_type.template_name;
}
const CBMRegisteredFunc *invoke =
class_qn ? php_lookup_method(ctx, class_qn, "__invoke") : NULL;
if (invoke && invoke->qualified_name) {
emit_resolved_reason_kind(ctx, invoke->qualified_name, "php_invokable_reference", 0.95f,
name, kind);
}
return;
}
/* `use function Foo\\bar;` mapping. */
const char *target = find_use(ctx, name, CBM_PHP_USE_FUNCTION);
if (target) {
const CBMRegisteredFunc *f = cbm_registry_lookup_func(ctx->registry, target);
if (f) {
emit_resolved(ctx, f->qualified_name, "php_function_namespaced", 0.95f);
emit_resolved_kind(ctx, f->qualified_name, "php_function_namespaced", 0.95f, kind);
return;
}
}
@@ -1472,7 +1636,7 @@ static void resolve_function_call(PHPLSPContext *ctx, TSNode call) {
const char *ns_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->current_namespace_qn, name);
const CBMRegisteredFunc *f = cbm_registry_lookup_func(ctx->registry, ns_qn);
if (f) {
emit_resolved(ctx, f->qualified_name, "php_function_namespaced", 0.95f);
emit_resolved_kind(ctx, f->qualified_name, "php_function_namespaced", 0.95f, kind);
return;
}
}
@@ -1504,7 +1668,7 @@ static void resolve_function_call(PHPLSPContext *ctx, TSNode call) {
}
}
if (best) {
emit_resolved(ctx, best->qualified_name, "php_function_global_fallback", 0.70f);
emit_resolved_kind(ctx, best->qualified_name, "php_function_global_fallback", 0.70f, kind);
return;
}
/* Unresolved — but DO NOT emit unresolved noise here; the unified
@@ -1513,11 +1677,15 @@ static void resolve_function_call(PHPLSPContext *ctx, TSNode call) {
(void)emit_unresolved;
}
static void resolve_member_call(PHPLSPContext *ctx, TSNode call) {
static void resolve_member_call(PHPLSPContext *ctx, TSNode call, CBMResolvedKind kind) {
TSNode obj = ts_node_child_by_field_name(call, "object", 6);
TSNode name_node = ts_node_child_by_field_name(call, "name", 4);
if (ts_node_is_null(name_node))
return;
if (kind == CBM_RESOLVED_CALL_REFERENCE &&
php_callable_reference_has_dynamic_member_name(ctx, call)) {
return;
}
char *method_name = php_node_text(ctx, name_node);
if (!method_name)
return;
@@ -1541,7 +1709,7 @@ static void resolve_member_call(PHPLSPContext *ctx, TSNode call) {
const char *strategy = (f->receiver_type && strcmp(f->receiver_type, class_qn) == 0)
? "php_method_typed"
: "php_method_inherited";
emit_resolved(ctx, f->qualified_name, strategy, 0.95f);
emit_resolved_kind(ctx, f->qualified_name, strategy, 0.95f, kind);
return;
}
/* Receiver known but method missing — magic __call? The call dispatches to
@@ -1550,8 +1718,8 @@ static void resolve_member_call(PHPLSPContext *ctx, TSNode call) {
* so stash it in reason for the join (lsp_resolve.h, php_method_dynamic).
* Emit above the join's confidence floor — dispatch to __call is certain. */
if (class_has_magic_call(ctx, class_qn, false)) {
emit_resolved_reason(ctx, cbm_arena_sprintf(ctx->arena, "%s.__call", class_qn),
"php_method_dynamic", 0.85f, method_name);
emit_resolved_reason_kind(ctx, cbm_arena_sprintf(ctx->arena, "%s.__call", class_qn),
"php_method_dynamic", 0.85f, method_name, kind);
return;
}
/* Receiver known but class not in registry (e.g. vendor type not indexed,
@@ -1559,15 +1727,19 @@ static void resolve_member_call(PHPLSPContext *ctx, TSNode call) {
* at "<class_qn>.<method>" so the pipeline bridge can BLOCK the unified
* extractor's likely-incorrect short-name fallback. The bridge filters
* unknown targets and yields no edge — better than a wrong edge. */
emit_resolved(ctx, cbm_arena_sprintf(ctx->arena, "%s.%s", class_qn, method_name),
"php_method_typed_unindexed", 0.55f);
emit_resolved_kind(ctx, cbm_arena_sprintf(ctx->arena, "%s.%s", class_qn, method_name),
"php_method_typed_unindexed", 0.55f, kind);
}
static void resolve_static_call(PHPLSPContext *ctx, TSNode call) {
static void resolve_static_call(PHPLSPContext *ctx, TSNode call, CBMResolvedKind kind) {
TSNode scope = ts_node_child_by_field_name(call, "scope", 5);
TSNode name_node = ts_node_child_by_field_name(call, "name", 4);
if (ts_node_is_null(name_node))
return;
if (kind == CBM_RESOLVED_CALL_REFERENCE &&
php_callable_reference_has_dynamic_member_name(ctx, call)) {
return;
}
char *method_name = php_node_text(ctx, name_node);
if (!method_name)
return;
@@ -1593,20 +1765,30 @@ static void resolve_static_call(PHPLSPContext *ctx, TSNode call) {
const CBMRegisteredFunc *f = php_lookup_method(ctx, class_qn, method_name);
if (f) {
emit_resolved(ctx, f->qualified_name, strategy, 0.95f);
emit_resolved_kind(ctx, f->qualified_name, strategy, 0.95f, kind);
return;
}
/* A first-class reference to an absent static method has an exact target
* only when a materialized `__callStatic` exists. Preserve the source name
* as the occurrence-join reason, but point at the real method node. Normal
* facade invocations retain their existing low-confidence behavior. */
const CBMRegisteredFunc *magic_static = php_lookup_method(ctx, class_qn, "__callStatic");
if (magic_static && kind == CBM_RESOLVED_CALL_REFERENCE) {
emit_resolved_reason_kind(ctx, magic_static->qualified_name, "php_static_magic_reference",
0.95f, method_name, kind);
return;
}
/* Facade detection: class has __callStatic in its chain. */
if (class_has_magic_call(ctx, class_qn, true)) {
emit_resolved(ctx, cbm_arena_sprintf(ctx->arena, "%s.%s", class_qn, method_name),
"php_dynamic_unresolved", 0.10f);
if (magic_static) {
emit_resolved_kind(ctx, cbm_arena_sprintf(ctx->arena, "%s.%s", class_qn, method_name),
"php_dynamic_unresolved", 0.10f, kind);
return;
}
/* Class resolved (e.g., from a `use` clause), method unknown — emit
* synthetic resolved call so the pipeline bridge can suppress the
* unified extractor's name-fallback misroute. */
emit_resolved(ctx, cbm_arena_sprintf(ctx->arena, "%s.%s", class_qn, method_name),
"php_static_unindexed", 0.55f);
emit_resolved_kind(ctx, cbm_arena_sprintf(ctx->arena, "%s.%s", class_qn, method_name),
"php_static_unindexed", 0.55f, kind);
}
/* ── type narrowing ─────────────────────────────────────────────────
@@ -2158,14 +2340,32 @@ static void php_resolve_calls_in_node_inner(PHPLSPContext *ctx, TSNode node) {
return; /* process_if_statement already walked body under narrowing */
}
/* Call-resolution dispatch. */
/* Call-resolution dispatch. A first-class callable has the same outer AST
* node as an invocation, so carry the semantic kind alongside resolution
* and skip invocation-only narrowing/binding effects. */
bool is_callable_reference = php_is_first_class_callable(node);
CBMResolvedKind resolved_kind =
is_callable_reference ? CBM_RESOLVED_CALL_REFERENCE : CBM_RESOLVED_INVOCATION;
if (strcmp(kind, "function_call_expression") == 0) {
resolve_function_call(ctx, node);
int first = ctx->resolved_calls ? ctx->resolved_calls->count : -1;
resolve_function_call(ctx, node, resolved_kind);
if (is_callable_reference) {
php_stamp_resolved_site(ctx, first, php_callable_reference_leaf(ctx, node, kind));
} else {
php_stamp_resolved_site(ctx, first, node);
}
/* assert(...) in current scope narrows sequentially. */
apply_assert_narrowing(ctx, node);
if (!is_callable_reference)
apply_assert_narrowing(ctx, node);
} else if (strcmp(kind, "member_call_expression") == 0 ||
strcmp(kind, "nullsafe_member_call_expression") == 0) {
resolve_member_call(ctx, node);
int first = ctx->resolved_calls ? ctx->resolved_calls->count : -1;
resolve_member_call(ctx, node, resolved_kind);
if (is_callable_reference) {
php_stamp_resolved_site(ctx, first, php_callable_reference_leaf(ctx, node, kind));
} else {
php_stamp_resolved_site(ctx, first, node);
}
/* Closure binding via $f->bindTo($obj). When $f is an inline
* closure literal AND the bindTo call's first arg is a typed
* variable, walk the closure body with $this rebound. We only
@@ -2174,7 +2374,7 @@ static void php_resolve_calls_in_node_inner(PHPLSPContext *ctx, TSNode node) {
* value-typing is Phase 6+ infrastructure). */
TSNode obj = ts_node_child_by_field_name(node, "object", 6);
TSNode mname = ts_node_child_by_field_name(node, "name", 4);
if (!ts_node_is_null(obj) && !ts_node_is_null(mname)) {
if (!is_callable_reference && !ts_node_is_null(obj) && !ts_node_is_null(mname)) {
char *mn = php_node_text(ctx, mname);
const char *ok = ts_node_type(obj);
if (mn && strcmp(mn, "bindTo") == 0 &&
@@ -2212,13 +2412,19 @@ static void php_resolve_calls_in_node_inner(PHPLSPContext *ctx, TSNode node) {
}
}
} else if (strcmp(kind, "scoped_call_expression") == 0) {
resolve_static_call(ctx, node);
int first = ctx->resolved_calls ? ctx->resolved_calls->count : -1;
resolve_static_call(ctx, node, resolved_kind);
if (is_callable_reference) {
php_stamp_resolved_site(ctx, first, php_callable_reference_leaf(ctx, node, kind));
} else {
php_stamp_resolved_site(ctx, first, node);
}
/* Closure binding via Closure::bind($f, $obj). Same shape as
* bindTo: rebind $this when the first arg is a closure literal
* and the second arg has a known class. */
TSNode scope = ts_node_child_by_field_name(node, "scope", 5);
TSNode mname2 = ts_node_child_by_field_name(node, "name", 4);
if (!ts_node_is_null(scope) && !ts_node_is_null(mname2)) {
if (!is_callable_reference && !ts_node_is_null(scope) && !ts_node_is_null(mname2)) {
char *cls = php_node_text(ctx, scope);
char *mn = php_node_text(ctx, mname2);
bool is_closure_bind = mn && strcmp(mn, "bind") == 0 && cls &&
@@ -2782,6 +2988,22 @@ static const CBMType *parse_return_type_text(CBMArena *arena, const char *text,
return cbm_type_named(arena, php_ns_to_dot(arena, first));
}
typedef struct {
const char *module_qn;
const char *namespace_qn;
const char **use_local_names;
const char **use_target_qns;
int use_count;
} PHPSignatureParseContext;
static const CBMType *php_signature_parse_type(CBMArena *arena, const char *text,
void *parser_ctx) {
const PHPSignatureParseContext *ctx = (const PHPSignatureParseContext *)parser_ctx;
return parse_return_type_text(arena, text, ctx ? ctx->module_qn : NULL,
ctx ? ctx->namespace_qn : NULL, ctx ? ctx->use_local_names : NULL,
ctx ? ctx->use_target_qns : NULL, ctx ? ctx->use_count : 0);
}
/* ── property type extraction ───────────────────────────────────────
*
* Walks the AST top-level for class/trait/interface bodies and registers
@@ -3514,7 +3736,7 @@ static void flatten_trait_into_class(PHPLSPContext *ctx, CBMTypeRegistry *reg, c
if (rets) {
rets[0] = cbm_type_named(ctx->arena, class_qn);
rets[1] = NULL;
new_sig = cbm_type_func(ctx->arena, NULL, NULL, rets);
new_sig = cbm_type_func_replace_returns(ctx->arena, src->signature, rets);
}
}
}
@@ -3815,7 +4037,8 @@ static void process_class_for_fields(PHPLSPContext *ctx, CBMTypeRegistry *reg, T
if (rets) {
rets[0] = parsed;
rets[1] = NULL;
rf->signature = cbm_type_func(ctx->arena, NULL, NULL, rets);
rf->signature =
cbm_type_func_replace_returns(ctx->arena, rf->signature, rets);
}
break;
}
@@ -3892,7 +4115,8 @@ static void process_class_for_fields(PHPLSPContext *ctx, CBMTypeRegistry *reg, T
if (rets) {
rets[0] = ret_t;
rets[1] = NULL;
rf->signature = cbm_type_func(ctx->arena, NULL, NULL, rets);
rf->signature = cbm_type_func_replace_returns(
ctx->arena, rf->signature, rets);
}
break;
}
@@ -3944,6 +4168,26 @@ static void php_lsp_collect_class_fields(PHPLSPContext *ctx, CBMTypeRegistry *re
}
}
void cbm_php_refine_lsp_registry(PHPLSPContext *ctx, CBMTypeRegistry *reg, TSNode root) {
if (!ctx || !reg || ts_node_is_null(root))
return;
php_class_field_table_t tab = {0};
php_lsp_collect_class_fields(ctx, reg, root, &tab);
for (int i = 0; i < tab.count; i++) {
php_class_fields_t *fields = &tab.items[i];
if (fields->count == 0)
continue;
for (int t = 0; t < reg->type_count; t++) {
if (strcmp(reg->types[t].qualified_name, fields->class_qn) == 0) {
reg->types[t].field_names = fields->field_names;
reg->types[t].field_types = fields->field_types;
break;
}
}
}
}
/* ── entry: cbm_run_php_lsp ─────────────────────────────────────── */
void cbm_run_php_lsp(CBMArena *arena, CBMFileResult *result, const char *source, int source_len,
@@ -4015,7 +4259,29 @@ void cbm_run_php_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
if (strcmp(d->label, "Method") == 0 && d->parent_class) {
rf.receiver_type = d->parent_class;
}
/* Build a minimal signature with just the return type. */
PHPSignatureParseContext parse_ctx = {
.module_qn = module_qn,
.namespace_qn = NULL,
.use_local_names = NULL,
.use_target_qns = NULL,
.use_count = 0,
};
const CBMType **param_types = NULL;
if (d->signature_param_types || d->signature_param_count > 0) {
param_types = cbm_type_materialize_signature_params(
arena, d->signature_param_types, d->signature_param_count,
php_signature_parse_type, &parse_ctx);
} else if (!d->signature_param_types && d->signature_param_count == 0 &&
d->param_types) {
/* Legacy CBMDefinition callers have no counted carrier. Keep
* their historical projection as a local-only fallback. */
int legacy_count = 0;
while (d->param_types[legacy_count])
legacy_count++;
param_types = cbm_type_materialize_signature_params(
arena, d->param_types, legacy_count, php_signature_parse_type, &parse_ctx);
}
/* Build a signature carrying ordered params plus the return type. */
const CBMType *ret_t =
d->return_type
? parse_return_type_text(arena, d->return_type, module_qn, NULL, NULL, NULL, 0)
@@ -4026,7 +4292,7 @@ void cbm_run_php_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
rets[0] = ret_t;
rets[1] = NULL;
}
rf.signature = cbm_type_func(arena, NULL, NULL, rets);
rf.signature = cbm_type_func(arena, NULL, param_types, rets);
cbm_registry_add_func(&reg, rf);
}
}
@@ -4040,7 +4306,6 @@ void cbm_run_php_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
* file defs are registered but before the call walk so eval_expr_type
* for $this->prop and $obj->prop sees the field types. */
{
php_class_field_table_t tab = {0};
/* First pass to populate the use map (so type names in property
* declarations resolve correctly). */
/* Collect top-level children once (O(n)); ts_node_child(root,i) is O(i) → O(n²). */
@@ -4055,25 +4320,10 @@ void cbm_run_php_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
collect_use_declaration(&ctx, c);
}
}
php_lsp_collect_class_fields(&ctx, &reg, root, &tab);
cbm_php_refine_lsp_registry(&ctx, &reg, root);
/* Reset namespace/use state — process_file will re-collect. */
ctx.current_namespace_qn = "";
ctx.use_count = 0;
for (int i = 0; i < tab.count; i++) {
php_class_fields_t *f = &tab.items[i];
if (f->count == 0)
continue;
/* Mutate the existing entry — registered types are stored by
* value in reg.types[], find the index and update it. */
for (int t = 0; t < reg.type_count; t++) {
if (strcmp(reg.types[t].qualified_name, f->class_qn) == 0) {
reg.types[t].field_names = f->field_names;
reg.types[t].field_types = f->field_types;
break;
}
}
}
}
php_lsp_process_file(&ctx, root);
@@ -4131,8 +4381,8 @@ static const char **php_split_pipe(CBMArena *arena, const char *text) {
* functions. Return type strings are parsed via parse_return_type_text using
* each def's def_module_qn so bare type names qualify against the module
* where the def lives, not the importer's module. */
static void php_register_lsp_defs(CBMArena *arena, CBMArena *idx_arena, CBMTypeRegistry *reg,
CBMLSPDef *defs, int def_count) {
void cbm_php_register_lsp_defs(CBMArena *arena, CBMArena *idx_arena, CBMTypeRegistry *reg,
const CBMLSPDef *defs, int def_count) {
/* Pass 1: types only. The method pass below probes the registry per
* Method def (receiver auto-registration); doing that against an
* unfinalized registry is a LINEAR scan over the growing type table —
@@ -4141,7 +4391,7 @@ static void php_register_lsp_defs(CBMArena *arena, CBMArena *idx_arena, CBMTypeR
* the hash, then register funcs/methods with O(1) lookups (post-finalize
* stub additions stay visible via the registry's tail-scan). */
for (int i = 0; i < def_count; i++) {
CBMLSPDef *d = &defs[i];
const CBMLSPDef *d = &defs[i];
if (!d->qualified_name || !d->short_name || !d->label)
continue;
@@ -4168,7 +4418,7 @@ static void php_register_lsp_defs(CBMArena *arena, CBMArena *idx_arena, CBMTypeR
/* Pass 2: functions and methods. */
for (int i = 0; i < def_count; i++) {
CBMLSPDef *d = &defs[i];
const CBMLSPDef *d = &defs[i];
if (!d->qualified_name || !d->short_name || !d->label)
continue;
@@ -4178,6 +4428,18 @@ static void php_register_lsp_defs(CBMArena *arena, CBMArena *idx_arena, CBMTypeR
rf.qualified_name = d->qualified_name; /* borrowed */
rf.short_name = d->short_name;
const char *def_mod = d->def_module_qn ? d->def_module_qn : "";
PHPSignatureParseContext parse_ctx = {
.module_qn = def_mod,
.namespace_qn = d->namespace_name,
.use_local_names = NULL,
.use_target_qns = NULL,
.use_count = 0,
};
const CBMType **param_types = cbm_type_materialize_signature_params(
arena, d->signature_param_types, d->signature_param_count, php_signature_parse_type,
&parse_ctx);
/* Build FUNC type from "|"-separated return-type texts. Each piece
* is the unqualified return-type expression as it appears in the
* def-defining module's source. parse_return_type_text qualifies
@@ -4192,7 +4454,6 @@ static void php_register_lsp_defs(CBMArena *arena, CBMArena *idx_arena, CBMTypeR
ret_types = (const CBMType **)cbm_arena_alloc(
arena, (size_t)(n + 1) * sizeof(const CBMType *));
if (ret_types) {
const char *def_mod = d->def_module_qn ? d->def_module_qn : "";
for (int j = 0; j < n; j++) {
ret_types[j] = parse_return_type_text(arena, ret_strs[j], def_mod, NULL,
NULL, NULL, 0);
@@ -4201,7 +4462,7 @@ static void php_register_lsp_defs(CBMArena *arena, CBMArena *idx_arena, CBMTypeR
}
}
}
rf.signature = cbm_type_func(arena, NULL, NULL, ret_types);
rf.signature = cbm_type_func(arena, NULL, param_types, ret_types);
if (strcmp(d->label, "Method") == 0 && d->receiver_type && d->receiver_type[0]) {
rf.receiver_type = d->receiver_type; /* borrowed */
@@ -4254,7 +4515,7 @@ void cbm_run_php_lsp_cross(CBMArena *arena, const char *source, int source_len,
* accumulate GBs across a large repo. The scratch dies with this call. */
CBMArena idx_arena;
cbm_arena_init(&idx_arena);
php_register_lsp_defs(arena, &idx_arena, &reg, defs, def_count);
cbm_php_register_lsp_defs(arena, &idx_arena, &reg, defs, def_count);
/* Finalize registry — O(1) lookups. See go_lsp.c "3c. Finalize"
* comment for the rationale. */
@@ -4275,7 +4536,6 @@ void cbm_run_php_lsp_cross(CBMArena *arena, const char *source, int source_len,
* to populate typed-property field maps so $this->prop and $obj->prop
* resolve to the right type during the call walk. */
{
php_class_field_table_t tab = {0};
/* Collect top-level children once (O(n)); ts_node_child(root,i) is O(i) → O(n²). */
uint32_t pkn = 0;
TSNode *pkids = cbm_lsp_collect_children(ctx.arena, root, &pkn);
@@ -4288,22 +4548,10 @@ void cbm_run_php_lsp_cross(CBMArena *arena, const char *source, int source_len,
collect_use_declaration(&ctx, c);
}
}
php_lsp_collect_class_fields(&ctx, &reg, root, &tab);
cbm_php_refine_lsp_registry(&ctx, &reg, root);
ctx.current_namespace_qn = "";
/* Reset to caller-supplied uses only — process_file re-adds AST uses. */
ctx.use_count = import_count;
for (int i = 0; i < tab.count; i++) {
php_class_fields_t *f = &tab.items[i];
if (f->count == 0)
continue;
for (int t = 0; t < reg.type_count; t++) {
if (strcmp(reg.types[t].qualified_name, f->class_qn) == 0) {
reg.types[t].field_names = f->field_names;
reg.types[t].field_types = f->field_types;
break;
}
}
}
}
php_lsp_process_file(&ctx, root);
@@ -4344,6 +4592,7 @@ void cbm_batch_php_lsp_cross(CBMArena *arena, CBMBatchPHPLSPFile *files, int fil
for (int j = 0; j < file_out.count; j++) {
CBMResolvedCall *src = &file_out.items[j];
CBMResolvedCall *dst = &out[f].items[j];
memset(dst, 0, sizeof(*dst));
dst->caller_qn =
src->caller_qn ? cbm_arena_strdup(arena, src->caller_qn) : NULL;
dst->callee_qn =
@@ -4351,6 +4600,9 @@ void cbm_batch_php_lsp_cross(CBMArena *arena, CBMBatchPHPLSPFile *files, int fil
dst->strategy = src->strategy ? cbm_arena_strdup(arena, src->strategy) : NULL;
dst->confidence = src->confidence;
dst->reason = src->reason ? cbm_arena_strdup(arena, src->reason) : NULL;
dst->kind = src->kind;
dst->site_start_byte = src->site_start_byte;
dst->site_end_byte = src->site_end_byte;
}
} else {
out[f].count = 0;
+7
View File
@@ -71,6 +71,10 @@ void php_lsp_add_use(PHPLSPContext *ctx, const char *local_name, const char *tar
/* Process a file's AST: walk top-level decls, then function/method bodies. */
void php_lsp_process_file(PHPLSPContext *ctx, TSNode root);
/* Refine an already-populated registry from local class ASTs: declared and
* PHPDoc returns, trait flattening, and typed fields. */
void cbm_php_refine_lsp_registry(PHPLSPContext *ctx, CBMTypeRegistry *reg, TSNode root);
/* Evaluate a PHP expression's type. May return NULL / CBM_TYPE_UNKNOWN. */
const CBMType *php_eval_expr_type(PHPLSPContext *ctx, TSNode node);
@@ -100,6 +104,9 @@ void cbm_php_stdlib_register(CBMTypeRegistry *reg, CBMArena *arena);
* `use` declarations from the AST are layered on top by process_file.
*
* Reuses go_lsp.h's CBMLSPDef so cross-language registration is uniform. */
void cbm_php_register_lsp_defs(CBMArena *arena, CBMArena *idx_arena, CBMTypeRegistry *reg,
const CBMLSPDef *defs, int def_count);
void cbm_run_php_lsp_cross(
CBMArena *arena,
const char *source, int source_len,
+723 -101
View File
File diff suppressed because it is too large Load Diff
+6 -4
View File
@@ -50,6 +50,7 @@ typedef struct {
// Built from CBMFileResult.imports.
const char **import_local_names;
const char **import_module_qns;
unsigned char *import_kinds; // internal PyDirectImportKind, classified once from the AST
int import_count;
// Current function/class context for resolving `self`/`cls` and emitting
@@ -69,8 +70,9 @@ typedef struct {
// appears in result->calls because a `subscript`/`binary_operator` is not
// a `call` node, so the syntactic extractor produced no call. When this is
// non-NULL, py_emit_dunder_call injects a matching synthetic CBMCall so the
// recovered dunder call becomes a real CALLS edge. NULL in the cross-file
// path (no result->calls available there). Mirrors RustLSPContext.syn_calls.
// recovered dunder call becomes a real CALLS edge. Cross-file callers use
// a separate arena-owned output and merge it into the file result after
// resolution. Mirrors RustLSPContext.syn_calls.
CBMCallArray *syn_calls;
// Lambda registry: simple linear list, looked up by name.
@@ -155,7 +157,7 @@ void cbm_run_py_lsp_cross(CBMArena *arena, const char *source, int source_len,
const char *module_qn, CBMLSPDef *defs, int def_count,
const char **import_names, const char **import_qns, int import_count,
TSTree *cached_tree, // NULL = parse internally
CBMResolvedCallArray *out);
CBMResolvedCallArray *out, CBMCallArray *synthetic_calls);
/* Tier 2: pre-built per-language registry (mirrors Go pilot).
* Filters all_defs to Python entries, builds + finalizes once. */
@@ -166,7 +168,7 @@ void cbm_run_py_lsp_cross_with_registry(CBMArena *arena, const char *source, int
CBMTypeRegistry *reg, // pre-built, finalized, READ-ONLY
const char **import_names, const char **import_qns,
int import_count, TSTree *cached_tree,
CBMResolvedCallArray *out);
CBMResolvedCallArray *out, CBMCallArray *synthetic_calls);
// --- Batch cross-file LSP ---
+1086 -278
View File
File diff suppressed because it is too large Load Diff
+49 -20
View File
@@ -55,9 +55,13 @@ typedef struct {
CBMArena *arena;
const char *source;
int source_len;
const char *root_source; /* immutable original file buffer */
const CBMTypeRegistry *registry;
CBMScope *current_scope;
/* Alias assignments beneath a branch/loop/match/closure cannot establish
* one unconditional post-region target, so they clear callable identity. */
int callable_control_flow_depth;
/* Negative-lookup memo for the registry-pure resolve cascades (trait
* method / sole-trait-impl / free-func fallback). Active ONLY when the
@@ -75,6 +79,11 @@ typedef struct {
const char **glob_module_qns;
int glob_count;
/* Exact names from top-level out-of-line `mod name;` declarations. A
* relative scoped type may be project-qualified only when its head is in
* this set; coincidentally same-named indexed modules stay external. */
CBMIdxMemo declared_modules;
/* Module-qualified name for this file (e.g. "<project>.<crate>.foo"). */
const char *module_qn;
@@ -107,6 +116,13 @@ typedef struct {
struct RustMacroRule **macro_rules_arr;
int macro_rules_count;
/* Original-file occurrence that established the lexical environment for
* the current macro expansion. Nested macro invocations are parsed from a
* synthetic transcriber buffer, so their tree-sitter offsets cannot be
* used to decide which source-level macro_rules! definition is visible. */
uint32_t macro_origin_byte;
bool macro_origin_valid;
/* Recursion guard for macro expansion. Real macro_rules! can be
* recursive; we cap at 8 nested expansions to keep the walker
* bounded. */
@@ -162,8 +178,9 @@ typedef struct {
* calls hidden inside macro token-trees (`format!("{}", d.label())`) —
* never appear in result->calls because the syntactic extractor cannot
* see them. When this is non-NULL the resolver injects matching synthetic
* CBMCall entries so those recovered calls become real edges. NULL in the
* cross-file path (no result available). */
* CBMCall entries so those recovered calls become real edges. Cross-file
* callers provide a separate output array and merge it into the owning
* file result with explicit arena-aware copying. */
CBMCallArray *syn_calls;
/* While >0, rust_emit_resolved_call also injects a matching synthetic
@@ -172,6 +189,19 @@ typedef struct {
* produced a call node. */
int inject_syn_calls;
/* Current invocation occurrence in ORIGINAL-file coordinates. Ordinary
* source calls use their tree-sitter node directly. Calls recovered by
* reparsing built-in macro arguments use the affine map below to translate
* the synthetic wrapper's offsets back to the copied token-tree span.
* macro_rules! transcriber text is intentionally left unmapped because
* substitution is not position-preserving. */
uint32_t emit_site_start_byte;
uint32_t emit_site_end_byte;
bool site_map_active;
uint32_t site_map_synthetic_start_byte;
uint32_t site_map_synthetic_end_byte;
uint32_t site_map_original_start_byte;
/* CBM_LSP_DEBUG=1 in env enables verbose stderr trace. */
bool debug;
@@ -235,6 +265,12 @@ void rust_process_statement(RustLSPContext *ctx, TSNode node);
const CBMRegisteredFunc *rust_lookup_method(RustLSPContext *ctx, const char *type_qn,
const char *member_name);
/* Populate the same per-file registry used by cbm_run_rust_lsp, including
* AST-declared return-type refinements. Exposed as a production-used seam for
* structural registry regression tests. */
void cbm_rust_build_local_registry(CBMArena *arena, CBMTypeRegistry *reg, CBMFileResult *result,
const char *module_qn, TSNode root, const char *source);
/* Entry point — called from `cbm_extract_file()` after the unified
* extractor has filled `result->defs`, `result->imports`, and
* `result->impl_traits`. Builds a per-file registry, parses the `use`
@@ -242,19 +278,6 @@ const CBMRegisteredFunc *rust_lookup_method(RustLSPContext *ctx, const char *typ
void cbm_run_rust_lsp(CBMArena *arena, CBMFileResult *result, const char *source, int source_len,
TSNode root);
/* Synthesize call edges for curated attribute proc-macros.
*
* For each `CBMDefinition` whose decorators include a known
* attribute proc-macro (`#[tokio::main]`, `#[tracing::instrument]`,
* `#[async_trait]`, …), emit synthetic edges from that definition
* to the helper functions the macro would have injected (e.g.
* `tokio::runtime::Runtime::new` + `block_on`). The edges carry
* strategy `lsp_proc_macro` so consumers can distinguish them from
* direct call attributions.
*
* Called automatically from `cbm_run_rust_lsp_with_manifest`. */
void cbm_rust_synth_proc_macro_edges(CBMArena *arena, CBMFileResult *result);
/* Same as `cbm_run_rust_lsp`, but accepts an optional Cargo manifest
* so the resolver can route paths whose head is a workspace member /
* declared dependency. Pass NULL to fall back to the manifest-free
@@ -293,8 +316,12 @@ typedef struct {
const char *embedded_types; /* "|"-separated embedded type QNs */
const char *field_defs; /* "|"-separated "name:type" pairs */
const char *method_names_str; /* "|"-separated method names for traits */
const char *trait_qn; /* impl Trait for Type → set on Method defs */
const char **signature_param_types; /* borrowed ordered parameter texts */
int signature_param_count; /* positional entries; "?" is unknown */
const char *trait_qn; /* raw impl-trait spelling; uniquely canonicalized */
bool is_interface; /* true for traits */
bool is_rust_impl_relation; /* independent type-level impl record */
bool is_abstract; /* required trait declaration (no default) */
} CBMRustLSPDef;
/* Run cross-file resolution on a single file. */
@@ -314,7 +341,7 @@ void cbm_run_rust_lsp_cross_with_manifest(CBMArena *arena, const char *source, i
const char **import_names, const char **import_qns,
int import_count, TSTree *cached_tree,
const struct CBMCargoManifest *manifest,
CBMResolvedCallArray *out);
CBMResolvedCallArray *out, CBMCallArray *synthetic_calls);
/* 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).
@@ -323,14 +350,16 @@ CBMTypeRegistry *cbm_rust_build_cross_registry(CBMArena *arena, CBMLSPDef *defs,
/* 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. */
* the per-file path uses (cross-crate #56). `synthetic_calls` is an optional,
* arena-owned output for exact carriers recovered from non-call syntax. Attribute
* proc-macros are handled by the graph's DECORATES + USAGE semantic passes, not
* by this resolver. */
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);
CBMResolvedCallArray *out, CBMCallArray *synthetic_calls);
/* Per-file input for batch cross-file Rust LSP processing. */
typedef struct {
+9 -143
View File
@@ -1,145 +1,11 @@
/* rust_proc_macros.c — Curated attribute proc-macro expansion.
/* Rust procedural-macro policy boundary.
*
* RUST_LSP_FOLLOWUP §A1 + improvement Option B: extend derive synthesis
* (already done for #[derive(...)]) to *attribute*-style proc-macros.
* Attribute and derive macros are represented by the generic graph semantic
* passes as DECORATES plus USAGE. This file intentionally emits no
* CBMResolvedCall records: without executing rustc and the proc-macro, helper
* invocations such as Runtime::block_on or Span::enter are not source-proven
* calls and must not be fabricated.
*
* We can't run proc-macros (would need rustc). But for the ~20
* most-used attribute proc-macros we KNOW the expansion shape and can
* synthesize equivalent calls/impls so the resolver doesn't go blind.
*
* Covered:
* - `#[tokio::main]` / `#[tokio::test]` — wrap fn body in
* `tokio::runtime::Runtime::new().unwrap().block_on(async { ... })`
* - `#[async_std::main]` — same shape via async_std executor
* - `#[async_trait::async_trait]` — methods on the trait become
* `Pin<Box<dyn Future>>` returning shapes; we just register the
* method bodies normally (the resolver already handles `async fn`)
* - `#[derive(thiserror::Error)]` — synthesise `source`, `description`
* (we already synth via the curated derive table; this just adds
* `thiserror::Error` to the alias chain)
* - `#[derive(serde::Serialize|Deserialize)]` — already covered by
* the curated derive table (alias `serde::Serialize` to
* `serde.Serialize`)
* - `#[clap(...)]` field attributes — no synthesis needed; the
* `#[derive(clap::Parser)]` outer macro handles the impl
* - `#[tracing::instrument]` — wraps the function body in a span;
* we synthesise a call to `tracing::span::Span::enter`
* - `#[rocket::main]`/`#[actix_web::main]` — same shape as tokio
* - `#[test]` / `#[bench]` — no synthesis; the function body is
* attributed as-is
*
* For each, we register a synthetic "wrapper call" QN per
* decorator → resolved-call edge so the project's tracing/dependency
* analyses see what the proc-macro injected. */
#include "../arena.h"
#include "../cbm.h"
#include "rust_lsp.h"
#include <string.h>
#include <stdio.h>
/* The attribute-decorator table. For each pattern in `decorator` text
* we emit a *synthetic call edge* attributed to the wrapped function
* so tracing tools can see the implicit dependency. The synthesis is
* conservative: we emit only when the decorator text matches one of
* the curated tokens — anything else is left alone. */
typedef struct {
const char* match; /* substring to find in the decorator text */
const char* edges[6]; /* synthesized callee QNs (NULL-terminated) */
} RustAttrSynth;
static const RustAttrSynth ATTR_SYNTH[] = {
{"tokio::main", {
"tokio.runtime.Runtime.new",
"tokio.runtime.Runtime.block_on",
NULL
}},
{"tokio::test", {
"tokio.runtime.Runtime.new",
"tokio.runtime.Runtime.block_on",
NULL
}},
{"async_std::main", {
"async_std.task.block_on",
NULL
}},
{"actix_web::main", {
"actix_web.rt.System.new",
"actix_web.rt.System.block_on",
NULL
}},
{"actix_rt::main", {
"actix_rt.System.new",
"actix_rt.System.block_on",
NULL
}},
{"rocket::main", {
"rocket.async_main",
NULL
}},
{"rocket::launch", {
"rocket.launch",
NULL
}},
{"tracing::instrument", {
"tracing.span.Span.enter",
NULL
}},
{"async_trait", {
/* The macro wraps each method's return in
* `Pin<Box<dyn Future<Output = R> + Send>>` — no method-name
* synthesis needed, just record the boxed-future bridging
* call so analyses see the async-trait shape. */
"alloc.boxed.Box.new",
"core.pin.Pin.new",
NULL
}},
{"wasm_bindgen", {
"wasm_bindgen.JsValue.from",
NULL
}},
{"napi", {
"napi.Env.create",
NULL
}},
{"pyo3::pyfunction", {
"pyo3.Python.with_gil",
NULL
}},
{"pyo3::pymethods", {
"pyo3.Python.with_gil",
NULL
}},
};
#define ATTR_SYNTH_COUNT (int)(sizeof(ATTR_SYNTH) / sizeof(ATTR_SYNTH[0]))
/* Inspect a definition's decorators and, if any matches a curated
* proc-macro attribute, emit synthetic edges from that definition's
* QN to the wrapper functions the macro would have injected. The
* edges are best-effort and tagged `lsp_proc_macro` so consumers can
* distinguish them from direct calls. */
void cbm_rust_synth_proc_macro_edges(CBMArena* arena, CBMFileResult* result) {
if (!arena || !result) return;
for (int i = 0; i < result->defs.count; i++) {
CBMDefinition* d = &result->defs.items[i];
if (!d->decorators || !d->qualified_name) continue;
for (int di = 0; d->decorators[di]; di++) {
const char* dec = d->decorators[di];
for (int t = 0; t < ATTR_SYNTH_COUNT; t++) {
const RustAttrSynth* s = &ATTR_SYNTH[t];
if (!strstr(dec, s->match)) continue;
/* Emit one resolved call per edge. */
for (int e = 0; s->edges[e]; e++) {
CBMResolvedCall rc;
memset(&rc, 0, sizeof(rc));
rc.caller_qn = d->qualified_name;
rc.callee_qn = cbm_arena_strdup(arena, s->edges[e]);
rc.strategy = "lsp_proc_macro";
rc.confidence = 0.78f; /* high-but-not-direct */
cbm_resolvedcall_push(&result->resolved_calls, arena, rc);
}
}
}
}
}
* Kept as a separate compilation unit because lsp_all.c includes the historic
* module directly; the empty policy boundary also makes accidental synthesis
* conspicuous in review. */
+62 -1
View File
@@ -33,7 +33,8 @@ static CBMScopeChunk* alloc_chunk(CBMScope* scope) {
return c;
}
void cbm_scope_bind(CBMScope* scope, const char* name, const CBMType* type) {
static void cbm_scope_bind_value(CBMScope *scope, const char *name, const CBMType *type,
const char *callable_qn) {
if (!scope || !name) {
return;
}
@@ -41,6 +42,7 @@ void cbm_scope_bind(CBMScope* scope, const char* name, const CBMType* type) {
for (int i = 0; i < c->used; i++) {
if (c->bindings[i].name && strcmp(c->bindings[i].name, name) == 0) {
c->bindings[i].type = type;
c->bindings[i].callable_qn = callable_qn;
return;
}
}
@@ -54,9 +56,19 @@ void cbm_scope_bind(CBMScope* scope, const char* name, const CBMType* type) {
}
head->bindings[head->used].name = name;
head->bindings[head->used].type = type;
head->bindings[head->used].callable_qn = callable_qn;
head->used++;
}
void cbm_scope_bind(CBMScope *scope, const char *name, const CBMType *type) {
cbm_scope_bind_value(scope, name, type, NULL);
}
void cbm_scope_bind_callable(CBMScope *scope, const char *name, const CBMType *type,
const char *callable_qn) {
cbm_scope_bind_value(scope, name, type, callable_qn);
}
const CBMType* cbm_scope_lookup(const CBMScope* scope, const char* name) {
if (!name) {
return cbm_type_unknown();
@@ -72,3 +84,52 @@ const CBMType* cbm_scope_lookup(const CBMScope* scope, const char* name) {
}
return cbm_type_unknown();
}
bool cbm_scope_contains(const CBMScope *scope, const char *name) {
if (!name) {
return false;
}
for (const CBMScope *s = scope; s != NULL; s = s->parent) {
for (const CBMScopeChunk *c = s->chunks; c != NULL; c = c->next) {
for (int i = 0; i < c->used; i++) {
if (c->bindings[i].name && strcmp(c->bindings[i].name, name) == 0) {
return true;
}
}
}
}
return false;
}
const char *cbm_scope_lookup_callable(const CBMScope *scope, const char *name) {
if (!name) {
return NULL;
}
for (const CBMScope *s = scope; s != NULL; s = s->parent) {
for (const CBMScopeChunk *c = s->chunks; c != NULL; c = c->next) {
for (int i = 0; i < c->used; i++) {
if (c->bindings[i].name && strcmp(c->bindings[i].name, name) == 0) {
return c->bindings[i].callable_qn;
}
}
}
}
return NULL;
}
bool cbm_scope_update_callable(CBMScope *scope, const char *name, const char *callable_qn) {
if (!name) {
return false;
}
for (CBMScope *s = scope; s != NULL; s = s->parent) {
for (CBMScopeChunk *c = s->chunks; c != NULL; c = c->next) {
for (int i = 0; i < c->used; i++) {
if (c->bindings[i].name && strcmp(c->bindings[i].name, name) == 0) {
c->bindings[i].callable_qn = callable_qn;
return true;
}
}
}
}
return false;
}
+19
View File
@@ -8,6 +8,11 @@
typedef struct {
const char* name;
const CBMType* type;
/* Exact callable value carried by this lexical binding, or NULL when the
* binding is not proven to denote one callable. This is deliberately
* identity metadata rather than another CBMType kind: aliases need both
* their ordinary type and the graph QN of the value they reference. */
const char *callable_qn;
} CBMVarBinding;
#define CBM_SCOPE_CHUNK_BINDINGS 16
@@ -56,6 +61,20 @@ static inline int cbm_lsp_max_walk_depth(void) {
CBMScope* cbm_scope_push(CBMArena* a, CBMScope* current);
CBMScope* cbm_scope_pop(CBMScope* scope);
void cbm_scope_bind(CBMScope* scope, const char* name, const CBMType* type);
/* Bind a value whose identity is one exact callable. A later ordinary
* cbm_scope_bind of the same name clears this identity, so reassignment fails
* closed instead of leaking a stale alias target. */
void cbm_scope_bind_callable(CBMScope *scope, const char *name, const CBMType *type,
const char *callable_qn);
const CBMType* cbm_scope_lookup(const CBMScope* scope, const char* name);
/* True when any lexical frame contains name, even when its type is UNKNOWN. */
bool cbm_scope_contains(const CBMScope *scope, const char *name);
/* Return the exact callable QN from the nearest binding. A nearer ordinary
* binding shadows a parent's callable and therefore returns NULL. */
const char *cbm_scope_lookup_callable(const CBMScope *scope, const char *name);
/* Replace (or clear with NULL) callable identity on the nearest existing
* lexical binding. Returns false when name is unbound. This is for assignment;
* declarations should continue to use cbm_scope_bind[_callable]. */
bool cbm_scope_update_callable(CBMScope *scope, const char *name, const char *callable_qn);
#endif // CBM_LSP_SCOPE_H
+677 -131
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -40,6 +40,10 @@ typedef struct {
// Output: resolved calls accumulate here.
CBMResolvedCallArray *resolved_calls;
// Optional raw usages owned by the per-file extraction result. The scope
// pass marks occurrence-exact module/import shadows without emitting a
// semantic reference record for them.
CBMUsageArray *usages;
// Type-parameter scope (innermost generic function/class).
// type_param_constraints may be NULL or shorter — entries default to "any".
@@ -51,6 +55,7 @@ typedef struct {
bool js_mode;
bool jsx_mode;
bool dts_mode;
bool cross_file_mode; // project-wide registry is available for exact reference proof
bool strict; // tsconfig "strict": true → fewer implicit-any fallbacks
bool debug; // CBM_LSP_DEBUG env
+115
View File
@@ -286,6 +286,58 @@ int cbm_free_func_iter_next(CBMFreeFuncIter *it) {
return -1;
}
void cbm_registry_methods(const CBMTypeRegistry *reg, const char *receiver_qn,
const char *method_name, CBMMethodIter *out) {
memset(out, 0, sizeof(*out));
out->reg = reg;
out->receiver_qn = receiver_qn;
out->method_name = method_name;
out->chain_idx = -1;
if (!reg || !receiver_qn || !method_name) {
return;
}
out->hash = fnv1a_pair(receiver_qn, method_name);
if (reg->method_buckets && reg->method_bucket_count > 0) {
int slot = (int)(out->hash & (uint64_t)(reg->method_bucket_count - 1));
out->chain_idx = reg->method_buckets[slot];
out->tail_i = reg->func_qn_entry_count;
out->tail_end = reg->func_count;
} else {
out->tail_i = 0;
out->tail_end = reg->func_count;
}
}
int cbm_method_iter_next(CBMMethodIter *it) {
if (!it || !it->reg || !it->receiver_qn || !it->method_name) {
return -1;
}
const CBMTypeRegistry *reg = it->reg;
while (it->chain_idx >= 0) {
const CBMRegistryHashEntry *e = &reg->method_entries[it->chain_idx];
int p = e->payload_index;
uint64_t h = e->hash;
it->chain_idx = e->next_index;
if (h != it->hash) {
continue;
}
const CBMRegisteredFunc *f = &reg->funcs[p];
if (f->receiver_type && f->short_name && strcmp(f->receiver_type, it->receiver_qn) == 0 &&
strcmp(f->short_name, it->method_name) == 0) {
return p;
}
}
while (it->tail_i < it->tail_end) {
int p = it->tail_i++;
const CBMRegisteredFunc *f = &reg->funcs[p];
if (f->receiver_type && f->short_name && strcmp(f->receiver_type, it->receiver_qn) == 0 &&
strcmp(f->short_name, it->method_name) == 0) {
return p;
}
}
return -1;
}
void cbm_registry_finalize_into(CBMTypeRegistry *reg, CBMArena *idx_arena) {
if (!reg || !idx_arena)
return;
@@ -604,6 +656,27 @@ const CBMRegisteredFunc *cbm_registry_lookup_method_by_args(const CBMTypeRegistr
range_pi = pi;
}
}
/* Mirror lookup_method_self's post-finalize visibility contract. The
* hash chain covers the finalize-time prefix only; overloads added
* later must participate in the same exact/range/first ordering. */
for (int pi = reg->func_qn_entry_count; pi < reg->func_count; pi++) {
const CBMRegisteredFunc *f = &reg->funcs[pi];
if (!f->receiver_type || !f->short_name || strcmp(f->receiver_type, receiver_qn) != 0 ||
strcmp(f->short_name, method_name) != 0)
continue;
if (first_pi < 0 || pi < first_pi)
first_pi = pi;
int pc = count_func_params(f);
if (pc == arg_count) {
if (exact_pi < 0 || pi < exact_pi)
exact_pi = pi;
continue;
}
int min_pc = (f->min_params >= 0) ? f->min_params : pc;
if (arg_count >= min_pc && arg_count <= pc && (range_pi < 0 || pi < range_pi)) {
range_pi = pi;
}
}
int sel = exact_pi >= 0 ? exact_pi : (range_pi >= 0 ? range_pi : first_pi);
if (sel >= 0)
return &reg->funcs[sel];
@@ -776,6 +849,19 @@ const CBMRegisteredFunc *cbm_registry_lookup_method_by_types(const CBMTypeRegist
best_pi = pi;
}
}
for (int pi = reg->func_qn_entry_count; pi < reg->func_count; pi++) {
const CBMRegisteredFunc *f = &reg->funcs[pi];
if (!f->receiver_type || !f->short_name || strcmp(f->receiver_type, receiver_qn) != 0 ||
strcmp(f->short_name, method_name) != 0)
continue;
if (first_pi < 0 || pi < first_pi)
first_pi = pi;
int s = score_overload_match(f, arg_types, arg_count);
if (s > best_score || (s == best_score && best_pi >= 0 && pi < best_pi)) {
best_score = s;
best_pi = pi;
}
}
// best_score starts at 0; a score of 0 means "wrong arg count" → no
// match, exactly as the linear scan (which never sets best on s==0).
int sel = (best_pi >= 0 && best_score > 0) ? best_pi : first_pi;
@@ -857,6 +943,18 @@ const CBMRegisteredFunc *cbm_registry_lookup_symbol_by_types(const CBMTypeRegist
best_pi = pi;
}
}
for (int pi = reg->func_qn_entry_count; pi < reg->func_count; pi++) {
const CBMRegisteredFunc *f = &reg->funcs[pi];
if (!f->qualified_name || strcmp(f->qualified_name, buf) != 0)
continue;
if (first_pi < 0 || pi < first_pi)
first_pi = pi;
int s = score_overload_match(f, arg_types, arg_count);
if (s > best_score || (s == best_score && best_pi >= 0 && pi < best_pi)) {
best_score = s;
best_pi = pi;
}
}
int sel = (best_pi >= 0 && best_score > 0) ? best_pi : first_pi;
if (sel >= 0)
return &reg->funcs[sel];
@@ -935,6 +1033,23 @@ const CBMRegisteredFunc *cbm_registry_lookup_symbol_by_args(const CBMTypeRegistr
range_pi = pi;
}
}
for (int pi = reg->func_qn_entry_count; pi < reg->func_count; pi++) {
const CBMRegisteredFunc *f = &reg->funcs[pi];
if (!f->qualified_name || strcmp(f->qualified_name, buf) != 0)
continue;
if (first_pi < 0 || pi < first_pi)
first_pi = pi;
int pc = count_func_params(f);
if (pc == arg_count) {
if (exact_pi < 0 || pi < exact_pi)
exact_pi = pi;
continue;
}
int min_pc = (f->min_params >= 0) ? f->min_params : pc;
if (arg_count >= min_pc && arg_count <= pc && (range_pi < 0 || pi < range_pi)) {
range_pi = pi;
}
}
int sel = exact_pi >= 0 ? exact_pi : (range_pi >= 0 ? range_pi : first_pi);
if (sel >= 0)
return &reg->funcs[sel];
+38 -10
View File
@@ -5,18 +5,24 @@
#include "../arena.h"
#include <stdbool.h>
// Decorator-derived flags (Python). Added at struct tail so existing
// Language-specific function metadata. Added at struct tail so existing
// callers that memset to zero before populating other fields keep working.
typedef enum {
CBM_FUNC_FLAG_NONE = 0,
CBM_FUNC_FLAG_PROPERTY = 1 << 0, // @property -> obj.attr returns getter return
CBM_FUNC_FLAG_CLASSMETHOD = 1 << 1, // @classmethod -> first arg is cls (the class)
CBM_FUNC_FLAG_STATICMETHOD = 1 << 2, // @staticmethod -> no implicit self/cls
CBM_FUNC_FLAG_ABSTRACTMETHOD = 1 << 3, // @abstractmethod -> still callable for resolution
CBM_FUNC_FLAG_OVERLOAD = 1 << 4, // @overload entry — non-implementation stub
CBM_FUNC_FLAG_ASYNC = 1 << 5, // async def — return is Coroutine[..., T]
CBM_FUNC_FLAG_GENERATOR = 1 << 6, // contains yield — return is Generator[T, ...]
CBM_FUNC_FLAG_FINAL = 1 << 7, // @final — overrides not allowed
CBM_FUNC_FLAG_PROPERTY = 1 << 0, // @property -> obj.attr returns getter return
CBM_FUNC_FLAG_CLASSMETHOD = 1 << 1, // @classmethod -> first arg is cls (the class)
CBM_FUNC_FLAG_STATICMETHOD = 1 << 2, // @staticmethod -> no implicit self/cls
CBM_FUNC_FLAG_ABSTRACTMETHOD = 1 << 3, // @abstractmethod -> still callable for resolution
CBM_FUNC_FLAG_OVERLOAD = 1 << 4, // @overload entry — non-implementation stub
CBM_FUNC_FLAG_ASYNC = 1 << 5, // async def — return is Coroutine[..., T]
CBM_FUNC_FLAG_GENERATOR = 1 << 6, // contains yield — return is Generator[T, ...]
CBM_FUNC_FLAG_FINAL = 1 << 7, // @final — overrides not allowed
CBM_FUNC_FLAG_RUST_TRAIT_IMPL = 1 << 8, // exact method from impl Trait for Type
CBM_FUNC_FLAG_RUST_ABSTRACT = 1 << 9, // required trait method without a default body
/* Python only: more than one registered definition has this exact QN.
* Ordinary call resolution keeps its historical language-specific choice,
* but a function value cannot name one materialized definition exactly. */
CBM_FUNC_FLAG_AMBIGUOUS_BINDING = 1 << 10,
} CBMFuncFlags;
// Registered function/method with full type signature.
@@ -27,9 +33,13 @@ typedef struct {
const CBMType *signature; // FUNC type with param/return types
const char **type_param_names; // NULL-terminated, e.g., ["T", "R", NULL] for generics
int min_params; // Minimum required params (excluding defaulted). -1 = unknown.
int flags; // CBM_FUNC_FLAG_* bitfield (Python decorator info; 0 elsewhere)
int flags; // CBM_FUNC_FLAG_* bitfield
const char **decorator_qns; // NULL-terminated decorator QNs (Python only); used for
// user-decorator return-type substitution.
/* Rust only: canonical trait QN for a concrete trait-impl method. It may
* remain NULL when raw cross-file provenance is ambiguous; the Rust trait
* flag still prevents that method from being mistaken for inherent. */
const char *impl_trait_qn;
} CBMRegisteredFunc;
// Registered type with fields and method names.
@@ -242,6 +252,24 @@ void cbm_registry_free_funcs_by_short_name(const CBMTypeRegistry *reg, const cha
CBMFreeFuncIter *out);
int cbm_free_func_iter_next(CBMFreeFuncIter *it);
// Iterate function indices for one exact (receiver QN, method name) key. This
// exposes the existing finalized method bucket without making Rust scan the
// project-wide func array merely to distinguish inherent and trait-impl
// entries that intentionally share the same source-level QN. The caller may
// filter on language-specific flags. Read-only and allocation-free.
typedef struct {
const CBMTypeRegistry *reg;
const char *receiver_qn;
const char *method_name;
uint64_t hash;
int chain_idx;
int tail_i;
int tail_end;
} CBMMethodIter;
void cbm_registry_methods(const CBMTypeRegistry *reg, const char *receiver_qn,
const char *method_name, CBMMethodIter *out);
int cbm_method_iter_next(CBMMethodIter *it);
// --- TS-specific helpers (return NULL for types without these signatures) ---
// If the type has a call signature (e.g., `interface F { (x:number): string }`), return
+35
View File
@@ -115,6 +115,41 @@ const CBMType *cbm_type_func(CBMArena *a, const char **param_names, const CBMTyp
return t;
}
const CBMType **cbm_type_materialize_signature_params(CBMArena *a, const char *const *type_texts,
int count, CBMTypeTextParser parser,
void *parser_ctx) {
if (count <= 0)
return NULL;
const CBMType **types =
(const CBMType **)cbm_arena_alloc(a, ((size_t)count + 1) * sizeof(const CBMType *));
if (!types)
return NULL;
for (int i = 0; i < count; i++) {
const char *text = type_texts ? type_texts[i] : NULL;
if (!text || text[0] == '\0' || strcmp(text, "?") == 0) {
types[i] = cbm_type_unknown();
continue;
}
const CBMType *parsed = parser ? parser(a, text, parser_ctx) : NULL;
types[i] = parsed ? parsed : cbm_type_unknown();
}
types[count] = NULL;
return types;
}
const CBMType *cbm_type_func_replace_returns(CBMArena *a, const CBMType *old_signature,
const CBMType *const *new_return_types) {
if (!old_signature || old_signature->kind != CBM_TYPE_FUNC)
return cbm_type_unknown();
/* cbm_type_func only reads and then copies this vector into arena memory. */
return cbm_type_func(a, old_signature->data.func.param_names,
old_signature->data.func.param_types, (const CBMType **)new_return_types);
}
const CBMType *cbm_type_builtin(CBMArena *a, const char *name) {
CBMType *t = (CBMType *)cbm_arena_alloc(a, sizeof(CBMType));
if (!t)
+11
View File
@@ -45,6 +45,9 @@ typedef enum {
// Forward declaration
typedef struct CBMType CBMType;
// Language-specific adapter used to parse one ordered signature type spelling.
typedef const CBMType *(*CBMTypeTextParser)(CBMArena *arena, const char *text, void *parser_ctx);
// CBMTypeParam represents a generic type parameter with optional constraint.
typedef struct {
const char* name; // "T", "K", "V"
@@ -153,6 +156,14 @@ const CBMType* cbm_type_slice(CBMArena* a, const CBMType* elem);
const CBMType* cbm_type_map(CBMArena* a, const CBMType* key, const CBMType* value);
const CBMType* cbm_type_channel(CBMArena* a, const CBMType* elem, int direction);
const CBMType* cbm_type_func(CBMArena* a, const char** param_names, const CBMType** param_types, const CBMType** return_types);
// Materialize exactly count positional parameter slots. NULL, empty, exact "?",
// and parser failures become UNKNOWN; the returned vector is NULL-terminated.
const CBMType **cbm_type_materialize_signature_params(CBMArena *a, const char *const *type_texts,
int count, CBMTypeTextParser parser,
void *parser_ctx);
// Rebuild a FUNC with new returns while preserving its parameter names/types.
const CBMType *cbm_type_func_replace_returns(CBMArena *a, const CBMType *old_signature,
const CBMType *const *new_return_types);
const CBMType* cbm_type_builtin(CBMArena* a, const char* name);
const CBMType* cbm_type_tuple(CBMArena* a, const CBMType** elems, int count);
const CBMType* cbm_type_type_param(CBMArena* a, const char* name);
+41 -13
View File
@@ -1,10 +1,11 @@
#!/usr/bin/env bash
# repro.sh — Build + run the cumulative BUG-REPRODUCTION suite (test-repro).
#
# Unlike test.sh (the gating suite, must be GREEN), this suite is RED by design:
# every case reproduces an open bug. So we distinguish two outcomes:
# Unlike test.sh (the gating suite, must be GREEN), this suite tracks open RED
# reproductions alongside GREEN controls and regression guards. We distinguish:
# - BUILD/LINK failure → real breakage → exit non-zero (fail the CI job).
# - Test redness → EXPECTED → report the count, exit 0 (green board).
# - Reproduced RED cases → expected board data → report the count, exit 0.
# - Any skipped case → incomplete board → report it, exit non-zero.
#
# Usage: scripts/repro.sh [CC=clang] [CXX=clang++] [--arch arm64|x86_64]
set -uo pipefail
@@ -37,7 +38,12 @@ done
print_env "repro.sh"
verify_compiler "$CC"
OUT="$ROOT/repro-out.txt"
OUT="$(mktemp "${TMPDIR:-/tmp}/cbm-repro-out.XXXXXX")"
if [[ -z "$OUT" ]]; then
echo "::error::unable to create bug-repro transcript"
exit 1
fi
trap 'rm -f "$OUT"' EXIT
# A RED reproduction fails its assertion and returns EARLY — before any cleanup —
# so LeakSanitizer would flag benign harness leaks on every red store-level test
# and abort. The board's signal is the FAIL rows, not leak-cleanliness (the leak
@@ -50,23 +56,45 @@ set +e
make -j"$NPROC" -f Makefile.cbm test-repro $MAKE_ARGS 2>&1 | tee "$OUT"
set -e
# The runner prints a "<N> passed[, <M> failed]" summary line only if it actually
# ran. No summary line ⇒ the build/link failed ⇒ real breakage.
if ! grep -qE '[0-9]+ passed' "$OUT"; then
# The runner's final aggregate is the only line that starts with the count.
# Per-suite lines also contain "N passed, M failed", so selecting the first
# match undercounts every multi-suite run.
summary_line="$(grep -E '^[[:space:]]*[0-9]+ passed(, [0-9]+ failed)?(, [0-9]+ skipped)?[[:space:]]*$' "$OUT" | tail -1 || true)"
if [[ -z "$summary_line" ]]; then
echo "::error::bug-repro runner did not execute — build or link failure"
exit 1
fi
reproduced=$(grep -oE '[0-9]+ failed' "$OUT" | head -1 | grep -oE '[0-9]+' || echo 0)
green=$(grep -oE '[0-9]+ passed' "$OUT" | head -1 | grep -oE '[0-9]+' || echo 0)
green="$(printf '%s\n' "$summary_line" | grep -oE '[0-9]+ passed' | grep -oE '[0-9]+')"
reproduced="$(printf '%s\n' "$summary_line" | grep -oE '[0-9]+ failed' | grep -oE '[0-9]+' || echo 0)"
skipped="$(printf '%s\n' "$summary_line" | grep -oE '[0-9]+ skipped' | grep -oE '[0-9]+' || echo 0)"
{
if ((green + reproduced == 0)); then
echo "::error::bug-repro runner executed zero tests"
exit 1
fi
emit_board_summary() {
echo "## Bug-reproduction board — ${OS:-$(uname -s)} ${ARCH:-}"
echo ""
echo "- **${reproduced}** open bug(s) still reproduced (RED — expected)"
echo "- **${green}** case(s) PASSING — candidate-fixed → verify + close the issue + promote the guard to the gating suite"
} >> "${GITHUB_STEP_SUMMARY:-/dev/stderr}"
echo "- **${green}** case(s) passing control(s) or candidate-fix candidate(s); verify each RED→GREEN transition before closing an issue"
if ((skipped > 0)); then
echo "- **${skipped}** case(s) SKIPPED — board is incomplete"
fi
}
if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then
emit_board_summary >> "$GITHUB_STEP_SUMMARY"
else
emit_board_summary >&2
fi
echo "=== bug-repro board: ${reproduced} reproduced (RED), ${green} passing control(s) or candidate-fix candidate(s) ==="
if ((skipped > 0)); then
echo "::warning::bug-repro board incomplete — ${skipped} case(s) skipped"
exit 1
fi
echo "=== bug-repro board: ${reproduced} reproduced (RED), ${green} passing (candidate-fixed) ==="
# Green board: the suite ran. Redness is the data, not a job failure.
exit 0
+1 -1
View File
@@ -576,7 +576,7 @@ static const char skill_content[] =
"\n"
"## Edge Types\n"
"CALLS, HTTP_CALLS, ASYNC_CALLS, DATA_FLOWS, IMPORTS, DEFINES, DEFINES_METHOD,\n"
"HANDLES, IMPLEMENTS, OVERRIDE, USAGE, CONFIGURES, FILE_CHANGES_WITH,\n"
"HANDLES, IMPLEMENTS, OVERRIDE, USAGE, CALL_REFERENCE, CONFIGURES, FILE_CHANGES_WITH,\n"
"SIMILAR_TO, SEMANTICALLY_RELATED, CONTAINS_FILE, CONTAINS_FOLDER,\n"
"CONTAINS_PACKAGE\n"
"\n"
+1 -1
View File
@@ -48,7 +48,7 @@ static const char *ALWAYS_SKIP_DIRS[] = {
/* Deploy */
".vercel", ".netlify", "deploy", "deployed",
/* Misc */
".qdrant_code_embeddings", ".tmp", "vendor", "vendored", NULL};
".codebase-memory", ".qdrant_code_embeddings", ".tmp", "vendor", "vendored", NULL};
static const char *FAST_SKIP_DIRS[] = {
"generated", "gen", "auto-generated", "fixtures", "testdata", "test_data",
+33 -3
View File
@@ -14,6 +14,7 @@
#include "foundation/constants.h"
#include "foundation/platform.h" /* cbm_safe_getenv */
#include "foundation/compat_fs.h"
#include "foundation/sha256.h"
enum { MAX_CONFIG_SIZE = 65536 };
#include "foundation/log.h"
@@ -29,6 +30,26 @@ enum { MAX_CONFIG_SIZE = 65536 };
static const cbm_userconfig_t *g_userconfig = NULL;
static void userconfig_source_digest(const char *state, const void *bytes, size_t len,
char out[CBM_SHA256_HEX_LEN + 1]) {
static const char domain[] = "cbm-userconfig-source-v1";
cbm_sha256_ctx sha;
cbm_sha256_init(&sha);
cbm_sha256_update(&sha, domain, sizeof(domain));
cbm_sha256_update(&sha, state, strlen(state) + 1);
if (bytes && len > 0) {
cbm_sha256_update(&sha, bytes, len);
}
uint8_t digest[CBM_SHA256_DIGEST_LEN];
cbm_sha256_final(&sha, digest);
static const char hex[] = "0123456789abcdef";
for (int i = 0; i < CBM_SHA256_DIGEST_LEN; i++) {
out[i * 2] = hex[digest[i] >> 4];
out[i * 2 + 1] = hex[digest[i] & 0x0f];
}
out[CBM_SHA256_HEX_LEN] = '\0';
}
void cbm_set_user_lang_config(const cbm_userconfig_t *cfg) {
g_userconfig = cfg;
}
@@ -232,7 +253,9 @@ static int parse_extra_extensions(yyjson_val *root, cbm_userext_t **entries, int
* Silently ignores missing files. Logs warnings for corrupt JSON.
* Returns 0 on success (or absent file), -1 on alloc failure.
*/
static int load_config_file(const char *path, cbm_userext_t **entries, int *count) {
static int load_config_file(const char *path, cbm_userext_t **entries, int *count,
char source_sha256[CBM_SHA256_HEX_LEN + 1]) {
userconfig_source_digest("missing-or-unreadable", NULL, 0, source_sha256);
FILE *f = cbm_fopen(path, "rb");
if (!f) {
return 0; /* file absent — silently ignore */
@@ -240,11 +263,13 @@ static int load_config_file(const char *path, cbm_userext_t **entries, int *coun
if (fseek(f, 0, SEEK_END) != 0) {
(void)fclose(f);
userconfig_source_digest("seek-error", NULL, 0, source_sha256);
return 0;
}
long len = ftell(f);
if (fseek(f, 0, SEEK_SET) != 0) {
(void)fclose(f);
userconfig_source_digest("seek-error", NULL, 0, source_sha256);
return 0;
}
@@ -252,6 +277,9 @@ static int load_config_file(const char *path, cbm_userext_t **entries, int *coun
(void)fclose(f);
if (len > MAX_CONFIG_SIZE) {
cbm_log_warn("userconfig.file_too_large", "path", path);
userconfig_source_digest("oversized", NULL, 0, source_sha256);
} else {
userconfig_source_digest("empty", NULL, 0, source_sha256);
}
return 0;
}
@@ -268,6 +296,7 @@ static int load_config_file(const char *path, cbm_userext_t **entries, int *coun
nread = (size_t)len;
}
buf[nread] = '\0';
userconfig_source_digest("present", buf, nread, source_sha256);
yyjson_doc *doc = yyjson_read(buf, nread, 0);
free(buf);
@@ -301,7 +330,7 @@ cbm_userconfig_t *cbm_userconfig_load(const char *repo_path) {
char global_path[PATH_BUF_SZ];
snprintf(global_path, sizeof(global_path), "%s/codebase-memory-mcp/config.json", cfg_fallback);
if (load_config_file(global_path, &entries, &count) != 0) {
if (load_config_file(global_path, &entries, &count, cfg->global_source_sha256) != 0) {
for (int i = 0; i < count; i++) {
free(entries[i].ext);
}
@@ -313,11 +342,12 @@ cbm_userconfig_t *cbm_userconfig_load(const char *repo_path) {
int global_count = count; /* entries[0..global_count) are from global */
/* ── Step 2: Load project config ── */
userconfig_source_digest("not-applicable", NULL, 0, cfg->project_source_sha256);
if (repo_path && repo_path[0]) {
char project_path[PATH_BUF_SZ];
snprintf(project_path, sizeof(project_path), "%s/.codebase-memory.json", repo_path);
if (load_config_file(project_path, &entries, &count) != 0) {
if (load_config_file(project_path, &entries, &count, cfg->project_source_sha256) != 0) {
/* Free already-allocated entries */
for (int i = 0; i < count; i++) {
free(entries[i].ext);
+4
View File
@@ -18,6 +18,7 @@
#define CBM_USERCONFIG_H
#include "cbm.h" /* CBMLanguage */
#include "foundation/sha256.h"
/* ── Types ──────────────────────────────────────────────────────── */
@@ -29,6 +30,9 @@ typedef struct {
typedef struct {
cbm_userext_t *entries; /* heap-allocated array */
int count; /* number of entries */
/* Digests of the exact bytes/state consumed by cbm_userconfig_load(). */
char global_source_sha256[CBM_SHA256_HEX_LEN + 1];
char project_source_sha256[CBM_SHA256_HEX_LEN + 1];
} cbm_userconfig_t;
/* ── API ────────────────────────────────────────────────────────── */
+125 -8
View File
@@ -11,6 +11,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#ifdef _WIN32
@@ -118,6 +119,40 @@ cbm_dirent_t *cbm_readdir(cbm_dir_t *d) {
return &d->entry;
}
int cbm_path_info_utf8(const char *path, cbm_path_info_t *out) {
if (!path || !out) {
return CBM_NOT_FOUND;
}
wchar_t *wpath = cbm_utf8_to_wide(path);
if (!wpath) {
return CBM_NOT_FOUND;
}
WIN32_FILE_ATTRIBUTE_DATA data;
BOOL ok = GetFileAttributesExW(wpath, GetFileExInfoStandard, &data);
free(wpath);
if (!ok) {
return CBM_NOT_FOUND;
}
memset(out, 0, sizeof(*out));
out->is_directory = (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
out->is_symlink = (data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0;
out->is_regular = !out->is_directory && !out->is_symlink;
ULARGE_INTEGER file_size;
file_size.LowPart = data.nFileSizeLow;
file_size.HighPart = data.nFileSizeHigh;
out->size = (int64_t)file_size.QuadPart;
ULARGE_INTEGER written;
written.LowPart = data.ftLastWriteTime.dwLowDateTime;
written.HighPart = data.ftLastWriteTime.dwHighDateTime;
enum { NANOSECONDS_PER_WINDOWS_TICK = 100 };
const uint64_t windows_to_unix_ticks = UINT64_C(116444736000000000);
out->mtime_ns =
written.QuadPart >= windows_to_unix_ticks
? (int64_t)((written.QuadPart - windows_to_unix_ticks) * NANOSECONDS_PER_WINDOWS_TICK)
: 0;
return 0;
}
void cbm_closedir(cbm_dir_t *d) {
if (d) {
if (d->find_handle != INVALID_HANDLE_VALUE) {
@@ -658,13 +693,51 @@ cbm_dirent_t *cbm_readdir(cbm_dir_t *d) {
}
memcpy(d->entry.name, de->d_name, nlen);
d->entry.name[nlen] = '\0';
d->entry.is_dir = (de->d_type == DT_DIR);
d->entry.d_type = de->d_type;
unsigned char type = de->d_type;
#if defined(DT_UNKNOWN) && defined(AT_SYMLINK_NOFOLLOW)
if (type == DT_UNKNOWN) {
struct stat state;
if (fstatat(dirfd(d->dir), de->d_name, &state, AT_SYMLINK_NOFOLLOW) == 0) {
if (S_ISDIR(state.st_mode)) {
type = DT_DIR;
} else if (S_ISREG(state.st_mode)) {
type = DT_REG;
} else if (S_ISLNK(state.st_mode)) {
type = DT_LNK;
}
}
}
#endif
d->entry.is_dir = (type == DT_DIR);
d->entry.d_type = type;
return &d->entry;
}
return NULL;
}
int cbm_path_info_utf8(const char *path, cbm_path_info_t *out) {
if (!path || !out) {
return CBM_NOT_FOUND;
}
struct stat state;
if (lstat(path, &state) != 0) {
return CBM_NOT_FOUND;
}
memset(out, 0, sizeof(*out));
out->is_regular = S_ISREG(state.st_mode);
out->is_directory = S_ISDIR(state.st_mode);
out->is_symlink = S_ISLNK(state.st_mode);
out->size = (int64_t)state.st_size;
#ifdef __APPLE__
out->mtime_ns = ((int64_t)state.st_mtimespec.tv_sec * INT64_C(1000000000)) +
(int64_t)state.st_mtimespec.tv_nsec;
#else
out->mtime_ns =
((int64_t)state.st_mtim.tv_sec * INT64_C(1000000000)) + (int64_t)state.st_mtim.tv_nsec;
#endif
return 0;
}
void cbm_closedir(cbm_dir_t *d) {
if (d) {
if (d->dir) {
@@ -871,23 +944,67 @@ int cbm_rename_replace(const char *src, const char *dst) {
#endif
}
int cbm_rename_noreplace(const char *src, const char *dst) {
if (!src || !dst || !src[0] || !dst[0]) {
return CBM_NOT_FOUND;
}
#ifdef _WIN32
wchar_t *wsrc = cbm_utf8_to_wide(src);
wchar_t *wdst = cbm_utf8_to_wide(dst);
int ret = CBM_NOT_FOUND;
if (wsrc && wdst) {
ret = MoveFileExW(wsrc, wdst, MOVEFILE_COPY_ALLOWED | MOVEFILE_WRITE_THROUGH)
? 0
: CBM_NOT_FOUND;
}
free(wsrc);
free(wdst);
return ret;
#else
/* link()+unlink() provides no-overwrite semantics portably (including
* macOS, where renameat2(RENAME_NOREPLACE) is unavailable). Both paths
* are adjacent database files and therefore on the same filesystem. */
if (link(src, dst) != 0) {
return CBM_NOT_FOUND;
}
if (unlink(src) != 0) {
int saved_errno = errno;
(void)unlink(dst);
errno = saved_errno;
return CBM_NOT_FOUND;
}
return 0;
#endif
}
/* Remove a SQLite database's -wal/-shm sidecars (both platforms). Any code
* path that installs a FRESH database file where a previous generation lived
* must remove them before the new generation can be opened: SQLite decides
* whether to replay a WAL purely from the sidecar's own header/checksums, so
* a leftover WAL can splice old-generation pages into the new file (#897). */
void cbm_remove_db_sidecars(const char *db_path) {
int cbm_remove_db_sidecars_checked(const char *db_path) {
if (!db_path || !db_path[0]) {
return;
return CBM_NOT_FOUND;
}
enum { SIDECAR_PATH_MAX = 4096 };
char side[SIDECAR_PATH_MAX];
int n = snprintf(side, sizeof(side), "%s-wal", db_path);
if (n > 0 && (size_t)n < sizeof(side)) {
(void)cbm_unlink(side);
if (n <= 0 || (size_t)n >= sizeof(side)) {
return CBM_NOT_FOUND;
}
if (cbm_unlink(side) != 0 && errno != ENOENT) {
return CBM_NOT_FOUND;
}
n = snprintf(side, sizeof(side), "%s-shm", db_path);
if (n > 0 && (size_t)n < sizeof(side)) {
(void)cbm_unlink(side);
if (n <= 0 || (size_t)n >= sizeof(side)) {
return CBM_NOT_FOUND;
}
if (cbm_unlink(side) != 0 && errno != ENOENT) {
return CBM_NOT_FOUND;
}
return 0;
}
void cbm_remove_db_sidecars(const char *db_path) {
(void)cbm_remove_db_sidecars_checked(db_path);
}
+22 -1
View File
@@ -9,12 +9,13 @@
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
/* ── Directory iteration ──────────────────────────────────────── */
/* Max filename length (MAX_PATH on Windows, NAME_MAX on POSIX). */
#define CBM_DIRENT_NAME_MAX 260
#define CBM_DIRENT_NAME_MAX 1024
typedef struct cbm_dir cbm_dir_t;
@@ -24,6 +25,20 @@ typedef struct {
unsigned char d_type; /* DT_REG, DT_DIR, DT_LNK, etc. (POSIX only, 0 on Windows) */
} cbm_dirent_t;
/* Locale-independent metadata for a UTF-8 path. Symlinks/reparse points are
* reported rather than followed so semantic-input walkers cannot leave the
* repository through an alias. mtime_ns is Unix-epoch nanoseconds. */
typedef struct {
bool is_regular;
bool is_directory;
bool is_symlink;
int64_t size;
int64_t mtime_ns;
} cbm_path_info_t;
/* Returns 0 on success and -1 when the path cannot be inspected. */
int cbm_path_info_utf8(const char *path, cbm_path_info_t *out);
/* Open a directory for iteration. Returns NULL on error. */
cbm_dir_t *cbm_opendir(const char *path);
@@ -50,9 +65,15 @@ int cbm_unlink(const char *path);
* do this before the new generation can be opened; a leftover WAL is
* otherwise replayed on top of the new file (#897). */
void cbm_remove_db_sidecars(const char *db_path);
/* Checked form used at an atomic replacement boundary. Missing sidecars are
* success; any other deletion or path-construction failure is reported. */
int cbm_remove_db_sidecars_checked(const char *db_path);
/* rename() that replaces an existing destination on every platform
* (Windows rename fails with EEXIST; this uses write-through MoveFileExW). */
int cbm_rename_replace(const char *src, const char *dst);
/* Move a regular file only when dst does not exist. Never overwrites dst.
* Used for collision-safe evidence quarantine beside a database. */
int cbm_rename_noreplace(const char *src, const char *dst);
/* Canonicalize an EXISTING path (realpath / wide GetFullPathNameW). Locale-
* independent on Windows — never routes UTF-8 through the ANSI CRT (#973).
* out must be >= 4096 bytes. Returns 1 on success, 0 otherwise. */
+2 -1
View File
@@ -375,7 +375,8 @@ static const tool_def_t TOOLS[] = {
"RESPONSE: prefix-grouped tree rows by default — a shared (qn-prefix, file) group "
"header printed once, then `name label lines in out` per row (full qn = group prefix "
"+ dot + name). in/out = TOTAL degree across ALL edge types (DEFINES, "
"USAGE, CALLS, ...), NOT caller/callee counts — use trace_path for callers. Add per-node "
"USAGE, CALLS, CALL_REFERENCE, ...), NOT caller/callee counts — use trace_path for callers. "
"Add per-node "
"property columns via "
"fields (e.g. [\"complexity\",\"signature\",\"docstring\"]); format=\"json\" returns "
"the SAME tree model as structured JSON. "
+1 -1
View File
@@ -574,7 +574,7 @@ int cbm_artifact_export(const char *db_path, const char *repo_path, const char *
/* Get node/edge counts for metadata */
int nodes = 0;
int edges = 0;
cbm_store_t *count_store = cbm_store_open_path(db_path);
cbm_store_t *count_store = cbm_store_open_path_query(db_path);
if (count_store) {
nodes = cbm_store_count_nodes(count_store, project_name);
edges = cbm_store_count_edges(count_store, project_name);
+706 -66
View File
@@ -25,7 +25,9 @@
#include "graph_buffer/graph_buffer.h"
#include "foundation/constants.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Confidence floor below which LSP-resolved calls are ignored and the
@@ -35,6 +37,16 @@
* (Go, C/C++, Python, PHP). */
#define CBM_LSP_CONFIDENCE_FLOOR 0.6f
#if defined(CBM_CALL_REFERENCE_LOOKUP_TEST_API) && CBM_CALL_REFERENCE_LOOKUP_TEST_API
/* Test-build-only operation counter implemented by pass_usages.c. Keeping the
* increment behind this seam lets the regression measure the real sequential
* and fused-parallel materialization helper without retaining instrumentation
* in production builds. */
void cbm_pipeline_lsp_reference_lookup_test_note_row(void);
#else
static inline void cbm_pipeline_lsp_reference_lookup_test_note_row(void) {}
#endif
/* Bare last segment of a (possibly qualified) name, splitting on the LAST
* member/scope separator. C++ textual callees carry `::` (Class::method,
* Ns::f) and `->` (p->run), while the LSP records dotted internal QNs
@@ -61,6 +73,9 @@ static inline const char *cbm_lsp_bare_segment(const char *name) {
seg = p + SKIP_ONE;
}
}
while (*seg && isspace((unsigned char)*seg)) {
seg++;
}
return seg;
}
@@ -105,11 +120,238 @@ static inline bool cbm_pipeline_lsp_allow_tail_match(CBMLanguage lang) {
return lang == CBM_LANG_JAVA || lang == CBM_LANG_KOTLIN;
}
/* When a JVM callable-reference occurrence has no exact semantic target, its
* ordinary-USAGE fallback must still respect package/import reachability.
* `unique_name` and `suffix_match` are project-wide guesses: admitting either
* would bind an unresolved `::handler` in one package to an unrelated callable
* in another. Other languages retain their legacy value-usage fallback. */
static inline bool cbm_pipeline_call_reference_usage_fallback_allowed(CBMLanguage lang,
const char *strategy) {
if (lang != CBM_LANG_JAVA && lang != CBM_LANG_KOTLIN) {
return true;
}
if (!strategy || !strategy[0]) {
return false;
}
return strcmp(strategy, "unique_name") != 0 && strcmp(strategy, "suffix_match") != 0;
}
static inline bool cbm_pipeline_reference_candidate_fallback_allowed(
CBMLanguage lang, const CBMUsage *usage, const char *strategy, const char *const *import_names,
int import_count) {
if (!usage) {
return false;
}
bool reference_candidate =
usage->kind == CBM_USAGE_CALL_REFERENCE ||
(lang == CBM_LANG_KOTLIN && usage->kind == CBM_USAGE_VALUE && usage->may_be_call_reference);
if (!reference_candidate ||
cbm_pipeline_call_reference_usage_fallback_allowed(lang, strategy)) {
return true;
}
/* Kotlin's project import map currently names the imported module rather
* than the member. A syntactically explicit, locally imported `::name`
* may therefore resolve by unique name after LSP proves it is not a
* callable. Admit that ordinary USAGE only when the import map confirms
* the local spelling; an unimported same-name symbol remains unreachable. */
if (lang == CBM_LANG_KOTLIN && usage->ref_name && import_names) {
for (int i = 0; i < import_count; i++) {
if (import_names[i] && strcmp(import_names[i], usage->ref_name) == 0) {
return true;
}
}
}
return false;
}
/* Explicit reference syntax always participates in the semantic join. An
* ordinary value participates only when extraction marked the narrow direct
* argument shape in a language whose resolver can prove callable identity;
* without an exact LSP record it remains USAGE. */
static inline bool cbm_pipeline_usage_semantic_reference_candidate(const CBMUsage *usage) {
if (!usage) {
return false;
}
return usage->kind == CBM_USAGE_CALL_REFERENCE ||
(usage->kind == CBM_USAGE_VALUE && usage->may_be_call_reference);
}
/* Lexical extraction has whole-scope information that a source-order semantic
* walk can lack. In Python and JS/TS, a later local binding makes an earlier
* same-named value local too; an LSP row that still points at the module
* function must not override that proof. A callable-alias row is different:
* it explicitly resolves the local binding itself to its assigned callable. */
static inline bool cbm_pipeline_usage_allows_semantic_reference(const CBMUsage *usage,
const CBMResolvedCall *resolved) {
if (!usage || !resolved) {
return false;
}
if (usage->kind != CBM_USAGE_VALUE || !usage->semantic_reference_blocked ||
!usage->semantic_reference_local_shadow) {
return true;
}
return resolved->strategy && strcmp(resolved->strategy, "lsp_callable_alias") == 0;
}
/* A blocked module value can still retain an ordinary edge to a same-named
* non-callable declaration, but it must never fall back to an executable
* declaration. A local lexical shadow rejects every raw-name fallback because
* even a same-named module variable is a different binding. Exact semantic
* rows are joined before either rule. This label check keeps sequential and
* fused resolution aligned. */
static inline bool cbm_pipeline_node_is_callable_target(const cbm_gbuf_node_t *node) {
if (!node || !node->label) {
return false;
}
return strcmp(node->label, "Function") == 0 || strcmp(node->label, "Method") == 0 ||
strcmp(node->label, "Constructor") == 0 || strcmp(node->label, "Class") == 0;
}
static inline int cbm_pipeline_qn_class_method_tail_eq(const char *qn, const char *tail) {
const char *qt = cbm_pipeline_qn_class_method_tail(qn);
return qt && tail && strcmp(qt, tail) == 0;
}
static inline const cbm_gbuf_node_t *cbm_pipeline_lsp_target_node_policy(
const cbm_gbuf_t *gbuf, const char *project_name, const char *callee_qn, bool allow_tail_match,
bool allow_unique_callable_fallback);
/* Local and cross passes may spell one exact invocation target with and without
* the project prefix. Collapse that cross-language case only when exact graph
* lookup maps both spellings to one materialized node. JVM passes additionally
* reconcile file/project and declared-package QNs below; Class.method-tail
* equality alone is insufficient because two packages can declare the same
* class and method. */
static inline bool cbm_pipeline_invocation_targets_equal(const char *left_qn, const char *right_qn,
const cbm_gbuf_t *gbuf,
const char *project_name,
bool allow_tail_match) {
if (!left_qn || !right_qn) {
return false;
}
if (strcmp(left_qn, right_qn) == 0) {
return true;
}
if (!gbuf || !project_name) {
return false;
}
const cbm_gbuf_node_t *left_exact =
cbm_pipeline_lsp_target_node_policy(gbuf, project_name, left_qn, false, false);
const cbm_gbuf_node_t *right_exact =
cbm_pipeline_lsp_target_node_policy(gbuf, project_name, right_qn, false, false);
if (left_exact && right_exact) {
return left_exact->id == right_exact->id;
}
if (!allow_tail_match) {
return false;
}
const char *left_tail = cbm_pipeline_qn_class_method_tail(left_qn);
const char *right_tail = cbm_pipeline_qn_class_method_tail(right_qn);
if (!left_tail || !right_tail || strcmp(left_tail, right_tail) != 0) {
return false;
}
/* Two package-shaped rows that both reach a node only through the tail
* fallback are still distinct semantic claims. Require at least one row
* to name the materialized node exactly (or by project-prefix retry). */
if (!left_exact && !right_exact) {
return false;
}
const cbm_gbuf_node_t *left =
left_exact ? left_exact
: cbm_pipeline_lsp_target_node_policy(gbuf, project_name, left_qn, true, true);
const cbm_gbuf_node_t *right =
right_exact ? right_exact
: cbm_pipeline_lsp_target_node_policy(gbuf, project_name, right_qn, true, true);
return left && right && left->id == right->id;
}
/* A semantic join is occurrence-exact only when both producers recorded the
* same non-empty source span. Byte spans, unlike line numbers, distinguish
* repeated same-named references/operators on one line. */
static inline bool cbm_pipeline_source_site_present(uint32_t start, uint32_t end) {
return end > start;
}
static inline bool cbm_pipeline_source_site_legacy(uint32_t start, uint32_t end) {
return start == 0 && end == 0;
}
static inline bool cbm_pipeline_source_site_eq(uint32_t lhs_start, uint32_t lhs_end,
uint32_t rhs_start, uint32_t rhs_end) {
return cbm_pipeline_source_site_present(lhs_start, lhs_end) &&
cbm_pipeline_source_site_present(rhs_start, rhs_end) && lhs_start == rhs_start &&
lhs_end == rhs_end;
}
static inline bool cbm_pipeline_source_occurrence_eq(uint32_t lhs_start, uint32_t lhs_end,
CBMSourceOrigin lhs_origin, uint32_t rhs_start,
uint32_t rhs_end, CBMSourceOrigin rhs_origin) {
return lhs_origin == rhs_origin &&
cbm_pipeline_source_site_eq(lhs_start, lhs_end, rhs_start, rhs_end);
}
/* Rank an invocation record for one parser carrier. Exact occurrence identity
* is stronger than the legacy 0:0 compatibility join. Partial/reversed spans
* are corrupt metadata and never participate in either class. */
static inline int cbm_pipeline_invocation_site_rank(const CBMResolvedCall *resolved,
const CBMCall *call) {
if (!resolved || !call) {
return 0;
}
bool call_has_site =
cbm_pipeline_source_site_present(call->site_start_byte, call->site_end_byte);
bool call_is_legacy =
cbm_pipeline_source_site_legacy(call->site_start_byte, call->site_end_byte);
if (!call_has_site && !call_is_legacy) {
return 0;
}
if (cbm_pipeline_source_site_present(resolved->site_start_byte, resolved->site_end_byte)) {
return cbm_pipeline_source_occurrence_eq(call->site_start_byte, call->site_end_byte,
call->source_origin, resolved->site_start_byte,
resolved->site_end_byte, resolved->source_origin)
? 2
: 0;
}
if (!cbm_pipeline_source_site_legacy(resolved->site_start_byte, resolved->site_end_byte)) {
return 0;
}
return call->requires_lsp_resolution || call->source_origin != resolved->source_origin ? 0 : 1;
}
static inline bool cbm_pipeline_invocation_reason_join_strategy(const char *strategy) {
return strategy &&
(strcmp(strategy, "lsp_func_ptr") == 0 || strcmp(strategy, "lsp_callable_alias") == 0 ||
strcmp(strategy, "lsp_dll_resolve") == 0 ||
strcmp(strategy, "lsp_method_ref_ctor") == 0 ||
strcmp(strategy, "lsp_method_ref_ctor_synth") == 0 ||
strcmp(strategy, "lsp_dict_dispatch") == 0 ||
strcmp(strategy, "lsp_import_alias") == 0 ||
strcmp(strategy, "php_method_dynamic") == 0);
}
static inline bool cbm_pipeline_invocation_leaf_matches(const CBMResolvedCall *resolved,
const CBMCall *call, int site_rank) {
if (!resolved || !resolved->callee_qn || !call || !call->callee_name) {
return false;
}
const char *resolved_leaf = cbm_lsp_bare_segment(resolved->callee_qn);
const char *call_leaf = cbm_lsp_bare_segment(call->callee_name);
if (resolved_leaf && call_leaf && strcmp(resolved_leaf, call_leaf) == 0) {
return true;
}
/* Destructors intentionally join by their exact delete-expression
* occurrence because the parser carrier starts as a neutral "~" marker. */
if (site_rank == 2 && call->requires_lsp_resolution && resolved->strategy &&
strcmp(resolved->strategy, "lsp_destructor") == 0) {
return true;
}
/* Indirect resolvers retain the textual source leaf in reason. */
return cbm_pipeline_invocation_reason_join_strategy(resolved->strategy) && resolved->reason &&
call_leaf && strcmp(cbm_lsp_bare_segment(resolved->reason), call_leaf) == 0;
}
/* Look up the highest-confidence LSP-resolved call entry whose caller QN
* matches the textual call's enclosing function and whose callee QN
* short-name matches the textual callee. Returns a pointer into `arr`
@@ -119,8 +361,9 @@ static inline int cbm_pipeline_qn_class_method_tail_eq(const char *qn, const cha
* 1. exact caller_qn + callee short-name match wins first;
* 2. if no exact caller match exists AND allow_tail_match is set
* (JVM callers only, see cbm_pipeline_lsp_allow_tail_match), a
* unique Class.method tail match between rc->caller_qn and
* call->enclosing_func_qn may win;
* unique Class.method tail match may win; an occurrence-exact row may
* also reconcile Kotlin's package-vs-file top-level owner QNs when their
* bare caller leaves agree;
* 3. ambiguous tails return NULL so the registry fallback stays in
* control.
*
@@ -129,8 +372,9 @@ static inline int cbm_pipeline_qn_class_method_tail_eq(const char *qn, const cha
*
* The pointer returned aliases into `arr` and stays valid as long as the
* underlying CBMFileResult is alive. */
static inline const CBMResolvedCall *cbm_pipeline_find_lsp_resolution(
const CBMResolvedCallArray *arr, const CBMCall *call, bool allow_tail_match) {
static inline const CBMResolvedCall *cbm_pipeline_find_lsp_resolution_in_graph(
const CBMResolvedCallArray *arr, const CBMCall *call, bool allow_tail_match,
const cbm_gbuf_t *gbuf, const char *project_name) {
if (!arr || arr->count == 0 || !call) {
return NULL;
}
@@ -138,9 +382,14 @@ static inline const CBMResolvedCall *cbm_pipeline_find_lsp_resolution(
return NULL;
}
const CBMResolvedCall *best_exact = NULL;
const CBMResolvedCall *best_exact_caller = NULL;
int best_exact_caller_rank = 0;
bool exact_caller_ambiguous = false;
for (int i = 0; i < arr->count; i++) {
const CBMResolvedCall *rc = &arr->items[i];
if (rc->kind != CBM_RESOLVED_INVOCATION) {
continue;
}
if (!rc->caller_qn || !rc->callee_qn) {
continue;
}
@@ -150,77 +399,445 @@ static inline const CBMResolvedCall *cbm_pipeline_find_lsp_resolution(
if (strcmp(rc->caller_qn, call->enclosing_func_qn) != 0) {
continue;
}
const char *short_name = cbm_lsp_bare_segment(rc->callee_qn);
/* The call's callee_name is receiver-qualified for method/qualified
* calls ("c.inc", "A.Helper", "Math::square", "p->run"); the LSP
* records the resolved class-qualified callee_qn ("Class.inc"). Compare
* the bare last segment on BOTH sides so method-dispatch resolutions
* join — the LSP already did the receiver->type resolution, and matching
* the full "c.inc" against "inc" would always miss, silently dropping the
* type-aware LSP strategy to the weaker textual registry. Free-function
* calls (bare callee_name) are unaffected. */
const char *call_short = cbm_lsp_bare_segment(call->callee_name);
if (strcmp(short_name, call_short) != 0) {
/* Indirect/implicit resolution: the textual callee differs from the
* resolved callee_qn's short name. A function-pointer / DLL call's
* callee is the pointer name (`fp`); a C++ destructor's only textual
* anchor is the deleted operand (`p`, vs. the `T.~T` callee QN). In
* both the LSP stashed the original textual name in `reason`. Match
* the call site on that name, gated to those strategies so `reason`
* is never misread as an unresolved-call diagnostic. */
if (!(rc->reason && rc->strategy &&
(strcmp(rc->strategy, "lsp_func_ptr") == 0 ||
strcmp(rc->strategy, "lsp_dll_resolve") == 0 ||
strcmp(rc->strategy, "lsp_method_ref_ctor") == 0 ||
strcmp(rc->strategy, "lsp_method_ref_ctor_synth") == 0 ||
strcmp(rc->strategy, "lsp_dict_dispatch") == 0 ||
strcmp(rc->strategy, "lsp_import_alias") == 0 ||
strcmp(rc->strategy, "lsp_destructor") == 0 ||
strcmp(rc->strategy, "php_method_dynamic") == 0) &&
strcmp(cbm_lsp_bare_segment(rc->reason), call_short) == 0)) {
continue;
}
int site_rank = cbm_pipeline_invocation_site_rank(rc, call);
if (site_rank == 0 || !cbm_pipeline_invocation_leaf_matches(rc, call, site_rank)) {
continue;
}
if (!best_exact || rc->confidence > best_exact->confidence) {
best_exact = rc;
if (!best_exact_caller || site_rank > best_exact_caller_rank) {
best_exact_caller = rc;
best_exact_caller_rank = site_rank;
exact_caller_ambiguous = false;
continue;
}
if (site_rank < best_exact_caller_rank) {
continue;
}
if (!cbm_pipeline_invocation_targets_equal(best_exact_caller->callee_qn, rc->callee_qn,
gbuf, project_name, allow_tail_match)) {
exact_caller_ambiguous = true;
continue;
}
if (rc->confidence > best_exact_caller->confidence) {
best_exact_caller = rc;
}
}
if (best_exact) {
return best_exact;
if (best_exact_caller) {
return exact_caller_ambiguous ? NULL : best_exact_caller;
}
if (!allow_tail_match) {
return NULL;
}
const char *call_tail = cbm_pipeline_qn_class_method_tail(call->enclosing_func_qn);
if (!call_tail) {
return NULL;
}
const CBMResolvedCall *best_tail = NULL;
const CBMResolvedCall *best_tail_exact = NULL;
const CBMResolvedCall *best_tail_legacy = NULL;
bool tail_exact_ambiguous = false;
bool tail_legacy_ambiguous = false;
for (int i = 0; i < arr->count; i++) {
const CBMResolvedCall *rc = &arr->items[i];
if (rc->kind != CBM_RESOLVED_INVOCATION) {
continue;
}
if (!rc->caller_qn || !rc->callee_qn) {
continue;
}
if (rc->confidence < CBM_LSP_CONFIDENCE_FLOOR) {
continue;
}
const char *short_name = strrchr(rc->callee_qn, '.');
short_name = short_name ? short_name + SKIP_ONE : rc->callee_qn;
const char *call_leaf = cbm_pipeline_call_callee_leaf(call->callee_name);
if (!call_leaf || strcmp(short_name, call_leaf) != 0) {
int site_rank = cbm_pipeline_invocation_site_rank(rc, call);
if (site_rank == 0) {
continue;
}
if (!cbm_pipeline_qn_class_method_tail_eq(rc->caller_qn, call_tail)) {
bool caller_matches =
call_tail && cbm_pipeline_qn_class_method_tail_eq(rc->caller_qn, call_tail);
if (!caller_matches && site_rank == 2) {
const char *resolved_caller = cbm_lsp_bare_segment(rc->caller_qn);
const char *source_caller = cbm_lsp_bare_segment(call->enclosing_func_qn);
caller_matches =
resolved_caller && source_caller && strcmp(resolved_caller, source_caller) == 0;
}
if (!caller_matches || !cbm_pipeline_invocation_leaf_matches(rc, call, site_rank)) {
continue;
}
if (best_tail) {
return NULL;
const CBMResolvedCall **best = site_rank == 2 ? &best_tail_exact : &best_tail_legacy;
bool *ambiguous = site_rank == 2 ? &tail_exact_ambiguous : &tail_legacy_ambiguous;
if (!*best) {
*best = rc;
continue;
}
if (!cbm_pipeline_invocation_targets_equal((*best)->callee_qn, rc->callee_qn, gbuf,
project_name, allow_tail_match)) {
*ambiguous = true;
continue;
}
if (rc->confidence > (*best)->confidence) {
*best = rc;
}
best_tail = rc;
}
return best_tail;
if (best_tail_exact) {
return tail_exact_ambiguous ? NULL : best_tail_exact;
}
return tail_legacy_ambiguous ? NULL : best_tail_legacy;
}
/* Graph-free compatibility entry point for resolver unit tests. Raw distinct
* targets remain ambiguous because no materialized-node identity is available. */
static inline const CBMResolvedCall *cbm_pipeline_find_lsp_resolution(
const CBMResolvedCallArray *arr, const CBMCall *call, bool allow_tail_match) {
return cbm_pipeline_find_lsp_resolution_in_graph(arr, call, allow_tail_match, NULL, NULL);
}
static inline bool cbm_pipeline_reference_reason_matches(const CBMResolvedCall *resolved,
const char *reference_leaf) {
if (!resolved || !resolved->strategy || !resolved->reason || !reference_leaf) {
return false;
}
bool reason_join_strategy = strcmp(resolved->strategy, "php_method_dynamic") == 0 ||
strcmp(resolved->strategy, "php_static_magic_reference") == 0 ||
strcmp(resolved->strategy, "php_invokable_reference") == 0 ||
strcmp(resolved->strategy, "lsp_callable_alias") == 0 ||
strcmp(resolved->strategy, "lsp_callable_value") == 0 ||
strcmp(resolved->strategy, "lsp_callable_value_reference") == 0;
return reason_join_strategy &&
strcmp(cbm_lsp_bare_segment(resolved->reason), reference_leaf) == 0;
}
static inline const cbm_gbuf_node_t *cbm_pipeline_lsp_target_node(const cbm_gbuf_t *gbuf,
const char *project_name,
const char *callee_qn,
bool allow_tail_match) {
return cbm_pipeline_lsp_target_node_policy(gbuf, project_name, callee_qn, allow_tail_match,
true);
}
/* External Kotlin protocol carriers must resolve to the exact semantic target.
* A project-qualified retry is still exact, but Class.method and unique-leaf
* fallbacks could bind `kotlin.IntArray.iterator` to an unrelated project
* method and are therefore deliberately disabled. */
static inline const cbm_gbuf_node_t *cbm_pipeline_lsp_target_node_strict(const cbm_gbuf_t *gbuf,
const char *project_name,
const char *callee_qn,
bool allow_tail_match) {
(void)allow_tail_match;
return cbm_pipeline_lsp_target_node_policy(gbuf, project_name, callee_qn, false, false);
}
/* Keep exact-only lookup scoped to known external Kotlin namespaces. Project
* callable aliases and operators still need the normal JVM QN reconciliation
* path, while unresolved synthetic carriers remain fail-closed at the caller. */
static inline bool cbm_pipeline_kotlin_external_target(CBMLanguage lang, const char *callee_qn) {
if (lang != CBM_LANG_KOTLIN || !callee_qn) {
return false;
}
return strncmp(callee_qn, "kotlin.", strlen("kotlin.")) == 0 ||
strncmp(callee_qn, "java.", strlen("java.")) == 0 ||
strncmp(callee_qn, "javax.", strlen("javax.")) == 0;
}
/* Resolvers may report one graph target both project-relative and already
* project-prefixed. Raw string inequality is not ambiguity when both spellings
* resolve to the same materialized node; conversely, if both nodes exist they
* remain distinct and the reference must fail closed. */
static inline bool cbm_pipeline_reference_targets_equal(const char *left_qn, const char *right_qn,
const cbm_gbuf_t *gbuf,
const char *project_name,
bool allow_tail_match) {
if (!left_qn || !right_qn) {
return false;
}
if (strcmp(left_qn, right_qn) == 0) {
return true;
}
if (!gbuf || !project_name) {
return false;
}
/* Caller-tail fallback helps JVM rows find a materialized target, but it is
* not proof that two distinct semantic target QNs denote the same symbol.
* Equivalence is deliberately stricter: collapse only direct and
* project-prefix spellings of one graph node, otherwise stay ambiguous. */
(void)allow_tail_match;
const cbm_gbuf_node_t *left = cbm_pipeline_lsp_target_node(gbuf, project_name, left_qn, false);
const cbm_gbuf_node_t *right =
cbm_pipeline_lsp_target_node(gbuf, project_name, right_qn, false);
return left && right && left->id == right->id;
}
typedef struct {
const CBMResolvedCall *row;
int original_index;
} cbm_pipeline_lsp_reference_index_entry_t;
typedef struct {
cbm_pipeline_lsp_reference_index_entry_t *entries;
int count;
} cbm_pipeline_lsp_reference_index_t;
/* Sort by exact occurrence while retaining extraction order within one site.
* The final ordinal key is intentional: confidence ties keep the first row,
* so an unstable qsort tie must not silently change semantic selection. */
static inline int cbm_pipeline_lsp_reference_index_entry_cmp(const void *left_ptr,
const void *right_ptr) {
const cbm_pipeline_lsp_reference_index_entry_t *left =
(const cbm_pipeline_lsp_reference_index_entry_t *)left_ptr;
const cbm_pipeline_lsp_reference_index_entry_t *right =
(const cbm_pipeline_lsp_reference_index_entry_t *)right_ptr;
if (left->row->source_origin != right->row->source_origin) {
return left->row->source_origin < right->row->source_origin ? -1 : 1;
}
if (left->row->site_start_byte != right->row->site_start_byte) {
return left->row->site_start_byte < right->row->site_start_byte ? -1 : 1;
}
if (left->row->site_end_byte != right->row->site_end_byte) {
return left->row->site_end_byte < right->row->site_end_byte ? -1 : 1;
}
if (left->original_index == right->original_index) {
return 0;
}
return left->original_index < right->original_index ? -1 : 1;
}
/* Build one compact pointer index per extracted file. Invocation rows cannot
* participate in CALL_REFERENCE matching and are excluded. False means only
* allocation failure; callers must then use the unchanged linear matcher. */
static inline bool cbm_pipeline_lsp_reference_index_build(
const CBMResolvedCallArray *arr, cbm_pipeline_lsp_reference_index_t *index) {
if (!index) {
return false;
}
index->entries = NULL;
index->count = 0;
if (!arr || arr->count <= 0) {
return true;
}
int reference_count = 0;
for (int i = 0; i < arr->count; i++) {
if (arr->items[i].kind == CBM_RESOLVED_CALL_REFERENCE) {
reference_count++;
}
}
if (reference_count == 0) {
return true;
}
if ((size_t)reference_count > SIZE_MAX / sizeof(cbm_pipeline_lsp_reference_index_entry_t)) {
return false;
}
cbm_pipeline_lsp_reference_index_entry_t *entries =
(cbm_pipeline_lsp_reference_index_entry_t *)malloc((size_t)reference_count *
sizeof(*entries));
if (!entries) {
return false;
}
int next = 0;
for (int i = 0; i < arr->count; i++) {
if (arr->items[i].kind != CBM_RESOLVED_CALL_REFERENCE) {
continue;
}
entries[next].row = &arr->items[i];
entries[next].original_index = i;
next++;
}
qsort(entries, (size_t)reference_count, sizeof(*entries),
cbm_pipeline_lsp_reference_index_entry_cmp);
index->entries = entries;
index->count = reference_count;
return true;
}
static inline void cbm_pipeline_lsp_reference_index_free(
cbm_pipeline_lsp_reference_index_t *index) {
if (!index) {
return;
}
free(index->entries);
index->entries = NULL;
index->count = 0;
}
static inline int cbm_pipeline_lsp_reference_site_cmp_usage(const CBMResolvedCall *row,
const CBMUsage *usage) {
if (row->source_origin != usage->source_origin) {
return row->source_origin < usage->source_origin ? -1 : 1;
}
if (row->site_start_byte != usage->site_start_byte) {
return row->site_start_byte < usage->site_start_byte ? -1 : 1;
}
if (row->site_end_byte != usage->site_end_byte) {
return row->site_end_byte < usage->site_end_byte ? -1 : 1;
}
return 0;
}
static inline const CBMResolvedCall *cbm_pipeline_lsp_reference_view_row(
const CBMResolvedCallArray *arr, const cbm_pipeline_lsp_reference_index_entry_t *entries,
int first, int offset) {
return entries ? entries[first + offset].row : &arr->items[first + offset];
}
/* Look up the highest-confidence LSP resolution for a callable reference.
* Reference records intentionally use a separate kind from invocations: an
* explicit `Type::method`/`obj::method`, PHP first-class callable, or an exact
* TS function-value argument denotes a function value and must never enter
* the CALLS pipeline.
*
* Exact enclosing-function matches win. The unique Class.method caller-tail
* fallback is admitted only when the caller opted into the existing JVM-only
* rule via `allow_tail_match`; ambiguity returns NULL so the usage pipeline can
* fall back to the textual registry without fabricating a semantic match.
*
* `entries` is either NULL for the original full-array fallback or a sorted
* occurrence slice. The matching body is deliberately shared so indexing
* cannot diverge on caller, reason, ambiguity, target identity, or confidence. */
static inline const CBMResolvedCall *cbm_pipeline_find_lsp_reference_view_in_graph(
const CBMResolvedCallArray *arr, const cbm_pipeline_lsp_reference_index_entry_t *entries,
int first, int row_count, const CBMUsage *usage, bool allow_tail_match, const cbm_gbuf_t *gbuf,
const char *project_name) {
if ((!entries && !arr) || row_count <= 0 || !usage || !usage->enclosing_func_qn ||
!usage->ref_name) {
return NULL;
}
const char *ref_leaf = cbm_lsp_bare_segment(usage->ref_name);
if (!ref_leaf || !ref_leaf[0]) {
return NULL;
}
const CBMResolvedCall *best_exact = NULL;
bool exact_ambiguous = false;
for (int i = 0; i < row_count; i++) {
cbm_pipeline_lsp_reference_lookup_test_note_row();
const CBMResolvedCall *rc = cbm_pipeline_lsp_reference_view_row(arr, entries, first, i);
if (rc->kind != CBM_RESOLVED_CALL_REFERENCE || !rc->caller_qn || !rc->callee_qn ||
rc->confidence < CBM_LSP_CONFIDENCE_FLOOR) {
continue;
}
if (strcmp(rc->caller_qn, usage->enclosing_func_qn) != 0 ||
!cbm_pipeline_source_occurrence_eq(usage->site_start_byte, usage->site_end_byte,
usage->source_origin, rc->site_start_byte,
rc->site_end_byte, rc->source_origin)) {
continue;
}
bool name_matches = strcmp(cbm_lsp_bare_segment(rc->callee_qn), ref_leaf) == 0;
bool semantic_reason_matches = cbm_pipeline_reference_reason_matches(rc, ref_leaf);
if (!name_matches && !semantic_reason_matches) {
continue;
}
if (!best_exact) {
best_exact = rc;
continue;
}
if (!cbm_pipeline_reference_targets_equal(best_exact->callee_qn, rc->callee_qn, gbuf,
project_name, allow_tail_match)) {
exact_ambiguous = true;
continue;
}
if (rc->confidence > best_exact->confidence) {
best_exact = rc;
}
}
if (best_exact || !allow_tail_match) {
return exact_ambiguous ? NULL : best_exact;
}
const char *caller_tail = cbm_pipeline_qn_class_method_tail(usage->enclosing_func_qn);
if (!caller_tail) {
return NULL;
}
const CBMResolvedCall *best_tail = NULL;
bool tail_ambiguous = false;
for (int i = 0; i < row_count; i++) {
cbm_pipeline_lsp_reference_lookup_test_note_row();
const CBMResolvedCall *rc = cbm_pipeline_lsp_reference_view_row(arr, entries, first, i);
if (rc->kind != CBM_RESOLVED_CALL_REFERENCE || !rc->caller_qn || !rc->callee_qn ||
rc->confidence < CBM_LSP_CONFIDENCE_FLOOR) {
continue;
}
if (!cbm_pipeline_qn_class_method_tail_eq(rc->caller_qn, caller_tail) ||
!cbm_pipeline_source_occurrence_eq(usage->site_start_byte, usage->site_end_byte,
usage->source_origin, rc->site_start_byte,
rc->site_end_byte, rc->source_origin)) {
continue;
}
bool name_matches = strcmp(cbm_lsp_bare_segment(rc->callee_qn), ref_leaf) == 0;
bool semantic_reason_matches = cbm_pipeline_reference_reason_matches(rc, ref_leaf);
if (!name_matches && !semantic_reason_matches) {
continue;
}
if (!best_tail) {
best_tail = rc;
continue;
}
if (!cbm_pipeline_reference_targets_equal(best_tail->callee_qn, rc->callee_qn, gbuf,
project_name, allow_tail_match)) {
tail_ambiguous = true;
continue;
}
if (rc->confidence > best_tail->confidence) {
best_tail = rc;
}
}
return tail_ambiguous ? NULL : best_tail;
}
/* Full-array compatibility/fallback path. Allocation failure in either
* materializer deliberately lands here rather than dropping semantic edges. */
static inline const CBMResolvedCall *cbm_pipeline_find_lsp_reference_in_graph(
const CBMResolvedCallArray *arr, const CBMUsage *usage, bool allow_tail_match,
const cbm_gbuf_t *gbuf, const char *project_name) {
int count = arr ? arr->count : 0;
return cbm_pipeline_find_lsp_reference_view_in_graph(arr, NULL, 0, count, usage,
allow_tail_match, gbuf, project_name);
}
/* Binary-search the exact (origin,start,end) occurrence, then run the shared
* matcher only over that stable-order slice. A NULL index means construction
* failed and selects the original linear fallback above. */
static inline const CBMResolvedCall *cbm_pipeline_find_lsp_reference_indexed_in_graph(
const CBMResolvedCallArray *arr, const cbm_pipeline_lsp_reference_index_t *index,
const CBMUsage *usage, bool allow_tail_match, const cbm_gbuf_t *gbuf,
const char *project_name) {
if (!index) {
return cbm_pipeline_find_lsp_reference_in_graph(arr, usage, allow_tail_match, gbuf,
project_name);
}
if (!usage || !usage->enclosing_func_qn || !usage->ref_name || index->count <= 0) {
return NULL;
}
int low = 0;
int high = index->count;
while (low < high) {
int middle = low + (high - low) / 2;
if (cbm_pipeline_lsp_reference_site_cmp_usage(index->entries[middle].row, usage) < 0) {
low = middle + 1;
} else {
high = middle;
}
}
int first = low;
high = index->count;
while (low < high) {
int middle = low + (high - low) / 2;
if (cbm_pipeline_lsp_reference_site_cmp_usage(index->entries[middle].row, usage) <= 0) {
low = middle + 1;
} else {
high = middle;
}
}
int row_count = low - first;
return cbm_pipeline_find_lsp_reference_view_in_graph(
arr, index->entries, first, row_count, usage, allow_tail_match, gbuf, project_name);
}
/* String-strict helper retained for unit tests and callers without a graph.
* Production edge materialization uses the graph-aware variant above. */
static inline const CBMResolvedCall *cbm_pipeline_find_lsp_reference(
const CBMResolvedCallArray *arr, const CBMUsage *usage, bool allow_tail_match) {
return cbm_pipeline_find_lsp_reference_in_graph(arr, usage, allow_tail_match, NULL, NULL);
}
/* Resolve an LSP-emitted callee_qn to a graph-buffer node.
@@ -234,13 +851,14 @@ static inline const CBMResolvedCall *cbm_pipeline_find_lsp_resolution(
* cbm_pipeline_lsp_allow_tail_match), use the exact node-name index
* to narrow candidates by short method name and accept exactly one
* Function/Method whose qualified_name has the same Class.method
* tail.
* tail;
* 4. when a package-shaped top-level JVM QN cannot share a tail with the
* graph's file-shaped QN, accept one globally unique callable short name.
*
* Returns the matching node, or NULL if neither lookup hits. */
static inline const cbm_gbuf_node_t *cbm_pipeline_lsp_target_node(const cbm_gbuf_t *gbuf,
const char *project_name,
const char *callee_qn,
bool allow_tail_match) {
static inline const cbm_gbuf_node_t *cbm_pipeline_lsp_target_node_policy(
const cbm_gbuf_t *gbuf, const char *project_name, const char *callee_qn, bool allow_tail_match,
bool allow_unique_callable_fallback) {
if (!gbuf || !callee_qn) {
return NULL;
}
@@ -251,12 +869,19 @@ static inline const cbm_gbuf_node_t *cbm_pipeline_lsp_target_node(const cbm_gbuf
if (project_name && project_name[0]) {
size_t proj_len = strlen(project_name);
if (!(strncmp(callee_qn, project_name, proj_len) == 0 && callee_qn[proj_len] == '.')) {
char buf[CBM_SZ_1K];
int written = snprintf(buf, sizeof(buf), "%s.%s", project_name, callee_qn);
if (written > 0 && (size_t)written < sizeof(buf)) {
const cbm_gbuf_node_t *prefixed = cbm_gbuf_find_by_qn(gbuf, buf);
if (prefixed) {
return prefixed;
size_t callee_len = strlen(callee_qn);
if (proj_len <= SIZE_MAX - callee_len - 2U) {
size_t prefixed_len = proj_len + 1U + callee_len;
char *prefixed_qn = (char *)malloc(prefixed_len + 1U);
if (prefixed_qn) {
memcpy(prefixed_qn, project_name, proj_len);
prefixed_qn[proj_len] = '.';
memcpy(prefixed_qn + proj_len + 1U, callee_qn, callee_len + 1U);
const cbm_gbuf_node_t *prefixed = cbm_gbuf_find_by_qn(gbuf, prefixed_qn);
free(prefixed_qn);
if (prefixed) {
return prefixed;
}
}
}
}
@@ -278,6 +903,9 @@ static inline const cbm_gbuf_node_t *cbm_pipeline_lsp_target_node(const cbm_gbuf
}
const cbm_gbuf_node_t *match = NULL;
const cbm_gbuf_node_t *unique_callable = NULL;
bool tail_ambiguous = false;
bool callable_ambiguous = false;
for (int i = 0; i < hit_count; i++) {
const cbm_gbuf_node_t *cand = hits[i];
if (!cand || !cand->label || !cand->qualified_name) {
@@ -286,15 +914,27 @@ static inline const cbm_gbuf_node_t *cbm_pipeline_lsp_target_node(const cbm_gbuf
if (strcmp(cand->label, "Function") != 0 && strcmp(cand->label, "Method") != 0) {
continue;
}
if (!unique_callable) {
unique_callable = cand;
} else if (unique_callable->id != cand->id) {
callable_ambiguous = true;
}
if (!cbm_pipeline_qn_class_method_tail_eq(cand->qualified_name, callee_tail)) {
continue;
}
if (match) {
return NULL;
if (match && match->id != cand->id) {
tail_ambiguous = true;
continue;
}
match = cand;
}
return match;
if (match) {
return tail_ambiguous ? NULL : match;
}
if (!allow_unique_callable_fallback) {
return NULL;
}
return callable_ambiguous ? NULL : unique_callable;
}
#endif /* CBM_PIPELINE_LSP_RESOLVE_H */
+17 -2
View File
@@ -467,10 +467,16 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
/* LSP-resolved calls take precedence over registry-textual matching.
* Unique-tail fallbacks are JVM-only (see cbm_pipeline_lsp_allow_tail_match). */
bool allow_tail = cbm_pipeline_lsp_allow_tail_match(lang);
const CBMResolvedCall *lsp = cbm_pipeline_find_lsp_resolution(lsp_calls, call, allow_tail);
const CBMResolvedCall *lsp = cbm_pipeline_find_lsp_resolution_in_graph(
lsp_calls, call, allow_tail, ctx->gbuf, ctx->project_name);
if (lsp) {
bool exact_external_target = call->requires_lsp_resolution &&
cbm_pipeline_kotlin_external_target(lang, lsp->callee_qn);
const cbm_gbuf_node_t *target_node =
cbm_pipeline_lsp_target_node(ctx->gbuf, ctx->project_name, lsp->callee_qn, allow_tail);
exact_external_target ? cbm_pipeline_lsp_target_node_strict(
ctx->gbuf, ctx->project_name, lsp->callee_qn, allow_tail)
: cbm_pipeline_lsp_target_node(ctx->gbuf, ctx->project_name,
lsp->callee_qn, allow_tail);
if (target_node && source_node->id != target_node->id) {
cbm_resolution_t res = {0};
/* Use the gbuf node's QN so downstream edge props show the canonical
@@ -485,6 +491,15 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
}
}
/* Synthetic semantic candidates (currently implicit C++ operators) are
* valid calls only when the language resolver identifies a concrete
* invocation target. Textual registry/service fallbacks would bind an
* unresolved primitive expression such as `int + int` to an unrelated
* project `operator+`, fabricating a CALLS edge. */
if (call->requires_lsp_resolution) {
return 0;
}
/* Service-pattern HTTP/ASYNC client call (`requests.get(url)`): the service
* signal lives in the callee_name. The registry can mis-resolve such a call
* to a spurious builtin short-name match (e.g. `requests.get` ->
+193 -40
View File
@@ -405,8 +405,8 @@ static void create_channel_edges_for_file(cbm_pipeline_ctx_t *ctx, const CBMFile
* env key and link the enclosing function (or the file node) CONFIGURES-> it,
* so environment-driven configuration is visible even when the accessor is a
* stdlib symbol that never resolves to an in-graph callee. */
static int create_env_configures_for_file(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result,
const char *rel) {
int cbm_pipeline_create_env_configures_for_file(cbm_pipeline_ctx_t *ctx,
const CBMFileResult *result, const char *rel) {
int count = 0;
char *file_qn = NULL;
const cbm_gbuf_node_t *file_node = NULL;
@@ -481,6 +481,186 @@ static int create_import_edges_for_file(cbm_pipeline_ctx_t *ctx, const CBMFileRe
return count;
}
static bool objectscript_export_append_strings(CBMArena *arena, const char ***dst,
const char *const *src) {
if (!src) {
return true;
}
int old_count = 0;
int add_count = 0;
while (*dst && (*dst)[old_count]) {
old_count++;
}
while (src[add_count]) {
add_count++;
}
const char **items = (const char **)cbm_arena_alloc(arena, (size_t)(old_count + add_count + 1) *
sizeof(const char *));
if (!items) {
return false;
}
for (int i = 0; i < old_count; i++) {
items[i] = (*dst)[i];
}
for (int i = 0; i < add_count; i++) {
items[old_count + i] = src[i];
}
items[old_count + add_count] = NULL;
*dst = items;
return true;
}
#define OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, field, push_fn) \
do { \
int expected_count = (aggregate)->field.count + (part)->field.count; \
for (int item_i = 0; item_i < (part)->field.count; item_i++) { \
push_fn(&(aggregate)->field, &(aggregate)->arena, (part)->field.items[item_i]); \
} \
if ((aggregate)->field.count != expected_count) { \
return false; \
} \
} while (0)
static bool objectscript_export_append_primary_arrays(CBMFileResult *aggregate,
const CBMFileResult *part) {
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, defs, cbm_defs_push);
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, calls, cbm_calls_push);
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, imports, cbm_imports_push);
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, usages, cbm_usages_push);
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, throws, cbm_throws_push);
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, rw, cbm_rw_push);
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, type_refs, cbm_typerefs_push);
return true;
}
static bool objectscript_export_append_secondary_arrays(CBMFileResult *aggregate,
const CBMFileResult *part) {
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, env_accesses, cbm_envaccess_push);
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, type_assigns, cbm_typeassign_push);
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, impl_traits, cbm_impltrait_push);
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, resolved_calls, cbm_resolvedcall_push);
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, string_refs, cbm_stringref_push);
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, infra_bindings, cbm_infrabinding_push);
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, channels, cbm_channels_push);
return true;
}
/* Preserve every generated class's parse diagnostics. The generated UDL
* snippets all map back to one physical Studio Export file, so their compact
* range lists can be concatenated using the ordinary comma separator. */
static bool objectscript_export_append_error_ranges(CBMFileResult *aggregate,
const CBMFileResult *part) {
aggregate->parse_incomplete = aggregate->parse_incomplete || part->parse_incomplete;
aggregate->error_region_count += part->error_region_count;
if (!part->error_ranges || !part->error_ranges[0]) {
return true;
}
const char *combined = NULL;
if (aggregate->error_ranges && aggregate->error_ranges[0]) {
combined = cbm_arena_sprintf(&aggregate->arena, "%s,%s", aggregate->error_ranges,
part->error_ranges);
} else {
combined = cbm_arena_strdup(&aggregate->arena, part->error_ranges);
}
if (!combined) {
return false;
}
aggregate->error_ranges = combined;
return true;
}
/* Studio Export files may contain multiple <Class> elements, while the
* pipeline cache has one slot per physical file. Extract each generated UDL
* class independently (preserving the upstream parser behavior), then compose
* every extracted carrier into one result for the normal registry/call/usage/
* semantic passes. */
CBMFileResult *cbm_pipeline_extract_objectscript_export(
const char *source, int source_len, const char *project_name, const char *rel_path,
const CBMMacroTable *macro_table, const CBMReturnTypeTable *return_type_table) {
CBMArena export_arena;
cbm_arena_init(&export_arena);
int class_count = 0;
char **udl_strings = cbm_iris_export_to_udl(&export_arena, source, source_len, &class_count);
if (!udl_strings || class_count <= 0) {
cbm_arena_destroy(&export_arena);
return NULL;
}
CBMFileResult *aggregate = (CBMFileResult *)calloc(1, sizeof(CBMFileResult));
if (!aggregate) {
cbm_arena_destroy(&export_arena);
return NULL;
}
cbm_arena_init(&aggregate->arena);
if (aggregate->arena.nblocks == 0) {
cbm_free_result(aggregate);
cbm_arena_destroy(&export_arena);
return NULL;
}
aggregate->owned_results =
(CBMFileResult **)calloc((size_t)class_count, sizeof(CBMFileResult *));
if (!aggregate->owned_results) {
cbm_free_result(aggregate);
cbm_arena_destroy(&export_arena);
return NULL;
}
aggregate->cached_lang = CBM_LANG_OBJECTSCRIPT_UDL;
for (int ci = 0; ci < class_count; ci++) {
CBMFileResult *part = cbm_extract_file_ex(
udl_strings[ci], (int)strlen(udl_strings[ci]), CBM_LANG_OBJECTSCRIPT_UDL, project_name,
rel_path, CBM_EXTRACT_BUDGET, NULL, NULL, macro_table, return_type_table);
if (!part) {
continue;
}
/* The aggregate has no single parse tree. Later ObjectScript Export
* passes consume extracted carriers, not a raw-XML tree. */
cbm_free_tree(part);
if (!objectscript_export_append_primary_arrays(aggregate, part) ||
!objectscript_export_append_secondary_arrays(aggregate, part) ||
!objectscript_export_append_strings(&aggregate->arena, &aggregate->exports,
part->exports) ||
!objectscript_export_append_strings(&aggregate->arena, &aggregate->constants,
part->constants) ||
!objectscript_export_append_strings(&aggregate->arena, &aggregate->global_vars,
part->global_vars) ||
!objectscript_export_append_strings(&aggregate->arena, &aggregate->macros,
part->macros) ||
!objectscript_export_append_error_ranges(aggregate, part)) {
goto merge_failed;
}
if (!aggregate->module_qn) {
aggregate->module_qn = part->module_qn;
}
if (!aggregate->namespace_name) {
aggregate->namespace_name = part->namespace_name;
}
if (part->has_error) {
aggregate->has_error = true;
if (!aggregate->error_msg) {
aggregate->error_msg = part->error_msg;
}
}
aggregate->is_test_file = aggregate->is_test_file || part->is_test_file;
aggregate->owned_results[aggregate->owned_result_count++] = part;
continue;
merge_failed:
cbm_free_result(part);
cbm_free_result(aggregate);
cbm_arena_destroy(&export_arena);
return NULL;
}
aggregate->imports_count = aggregate->imports.count;
cbm_arena_destroy(&export_arena);
return aggregate;
}
#undef OBJECTSCRIPT_EXPORT_APPEND_ARRAY
int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
int file_count) {
cbm_log_info("pass.start", "pass", "definitions", "files", itoa_log(file_count));
@@ -568,42 +748,15 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
continue;
}
/* ObjectScript Studio Export XML: transcode each <Class> to UDL and
* extract it as CBM_LANG_OBJECTSCRIPT_UDL. The XML→UDL mapping is 1:1,
* so the same UDL extractor handles the result. These files are not
* cached (their defs/edges are emitted directly here). */
if (lang == CBM_LANG_OBJECTSCRIPT_EXPORT) {
CBMArena export_arena;
cbm_arena_init(&export_arena);
int class_count = 0;
char **udl_strings =
cbm_iris_export_to_udl(&export_arena, source, source_len, &class_count);
free(source);
for (int ci = 0; ci < class_count; ci++) {
CBMFileResult *xr = cbm_extract_file_ex(
udl_strings[ci], (int)strlen(udl_strings[ci]), CBM_LANG_OBJECTSCRIPT_UDL,
ctx->project_name, rel, CBM_EXTRACT_BUDGET, NULL, NULL, ctx->macro_table, NULL);
if (!xr) {
continue;
}
for (int d = 0; d < xr->defs.count; d++) {
process_def(ctx, &xr->defs.items[d], rel);
total_defs++;
}
total_calls += xr->calls.count;
total_imports += create_import_edges_for_file(ctx, xr, rel, NULL);
create_channel_edges_for_file(ctx, xr, rel);
create_env_configures_for_file(ctx, xr, rel);
cbm_free_result(xr);
}
cbm_arena_destroy(&export_arena);
continue;
}
/* Extract */
CBMFileResult *result = cbm_extract_file_ex(
source, source_len, lang, ctx->project_name, rel, CBM_EXTRACT_BUDGET, NULL,
NULL /* no extra defines or include paths */, ctx->macro_table, NULL);
/* Studio Export XML is transformed to one cacheable aggregate so later
* passes see the same calls/usages/semantic carriers as native UDL. */
CBMFileResult *result =
lang == CBM_LANG_OBJECTSCRIPT_EXPORT
? cbm_pipeline_extract_objectscript_export(source, source_len, ctx->project_name,
rel, ctx->macro_table, NULL)
: cbm_extract_file_ex(
source, source_len, lang, ctx->project_name, rel, CBM_EXTRACT_BUDGET, NULL,
NULL /* no extra defines or include paths */, ctx->macro_table, NULL);
free(source);
if (!result) {
@@ -647,7 +800,7 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
* map is available without the cache (single-file scope). */
total_imports += create_import_edges_for_file(ctx, result, rel, NULL);
create_channel_edges_for_file(ctx, result, rel);
create_env_configures_for_file(ctx, result, rel);
cbm_pipeline_create_env_configures_for_file(ctx, result, rel);
cbm_free_result(result);
}
}
@@ -680,7 +833,7 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
total_imports +=
create_import_edges_for_file(ctx, result, files[i].rel_path, namespace_map);
create_channel_edges_for_file(ctx, result, files[i].rel_path);
create_env_configures_for_file(ctx, result, files[i].rel_path);
cbm_pipeline_create_env_configures_for_file(ctx, result, files[i].rel_path);
}
cbm_pipeline_namespace_map_free(namespace_map);
if (owns_local_cache) {
+508 -72
View File
@@ -15,6 +15,7 @@
*/
#include "pipeline/pass_lsp_cross.h"
#include "pipeline/pipeline_internal.h"
#include "pipeline/lsp_resolve.h"
#include "lsp/go_lsp.h"
#include "lsp/c_lsp.h"
#include "lsp/py_lsp.h"
@@ -31,6 +32,7 @@
#include "foundation/compat_fs.h"
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
@@ -234,6 +236,19 @@ static const char *pxc_infer_jvm_namespace(CBMArena *arena, const char *rel_path
return ns;
}
static const char *pxc_qn_leaf(const char *name) {
if (!name) {
return NULL;
}
const char *leaf = name;
for (const char *p = name; *p; p++) {
if (*p == '.' || *p == ':' || *p == '/' || *p == '\\') {
leaf = p + 1;
}
}
return leaf;
}
/* Convert one CBMDefinition into a CBMLSPDef. Returns 0 on success, -1
* to skip (unsupported label or missing required field). dst gets borrowed
* pointers into src and into `arena` for synthesised composites. */
@@ -260,7 +275,39 @@ static int pxc_build_lsp_def(CBMArena *arena, const CBMDefinition *src, const ch
* piece, which is what's already stored. */
dst->return_types = src->return_type;
dst->embedded_types = pxc_join_pipe(arena, src->base_classes);
dst->signature_param_types = src->signature_param_types;
dst->signature_param_count = src->signature_param_count;
dst->lang = lang;
dst->decorators = src->decorators;
if (lang == CBM_LANG_RUST) {
/* Exact impl-block provenance is captured while the Rust impl node is
* still on hand. Do not reconstruct it later from leaf names. */
dst->trait_qn = src->impl_trait;
dst->is_abstract = src->is_abstract;
}
return 0;
}
/* Carry one Rust type-level impl independently of any method definition.
* `impl Trait for Type {}` is semantically meaningful even when the block is
* empty (the trait may provide defaults), so attaching the relation only to
* concrete method records is lossy. */
static int pxc_build_rust_impl_relation(CBMArena *arena, const CBMImplTrait *impl,
const char *project_name, const char *rel_path,
const char *module_qn, CBMLSPDef *dst) {
if (!arena || !impl || !impl->trait_name || !impl->struct_name || !impl->struct_qn) {
return -1;
}
const char *receiver_qn = impl->struct_qn;
memset(dst, 0, sizeof(*dst));
dst->qualified_name = receiver_qn;
dst->short_name = pxc_qn_leaf(receiver_qn);
dst->label = "RustImpl";
dst->receiver_type = receiver_qn;
dst->def_module_qn = module_qn;
dst->trait_qn = impl->trait_name; /* raw; canonicalized by Rust registry */
dst->lang = CBM_LANG_RUST;
dst->is_rust_impl_relation = true;
return 0;
}
@@ -272,8 +319,12 @@ CBMLSPDef *cbm_pxc_collect_all_defs(CBMFileResult **cache, const cbm_file_info_t
int *out_count) {
int total = 0;
for (int i = 0; i < file_count; i++) {
if (cache[i])
if (cache[i]) {
total += cache[i]->defs.count;
if (files[i].language == CBM_LANG_RUST) {
total += cache[i]->impl_traits.count;
}
}
}
if (total == 0) {
*out_count = 0;
@@ -306,39 +357,198 @@ CBMLSPDef *cbm_pxc_collect_all_defs(CBMFileResult **cache, const cbm_file_info_t
idx++;
}
}
if (files[fi].language == CBM_LANG_RUST) {
for (int ii = 0; ii < cache[fi]->impl_traits.count; ii++) {
if (pxc_build_rust_impl_relation(
&cache[fi]->arena, &cache[fi]->impl_traits.items[ii], project_name,
files[fi].rel_path, def_modules[fi], &defs[idx]) == 0) {
idx++;
}
}
}
}
*out_count = idx;
return defs;
}
/* Build per-file import map (local_name -> resolved module QN) from gbuf
* IMPORTS edges. Mirrors build_import_map() in pass_parallel.c. Returns 0
* with *out_count = 0 when the file has no IMPORTS edges. Caller frees keys
* with pxc_free_import_map. */
static int pxc_build_import_map(const cbm_gbuf_t *gbuf, const char *project_name,
const char *rel_path, const char ***out_keys,
const char ***out_vals, int *out_count) {
/* Return the one source import path bound to `local_name`, or NULL when the
* extraction metadata is absent or two distinct imports bind the same local.
* The latter is deliberately fail-closed: choosing either path would turn an
* ambiguous Python value into a fabricated CALL_REFERENCE. */
static const char *pxc_unique_import_path(const CBMFileResult *result, const char *local_name,
bool *out_ambiguous) {
if (out_ambiguous)
*out_ambiguous = false;
if (!result || !local_name)
return NULL;
const char *path = NULL;
for (int i = 0; i < result->imports.count; i++) {
const CBMImport *imp = &result->imports.items[i];
if (!imp->local_name || !imp->module_path || strcmp(imp->local_name, local_name) != 0) {
continue;
}
if (path && strcmp(path, imp->module_path) != 0) {
if (out_ambiguous)
*out_ambiguous = true;
return NULL;
}
path = imp->module_path;
}
return path;
}
static const char *pxc_import_leaf(const char *path) {
if (!path)
return NULL;
const char *leaf = path;
for (const char *p = path; *p; p++) {
if (*p == '.' || *p == '/' || *p == '\\' || *p == ':')
leaf = p + 1;
}
return leaf;
}
static char *pxc_kotlin_import_from_metadata(const CBMFileResult *result, const char *local_name) {
bool ambiguous = false;
const char *path = pxc_unique_import_path(result, local_name, &ambiguous);
const char *leaf = pxc_import_leaf(path);
if (ambiguous || !path || !path[0] || path[0] == '.' || !leaf ||
strcmp(leaf, local_name) != 0) {
return NULL;
}
/* Keep the source package/member spelling. Kotlin cross registries are
* package-qualified (target.actual), not path/project-qualified. The
* resolver still requires a registered function/type before emitting an
* exact semantic relationship. */
return strdup(path);
}
/* IMPORTS edges correctly target dependency modules, but Python from-imports
* also carry a source-level member: `from target import handler` is extracted
* as module_path=`target.handler` while its edge targets Module `P.target`.
* Reattach that member only when the raw metadata proves one unique,
* non-aliased path. The shared Python registry must still materialize the
* resulting exact QN before it earns CALL_REFERENCE. */
static char *pxc_import_value_qn(CBMLanguage lang, const CBMFileResult *result,
const char *local_name, const cbm_gbuf_node_t *target) {
if (!target || !target->qualified_name)
return NULL;
if (lang == CBM_LANG_KOTLIN)
return pxc_kotlin_import_from_metadata(result, local_name);
if (lang != CBM_LANG_PYTHON)
return strdup(target->qualified_name);
bool ambiguous = false;
const char *path = pxc_unique_import_path(result, local_name, &ambiguous);
if (ambiguous)
return NULL;
const char *source_leaf = pxc_import_leaf(path);
const char *target_leaf = pxc_import_leaf(target->qualified_name);
if (!path || !source_leaf || !target_leaf || strcmp(source_leaf, local_name) != 0 ||
strcmp(source_leaf, target_leaf) == 0 || !target->label ||
(strcmp(target->label, "Module") != 0 && strcmp(target->label, "Folder") != 0)) {
return strdup(target->qualified_name);
}
size_t need = strlen(target->qualified_name) + 1 + strlen(source_leaf) + 1;
char *qualified = (char *)malloc(need);
if (!qualified)
return NULL;
snprintf(qualified, need, "%s.%s", target->qualified_name, source_leaf);
return qualified;
}
static bool pxc_import_map_has_local(const char *const *keys, int count, const char *local_name) {
if (!keys || !local_name)
return false;
for (int i = 0; i < count; i++) {
if (keys[i] && strcmp(keys[i], local_name) == 0)
return true;
}
return false;
}
/* Recover a Python import directly from extraction metadata when graph import
* materialization produced no edge. Prefer an already materialized exact node
* (including the shared project-prefix rule); otherwise accept only the
* ordinary non-aliased dotted spelling and let the sealed Python registry
* decide whether that QN denotes a class/function. A wrong candidate cannot
* earn a semantic edge because the resolver requires registry materialization. */
static char *pxc_python_import_from_metadata(const cbm_gbuf_t *gbuf, const char *project_name,
const char *local_name, const char *module_path) {
if (!gbuf || !local_name || !module_path)
return NULL;
const char *source_leaf = pxc_import_leaf(module_path);
if (!source_leaf || strcmp(source_leaf, local_name) != 0 || module_path[0] == '.')
return NULL;
const cbm_gbuf_node_t *exact =
cbm_pipeline_lsp_target_node(gbuf, project_name, module_path, false);
if (exact && exact->qualified_name)
return strdup(exact->qualified_name);
const char *last_dot = strrchr(module_path, '.');
if (last_dot && last_dot > module_path) {
size_t module_len = (size_t)(last_dot - module_path);
char *module = (char *)malloc(module_len + 1);
if (!module)
return NULL;
memcpy(module, module_path, module_len);
module[module_len] = '\0';
const cbm_gbuf_node_t *module_node =
cbm_pipeline_lsp_target_node(gbuf, project_name, module, false);
free(module);
if (module_node && module_node->qualified_name && module_node->label &&
(strcmp(module_node->label, "Module") == 0 ||
strcmp(module_node->label, "Folder") == 0)) {
size_t need = strlen(module_node->qualified_name) + 1 + strlen(source_leaf) + 1;
char *qualified = (char *)malloc(need);
if (!qualified)
return NULL;
snprintf(qualified, need, "%s.%s", module_node->qualified_name, source_leaf);
return qualified;
}
}
if (!project_name || !project_name[0])
return strdup(module_path);
size_t need = strlen(project_name) + 1 + strlen(module_path) + 1;
char *qualified = (char *)malloc(need);
if (!qualified)
return NULL;
snprintf(qualified, need, "%s.%s", project_name, module_path);
return qualified;
}
/* Build per-file import map (local_name -> semantic import QN) from gbuf
* IMPORTS edges. Both pipeline drivers call this implementation. Returns 0
* with *out_count = 0 when the file has no IMPORTS edges. */
int cbm_pxc_build_import_map(const cbm_gbuf_t *gbuf, const char *project_name, const char *rel_path,
CBMLanguage lang, const CBMFileResult *result, const char ***out_keys,
const char ***out_vals, int *out_count) {
*out_keys = NULL;
*out_vals = NULL;
*out_count = 0;
char *file_qn = cbm_pipeline_fqn_compute(project_name, rel_path, "__file__");
if (!file_qn)
return 0;
const cbm_gbuf_node_t *file_node = cbm_gbuf_find_by_qn(gbuf, file_qn);
free(file_qn);
if (!file_node)
return 0;
const cbm_gbuf_edge_t **edges = NULL;
int edge_count = 0;
int rc =
cbm_gbuf_find_edges_by_source_type(gbuf, file_node->id, "IMPORTS", &edges, &edge_count);
if (rc != 0 || edge_count == 0)
char *file_qn = cbm_pipeline_fqn_compute(project_name, rel_path, "__file__");
const cbm_gbuf_node_t *file_node = file_qn ? cbm_gbuf_find_by_qn(gbuf, file_qn) : NULL;
free(file_qn);
if (file_node && cbm_gbuf_find_edges_by_source_type(gbuf, file_node->id, "IMPORTS", &edges,
&edge_count) != 0) {
edges = NULL;
edge_count = 0;
}
int metadata_count =
(lang == CBM_LANG_PYTHON || lang == CBM_LANG_KOTLIN) && result ? result->imports.count : 0;
if (edge_count == 0 && metadata_count == 0)
return 0;
const char **keys = (const char **)calloc((size_t)edge_count, sizeof(const char *));
const char **vals = (const char **)calloc((size_t)edge_count, sizeof(const char *));
size_t capacity = (size_t)edge_count + (size_t)metadata_count;
const char **keys = (const char **)calloc(capacity, sizeof(const char *));
const char **vals = (const char **)calloc(capacity, sizeof(const char *));
if (!keys || !vals) {
free(keys);
free(vals);
@@ -363,8 +573,43 @@ static int pxc_build_import_map(const cbm_gbuf_t *gbuf, const char *project_name
continue;
memcpy(local, start, n);
local[n] = '\0';
char *value = pxc_import_value_qn(lang, result, local, target);
if (!value) {
free(local);
continue;
}
keys[count] = local;
vals[count] = target->qualified_name; /* borrowed from gbuf */
vals[count] = value;
count++;
}
/* IMPORTS edges are best-effort graph relationships. The cross-LSP still
* has the authoritative extraction metadata in both sequential and
* parallel caches, so a missing edge must not erase an otherwise exact
* Python import. Add only locals not already represented by an edge. */
for (int i = 0; i < metadata_count; i++) {
const CBMImport *imp = &result->imports.items[i];
if (!imp->local_name || !imp->local_name[0] || !imp->module_path ||
pxc_import_map_has_local(keys, count, imp->local_name)) {
continue;
}
bool ambiguous = false;
const char *path = pxc_unique_import_path(result, imp->local_name, &ambiguous);
if (ambiguous || !path)
continue;
char *value =
lang == CBM_LANG_KOTLIN
? pxc_kotlin_import_from_metadata(result, imp->local_name)
: pxc_python_import_from_metadata(gbuf, project_name, imp->local_name, path);
if (!value)
continue;
char *local = strdup(imp->local_name);
if (!local) {
free(value);
continue;
}
keys[count] = local;
vals[count] = value;
count++;
}
*out_keys = keys;
@@ -373,13 +618,17 @@ static int pxc_build_import_map(const cbm_gbuf_t *gbuf, const char *project_name
return 0;
}
static void pxc_free_import_map(const char **keys, const char **vals, int count) {
void cbm_pxc_free_import_map(const char **keys, const char **vals, int count) {
if (keys) {
for (int i = 0; i < count; i++)
free((void *)keys[i]);
free((void *)keys);
}
free((void *)vals); /* vals strings borrowed from gbuf — don't free elements */
if (vals) {
for (int i = 0; i < count; i++)
free((void *)vals[i]);
free((void *)vals);
}
}
/* Detect TS dialect flags from a relative path. */
@@ -424,10 +673,14 @@ bool cbm_pxc_has_cross_lsp(CBMLanguage lang) {
/* Append cross-file results from `src_out` (allocated in a scratch arena
* about to be destroyed) into `dst_calls` (lives in cache_entry->arena),
* copying every string field into dst_arena. Skips entries whose
* (caller_qn, callee_qn) is already present — avoids inflating the array with
* cross-file duplicates of per-file LSP output.
* (kind, caller_qn, callee_qn, source span) is already present — avoids
* inflating the array with cross-file duplicates without conflating distinct
* same-named occurrences. For an exact duplicate, retain the higher-confidence
* record.
*
* Dedup uses a hash set keyed on "caller\x1fcallee", giving O(1) membership.
* Dedup uses a hash table keyed on
* "kind\x1fcaller\x1fcallee\x1fstart:end\x1forigin", giving O(1)
* membership.
* The previous linear strcmp scan made each append O(n), so a file that
* resolved very many cross-calls turned the whole append into O(n^2) and could
* peg a core for minutes (observed: an index hung in pxc_append_results/strcmp).
@@ -444,9 +697,16 @@ static void pxc_append_results(CBMArena *dst_arena, CBMResolvedCallArray *dst_ca
for (int i = 0; i < dst_calls->count; i++) {
const CBMResolvedCall *rc = &dst_calls->items[i];
if (rc->caller_qn && rc->callee_qn) {
char *k = cbm_arena_sprintf(&keys, "%s\x1f%s", rc->caller_qn, rc->callee_qn);
char *k = cbm_arena_sprintf(&keys, "%u\x1f%s\x1f%s\x1f%u:%u\x1f%u", (unsigned)rc->kind,
rc->caller_qn, rc->callee_qn, rc->site_start_byte,
rc->site_end_byte, (unsigned)rc->source_origin);
if (k) {
cbm_ht_set(seen, k, (void *)1);
void *prior = cbm_ht_get(seen, k);
if (!prior ||
rc->confidence > dst_calls->items[(int)(uintptr_t)prior - 1].confidence) {
const char *stored = cbm_ht_get_key(seen, k);
cbm_ht_set(seen, stored ? stored : k, (void *)(uintptr_t)(i + 1));
}
}
}
}
@@ -455,20 +715,104 @@ static void pxc_append_results(CBMArena *dst_arena, CBMResolvedCallArray *dst_ca
const CBMResolvedCall *src = &src_out->items[j];
if (!src->caller_qn || !src->callee_qn)
continue;
char *k = cbm_arena_sprintf(&keys, "%s\x1f%s", src->caller_qn, src->callee_qn);
if (k && cbm_ht_has(seen, k))
char *k = cbm_arena_sprintf(&keys, "%u\x1f%s\x1f%s\x1f%u:%u\x1f%u", (unsigned)src->kind,
src->caller_qn, src->callee_qn, src->site_start_byte,
src->site_end_byte, (unsigned)src->source_origin);
void *prior = k ? cbm_ht_get(seen, k) : NULL;
if (prior) {
CBMResolvedCall *dst = &dst_calls->items[(int)(uintptr_t)prior - 1];
if (src->confidence > dst->confidence) {
dst->caller_qn = cbm_arena_strdup(dst_arena, src->caller_qn);
dst->callee_qn = cbm_arena_strdup(dst_arena, src->callee_qn);
dst->strategy = src->strategy ? cbm_arena_strdup(dst_arena, src->strategy) : NULL;
dst->confidence = src->confidence;
dst->reason = src->reason ? cbm_arena_strdup(dst_arena, src->reason) : NULL;
dst->kind = src->kind;
dst->site_start_byte = src->site_start_byte;
dst->site_end_byte = src->site_end_byte;
dst->source_origin = src->source_origin;
}
continue;
if (k) {
cbm_ht_set(seen, k, (void *)1);
}
CBMResolvedCall dst;
memset(&dst, 0, sizeof(dst));
CBMResolvedCall dst = {0};
dst.caller_qn = cbm_arena_strdup(dst_arena, src->caller_qn);
dst.callee_qn = cbm_arena_strdup(dst_arena, src->callee_qn);
dst.strategy = src->strategy ? cbm_arena_strdup(dst_arena, src->strategy) : NULL;
dst.confidence = src->confidence;
dst.reason = src->reason ? cbm_arena_strdup(dst_arena, src->reason) : NULL;
dst.kind = src->kind;
dst.site_start_byte = src->site_start_byte;
dst.site_end_byte = src->site_end_byte;
dst.source_origin = src->source_origin;
cbm_resolvedcall_push(dst_calls, dst_arena, dst);
if (k) {
cbm_ht_set(seen, k, (void *)(uintptr_t)dst_calls->count);
}
}
cbm_ht_free(seen);
cbm_arena_destroy(&keys);
}
/* Merge exact synthetic call carriers produced by a cross-LSP resolver. The
* source may live in the per-file result arena or in cbm_pxc_run_one's scratch
* arena, so every pointer-bearing field is copied explicitly. Invalid/legacy
* zero-span carriers are rejected: a requires-LSP carrier is safe only when it
* can join the semantic record for the same source occurrence. */
static void pxc_append_synthetic_calls(CBMArena *dst_arena, CBMCallArray *dst_calls,
const CBMCallArray *src_calls) {
if (!dst_arena || !dst_calls || !src_calls || src_calls->count <= 0)
return;
CBMArena keys;
cbm_arena_init(&keys);
CBMHashTable *seen = cbm_ht_create((uint32_t)(dst_calls->count + src_calls->count + 1));
for (int i = 0; i < dst_calls->count; i++) {
const CBMCall *call = &dst_calls->items[i];
if (!call->callee_name || !call->enclosing_func_qn ||
call->site_end_byte <= call->site_start_byte) {
continue;
}
char *key = cbm_arena_sprintf(
&keys, "%s\x1f%s\x1f%u:%u\x1f%u\x1f%d:%d", call->enclosing_func_qn, call->callee_name,
call->site_start_byte, call->site_end_byte, (unsigned)call->source_origin,
call->requires_lsp_resolution ? 1 : 0, call->is_method ? 1 : 0);
if (key && !cbm_ht_get(seen, key))
cbm_ht_set(seen, key, (void *)(uintptr_t)(i + 1));
}
for (int i = 0; i < src_calls->count; i++) {
const CBMCall *src = &src_calls->items[i];
if (!src->requires_lsp_resolution || !src->callee_name || !src->enclosing_func_qn ||
src->site_end_byte <= src->site_start_byte) {
continue;
}
char *key = cbm_arena_sprintf(
&keys, "%s\x1f%s\x1f%u:%u\x1f%u\x1f%d:%d", src->enclosing_func_qn, src->callee_name,
src->site_start_byte, src->site_end_byte, (unsigned)src->source_origin,
src->requires_lsp_resolution ? 1 : 0, src->is_method ? 1 : 0);
if (key && cbm_ht_get(seen, key))
continue;
CBMCall dst = *src;
dst.callee_name = cbm_arena_strdup(dst_arena, src->callee_name);
dst.enclosing_func_qn = cbm_arena_strdup(dst_arena, src->enclosing_func_qn);
dst.first_string_arg =
src->first_string_arg ? cbm_arena_strdup(dst_arena, src->first_string_arg) : NULL;
dst.second_arg_name =
src->second_arg_name ? cbm_arena_strdup(dst_arena, src->second_arg_name) : NULL;
for (int ai = 0; ai < CBM_MAX_CALL_ARGS; ai++) {
dst.args[ai].expr =
src->args[ai].expr ? cbm_arena_strdup(dst_arena, src->args[ai].expr) : NULL;
dst.args[ai].value =
src->args[ai].value ? cbm_arena_strdup(dst_arena, src->args[ai].value) : NULL;
dst.args[ai].keyword =
src->args[ai].keyword ? cbm_arena_strdup(dst_arena, src->args[ai].keyword) : NULL;
}
cbm_calls_push(dst_calls, dst_arena, dst);
if (key)
cbm_ht_set(seen, key, (void *)(uintptr_t)dst_calls->count);
}
cbm_ht_free(seen);
@@ -498,12 +842,8 @@ 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
* share their first 9 string fields; CBMRustLSPDef adds `trait_qn` before
* `is_interface` whereas CBMLSPDef has `is_interface` followed by `lang`,
* so a memcpy is unsafe — copy field-by-field. trait_qn is left NULL
* because the pipeline's collect-all-defs step does not carry the
* impl-Trait-for-Type linkage; the resolver still recovers trait dispatch
* from the in-file walk (the cross-file path only needs receiver_type). */
* 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) {
if (!defs || def_count <= 0)
return NULL;
@@ -521,8 +861,12 @@ static CBMRustLSPDef *pxc_lspdefs_to_rust(CBMArena *arena, const CBMLSPDef *defs
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].trait_qn = NULL;
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;
}
return out;
}
@@ -543,6 +887,8 @@ void cbm_pxc_run_one(CBMLanguage lang, CBMFileResult *r, const char *source, int
cbm_arena_init(&scratch);
CBMResolvedCallArray out;
memset(&out, 0, sizeof(out));
CBMCallArray synthetic_calls;
memset(&synthetic_calls, 0, sizeof(synthetic_calls));
switch (lang) {
case CBM_LANG_GO:
@@ -563,7 +909,7 @@ void cbm_pxc_run_one(CBMLanguage lang, CBMFileResult *r, const char *source, int
}
case CBM_LANG_PYTHON:
cbm_run_py_lsp_cross(&scratch, source, source_len, module_qn, defs, def_count, imp_names,
imp_qns, imp_count, tree, &out);
imp_qns, imp_count, tree, &out, &synthetic_calls);
break;
case CBM_LANG_PHP:
cbm_run_php_lsp_cross(&scratch, source, source_len, module_qn, defs, def_count, imp_names,
@@ -586,7 +932,7 @@ void cbm_pxc_run_one(CBMLanguage lang, CBMFileResult *r, const char *source, int
CBMRustLSPDef *rdefs = pxc_lspdefs_to_rust(&scratch, defs, 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,
g_pxc_rust_manifest, &out);
g_pxc_rust_manifest, &out, &synthetic_calls);
break;
}
default:
@@ -594,6 +940,7 @@ void cbm_pxc_run_one(CBMLanguage lang, CBMFileResult *r, const char *source, int
}
pxc_append_results(&r->arena, &r->resolved_calls, &out);
pxc_append_synthetic_calls(&r->arena, &r->calls, &synthetic_calls);
cbm_arena_destroy(&scratch);
}
@@ -655,12 +1002,20 @@ void cbm_pxc_dispatch_file(CBMLanguage lang, CBMFileResult *result, const char *
cbm_go_fast_resolve_qualified_calls(result, prebuilt, imp_keys, imp_vals, imp_count);
used_prebuilt = true;
break;
case CBM_LANG_PYTHON:
cbm_run_py_lsp_cross_with_registry(&result->arena, source, source_len, def_module,
prebuilt, imp_keys, imp_vals, imp_count,
result->cached_tree, &result->resolved_calls);
case CBM_LANG_PYTHON: {
CBMArena scratch;
cbm_arena_init(&scratch);
CBMResolvedCallArray out = {0};
CBMCallArray synthetic_calls = {0};
cbm_run_py_lsp_cross_with_registry(&scratch, source, source_len, def_module, prebuilt,
imp_keys, imp_vals, imp_count, result->cached_tree,
&out, &synthetic_calls);
pxc_append_results(&result->arena, &result->resolved_calls, &out);
pxc_append_synthetic_calls(&result->arena, &result->calls, &synthetic_calls);
cbm_arena_destroy(&scratch);
used_prebuilt = true;
break;
}
case CBM_LANG_C:
case CBM_LANG_CPP:
case CBM_LANG_CUDA:
@@ -690,10 +1045,11 @@ void cbm_pxc_dispatch_file(CBMLanguage lang, CBMFileResult *result, const char *
CBMLSPDef *ts_filtered = NULL;
if (module_def_index) {
int fc = 0;
ts_filtered = cbm_pxc_filter_defs_for_file(module_def_index, all_defs, lang,
result->namespace_name, def_module,
imp_vals, imp_count, &fc);
if (ts_filtered) {
bool filter_succeeded = false;
ts_filtered = cbm_pxc_filter_defs_for_file(
module_def_index, all_defs, lang, result->namespace_name, def_module, imp_vals,
imp_count, &fc, &filter_succeeded);
if (filter_succeeded) {
ts_defs = ts_filtered;
ts_def_count = fc;
}
@@ -728,10 +1084,11 @@ void cbm_pxc_dispatch_file(CBMLanguage lang, CBMFileResult *result, const char *
int file_def_count = all_def_count;
if (module_def_index && lang != CBM_LANG_RUST) {
int filtered_count = 0;
filtered =
cbm_pxc_filter_defs_for_file(module_def_index, all_defs, lang, result->namespace_name,
def_module, imp_vals, imp_count, &filtered_count);
if (filtered) {
bool filter_succeeded = false;
filtered = cbm_pxc_filter_defs_for_file(module_def_index, all_defs, lang,
result->namespace_name, def_module, imp_vals,
imp_count, &filtered_count, &filter_succeeded);
if (filter_succeeded) {
file_defs = filtered;
file_def_count = filtered_count;
}
@@ -739,11 +1096,16 @@ void cbm_pxc_dispatch_file(CBMLanguage lang, CBMFileResult *result, const char *
if (lang == CBM_LANG_RUST) {
CBMTypeRegistry *shared = rust_shared_get ? rust_shared_get(rust_shared_ctx) : NULL;
if (shared) {
cbm_run_rust_lsp_cross_with_registry(&result->arena, source, source_len, def_module,
shared, imp_keys, imp_vals, imp_count,
result->cached_tree, cbm_pxc_get_rust_manifest(),
&result->resolved_calls,
/*result=*/NULL);
CBMArena scratch;
cbm_arena_init(&scratch);
CBMResolvedCallArray out = {0};
CBMCallArray synthetic_calls = {0};
cbm_run_rust_lsp_cross_with_registry(
&scratch, source, source_len, def_module, shared, imp_keys, imp_vals, imp_count,
result->cached_tree, cbm_pxc_get_rust_manifest(), &out, &synthetic_calls);
pxc_append_results(&result->arena, &result->resolved_calls, &out);
pxc_append_synthetic_calls(&result->arena, &result->calls, &synthetic_calls);
cbm_arena_destroy(&scratch);
} else {
cbm_pxc_run_one(lang, result, source, source_len, def_module, file_defs, file_def_count,
imp_keys, imp_vals, imp_count);
@@ -877,8 +1239,8 @@ int cbm_pipeline_pass_lsp_cross(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *
const char **imp_keys = NULL;
const char **imp_vals = NULL;
int imp_count = 0;
pxc_build_import_map(ctx->gbuf, ctx->project_name, files[i].rel_path, &imp_keys, &imp_vals,
&imp_count);
cbm_pxc_build_import_map(ctx->gbuf, ctx->project_name, files[i].rel_path, lang, cache[i],
&imp_keys, &imp_vals, &imp_count);
/* Journal around the resolve: a hang here must be attributed to THIS
* file, not to a stale extraction marker (the innocent-quarantine
@@ -891,7 +1253,7 @@ int cbm_pipeline_pass_lsp_cross(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *
per_lang_calls++;
processed++;
pxc_free_import_map(imp_keys, imp_vals, imp_count);
cbm_pxc_free_import_map(imp_keys, imp_vals, imp_count);
free(source);
}
@@ -1003,6 +1365,15 @@ static bool pxc_is_jvm_lang(CBMLanguage lang) {
return lang == CBM_LANG_JAVA || lang == CBM_LANG_KOTLIN;
}
/* Java and Kotlin files without a package declaration still share one JVM
* default package. The hash table intentionally rejects empty keys, so use an
* internal-only sentinel to make that package selectable across files. */
static const char *pxc_namespace_index_key(const char *namespace_name) {
return namespace_name && namespace_name[0] ? namespace_name
: "\x1f"
"cbm-jvm-default-package";
}
static bool pxc_def_lang_matches(CBMLanguage caller_lang, CBMLanguage def_lang) {
if (pxc_is_jvm_lang(caller_lang)) {
return pxc_is_jvm_lang(def_lang);
@@ -1023,6 +1394,65 @@ static void pxc_mark_module_defs(const CBMModuleDefIndex *idx, bool *selected,
}
}
/* Import maps bind source locals to semantic targets. A from-import therefore
* carries a symbol QN (`project.module.Class` / `project.module.function`),
* while the def index is keyed by the declaring file module
* (`project.module`). Select the nearest materialized module prefix; the
* language registry still has to prove the full target QN before an edge is
* emitted, so this broadens the candidate set without weakening precision. */
static void pxc_mark_import_defs(const CBMModuleDefIndex *idx, bool *selected,
const CBMLSPDef *all_defs, CBMLanguage caller_lang,
const char *import_qn, int *total) {
if (!idx || !idx->ht || !import_qn || !import_qn[0]) {
return;
}
char *candidate = strdup(import_qn);
if (!candidate) {
return;
}
bool matched = false;
for (;;) {
pxc_module_entry_t *entry = (pxc_module_entry_t *)cbm_ht_get(idx->ht, candidate);
if (entry) {
int added = pxc_mark_entry_defs(selected, entry, all_defs, caller_lang);
if (total) {
*total += added;
}
matched = true;
break;
}
char *dot = strrchr(candidate, '.');
if (!dot) {
break;
}
*dot = '\0';
}
/* Kotlin/Java source imports are package-qualified while file modules are
* path-qualified. If no module prefix matched, walk the declared-package
* index from the full import toward its package. This only selects a small
* candidate registry; the language resolver must still prove the symbol. */
if (!matched && pxc_is_jvm_lang(caller_lang) && idx->namespace_ht) {
strcpy(candidate, import_qn);
for (;;) {
pxc_module_entry_t *entry = (pxc_module_entry_t *)cbm_ht_get(
idx->namespace_ht, pxc_namespace_index_key(candidate));
if (entry) {
int added = pxc_mark_entry_defs(selected, entry, all_defs, caller_lang);
if (total) {
*total += added;
}
break;
}
char *dot = strrchr(candidate, '.');
if (!dot) {
break;
}
*dot = '\0';
}
}
free(candidate);
}
CBMModuleDefIndex *cbm_pxc_build_module_def_index(CBMLSPDef *all_defs, int def_count) {
if (!all_defs || def_count <= 0) {
return NULL;
@@ -1043,7 +1473,9 @@ CBMModuleDefIndex *cbm_pxc_build_module_def_index(CBMLSPDef *all_defs, int def_c
pxc_module_entry_add_index(pxc_module_entry_get_or_create(ht, all_defs[i].def_module_qn),
i);
pxc_module_entry_add_index(
pxc_module_entry_get_or_create(namespace_ht, all_defs[i].namespace_name), i);
pxc_module_entry_get_or_create(namespace_ht,
pxc_namespace_index_key(all_defs[i].namespace_name)),
i);
}
CBMModuleDefIndex *idx = (CBMModuleDefIndex *)calloc(1, sizeof(*idx));
@@ -1078,11 +1510,14 @@ void cbm_pxc_free_module_def_index(CBMModuleDefIndex *idx) {
CBMLSPDef *cbm_pxc_filter_defs_for_file(const CBMModuleDefIndex *idx, CBMLSPDef *all_defs,
CBMLanguage caller_lang, const char *caller_namespace,
const char *own_module, const char *const *imp_qns,
int imp_count, int *out_count) {
int imp_count, int *out_count, bool *out_success) {
if (out_count) {
*out_count = 0;
}
if (!idx || !idx->ht || !all_defs || !out_count || idx->def_count <= 0) {
if (out_success) {
*out_success = false;
}
if (!idx || !idx->ht || !all_defs || !out_count || !out_success || idx->def_count <= 0) {
return NULL;
}
@@ -1094,17 +1529,17 @@ CBMLSPDef *cbm_pxc_filter_defs_for_file(const CBMModuleDefIndex *idx, CBMLSPDef
int total = 0;
pxc_mark_module_defs(idx, selected, all_defs, caller_lang, own_module, &total);
for (int i = 0; i < imp_count; i++) {
pxc_mark_module_defs(idx, selected, all_defs, caller_lang, imp_qns[i], &total);
pxc_mark_import_defs(idx, selected, all_defs, caller_lang, imp_qns[i], &total);
}
if (pxc_is_jvm_lang(caller_lang) && caller_namespace && caller_namespace[0] &&
idx->namespace_ht) {
pxc_module_entry_t *e =
(pxc_module_entry_t *)cbm_ht_get(idx->namespace_ht, caller_namespace);
if (pxc_is_jvm_lang(caller_lang) && idx->namespace_ht) {
const char *namespace_key = pxc_namespace_index_key(caller_namespace);
pxc_module_entry_t *e = (pxc_module_entry_t *)cbm_ht_get(idx->namespace_ht, namespace_key);
total += pxc_mark_entry_defs(selected, e, all_defs, caller_lang);
}
if (total == 0) {
free(selected);
*out_success = true;
return NULL;
}
@@ -1121,6 +1556,7 @@ CBMLSPDef *cbm_pxc_filter_defs_for_file(const CBMModuleDefIndex *idx, CBMLSPDef
}
}
*out_count = n;
*out_success = true;
free(selected);
return out;
}
+15 -2
View File
@@ -58,6 +58,17 @@ CBMLSPDef *cbm_pxc_collect_all_defs(CBMFileResult **cache, const cbm_file_info_t
void cbm_pxc_ts_modes(CBMLanguage lang, const char *rel_path, bool *out_js, bool *out_jsx,
bool *out_dts);
/* Build the local-name -> semantic import-QN map consumed by cross-file LSPs.
* Both sequential and parallel drivers use this exact helper so import
* metadata cannot diverge between pipelines. Values are owned by the returned
* map (not borrowed from gbuf); release both arrays with
* cbm_pxc_free_import_map(). */
int cbm_pxc_build_import_map(const cbm_gbuf_t *gbuf, const char *project_name, const char *rel_path,
CBMLanguage lang, const CBMFileResult *result, const char ***out_keys,
const char ***out_vals, int *out_count);
void cbm_pxc_free_import_map(const char **keys, const char **vals, int count);
/* ── Per-module def index (the gopls "package summary" pattern) ──
*
* The hot path used to register ALL all_defs[] into a fresh registry
@@ -87,11 +98,13 @@ void cbm_pxc_free_module_def_index(CBMModuleDefIndex *idx);
* `src/main/kotlin` roots without import statements. String fields inside
* each entry are borrowed from the original all_defs[] arena (caller keeps
* it alive). Caller frees the returned array with free(). Writes the entry
* count to *out_count. Returns NULL if no matches (with *out_count = 0). */
* count to *out_count and sets *out_success on every valid selection. A valid
* empty selection returns NULL with *out_count = 0 and *out_success = true;
* NULL with *out_success = false means invalid input or allocation failure. */
CBMLSPDef *cbm_pxc_filter_defs_for_file(const CBMModuleDefIndex *idx, CBMLSPDef *all_defs,
CBMLanguage caller_lang, const char *caller_namespace,
const char *own_module, const char *const *imp_qns,
int imp_count, int *out_count);
int imp_count, int *out_count, bool *out_success);
/* ── Tier 2 full: pre-built per-language cross-LSP registries ─────
*
+386 -166
View File
@@ -89,7 +89,6 @@ enum { PP_CSHARP_M_PREFIX_LEN = 2 };
#include "cbm.h"
#include "arena.h"
#include "macro_table.h"
#include "iris_export_xml.h"
#include "simhash/minhash.h"
#include "semantic/ast_profile.h"
@@ -106,6 +105,7 @@ enum { PP_CSHARP_M_PREFIX_LEN = 2 };
* not re-pay the full nap tax on every file pull when napping cannot reclaim
* memory (the resident floor, not in-flight transients, holds the budget). */
static _Atomic long g_bp_nap_cycles = 0;
static _Atomic uint64_t g_lsp_linear_fallback_rows = 0;
long cbm_pp_bp_nap_cycles(void) {
return atomic_load_explicit(&g_bp_nap_cycles, memory_order_relaxed);
@@ -115,6 +115,14 @@ void cbm_pp_bp_nap_cycles_reset(void) {
atomic_store_explicit(&g_bp_nap_cycles, 0, memory_order_relaxed);
}
uint64_t cbm_pp_lsp_linear_fallback_rows(void) {
return atomic_load_explicit(&g_lsp_linear_fallback_rows, memory_order_relaxed);
}
void cbm_pp_lsp_linear_fallback_rows_reset(void) {
atomic_store_explicit(&g_lsp_linear_fallback_rows, 0, memory_order_relaxed);
}
/* Parse a positive MB-valued retention env knob (CBM_RETAIN_*_MB) into bytes.
* Follows the limits.c strtol convention: unset / unparseable / non-positive
* → return 0 so the caller keeps its derived default. */
@@ -528,68 +536,6 @@ static void build_def_props(char *buf, size_t bufsize, const CBMDefinition *def)
}
}
/* Build import map from graph buffer IMPORTS edges (read-only access to gbuf). */
static int build_import_map(const cbm_gbuf_t *gbuf, const char *project_name, const char *rel_path,
const char ***out_keys, const char ***out_vals, int *out_count) {
*out_keys = NULL;
*out_vals = NULL;
*out_count = 0;
char *file_qn = cbm_pipeline_fqn_compute(project_name, rel_path, "__file__");
const cbm_gbuf_node_t *file_node = cbm_gbuf_find_by_qn(gbuf, file_qn);
free(file_qn);
if (!file_node) {
return 0;
}
const cbm_gbuf_edge_t **edges = NULL;
int edge_count = 0;
int rc =
cbm_gbuf_find_edges_by_source_type(gbuf, file_node->id, "IMPORTS", &edges, &edge_count);
if (rc != 0 || edge_count == 0) {
return 0;
}
const char **keys = calloc(edge_count, sizeof(const char *));
const char **vals = calloc(edge_count, sizeof(const char *));
int count = 0;
for (int i = 0; i < edge_count; i++) {
const cbm_gbuf_edge_t *e = edges[i];
const cbm_gbuf_node_t *target = cbm_gbuf_find_by_id(gbuf, e->target_id);
if (!target || !e->properties_json) {
continue;
}
const char *start = strstr(e->properties_json, "\"local_name\":\"");
if (start) {
start += strlen("\"local_name\":\"");
const char *end = strchr(start, '"');
if (end && end > start) {
keys[count] = cbm_strndup(start, end - start);
vals[count] = target->qualified_name;
count++;
}
}
}
*out_keys = keys;
*out_vals = vals;
*out_count = count;
return 0;
}
static void free_import_map(const char **keys, const char **vals, int count) {
if (keys) {
for (int i = 0; i < count; i++) {
free((void *)keys[i]);
}
free((void *)keys);
}
if (vals) {
free((void *)vals);
}
}
/* True for languages whose module QN derives from the CONTAINING DIRECTORY
* (Java/Go package). MUST match cbm_lang_module_is_dir() (internal/cbm/helpers.c)
* and pxc_module_is_dir() (pass_lsp_cross.c) so same-module callee resolution
@@ -634,11 +580,14 @@ static void extract_decorator_func(const char *dec, char *out, size_t outsz) {
return;
}
const char *start = dec;
if (*start == '@') {
while (*start == '@' || *start == '#' || *start == '[' || *start == ' ') {
start++;
}
const char *paren = strchr(start, '(');
size_t len = paren ? (size_t)(paren - start) : strlen(start);
size_t len = 0;
while (start[len] && start[len] != '(' && start[len] != '[' && start[len] != ']' &&
start[len] != ' ' && start[len] != ',') {
len++;
}
if (len == 0 || len >= outsz) {
return;
}
@@ -886,38 +835,17 @@ static void extract_worker(int worker_id, void *ctx_ptr) {
uint64_t file_t0 = extract_now_ns();
/* ObjectScript Studio Export XML: transcode each <Class> to UDL and
* extract directly into the local gbuf (the per-file cache holds a single
* result, so multi-class Export files are processed inline here). */
if (fi->language == CBM_LANG_OBJECTSCRIPT_EXPORT) {
CBMArena ea;
cbm_arena_init(&ea);
int cc = 0;
char **udls = cbm_iris_export_to_udl(&ea, source, source_len, &cc);
for (int ci = 0; ci < cc; ci++) {
CBMFileResult *xr =
cbm_extract_file_ex(udls[ci], (int)strlen(udls[ci]), CBM_LANG_OBJECTSCRIPT_UDL,
ec->project_name, fi->rel_path, CBM_EXTRACT_BUDGET, NULL,
NULL, ec->macro_table, ec->return_type_table);
if (!xr) {
continue;
}
for (int d = 0; d < xr->defs.count; d++) {
CBMDefinition *def = &xr->defs.items[d];
if (def->qualified_name && def->name) {
insert_def_into_gbuf(ws, fi, def);
}
}
cbm_free_result(xr);
}
cbm_arena_destroy(&ea);
free_source(source);
continue;
}
CBMFileResult *result = cbm_extract_file_ex(
source, source_len, fi->language, ec->project_name, fi->rel_path, CBM_EXTRACT_BUDGET,
NULL, NULL, ec->macro_table, ec->return_type_table);
/* Export XML uses the same cache slot as every physical file, so its
* generated classes are composed before entering the common registry
* and resolution lifecycle. */
CBMFileResult *result =
fi->language == CBM_LANG_OBJECTSCRIPT_EXPORT
? cbm_pipeline_extract_objectscript_export(source, source_len, ec->project_name,
fi->rel_path, ec->macro_table,
ec->return_type_table)
: cbm_extract_file_ex(source, source_len, fi->language, ec->project_name,
fi->rel_path, CBM_EXTRACT_BUDGET, NULL, NULL, ec->macro_table,
ec->return_type_table);
uint64_t file_elapsed_ms = (extract_now_ns() - file_t0) / PP_USEC_PER_MS;
@@ -1395,6 +1323,7 @@ int cbm_build_registry_from_cache(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
imports_edges += create_imports_edges(ctx, result, rel, namespace_map);
create_channel_edges(ctx, result, rel);
cbm_pipeline_create_env_configures_for_file(ctx, result, rel);
}
cbm_pipeline_namespace_map_free(namespace_map);
@@ -2182,46 +2111,205 @@ static void lsp_idx_free_key(const char *key, void *value, void *ud) {
free((char *)key);
}
/* A hash key may have multiple semantic rows. Keep a permanent tombstone for
* distinct targets so O(1) lookup cannot turn ambiguity into a confidence
* contest; duplicate rows for the same target may still keep the best score. */
static CBMResolvedCall *const LSP_IDX_AMBIGUOUS = (CBMResolvedCall *)(uintptr_t)1;
static bool lsp_idx_keep_best(CBMHashTable *index, const char *key, CBMResolvedCall *candidate,
const cbm_gbuf_t *gbuf, const char *project_name,
bool allow_tail_match) {
if (!index || !key || !candidate) {
return false;
}
CBMResolvedCall *existing = (CBMResolvedCall *)cbm_ht_get(index, key);
if (!existing) {
char *owned_key = strdup(key);
if (owned_key) {
cbm_ht_set(index, owned_key, candidate);
if (cbm_ht_get(index, key) == candidate) {
return true;
}
free(owned_key);
}
return false;
}
if (existing == LSP_IDX_AMBIGUOUS) {
return true;
}
if (!cbm_pipeline_invocation_targets_equal(existing->callee_qn, candidate->callee_qn, gbuf,
project_name, allow_tail_match)) {
const char *stored_key = cbm_ht_get_key(index, key);
if (stored_key) {
cbm_ht_set(index, stored_key, LSP_IDX_AMBIGUOUS);
}
return cbm_ht_get(index, key) != NULL;
}
if (candidate->confidence > existing->confidence) {
const char *stored_key = cbm_ht_get_key(index, key);
if (stored_key) {
cbm_ht_set(index, stored_key, candidate);
}
}
return cbm_ht_get(index, key) != NULL;
}
/* Caller QNs are user-controlled graph identifiers and can legitimately exceed
* the old 1 KiB stack buffer. Build occurrence keys at their exact length so an
* exact semantic row can never disappear merely because its caller is long. */
static char *lsp_idx_key(const char *caller_qn, const char *leaf, bool exact_site,
uint32_t site_start_byte, uint32_t site_end_byte,
CBMSourceOrigin source_origin) {
if (!caller_qn || !leaf) {
return NULL;
}
size_t caller_len = strlen(caller_qn);
size_t leaf_len = strlen(leaf);
size_t suffix_len = exact_site ? 48U : 16U;
if (caller_len > SIZE_MAX - leaf_len - suffix_len - 2U) {
return NULL;
}
size_t cap = caller_len + 1U + leaf_len + suffix_len + 1U;
char *key = (char *)malloc(cap);
if (!key) {
return NULL;
}
int written = exact_site
? snprintf(key, cap, "%s|%s|%u:%u|%u", caller_qn, leaf, site_start_byte,
site_end_byte, (unsigned)source_origin)
: snprintf(key, cap, "%s|%s|%u", caller_qn, leaf, (unsigned)source_origin);
if (written <= 0 || (size_t)written >= cap) {
free(key);
return NULL;
}
return key;
}
static bool lsp_idx_insert_leaf(CBMHashTable *index, CBMResolvedCall *candidate, const char *leaf,
bool exact_site, const cbm_gbuf_t *gbuf, const char *project_name,
bool allow_tail_match) {
if (!index || !candidate || !candidate->caller_qn || !leaf || !leaf[0]) {
return false;
}
char *key = lsp_idx_key(candidate->caller_qn, leaf, exact_site, candidate->site_start_byte,
candidate->site_end_byte, candidate->source_origin);
if (!key) {
return false;
}
bool inserted = lsp_idx_keep_best(index, key, candidate, gbuf, project_name, allow_tail_match);
free(key);
return inserted;
}
static const CBMResolvedCall *lsp_idx_lookup(const CBMHashTable *index, const CBMCall *call,
bool exact_site, bool *key_built, bool *ambiguous) {
if (key_built) {
*key_built = false;
}
if (ambiguous) {
*ambiguous = false;
}
if (!index || !call || !call->enclosing_func_qn || !call->callee_name) {
return NULL;
}
const char *leaf = cbm_pipeline_call_callee_leaf(call->callee_name);
if (!leaf || !leaf[0]) {
return NULL;
}
char *key = lsp_idx_key(call->enclosing_func_qn, leaf, exact_site, call->site_start_byte,
call->site_end_byte, call->source_origin);
if (!key) {
return NULL;
}
if (key_built) {
*key_built = true;
}
const CBMResolvedCall *candidate = (const CBMResolvedCall *)cbm_ht_get(index, key);
free(key);
if (candidate == LSP_IDX_AMBIGUOUS) {
if (ambiguous) {
*ambiguous = true;
}
return NULL;
}
if (!candidate || candidate->kind != CBM_RESOLVED_INVOCATION || !candidate->caller_qn ||
strcmp(candidate->caller_qn, call->enclosing_func_qn) != 0 ||
candidate->confidence < CBM_LSP_CONFIDENCE_FLOOR) {
return NULL;
}
int site_rank = cbm_pipeline_invocation_site_rank(candidate, call);
int expected_rank = exact_site ? 2 : 1;
return site_rank == expected_rank &&
cbm_pipeline_invocation_leaf_matches(candidate, call, site_rank)
? candidate
: NULL;
}
/* Resolve calls for one file and emit CALLS/HTTP_CALLS/ASYNC_CALLS edges. */
static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CBMFileResult *result,
const char *rel, const char *module_qn, const char **imp_keys,
const char **imp_vals, int imp_count, CBMLanguage lang) {
/* Build a per-file hash index of resolved_calls keyed by
* "caller_qn|callee_short" for O(1) lookup. cbm_pipeline_find_lsp_
* resolution would otherwise do an O(N) linear scan over
* resolved_calls for EACH of result->calls.count calls — the
* dominant cost in parallel_resolve on kubernetes (~50s of pure
* scanning). On insert, keep the highest-confidence entry per key
* (matches the original "best" tie-break). Skip the build entirely
* when there are no calls (nothing to look up) or no resolved
* entries (lookups would all miss). */
CBMHashTable *lsp_idx = NULL;
/* Two occurrence-aware indexes preserve the authoritative matcher's
* primary ordering without restoring its O(calls × resolutions) scan:
* exact caller+leaf+span first, then the legacy caller+leaf fallback.
* Repeated same-leaf calls therefore remain O(1), and a high-confidence
* 0:0 record can never hide an exact source occurrence. */
CBMHashTable *lsp_exact_idx = NULL;
CBMHashTable *lsp_legacy_idx = NULL;
bool lsp_exact_idx_complete = true;
bool lsp_legacy_idx_complete = true;
bool allow_tail = cbm_pipeline_lsp_allow_tail_match(lang);
if (result->calls.count > 0 && result->resolved_calls.count > 0) {
lsp_idx = cbm_ht_create((uint32_t)result->resolved_calls.count * 2u + 16u);
if (lsp_idx) {
uint32_t capacity = (uint32_t)result->resolved_calls.count * 2u + 16u;
lsp_exact_idx = cbm_ht_create(capacity);
lsp_legacy_idx = cbm_ht_create(capacity);
if (!lsp_exact_idx) {
lsp_exact_idx_complete = false;
}
if (!lsp_legacy_idx) {
lsp_legacy_idx_complete = false;
}
if (lsp_exact_idx || lsp_legacy_idx) {
for (int i = 0; i < result->resolved_calls.count; i++) {
CBMResolvedCall *rc_e = &result->resolved_calls.items[i];
if (!rc_e->caller_qn || !rc_e->callee_qn ||
if (rc_e->kind != CBM_RESOLVED_INVOCATION || !rc_e->caller_qn || !rc_e->callee_qn ||
rc_e->confidence < CBM_LSP_CONFIDENCE_FLOOR) {
continue;
}
const char *short_name = strrchr(rc_e->callee_qn, '.');
short_name = short_name ? short_name + 1 : rc_e->callee_qn;
char key[1024];
int kn = snprintf(key, sizeof(key), "%s|%s", rc_e->caller_qn, short_name);
if (kn <= 0 || kn >= (int)sizeof(key))
bool exact_site =
cbm_pipeline_source_site_present(rc_e->site_start_byte, rc_e->site_end_byte);
bool legacy_site =
cbm_pipeline_source_site_legacy(rc_e->site_start_byte, rc_e->site_end_byte);
if (!exact_site && !legacy_site) {
continue;
CBMResolvedCall *existing = (CBMResolvedCall *)cbm_ht_get(lsp_idx, key);
if (!existing) {
/* New entry — strdup so the key outlives the loop body. */
char *kdup = strdup(key);
if (kdup)
cbm_ht_set(lsp_idx, kdup, rc_e);
} else if (rc_e->confidence > existing->confidence) {
/* Update value; reuse stored key pointer to avoid leak. */
const char *skey = cbm_ht_get_key(lsp_idx, key);
if (skey)
cbm_ht_set(lsp_idx, skey, rc_e);
}
CBMHashTable *index = exact_site ? lsp_exact_idx : lsp_legacy_idx;
bool inserted =
lsp_idx_insert_leaf(index, rc_e, cbm_lsp_bare_segment(rc_e->callee_qn),
exact_site, rc->main_gbuf, rc->project_name, allow_tail);
if (!inserted) {
if (exact_site) {
lsp_exact_idx_complete = false;
} else {
lsp_legacy_idx_complete = false;
}
}
if (rc_e->reason && cbm_pipeline_invocation_reason_join_strategy(rc_e->strategy)) {
inserted = lsp_idx_insert_leaf(index, rc_e, cbm_lsp_bare_segment(rc_e->reason),
exact_site, rc->main_gbuf, rc->project_name,
allow_tail);
if (!inserted) {
if (exact_site) {
lsp_exact_idx_complete = false;
} else {
lsp_legacy_idx_complete = false;
}
}
}
if (exact_site && rc_e->strategy && strcmp(rc_e->strategy, "lsp_destructor") == 0 &&
!lsp_idx_insert_leaf(index, rc_e, "~", true, rc->main_gbuf, rc->project_name,
allow_tail)) {
lsp_exact_idx_complete = false;
}
}
}
@@ -2247,25 +2335,37 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
* LSP overrides so a project doesn't get different attributions
* depending on whether parallel mode kicked in. Unique-tail
* fallbacks are JVM-only (see cbm_pipeline_lsp_allow_tail_match). */
bool allow_tail = cbm_pipeline_lsp_allow_tail_match(lang);
cbm_resolution_t res = {0};
const CBMResolvedCall *lsp = NULL;
bool exact_key_built = true;
bool exact_key_ambiguous = false;
bool legacy_key_built = true;
_rc_t0 = extract_now_ns();
if (lsp_idx && call->enclosing_func_qn) {
const char *call_leaf = cbm_pipeline_call_callee_leaf(call->callee_name);
char key[1024];
int kn = call_leaf
? snprintf(key, sizeof(key), "%s|%s", call->enclosing_func_qn, call_leaf)
: -1;
if (kn > 0 && kn < (int)sizeof(key)) {
lsp = (const CBMResolvedCall *)cbm_ht_get(lsp_idx, key);
}
if (cbm_pipeline_source_site_present(call->site_start_byte, call->site_end_byte)) {
lsp = lsp_idx_lookup(lsp_exact_idx, call, true, &exact_key_built, &exact_key_ambiguous);
}
if (!lsp) {
bool exact_index_authoritative = lsp_exact_idx_complete && exact_key_built;
if (!lsp && !exact_index_authoritative) {
/* An incomplete exact index must fail closed: consult the
* authoritative matcher before a legacy row can win. */
atomic_fetch_add_explicit(&g_lsp_linear_fallback_rows,
(uint64_t)result->resolved_calls.count, memory_order_relaxed);
lsp = cbm_pipeline_find_lsp_resolution_in_graph(
&result->resolved_calls, call, allow_tail, rc->main_gbuf, rc->project_name);
}
if (!lsp && !call->requires_lsp_resolution && exact_index_authoritative &&
!exact_key_ambiguous) {
lsp = lsp_idx_lookup(lsp_legacy_idx, call, false, &legacy_key_built, NULL);
}
if (!lsp && (!lsp_legacy_idx_complete || !legacy_key_built || allow_tail ||
call->requires_lsp_resolution)) {
/* Fallback to the linear scan for edge cases the index may
* miss (e.g. callee_name that wasn't the registered short
* name). Keeps semantics identical. */
lsp = cbm_pipeline_find_lsp_resolution(&result->resolved_calls, call, allow_tail);
atomic_fetch_add_explicit(&g_lsp_linear_fallback_rows,
(uint64_t)result->resolved_calls.count, memory_order_relaxed);
lsp = cbm_pipeline_find_lsp_resolution_in_graph(
&result->resolved_calls, call, allow_tail, rc->main_gbuf, rc->project_name);
}
atomic_fetch_add_explicit(&rc->time_ns_rc_lsp_lookup, extract_now_ns() - _rc_t0,
memory_order_relaxed);
@@ -2275,8 +2375,13 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
/* Canonicalise to the gbuf node's QN so res.qualified_name matches
* the gbuf even when the cross-file fallback had to prefix the
* project name. */
lsp_target = cbm_pipeline_lsp_target_node(rc->main_gbuf, rc->project_name,
lsp->callee_qn, allow_tail);
bool exact_external_target = call->requires_lsp_resolution &&
cbm_pipeline_kotlin_external_target(lang, lsp->callee_qn);
lsp_target = exact_external_target
? cbm_pipeline_lsp_target_node_strict(rc->main_gbuf, rc->project_name,
lsp->callee_qn, allow_tail)
: cbm_pipeline_lsp_target_node(rc->main_gbuf, rc->project_name,
lsp->callee_qn, allow_tail);
if (lsp_target) {
res.qualified_name = lsp_target->qualified_name;
res.strategy = lsp->strategy ? lsp->strategy : "lsp_override";
@@ -2295,14 +2400,24 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
* losing every alias-imported JSX component edge on the parallel path
* (~21% of a Next.js call graph) while the sequential pass, which falls
* THROUGH to the registry here, kept them. This restores seq/parallel
* parity via the import_map / unique_name resolution. */
if (!res.qualified_name || !res.qualified_name[0]) {
* parity via the import_map / unique_name resolution. Synthetic
* semantic candidates are deliberately excluded: they require an
* exact LSP target and must fail closed rather than accepting a textual
* registry match. */
if ((!res.qualified_name || !res.qualified_name[0]) && !call->requires_lsp_resolution) {
res = cbm_registry_resolve(rc->registry, call->callee_name, module_qn, imp_keys,
imp_vals, imp_count);
}
atomic_fetch_add_explicit(&rc->time_ns_rc_resolve, extract_now_ns() - _rc_t0,
memory_order_relaxed);
/* A synthetic semantic candidate is an invocation only when the LSP
* resolved it to a concrete graph node. Never let registry, field-name,
* route, or service heuristics manufacture a target for it. */
if (call->requires_lsp_resolution && !lsp_target) {
continue;
}
_rc_t0 = extract_now_ns();
try_field_type_hint(rc, &res, call->callee_name, source_node->id);
atomic_fetch_add_explicit(&rc->time_ns_rc_hint, extract_now_ns() - _rc_t0,
@@ -2428,16 +2543,24 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
memory_order_relaxed);
ws->calls_resolved++;
}
if (lsp_idx) {
cbm_ht_foreach(lsp_idx, lsp_idx_free_key, NULL);
cbm_ht_free(lsp_idx);
if (lsp_exact_idx) {
cbm_ht_foreach(lsp_exact_idx, lsp_idx_free_key, NULL);
cbm_ht_free(lsp_exact_idx);
}
if (lsp_legacy_idx) {
cbm_ht_foreach(lsp_legacy_idx, lsp_idx_free_key, NULL);
cbm_ht_free(lsp_legacy_idx);
}
}
/* Resolve usages for one file. */
static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws,
CBMFileResult *result, const char *rel, const char *module_qn,
const char **imp_keys, const char **imp_vals, int imp_count) {
const char **imp_keys, const char **imp_vals, int imp_count,
CBMLanguage lang) {
cbm_pipeline_lsp_reference_index_t reference_index = {0};
bool reference_index_ready =
cbm_pipeline_lsp_reference_index_build(&result->resolved_calls, &reference_index);
for (int u = 0; u < result->usages.count; u++) {
CBMUsage *usage = &result->usages.items[u];
if (!usage->ref_name) {
@@ -2448,12 +2571,45 @@ static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws,
if (!src) {
continue;
}
cbm_resolution_t res = cbm_registry_resolve(rc->registry, usage->ref_name, module_qn,
imp_keys, imp_vals, imp_count);
if (!res.qualified_name || res.qualified_name[0] == '\0') {
continue;
const cbm_gbuf_node_t *tgt = NULL;
bool precise_call_reference = false;
const CBMResolvedCall *semantic_reference = NULL;
if (cbm_pipeline_usage_semantic_reference_candidate(usage)) {
bool allow_tail = cbm_pipeline_lsp_allow_tail_match(lang);
semantic_reference = cbm_pipeline_find_lsp_reference_indexed_in_graph(
&result->resolved_calls, reference_index_ready ? &reference_index : NULL, usage,
allow_tail, rc->main_gbuf, rc->project_name);
if (semantic_reference &&
!cbm_pipeline_usage_allows_semantic_reference(usage, semantic_reference)) {
semantic_reference = NULL;
}
if (semantic_reference) {
tgt = cbm_pipeline_lsp_target_node(rc->main_gbuf, rc->project_name,
semantic_reference->callee_qn, allow_tail);
precise_call_reference = cbm_pipeline_node_is_callable_target(tgt);
}
}
if (!tgt) {
/* Exact semantic ownership beats textual name fallback even when
* the semantic target is not materialized in this graph. */
if (semantic_reference) {
continue;
}
cbm_resolution_t res = cbm_registry_resolve(rc->registry, usage->ref_name, module_qn,
imp_keys, imp_vals, imp_count);
if (!res.qualified_name || res.qualified_name[0] == '\0') {
continue;
}
if (!cbm_pipeline_reference_candidate_fallback_allowed(lang, usage, res.strategy,
imp_keys, imp_count)) {
continue;
}
tgt = cbm_gbuf_find_by_qn(rc->main_gbuf, res.qualified_name);
if (usage->semantic_reference_blocked && (usage->semantic_reference_local_shadow ||
cbm_pipeline_node_is_callable_target(tgt))) {
continue;
}
}
const cbm_gbuf_node_t *tgt = cbm_gbuf_find_by_qn(rc->main_gbuf, res.qualified_name);
if (!tgt || src->id == tgt->id) {
continue;
}
@@ -2461,9 +2617,11 @@ static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws,
char esc_ref[CBM_SZ_256]; /* sliced source text: escape quotes/newlines */
cbm_json_escape(esc_ref, sizeof(esc_ref), usage->ref_name);
snprintf(uprops, sizeof(uprops), "{\"callee\":\"%s\"}", esc_ref);
cbm_gbuf_insert_edge(ws->local_edge_buf, src->id, tgt->id, "USAGE", uprops);
const char *edge_type = precise_call_reference ? "CALL_REFERENCE" : "USAGE";
cbm_gbuf_insert_edge(ws->local_edge_buf, src->id, tgt->id, edge_type, uprops);
ws->usages_resolved++;
}
cbm_pipeline_lsp_reference_index_free(&reference_index);
}
/* Resolve throws/raises for one file. */
@@ -2572,6 +2730,14 @@ static void resolve_def_decorators(resolve_ctx_t *rc, resolve_worker_state_t *ws
if (res.qualified_name && res.qualified_name[0] != '\0') {
dn = cbm_gbuf_find_by_qn(rc->main_gbuf, res.qualified_name);
}
/* Qualified Rust proc-macro paths must not degrade to the decorated
* function itself through the registry's same-module suffix fallback
* (`#[tokio::main]` on local `main`). Preserve the full external path
* through the synthetic Decorator node instead. */
if (dn && dn->id == node->id && def->decorators[dc][0] == '#' &&
def->decorators[dc][1] == '[' && strstr(fn, "::")) {
dn = NULL;
}
int64_t dn_id = 0;
if (dn) {
dn_id = dn->id;
@@ -2678,6 +2844,55 @@ static CBMTypeRegistry *pp_rust_shared_registry_get(void *ctx) {
return pp_rust_shared_registry((resolve_ctx_t *)ctx);
}
static int pp_call_reference_site_count(const CBMFileResult *result, CBMLanguage lang) {
int count = 0;
for (int i = 0; i < result->usages.count; i++) {
if (cbm_pipeline_usage_semantic_reference_candidate(&result->usages.items[i])) {
count++;
}
}
return count;
}
static int pp_qualified_lsp_site_count(const CBMFileResult *result) {
int count = 0;
for (int i = 0; i < result->resolved_calls.count; i++) {
const CBMResolvedCall *resolved = &result->resolved_calls.items[i];
if ((resolved->kind == CBM_RESOLVED_INVOCATION ||
resolved->kind == CBM_RESOLVED_CALL_REFERENCE) &&
resolved->caller_qn && resolved->callee_qn &&
resolved->confidence >= CBM_LSP_CONFIDENCE_FLOOR) {
count++;
}
}
return count;
}
/* A per-file semantic walk can discover a faithful source occurrence that the
* syntax extractor cannot represent as a CBMCall yet: Python operators and
* calls hidden in Rust built-in macro token trees are the canonical examples.
* An exact unresolved/low-confidence record is therefore an explicit request
* for the project-registry pass, even when the parser-backed site count is
* zero or already fully qualified. Zero-span generated semantics are excluded:
* without source provenance they cannot safely join a graph carrier. */
static bool pp_has_pending_lsp_site(const CBMFileResult *result) {
for (int i = 0; i < result->resolved_calls.count; i++) {
const CBMResolvedCall *resolved = &result->resolved_calls.items[i];
if (resolved->kind != CBM_RESOLVED_INVOCATION &&
resolved->kind != CBM_RESOLVED_CALL_REFERENCE) {
continue;
}
if (!resolved->caller_qn || !resolved->callee_qn ||
resolved->site_end_byte <= resolved->site_start_byte) {
continue;
}
if (resolved->reason || resolved->confidence < CBM_LSP_CONFIDENCE_FLOOR) {
return true;
}
}
return false;
}
static void resolve_worker(int worker_id, void *ctx_ptr) {
resolve_ctx_t *rc = ctx_ptr;
resolve_worker_state_t *ws = &rc->workers[worker_id];
@@ -2737,19 +2952,23 @@ static void resolve_worker(int worker_id, void *ctx_ptr) {
}
/* Cross-file LSP is a per-file tree-sitter re-parse + AST walk +
* registry lookups — ~50-150ms per file. It can ONLY find calls
* that exist in the AST. If the per-file extract found zero calls,
* cross-LSP will too: the AST is the same. For non-JVM languages,
* skip when per-file LSP already produced at least as many resolved
* entries as textual calls. Java/Kotlin per-file LSP can fill the
* count with constructors or same-file calls while a mixed-source-root
* Java↔Kotlin call remains unresolved, so JVM callers run whenever
* calls exist. */
* registry lookups — ~50-150ms per file. It can only resolve semantic
* sites present in that AST: invocations plus explicit callable
* references. A file containing only a callable reference still needs
* this pass. For non-JVM languages, skip when per-file LSP already
* produced at least as many qualifying typed resolutions as sites.
* Java/Kotlin per-file LSP can fill the count with same-file sites while
* a mixed-source-root Java↔Kotlin site remains unresolved, so JVM
* callers run whenever either kind of site exists. */
bool jvm_cross_lsp = (lang == CBM_LANG_JAVA || lang == CBM_LANG_KOTLIN);
int call_reference_sites = pp_call_reference_site_count(result, lang);
int semantic_sites = result->calls.count + call_reference_sites;
int qualified_lsp_sites = pp_qualified_lsp_site_count(result);
bool pending_lsp_site = pp_has_pending_lsp_site(result);
bool cross_lsp_eligible =
(rc->all_defs && rc->def_count > 0 && cbm_pxc_has_cross_lsp(lang) &&
result->calls.count > 0 &&
(jvm_cross_lsp || result->resolved_calls.count < result->calls.count) &&
(semantic_sites > 0 || pending_lsp_site) &&
(jvm_cross_lsp || pending_lsp_site || qualified_lsp_sites < semantic_sites) &&
!is_generated);
/* Skip files with nothing else to resolve and no cross-LSP work. */
@@ -2766,7 +2985,8 @@ static void resolve_worker(int worker_id, void *ctx_ptr) {
const char **imp_vals = NULL;
int imp_count = 0;
uint64_t _imp_t0 = extract_now_ns();
build_import_map(rc->main_gbuf, rc->project_name, rel, &imp_keys, &imp_vals, &imp_count);
cbm_pxc_build_import_map(rc->main_gbuf, rc->project_name, rel, lang, result, &imp_keys,
&imp_vals, &imp_count);
atomic_fetch_add_explicit(&rc->time_ns_import_map, extract_now_ns() - _imp_t0,
memory_order_relaxed);
@@ -2882,7 +3102,7 @@ static void resolve_worker(int worker_id, void *ctx_ptr) {
/* ── USAGE resolution ──────────────────────────────────── */
_ph_t0 = extract_now_ns();
resolve_file_usages(rc, ws, result, rel, module_qn, imp_keys, imp_vals, imp_count);
resolve_file_usages(rc, ws, result, rel, module_qn, imp_keys, imp_vals, imp_count, lang);
atomic_fetch_add_explicit(&rc->time_ns_usages, extract_now_ns() - _ph_t0,
memory_order_relaxed);
@@ -2908,7 +3128,7 @@ static void resolve_worker(int worker_id, void *ctx_ptr) {
cbm_registry_resolve_cache_end();
free(module_qn);
free_import_map(imp_keys, imp_vals, imp_count);
cbm_pxc_free_import_map(imp_keys, imp_vals, imp_count);
atomic_fetch_add_explicit(&rc->time_ns_total_loop, extract_now_ns() - _loop_t0,
memory_order_relaxed);
+9
View File
@@ -400,6 +400,15 @@ static void resolve_decorator(cbm_pipeline_ctx_t *ctx, const cbm_gbuf_node_t *no
if (res.qualified_name && res.qualified_name[0] != '\0') {
dec = cbm_gbuf_find_by_qn(ctx->gbuf, res.qualified_name);
}
/* A qualified Rust proc-macro path can collide with the decorated
* function's own name: resolving `#[tokio::main]` from module `main`
* may fall back from `tokio::main` to same-module `main`. That is not a
* local decorator target (and the self-edge is discarded below), so keep
* the full external spelling by materialising the synthetic decorator. */
if (dec && dec->id == node->id && decorator[0] == '#' && decorator[1] == '[' &&
strstr(func_name, "::")) {
dec = NULL;
}
if (!dec) {
/* The decorator target is not a local symbol (external attribute /
* stdlib annotation / proc-macro derive). Materialise a synthetic
+83 -109
View File
@@ -2,7 +2,8 @@
* pass_usages.c — Resolve usages, throws, and read/write edges.
*
* For each file, re-extracts and resolves:
* - USAGE edges: identifier references (not calls) to registered symbols
* - USAGE edges: ordinary identifier references to registered symbols
* - CALL_REFERENCE edges: explicit callable-reference syntax (not invocations)
* - THROWS/RAISES edges: exception types
* - READS/WRITES edges: variable read/write access patterns
*
@@ -15,6 +16,8 @@
#include "foundation/str_util.h" // cbm_json_escape
#include "pipeline/pipeline.h"
#include "pipeline/pipeline_internal.h"
#include "pipeline/lsp_resolve.h"
#include "pipeline/pass_lsp_cross.h"
#include "graph_buffer/graph_buffer.h"
#include "foundation/log.h"
#include "foundation/compat.h"
@@ -26,6 +29,24 @@
#include <stdlib.h>
#include <string.h>
#if defined(CBM_CALL_REFERENCE_LOOKUP_TEST_API) && CBM_CALL_REFERENCE_LOOKUP_TEST_API
#include <stdatomic.h>
static _Atomic uint64_t g_lsp_reference_lookup_rows_examined = 0;
void cbm_pipeline_lsp_reference_lookup_test_note_row(void) {
atomic_fetch_add_explicit(&g_lsp_reference_lookup_rows_examined, 1, memory_order_relaxed);
}
void cbm_pipeline_lsp_reference_lookup_test_reset(void) {
atomic_store_explicit(&g_lsp_reference_lookup_rows_examined, 0, memory_order_relaxed);
}
uint64_t cbm_pipeline_lsp_reference_lookup_test_rows_examined(void) {
return atomic_load_explicit(&g_lsp_reference_lookup_rows_examined, memory_order_relaxed);
}
#endif
/* True for languages whose module QN derives from the CONTAINING DIRECTORY
* (Java/Go package). MUST match cbm_lang_module_is_dir() (internal/cbm/helpers.c)
* so same-module resolution keys against the directory-based def-node QNs. */
@@ -87,108 +108,18 @@ static bool is_checked_exception(const char *name) {
return true; /* Default: treat as checked */
}
/* Build import map from cached extraction result (fast path). */
static int build_import_map_from_cache(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result,
const char ***out_keys, const char ***out_vals,
int *out_count) {
const char **keys = calloc((size_t)result->imports.count, sizeof(const char *));
const char **vals = calloc((size_t)result->imports.count, sizeof(const char *));
int count = 0;
for (int i = 0; i < result->imports.count; i++) {
const CBMImport *imp = &result->imports.items[i];
if (!imp->local_name || !imp->local_name[0] || !imp->module_path) {
continue;
}
char *target_qn = cbm_pipeline_fqn_module(ctx->project_name, imp->module_path);
const cbm_gbuf_node_t *target = cbm_gbuf_find_by_qn(ctx->gbuf, target_qn);
free(target_qn);
if (!target) {
continue;
}
keys[count] = strdup(imp->local_name);
vals[count] = target->qualified_name;
count++;
}
*out_keys = keys;
*out_vals = vals;
*out_count = count;
return 0;
}
/* Build import map from graph buffer IMPORTS edges (slow path). */
static int build_import_map_from_edges(cbm_pipeline_ctx_t *ctx, const char *rel_path,
const char ***out_keys, const char ***out_vals,
int *out_count) {
char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel_path, "__file__");
const cbm_gbuf_node_t *file_node = cbm_gbuf_find_by_qn(ctx->gbuf, file_qn);
free(file_qn);
if (!file_node) {
return 0;
}
const cbm_gbuf_edge_t **edges = NULL;
int edge_count = 0;
int rc = cbm_gbuf_find_edges_by_source_type(ctx->gbuf, file_node->id, "IMPORTS", &edges,
&edge_count);
if (rc != 0 || edge_count == 0) {
return 0;
}
const char **keys = calloc(edge_count, sizeof(const char *));
const char **vals = calloc(edge_count, sizeof(const char *));
int count = 0;
for (int i = 0; i < edge_count; i++) {
const cbm_gbuf_edge_t *e = edges[i];
const cbm_gbuf_node_t *target = cbm_gbuf_find_by_id(ctx->gbuf, e->target_id);
if (!target || !e->properties_json) {
continue;
}
const char *start = strstr(e->properties_json, "\"local_name\":\"");
if (start) {
start += strlen("\"local_name\":\"");
const char *end = strchr(start, '"');
if (end && end > start) {
keys[count] = cbm_strndup(start, end - start);
vals[count] = target->qualified_name;
count++;
}
}
}
*out_keys = keys;
*out_vals = vals;
*out_count = count;
return 0;
}
/* Build per-file import map from cached extraction result or graph buffer edges. */
/* Build the same semantic import map used by cross-LSP and parallel resolution.
* The old sequential-only shortcut treated raw paths such as `./types` as QNs,
* so duplicate symbol names fell through to an ambiguous unique-name lookup. */
static int build_import_map(cbm_pipeline_ctx_t *ctx, const char *rel_path,
const CBMFileResult *result, const char ***out_keys,
const char ***out_vals, int *out_count) {
*out_keys = NULL;
*out_vals = NULL;
*out_count = 0;
if (result && result->imports.count > 0) {
return build_import_map_from_cache(ctx, result, out_keys, out_vals, out_count);
}
return build_import_map_from_edges(ctx, rel_path, out_keys, out_vals, out_count);
const char ***out_vals, int *out_count, CBMLanguage lang) {
return cbm_pxc_build_import_map(ctx->gbuf, ctx->project_name, rel_path, lang, result, out_keys,
out_vals, out_count);
}
static void free_import_map(const char **keys, const char **vals, int count) {
if (keys) {
for (int i = 0; i < count; i++) {
free((void *)keys[i]);
}
free((void *)keys);
}
if (vals) {
free((void *)vals);
}
cbm_pxc_free_import_map(keys, vals, count);
}
/* Find the graph buffer node for an enclosing function QN, falling back to file node. */
@@ -215,26 +146,67 @@ static const cbm_gbuf_node_t *find_enclosing_node(cbm_pipeline_ctx_t *ctx, const
/* Resolve USAGE edges for one file's extracted usages. */
static int resolve_usage_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result,
const char *rel, const char *module_qn, const char **imp_keys,
const char **imp_vals, int imp_count) {
const char **imp_vals, int imp_count, CBMLanguage lang) {
int resolved = 0;
cbm_pipeline_lsp_reference_index_t reference_index = {0};
bool reference_index_ready =
cbm_pipeline_lsp_reference_index_build(&result->resolved_calls, &reference_index);
for (int u = 0; u < result->usages.count; u++) {
CBMUsage *usage = &result->usages.items[u];
if (!usage->ref_name) {
continue;
}
const cbm_gbuf_node_t *src = find_enclosing_node(ctx, usage->enclosing_func_qn, rel);
if (!src) {
continue;
}
cbm_resolution_t res = cbm_registry_resolve(ctx->registry, usage->ref_name, module_qn,
imp_keys, imp_vals, imp_count);
if (!res.qualified_name || res.qualified_name[0] == '\0') {
continue;
const cbm_gbuf_node_t *tgt = NULL;
bool precise_call_reference = false;
const CBMResolvedCall *semantic_reference = NULL;
if (cbm_pipeline_usage_semantic_reference_candidate(usage)) {
bool allow_tail = cbm_pipeline_lsp_allow_tail_match(lang);
semantic_reference = cbm_pipeline_find_lsp_reference_indexed_in_graph(
&result->resolved_calls, reference_index_ready ? &reference_index : NULL, usage,
allow_tail, ctx->gbuf, ctx->project_name);
if (semantic_reference &&
!cbm_pipeline_usage_allows_semantic_reference(usage, semantic_reference)) {
semantic_reference = NULL;
}
if (semantic_reference) {
tgt = cbm_pipeline_lsp_target_node(ctx->gbuf, ctx->project_name,
semantic_reference->callee_qn, allow_tail);
precise_call_reference = cbm_pipeline_node_is_callable_target(tgt);
}
}
const cbm_gbuf_node_t *tgt = cbm_gbuf_find_by_qn(ctx->gbuf, res.qualified_name);
/* A syntactically explicit reference remains useful when exact semantic
* resolution is unavailable, but the textual registry fallback proves
* only value use—not an occurrence-exact callable target. Emit USAGE in
* that case; CALL_REFERENCE is reserved for the exact LSP join. */
if (!tgt) {
/* An occurrence-exact semantic record owns this reference even when
* its target is not materialized in the graph (for example, a
* Kotlin local function). Falling back by raw name here would bind
* an unrelated same-named declaration. */
if (semantic_reference) {
continue;
}
cbm_resolution_t res = cbm_registry_resolve(ctx->registry, usage->ref_name, module_qn,
imp_keys, imp_vals, imp_count);
if (!res.qualified_name || res.qualified_name[0] == '\0') {
continue;
}
if (!cbm_pipeline_reference_candidate_fallback_allowed(lang, usage, res.strategy,
imp_keys, imp_count)) {
continue;
}
tgt = cbm_gbuf_find_by_qn(ctx->gbuf, res.qualified_name);
if (usage->semantic_reference_blocked && (usage->semantic_reference_local_shadow ||
cbm_pipeline_node_is_callable_target(tgt))) {
continue;
}
}
if (!tgt || src->id == tgt->id) {
continue;
}
@@ -245,9 +217,11 @@ static int resolve_usage_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *res
cbm_json_escape(esc_ref, sizeof(esc_ref), usage->ref_name);
char uprops[CBM_SZ_512];
snprintf(uprops, sizeof(uprops), "{\"callee\":\"%s\"}", esc_ref);
cbm_gbuf_insert_edge(ctx->gbuf, src->id, tgt->id, "USAGE", uprops);
const char *edge_type = precise_call_reference ? "CALL_REFERENCE" : "USAGE";
cbm_gbuf_insert_edge(ctx->gbuf, src->id, tgt->id, edge_type, uprops);
resolved++;
}
cbm_pipeline_lsp_reference_index_free(&reference_index);
return resolved;
}
@@ -368,13 +342,13 @@ int cbm_pipeline_pass_usages(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *fil
const char **imp_keys = NULL;
const char **imp_vals = NULL;
int imp_count = 0;
build_import_map(ctx, rel, result, &imp_keys, &imp_vals, &imp_count);
build_import_map(ctx, rel, result, &imp_keys, &imp_vals, &imp_count, files[i].language);
char *module_qn = cbm_pipeline_fqn_module_dir(ctx->project_name, rel,
pu_module_is_dir(files[i].language));
usage_resolved +=
resolve_usage_edges(ctx, result, rel, module_qn, imp_keys, imp_vals, imp_count);
usage_resolved += resolve_usage_edges(ctx, result, rel, module_qn, imp_keys, imp_vals,
imp_count, files[i].language);
throw_resolved +=
resolve_throw_edges(ctx, result, rel, module_qn, imp_keys, imp_vals, imp_count);
rw_resolved += resolve_rw_edges(ctx, result, rel, module_qn, imp_keys, imp_vals, imp_count);
+35 -13
View File
@@ -289,24 +289,29 @@ static cbm_path_alias_map_t *load_tsconfig_file(const char *abs_path, const char
/* ── Public API ────────────────────────────────────────────────── */
static void free_alias_map(cbm_path_alias_map_t *map) {
if (!map) {
return;
}
for (int j = 0; j < map->count; j++) {
free(map->entries[j].alias_prefix);
free(map->entries[j].alias_suffix);
free(map->entries[j].target_prefix);
free(map->entries[j].target_suffix);
}
free(map->entries);
free(map->base_url);
free(map);
}
void cbm_path_alias_collection_free(cbm_path_alias_collection_t *coll) {
if (!coll) {
return;
}
for (int i = 0; i < coll->count; i++) {
free(coll->scopes[i].dir_prefix);
if (coll->scopes[i].map) {
cbm_path_alias_map_t *map = coll->scopes[i].map;
for (int j = 0; j < map->count; j++) {
free(map->entries[j].alias_prefix);
free(map->entries[j].alias_suffix);
free(map->entries[j].target_prefix);
free(map->entries[j].target_suffix);
}
free(map->entries);
free(map->base_url);
free(map);
}
free(coll->scopes[i].source_rel_path);
free_alias_map(coll->scopes[i].map);
}
free(coll->scopes);
free(coll);
@@ -472,7 +477,24 @@ cbm_path_alias_collection_t *cbm_load_path_aliases_excluded(const char *repo_pat
if (!map) {
continue;
}
coll->scopes[coll->count].dir_prefix = strdup(hits[i].rel);
const char *base = strrchr(hits[i].abs, '/');
base = base ? base + 1 : hits[i].abs;
char selected_rel[CBM_SZ_512];
if (hits[i].rel[0]) {
snprintf(selected_rel, sizeof(selected_rel), "%s/%s", hits[i].rel, base);
} else {
snprintf(selected_rel, sizeof(selected_rel), "%s", base);
}
char *dir_prefix = strdup(hits[i].rel);
char *source_rel_path = strdup(selected_rel);
if (!dir_prefix || !source_rel_path) {
free(dir_prefix);
free(source_rel_path);
free_alias_map(map);
continue;
}
coll->scopes[coll->count].dir_prefix = dir_prefix;
coll->scopes[coll->count].source_rel_path = source_rel_path;
coll->scopes[coll->count].map = map;
coll->count++;
}
+1
View File
@@ -55,6 +55,7 @@ typedef struct {
* pick up the map of the nearest ancestor scope. */
typedef struct {
char *dir_prefix;
char *source_rel_path; /* selected config file (tsconfig beats jsconfig) */
cbm_path_alias_map_t *map;
} cbm_path_alias_scope_t;
+623 -247
View File
@@ -43,6 +43,13 @@ enum { CBM_DIR_PERMS = 0755, PL_RING = 4, PL_RING_MASK = 3, PL_SEQ_PASSES = 6, P
#include <stdatomic.h>
#include <sys/stat.h>
#include <time.h>
#ifdef _WIN32
#include <process.h>
#define cbm_pipeline_getpid _getpid
#else
#include <unistd.h>
#define cbm_pipeline_getpid getpid
#endif
static inline void *intptr_to_ptr(intptr_t v) {
void *p;
@@ -55,6 +62,68 @@ static inline void *intptr_to_ptr(intptr_t v) {
* Atomic spinlock: 0 = free, 1 = locked. */
static atomic_int g_pipeline_busy = 0;
#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API
static atomic_bool g_persist_test_fail_after_stage_dump = false;
static atomic_bool g_persist_test_cancel_after_predump = false;
static atomic_bool g_persist_test_cancel_after_destination_prepare = false;
static atomic_bool g_persist_test_fail_adr_capture = false;
static cbm_pipeline_test_hook_fn g_persist_test_before_final_manifest = NULL;
static void *g_persist_test_before_final_manifest_userdata = NULL;
void cbm_pipeline_incremental_test_fail_after_stage_dump_once(void) {
atomic_store(&g_persist_test_fail_after_stage_dump, true);
}
void cbm_pipeline_incremental_test_cancel_after_predump_once(void) {
atomic_store(&g_persist_test_cancel_after_predump, true);
}
void cbm_pipeline_incremental_test_cancel_after_destination_prepare_once(void) {
atomic_store(&g_persist_test_cancel_after_destination_prepare, true);
}
void cbm_pipeline_incremental_test_fail_adr_capture_once(void) {
atomic_store(&g_persist_test_fail_adr_capture, true);
}
void cbm_pipeline_incremental_test_before_final_manifest_once(cbm_pipeline_test_hook_fn hook,
void *userdata) {
g_persist_test_before_final_manifest = hook;
g_persist_test_before_final_manifest_userdata = userdata;
}
void cbm_pipeline_persist_test_run_before_final_manifest(void) {
cbm_pipeline_test_hook_fn hook = g_persist_test_before_final_manifest;
void *userdata = g_persist_test_before_final_manifest_userdata;
g_persist_test_before_final_manifest = NULL;
g_persist_test_before_final_manifest_userdata = NULL;
if (hook) {
hook(userdata);
}
}
bool cbm_pipeline_persist_test_take_failure_after_stage_dump(void) {
return atomic_exchange(&g_persist_test_fail_after_stage_dump, false);
}
bool cbm_pipeline_persist_test_take_cancel_after_predump(void) {
return atomic_exchange(&g_persist_test_cancel_after_predump, false);
}
bool cbm_pipeline_persist_test_take_cancel_after_destination_prepare(void) {
return atomic_exchange(&g_persist_test_cancel_after_destination_prepare, false);
}
void cbm_pipeline_persist_test_reset_faults(void) {
atomic_store(&g_persist_test_fail_after_stage_dump, false);
atomic_store(&g_persist_test_cancel_after_predump, false);
atomic_store(&g_persist_test_cancel_after_destination_prepare, false);
atomic_store(&g_persist_test_fail_adr_capture, false);
g_persist_test_before_final_manifest = NULL;
g_persist_test_before_final_manifest_userdata = NULL;
}
#endif
bool cbm_pipeline_try_lock(void) {
return atomic_exchange(&g_pipeline_busy, 1) == 0;
}
@@ -188,6 +257,24 @@ cbm_pipeline_t *cbm_pipeline_new(const char *repo_path, const char *db_path,
return p;
}
static int pipeline_refresh_git_context(cbm_pipeline_t *p) {
cbm_git_context_t fresh = {0};
if (!p || cbm_git_context_resolve(p->repo_path, &fresh) != 0) {
cbm_git_context_free(&fresh);
return CBM_NOT_FOUND;
}
char *fresh_branch_qn = cbm_git_context_branch_qn(p->project_name, &fresh);
if (!fresh_branch_qn) {
cbm_git_context_free(&fresh);
return CBM_NOT_FOUND;
}
cbm_git_context_free(&p->git_ctx);
free(p->branch_qn);
p->git_ctx = fresh;
p->branch_qn = fresh_branch_qn;
return 0;
}
void cbm_pipeline_set_persistence(cbm_pipeline_t *p, bool enabled) {
if (p) {
p->persistence = enabled;
@@ -979,6 +1066,9 @@ static int run_parallel_pipeline(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx,
cbm_log_info("pass.timing", "pass", "parallel_extract", "elapsed_ms",
itoa_buf((int)elapsed_ms(*t)));
if (rc != 0 || check_cancel(p)) {
for (int i = 0; i < file_count; i++) {
cbm_free_result(cache[i]);
}
free(cache);
return rc != 0 ? rc : CBM_NOT_FOUND;
}
@@ -1005,6 +1095,15 @@ static int run_parallel_pipeline(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx,
free(cache);
return rc != 0 ? rc : CBM_NOT_FOUND;
}
/* Registry consumers may materialize serial nodes (Channel, EnvVar, and
* future carrier-derived resources) after parallel extraction established
* the shared allocator watermark. Advance the atomic allocator before
* resolve workers resume; otherwise their IDs and the later next-id reset
* can collide with those nodes and orphan freshly inserted edges. */
int64_t registry_next_id = cbm_gbuf_next_id(p->gbuf);
if (registry_next_id > atomic_load(&shared_ids)) {
atomic_store(&shared_ids, registry_next_id);
}
/* Cross-file LSP precondition: build a project-wide CBMLSPDef[]
* once. The fused resolve_worker invokes cbm_pxc_run_one(_ts) per
* file using these defs + the file's IMPORTS map, so cross-file
@@ -1100,77 +1199,76 @@ static int run_parallel_pipeline(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx,
return check_cancel(p) ? CBM_NOT_FOUND : 0;
}
/* Try incremental pipeline or delete old DB for reindex.
* Returns >= 0 if incremental was used (the return code), or -1 to proceed with full. */
static int try_incremental_or_delete_db(cbm_pipeline_t *p, cbm_file_info_t *files, int file_count) {
static int capture_existing_adr(cbm_pipeline_t *p, const char *db_path) {
#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API
if (atomic_exchange(&g_persist_test_fail_adr_capture, false)) {
return CBM_PIPELINE_ABORT_PRESERVE_DB;
}
#endif
cbm_store_t *adr_store = cbm_store_open_path_query(db_path);
if (!adr_store) {
return CBM_PIPELINE_ABORT_PRESERVE_DB;
}
cbm_adr_t existing = {0};
int adr_rc = cbm_store_adr_get(adr_store, p->project_name, &existing);
if (adr_rc == CBM_STORE_NOT_FOUND) {
cbm_store_close(adr_store);
free(p->saved_adr);
p->saved_adr = NULL;
return 0;
}
if (adr_rc != CBM_STORE_OK || !existing.content) {
cbm_store_adr_free(&existing);
cbm_store_close(adr_store);
return CBM_PIPELINE_ABORT_PRESERVE_DB;
}
char *saved = strdup(existing.content);
cbm_store_adr_free(&existing);
cbm_store_close(adr_store);
if (!saved) {
return CBM_PIPELINE_ABORT_PRESERVE_DB;
}
free(p->saved_adr);
p->saved_adr = saved;
return 0;
}
/* Route an existing generation. Full rebuilds never delete the live DB here:
* publication owns the eventual atomic replacement after every pass and
* metadata write has succeeded. */
static int try_incremental_or_delete_db(cbm_pipeline_t *p, cbm_file_info_t *files, int file_count,
const cbm_file_hash_t *baseline_manifest,
int baseline_count) {
char *db_path = resolve_db_path(p);
if (!db_path) {
return CBM_NOT_FOUND;
return CBM_PIPELINE_FORCE_FULL_REINDEX;
}
struct stat db_st;
if (stat(db_path, &db_st) != 0) {
free(db_path);
return CBM_NOT_FOUND;
return CBM_PIPELINE_FORCE_FULL_REINDEX;
}
cbm_store_t *check_store = cbm_store_open_path(db_path);
if (check_store && cbm_store_check_integrity(check_store)) {
cbm_file_hash_t *hashes = NULL;
int hash_count = 0;
cbm_store_get_file_hashes(check_store, p->project_name, &hashes, &hash_count);
cbm_store_free_file_hashes(hashes, hash_count);
cbm_store_close(check_store);
if (hash_count > 0 && file_count <= hash_count + (hash_count / PAIR_LEN)) {
cbm_log_info("pipeline.route", "path", "incremental", "stored_hashes",
itoa_buf(hash_count));
int rc = cbm_pipeline_run_incremental(p, db_path, files, file_count);
free(db_path);
return rc;
}
if (hash_count > 0) {
cbm_log_info("pipeline.route", "path", "mode_change_reindex", "stored_hashes",
itoa_buf(hash_count), "discovered", itoa_buf(file_count));
}
} else if (check_store) {
cbm_store_t *check_store = cbm_store_open_path_query(db_path);
bool valid = check_store && cbm_store_check_integrity(check_store);
if (check_store) {
cbm_store_close(check_store);
}
cbm_log_info("pipeline.route", "path", "reindex", "action", "deleting old db");
/* Capture any ADR before deleting the DB so the full-reindex rebuild can
* restore it (project_summaries is otherwise lost). Issue #516. */
{
cbm_store_t *adr_store = cbm_store_open_path(db_path);
if (adr_store) {
cbm_adr_t existing;
if (cbm_store_adr_get(adr_store, p->project_name, &existing) == CBM_STORE_OK) {
if (existing.content) {
free(p->saved_adr);
p->saved_adr = strdup(existing.content);
}
cbm_store_adr_free(&existing);
}
cbm_store_close(adr_store);
if (!valid) {
cbm_log_warn("pipeline.route", "path", "full", "reason", "invalid_existing_db");
free(db_path);
return CBM_PIPELINE_FORCE_FULL_REINDEX;
}
cbm_log_info("pipeline.route", "path", "incremental_manifest");
int rc = cbm_pipeline_run_incremental(p, db_path, files, file_count, baseline_manifest,
baseline_count);
if (rc == CBM_PIPELINE_FORCE_FULL_REINDEX) {
int adr_rc = capture_existing_adr(p, db_path);
if (adr_rc != 0) {
rc = adr_rc;
}
}
cbm_unlink(db_path);
char wal[PL_WAL_BUF];
char shm[PL_WAL_BUF];
snprintf(wal, sizeof(wal), "%s-wal", db_path);
snprintf(shm, sizeof(shm), "%s-shm", db_path);
cbm_unlink(wal);
cbm_unlink(shm);
free(db_path);
return CBM_NOT_FOUND;
}
/* Get platform-specific mtime in nanoseconds. */
static int64_t stat_mtime_ns(const struct stat *fst) {
#ifdef __APPLE__
return ((int64_t)fst->st_mtimespec.tv_sec * PL_NSEC_PER_SEC) +
(int64_t)fst->st_mtimespec.tv_nsec;
#elif defined(_WIN32)
return (int64_t)fst->st_mtime * 1000000000LL;
#else
return ((int64_t)fst->st_mtim.tv_sec * PL_NSEC_PER_SEC) + (int64_t)fst->st_mtim.tv_nsec;
#endif
return rc;
}
static const char *pipeline_mode_name(cbm_index_mode_t mode) {
@@ -1186,9 +1284,348 @@ static const char *pipeline_mode_name(cbm_index_mode_t mode) {
}
}
static int pipeline_mode_coverage_rank(cbm_index_mode_t mode) {
switch (mode) {
case CBM_MODE_FULL:
return 3;
case CBM_MODE_MODERATE:
return 2;
case CBM_MODE_FAST:
return 1;
default:
return 0;
}
}
/* Index modes are additive: a cheaper run may refresh a fuller graph, but it
* must never erase files that the cheaper discovery intentionally skips. The
* exact-manifest pipeline therefore keeps the most comprehensive successfully
* published mode and performs any changed rebuild at that coverage level. */
static void promote_mode_to_existing_coverage(cbm_pipeline_t *p) {
if (!p || !p->project_name) {
return;
}
char *db_path = resolve_db_path(p);
if (!db_path) {
return;
}
cbm_store_t *store = cbm_store_open_path_query(db_path);
free(db_path);
if (!store) {
return;
}
cbm_coverage_meta_t meta = {0};
if (cbm_store_coverage_meta_get(store, p->project_name, &meta) == CBM_STORE_OK &&
meta.index_mode) {
cbm_index_mode_t stored_mode = p->mode;
if (strcmp(meta.index_mode, "full") == 0) {
stored_mode = CBM_MODE_FULL;
} else if (strcmp(meta.index_mode, "moderate") == 0) {
stored_mode = CBM_MODE_MODERATE;
} else if (strcmp(meta.index_mode, "fast") == 0) {
stored_mode = CBM_MODE_FAST;
}
if (pipeline_mode_coverage_rank(stored_mode) > pipeline_mode_coverage_rank(p->mode)) {
cbm_log_info("pipeline.mode", "requested", pipeline_mode_name(p->mode), "effective",
pipeline_mode_name(stored_mode), "reason", "preserve_existing_coverage");
p->mode = stored_mode;
}
}
cbm_store_coverage_meta_clear(&meta);
cbm_store_close(store);
}
int cbm_pipeline_refresh_artifact(cbm_pipeline_t *p, const char *db_path) {
if (!p || !db_path || !p->repo_path || !p->project_name) {
return 0;
}
bool existing = cbm_artifact_exists(p->repo_path);
if (!p->persistence && !existing) {
return 0;
}
int quality = p->persistence ? CBM_ARTIFACT_BEST : CBM_ARTIFACT_FAST;
int rc = cbm_artifact_export(db_path, p->repo_path, p->project_name, quality);
if (rc != 0) {
const char *err = cbm_artifact_export_last_error();
if (p->persistence) {
cbm_log_error("pipeline.err", "phase", "artifact_export", "err", err ? err : "unknown");
return rc;
}
cbm_log_warn("artifact.refresh_failed", "err", err ? err : "unknown");
}
return 0;
}
static atomic_ullong g_generation_stage_counter = 0;
static void discard_generation_stage(const char *stage_path) {
if (!stage_path) {
return;
}
cbm_unlink(stage_path);
cbm_remove_db_sidecars(stage_path);
}
static int generation_rebuild_fts(cbm_store_t *store) {
if (cbm_store_exec(store, "INSERT INTO nodes_fts(nodes_fts) VALUES('delete-all');") !=
CBM_STORE_OK) {
return CBM_STORE_ERR;
}
if (cbm_store_exec(store,
"INSERT INTO nodes_fts(rowid, name, qualified_name, label, file_path) "
"SELECT id, cbm_camel_split(name), qualified_name, label, file_path "
"FROM nodes;") == CBM_STORE_OK) {
return CBM_STORE_OK;
}
return cbm_store_exec(store,
"INSERT INTO nodes_fts(rowid, name, qualified_name, label, file_path) "
"SELECT id, name, qualified_name, label, file_path FROM nodes;");
}
typedef struct {
bool quarantined;
char backup_path[CBM_SZ_4K];
} cbm_replacement_prepare_t;
static int replacement_sidecar_path(char *out, size_t out_size, const char *base,
const char *suffix) {
int n = snprintf(out, out_size, "%s%s", base, suffix);
return n > 0 && (size_t)n < out_size ? 0 : CBM_PIPELINE_PERSIST_FAILED;
}
static bool replacement_path_exists(const char *path) {
cbm_path_info_t info;
return cbm_path_info_utf8(path, &info) == 0;
}
static int rollback_quarantined_generation(const char *db_path,
cbm_replacement_prepare_t *prepared) {
if (!prepared || !prepared->quarantined) {
return 0;
}
static const char *const suffixes[] = {"-wal", "-shm"};
if (cbm_rename_noreplace(prepared->backup_path, db_path) != 0) {
return CBM_PIPELINE_PERSIST_FAILED;
}
for (size_t i = 0; i < sizeof(suffixes) / sizeof(suffixes[0]); i++) {
char source[CBM_SZ_4K];
char destination[CBM_SZ_4K];
if (replacement_sidecar_path(source, sizeof(source), prepared->backup_path, suffixes[i]) !=
0 ||
replacement_sidecar_path(destination, sizeof(destination), db_path, suffixes[i]) != 0) {
return CBM_PIPELINE_PERSIST_FAILED;
}
if (replacement_path_exists(source) && cbm_rename_noreplace(source, destination) != 0) {
return CBM_PIPELINE_PERSIST_FAILED;
}
}
prepared->quarantined = false;
prepared->backup_path[0] = '\0';
return 0;
}
static int quarantine_existing_generation(const char *db_path,
cbm_replacement_prepare_t *prepared) {
if (!db_path || !prepared) {
return CBM_PIPELINE_PERSIST_FAILED;
}
static const char *const suffixes[] = {"-wal", "-shm"};
char candidate[CBM_SZ_4K];
for (int attempt = 0; attempt < 10000; attempt++) {
int n = attempt == 0
? snprintf(candidate, sizeof(candidate), "%s.corrupt", db_path)
: snprintf(candidate, sizeof(candidate), "%s.corrupt.%d", db_path, attempt);
if (n <= 0 || (size_t)n >= sizeof(candidate)) {
return CBM_PIPELINE_PERSIST_FAILED;
}
bool available = !replacement_path_exists(candidate);
for (size_t i = 0; available && i < sizeof(suffixes) / sizeof(suffixes[0]); i++) {
char candidate_sidecar[CBM_SZ_4K];
if (replacement_sidecar_path(candidate_sidecar, sizeof(candidate_sidecar), candidate,
suffixes[i]) != 0) {
return CBM_PIPELINE_PERSIST_FAILED;
}
available = !replacement_path_exists(candidate_sidecar);
}
if (!available) {
continue;
}
if (cbm_rename_noreplace(db_path, candidate) != 0) {
if (replacement_path_exists(candidate)) {
continue;
}
return CBM_PIPELINE_PERSIST_FAILED;
}
snprintf(prepared->backup_path, sizeof(prepared->backup_path), "%s", candidate);
prepared->quarantined = true;
for (size_t i = 0; i < sizeof(suffixes) / sizeof(suffixes[0]); i++) {
char source[CBM_SZ_4K];
char destination[CBM_SZ_4K];
if (replacement_sidecar_path(source, sizeof(source), db_path, suffixes[i]) != 0 ||
replacement_sidecar_path(destination, sizeof(destination), candidate,
suffixes[i]) != 0) {
(void)rollback_quarantined_generation(db_path, prepared);
return CBM_PIPELINE_PERSIST_FAILED;
}
if (replacement_path_exists(source) && cbm_rename_noreplace(source, destination) != 0) {
(void)rollback_quarantined_generation(db_path, prepared);
return CBM_PIPELINE_PERSIST_FAILED;
}
}
return 0;
}
return CBM_PIPELINE_PERSIST_FAILED;
}
static int prepare_existing_generation_for_replace(const char *db_path,
cbm_replacement_prepare_t *prepared) {
if (!prepared) {
return CBM_PIPELINE_PERSIST_FAILED;
}
memset(prepared, 0, sizeof(*prepared));
cbm_path_info_t info;
if (cbm_path_info_utf8(db_path, &info) == 0) {
if (!info.is_regular || info.is_symlink) {
return CBM_PIPELINE_PERSIST_FAILED;
}
int seal_rc = cbm_store_seal_existing_path_for_replace(db_path);
if (seal_rc == CBM_STORE_NOT_FOUND) {
return quarantine_existing_generation(db_path, prepared);
}
if (seal_rc != CBM_STORE_OK) {
return CBM_PIPELINE_PERSIST_FAILED;
}
}
return cbm_remove_db_sidecars_checked(db_path) == 0 ? 0 : CBM_PIPELINE_PERSIST_FAILED;
}
int cbm_pipeline_publish_generation(const cbm_pipeline_generation_t *generation) {
if (!generation || !generation->gbuf || !generation->final_db_path || !generation->project ||
generation->manifest_count < 0 ||
(generation->manifest_count > 0 && !generation->manifest) ||
generation->coverage_count < 0 ||
(generation->coverage_count > 0 && !generation->coverage)) {
return CBM_PIPELINE_PERSIST_FAILED;
}
if (generation->cancelled && atomic_load(generation->cancelled)) {
return CBM_PIPELINE_ABORT_PRESERVE_DB;
}
size_t stage_size = strlen(generation->final_db_path) + 96;
char *stage_path = malloc(stage_size);
if (!stage_path) {
return CBM_PIPELINE_PERSIST_FAILED;
}
unsigned long long serial = atomic_fetch_add(&g_generation_stage_counter, 1) + 1;
int path_n = snprintf(stage_path, stage_size, "%s.stage.%ld.%llu", generation->final_db_path,
(long)cbm_pipeline_getpid(), serial);
if (path_n < 0 || (size_t)path_n >= stage_size) {
free(stage_path);
return CBM_PIPELINE_PERSIST_FAILED;
}
discard_generation_stage(stage_path);
int dump_rc = cbm_gbuf_dump_to_sqlite(generation->gbuf, stage_path);
if (dump_rc != 0) {
discard_generation_stage(stage_path);
free(stage_path);
return CBM_PIPELINE_PERSIST_FAILED;
}
#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API
if (cbm_pipeline_persist_test_take_failure_after_stage_dump()) {
discard_generation_stage(stage_path);
free(stage_path);
return CBM_PIPELINE_PERSIST_FAILED;
}
#endif
if (generation->cancelled && atomic_load(generation->cancelled)) {
discard_generation_stage(stage_path);
free(stage_path);
return CBM_PIPELINE_ABORT_PRESERVE_DB;
}
cbm_store_t *store = cbm_store_open_path(stage_path);
if (!store) {
discard_generation_stage(stage_path);
free(stage_path);
return CBM_PIPELINE_PERSIST_FAILED;
}
bool ok = cbm_store_exec(store, "PRAGMA synchronous=FULL;") == CBM_STORE_OK;
ok = ok && cbm_store_delete_file_hashes(store, generation->project) == CBM_STORE_OK &&
cbm_store_upsert_file_hash_batch(store, generation->manifest,
generation->manifest_count) == CBM_STORE_OK;
if (ok && generation->adr_content) {
ok = cbm_store_adr_store(store, generation->project, generation->adr_content) ==
CBM_STORE_OK;
}
cbm_project_t project_info = {0};
bool have_project_info =
cbm_store_get_project(store, generation->project, &project_info) == CBM_STORE_OK;
cbm_coverage_meta_t meta = generation->coverage_meta;
meta.generation = have_project_info ? project_info.indexed_at : NULL;
meta.coverage_version = CBM_SEMANTIC_INDEX_VERSION;
meta.hash_records_complete = true;
if (!have_project_info ||
cbm_store_coverage_replace_ex(store, generation->project, generation->coverage,
generation->coverage_count, &meta) != CBM_STORE_OK) {
ok = false;
}
if (have_project_info) {
cbm_project_free_fields(&project_info);
}
if (generation_rebuild_fts(store) != CBM_STORE_OK || !cbm_store_check_integrity(store)) {
ok = false;
}
if (ok && cbm_store_seal_for_atomic_publish(store) != CBM_STORE_OK) {
ok = false;
}
cbm_store_close(store);
if (ok && cbm_remove_db_sidecars_checked(stage_path) != 0) {
ok = false;
}
if (!ok) {
discard_generation_stage(stage_path);
free(stage_path);
return CBM_PIPELINE_PERSIST_FAILED;
}
if (generation->cancelled && atomic_load(generation->cancelled)) {
discard_generation_stage(stage_path);
free(stage_path);
return CBM_PIPELINE_ABORT_PRESERVE_DB;
}
cbm_replacement_prepare_t prepared = {0};
if (prepare_existing_generation_for_replace(generation->final_db_path, &prepared) != 0) {
discard_generation_stage(stage_path);
free(stage_path);
return CBM_PIPELINE_PERSIST_FAILED;
}
#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API
if (cbm_pipeline_persist_test_take_cancel_after_destination_prepare() &&
generation->cancelled) {
atomic_store(generation->cancelled, true);
}
#endif
if (generation->cancelled && atomic_load(generation->cancelled)) {
int rollback_rc = rollback_quarantined_generation(generation->final_db_path, &prepared);
discard_generation_stage(stage_path);
free(stage_path);
return rollback_rc == 0 ? CBM_PIPELINE_ABORT_PRESERVE_DB : CBM_PIPELINE_PERSIST_FAILED;
}
if (cbm_rename_replace(stage_path, generation->final_db_path) != 0) {
(void)rollback_quarantined_generation(generation->final_db_path, &prepared);
discard_generation_stage(stage_path);
free(stage_path);
return CBM_PIPELINE_PERSIST_FAILED;
}
free(stage_path);
return 0;
}
/* Dump graph to SQLite and persist file hashes for incremental indexing. */
static int dump_and_persist_hashes(cbm_pipeline_t *p, const cbm_file_info_t *files, int file_count,
struct timespec *t) {
static int dump_and_persist_hashes(cbm_pipeline_t *p, const cbm_file_hash_t *baseline_manifest,
int baseline_count, struct timespec *t) {
cbm_clock_gettime(CLOCK_MONOTONIC, t);
char db_path[CBM_SZ_1K];
if (p->db_path) {
@@ -1207,190 +1644,95 @@ static int dump_and_persist_hashes(cbm_pipeline_t *p, const cbm_file_info_t *fil
*last_slash = '\0';
cbm_mkdir_p(db_dir, CBM_DIR_PERMS);
}
/* Capture committed counts BEFORE the dump. cbm_gbuf_dump_to_sqlite calls
* release_gbuf_indexes(), which frees node_by_qn (graph_buffer.c), after
* which cbm_gbuf_node_count() returns 0. Reading these post-dump left
* committed_nodes at 0, so the #334 plausibility gate never fired. */
cbm_file_hash_t *manifest = NULL;
int manifest_count = 0;
#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API
cbm_pipeline_persist_test_run_before_final_manifest();
#endif
if (cbm_pipeline_build_fresh_semantic_manifest(p->project_name, p->repo_path, p->mode,
&manifest, &manifest_count) != 0) {
cbm_log_error("pipeline.err", "phase", "semantic_manifest");
return CBM_PIPELINE_ABORT_PRESERVE_DB;
}
if (!cbm_pipeline_semantic_manifests_equal(baseline_manifest, baseline_count, manifest,
manifest_count)) {
cbm_log_warn("pipeline.abort", "reason", "semantic_inputs_changed");
cbm_pipeline_free_semantic_manifest(manifest, manifest_count);
return CBM_PIPELINE_ABORT_PRESERVE_DB;
}
int cov_total = p->file_errors_count + p->excluded_count + p->ignored_count;
cbm_coverage_row_t *cov = NULL;
int cov_count = 0;
bool coverage_rows_available = cov_total == 0;
if (cov_total > 0) {
cov = malloc((size_t)cov_total * sizeof(*cov));
if (cov) {
coverage_rows_available = true;
for (int i = 0; i < p->file_errors_count; i++) {
cov[cov_count++] = (cbm_coverage_row_t){.rel_path = p->file_errors[i].path,
.kind = p->file_errors[i].phase,
.detail = p->file_errors[i].reason};
}
for (int i = 0; i < p->excluded_count; i++) {
cov[cov_count++] = (cbm_coverage_row_t){.rel_path = p->excluded_dirs[i],
.kind = "not_indexed_dir",
.detail = "excluded subtree"};
}
for (int i = 0; i < p->ignored_count; i++) {
cov[cov_count++] = (cbm_coverage_row_t){.rel_path = p->ignored_files[i].rel_path,
.kind = "not_indexed_file",
.detail = p->ignored_files[i].reason};
}
}
}
cbm_pipeline_generation_t generation = {
.gbuf = p->gbuf,
.final_db_path = db_path,
.project = p->project_name,
.cancelled = &p->cancelled,
.manifest = manifest,
.manifest_count = manifest_count,
.adr_content = p->saved_adr,
.coverage = cov,
.coverage_count = cov_count,
.coverage_meta =
{
.index_mode = pipeline_mode_name(p->mode),
.recording_status =
!coverage_rows_available
? "unavailable"
: (p->ignored_total > p->ignored_count ? "truncated" : "complete"),
.ignored_files_stored = p->ignored_count,
.ignored_files_total = p->ignored_total,
.coverage_version = CBM_SEMANTIC_INDEX_VERSION,
.hash_records_complete = true,
},
};
/* Capture counts before the dump releases graph-buffer indexes. */
p->committed_nodes = cbm_gbuf_node_count(p->gbuf);
p->committed_edges = cbm_gbuf_edge_count(p->gbuf);
int rc = cbm_gbuf_dump_to_sqlite(p->gbuf, db_path);
int rc = cbm_pipeline_publish_generation(&generation);
free(cov);
cbm_pipeline_free_semantic_manifest(manifest, manifest_count);
if (rc != 0) {
cbm_log_error("pipeline.err", "phase", "dump");
return rc;
}
cbm_log_info("pass.timing", "pass", "dump", "elapsed_ms", itoa_buf((int)elapsed_ms(*t)));
/* Persist-tail spans (phase "persist"): attribute the ~60s that lands here
* AFTER cbm_gbuf_dump_to_sqlite returns. Active only under CBM_PROFILE. */
CBM_PROF_START(t_reopen);
cbm_store_t *hash_store = cbm_store_open_path(db_path);
CBM_PROF_END("persist", "1_reopen", t_reopen);
if (hash_store) {
bool hash_records_complete = true;
CBM_PROF_START(t_delhash);
if (cbm_store_delete_file_hashes(hash_store, p->project_name) != CBM_STORE_OK) {
hash_records_complete = false;
}
CBM_PROF_END("persist", "2_delete_file_hashes", t_delhash);
/* Restore the ADR captured before the dump. Surface a failed restore
* rather than silently dropping the ADR (the original #516 symptom). */
CBM_PROF_START(t_adr);
if (p->saved_adr) {
if (cbm_store_adr_store(hash_store, p->project_name, p->saved_adr) != CBM_STORE_OK) {
cbm_log_error("pipeline.err", "phase", "adr_restore", "project", p->project_name);
}
}
CBM_PROF_END("persist", "3_adr_restore", t_adr);
/* Batch the per-file hash upserts into ONE transaction. The per-file
* cbm_store_upsert_file_hash path autocommits, i.e. file_count fsyncs
* (~89k on the kernel); cbm_store_upsert_file_hash_batch wraps the same
* cached INSERT ... ON CONFLICT upsert in a single begin/commit. Same
* (project, rel_path, sha256="", mtime_ns, size) tuples, same replace
* semantics — only the transaction boundary changes. */
CBM_PROF_START(t_fh);
cbm_file_hash_t *fhashes = (cbm_file_hash_t *)malloc(
(size_t)(file_count > 0 ? file_count : 1) * sizeof(cbm_file_hash_t));
if (fhashes) {
int fh_n = 0;
for (int i = 0; i < file_count; i++) {
struct stat fst;
if (stat(files[i].path, &fst) == 0) {
fhashes[fh_n].project = p->project_name;
fhashes[fh_n].rel_path = files[i].rel_path;
fhashes[fh_n].sha256 = "";
fhashes[fh_n].mtime_ns = stat_mtime_ns(&fst);
fhashes[fh_n].size = fst.st_size;
fh_n++;
} else {
hash_records_complete = false;
}
}
if (cbm_store_upsert_file_hash_batch(hash_store, fhashes, fh_n) != CBM_STORE_OK) {
cbm_log_error("pipeline.err", "phase", "persist_file_hashes", "project",
p->project_name);
hash_records_complete = false;
}
free(fhashes);
} else {
/* OOM fallback: the original per-file path (identical result, slower). */
for (int i = 0; i < file_count; i++) {
struct stat fst;
if (stat(files[i].path, &fst) == 0) {
if (cbm_store_upsert_file_hash(hash_store, p->project_name, files[i].rel_path,
"", stat_mtime_ns(&fst),
fst.st_size) != CBM_STORE_OK) {
hash_records_complete = false;
}
} else {
hash_records_complete = false;
}
}
}
CBM_PROF_END_N("persist", "4_file_hashes", t_fh, file_count);
/* Coverage rows (#963): a full run's file_errors plus the by-design
* discovery exclusions are the complete coverage truth for the
* project. The dump recreated the DB file, so the separate
* index_coverage table starts empty — write only when there is
* something to record (AFTER hashes, so the deleted-file prune inside
* replace sees the live file set; not_indexed_* kinds are exempt from
* that prune — deliberately-unindexed paths have no hash rows). */
int cov_total = p->file_errors_count + p->excluded_count + p->ignored_count;
cbm_coverage_row_t *cov = NULL;
int cn = 0;
bool coverage_rows_available = cov_total == 0;
if (cov_total > 0) {
cov = (cbm_coverage_row_t *)malloc((size_t)cov_total * sizeof(*cov));
if (cov) {
coverage_rows_available = true;
for (int i = 0; i < p->file_errors_count; i++) {
cov[cn].rel_path = p->file_errors[i].path;
cov[cn].kind = p->file_errors[i].phase;
cov[cn].detail = p->file_errors[i].reason;
cn++;
}
for (int i = 0; i < p->excluded_count; i++) {
cov[cn].rel_path = p->excluded_dirs[i];
cov[cn].kind = "not_indexed_dir";
cov[cn].detail = "excluded subtree";
cn++;
}
for (int i = 0; i < p->ignored_count; i++) {
cov[cn].rel_path = p->ignored_files[i].rel_path;
cov[cn].kind = "not_indexed_file";
cov[cn].detail = p->ignored_files[i].reason;
cn++;
}
}
}
cbm_project_t project_info = {0};
bool have_project_info =
cbm_store_get_project(hash_store, p->project_name, &project_info) == CBM_STORE_OK;
const char *recording_status =
!coverage_rows_available
? "unavailable"
: (p->ignored_total > p->ignored_count ? "truncated" : "complete");
cbm_coverage_meta_t coverage_meta = {
.generation = have_project_info ? project_info.indexed_at : NULL,
.index_mode = pipeline_mode_name(p->mode),
.recording_status = recording_status,
.ignored_files_stored = p->ignored_count,
.ignored_files_total = p->ignored_total,
.coverage_version = 1,
.hash_records_complete = hash_records_complete,
};
if (cbm_store_coverage_replace_ex(hash_store, p->project_name, cov, cn, &coverage_meta) !=
CBM_STORE_OK) {
cbm_log_error("pipeline.err", "phase", "persist_coverage", "project", p->project_name);
}
free(cov);
if (have_project_info) {
cbm_project_free_fields(&project_info);
}
if (p->ignored_total > p->ignored_count) {
cbm_log_warn("index.ignored_capped", "stored", itoa_buf(p->ignored_count), "total",
itoa_buf(p->ignored_total));
}
/* FTS5 backfill: populate nodes_fts with camelCase-split names.
* Contentless FTS5 requires the special 'delete-all' command instead of
* DELETE FROM to wipe prior rows (there's no underlying content table).
* Falls back to plain names if cbm_camel_split is unavailable (which
* shouldn't happen because we always register it, but we stay defensive). */
CBM_PROF_START(t_fts);
cbm_store_exec(hash_store, "INSERT INTO nodes_fts(nodes_fts) VALUES('delete-all');");
if (cbm_store_exec(hash_store,
"INSERT INTO nodes_fts(rowid, name, qualified_name, label, file_path) "
"SELECT id, cbm_camel_split(name), qualified_name, label, file_path "
"FROM nodes;") != CBM_STORE_OK) {
cbm_store_exec(hash_store,
"INSERT INTO nodes_fts(rowid, name, qualified_name, label, file_path) "
"SELECT id, name, qualified_name, label, file_path FROM nodes;");
}
CBM_PROF_END("persist", "5_fts_backfill", t_fts);
cbm_store_close(hash_store);
cbm_log_info("pass.timing", "pass", "persist_hashes", "files", itoa_buf(file_count));
cbm_log_info("pass.timing", "pass", "dump_and_persist", "elapsed_ms",
itoa_buf((int)elapsed_ms(*t)), "files", itoa_buf(manifest_count));
if (p->ignored_total > p->ignored_count) {
cbm_log_warn("index.ignored_capped", "stored", itoa_buf(p->ignored_count), "total",
itoa_buf(p->ignored_total));
}
free(p->saved_adr);
p->saved_adr = NULL;
/* Export persistent artifact if enabled */
if (p->persistence) {
CBM_PROF_START(t_art);
int arc = cbm_artifact_export(db_path, p->repo_path, p->project_name, CBM_ARTIFACT_BEST);
CBM_PROF_END("persist", "6_artifact_export", t_art);
if (arc != 0) {
const char *err = cbm_artifact_export_last_error();
cbm_log_error("pipeline.err", "phase", "artifact_export", "err", err ? err : "unknown");
/* A failed persistence export intentionally fails the run; this used to be ignored. */
return arc;
}
}
return 0;
/* The SQLite generation is the commit point. Automatic refresh of an
* existing artifact is best-effort, but an explicitly requested artifact
* is caller-visible and must report an export failure. */
return cbm_pipeline_refresh_artifact(p, db_path);
}
/* Run githistory pass. */
@@ -1459,7 +1801,8 @@ static int run_tests_and_history(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx,
/* Run tests, git history, predump passes, and dump+persist. */
static int run_post_extraction(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx,
const cbm_file_info_t *files, int file_count) {
const cbm_file_info_t *files, int file_count,
const cbm_file_hash_t *baseline_manifest, int baseline_count) {
int rc = run_tests_and_history(p, ctx, files, file_count);
if (rc != 0) {
return rc;
@@ -1469,12 +1812,20 @@ static int run_post_extraction(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx,
run_predump_passes(p, ctx);
CBM_PROF_END("pipeline", "3_predump_passes_total", t_predump);
if (!check_cancel(p)) {
struct timespec t;
CBM_PROF_START(t_dump);
rc = dump_and_persist_hashes(p, files, file_count, &t);
CBM_PROF_END("pipeline", "4_dump_and_persist", t_dump);
#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API
if (cbm_pipeline_persist_test_take_cancel_after_predump()) {
atomic_store(&p->cancelled, 1);
}
#endif
if (check_cancel(p)) {
return CBM_PIPELINE_ABORT_PRESERVE_DB;
}
struct timespec t;
CBM_PROF_START(t_dump);
rc = dump_and_persist_hashes(p, baseline_manifest, baseline_count, &t);
CBM_PROF_END("pipeline", "4_dump_and_persist", t_dump);
return rc;
}
@@ -1514,6 +1865,17 @@ int cbm_pipeline_run(cbm_pipeline_t *p) {
struct timespec t0;
cbm_clock_gettime(CLOCK_MONOTONIC, &t0);
cbm_path_alias_collection_t *path_aliases = NULL;
cbm_file_hash_t *baseline_manifest = NULL;
int baseline_count = 0;
promote_mode_to_existing_coverage(p);
/* cbm_pipeline_new() may precede the actual run by an arbitrary interval.
* Refresh once here, then use this exact snapshot for both Branch graph
* construction and the baseline semantic manifest. */
if (pipeline_refresh_git_context(p) != 0) {
return CBM_NOT_FOUND;
}
/* C/C++ #define Macro nodes (#375) dominate extraction on macro-dense repos
* (≈49% of nodes on the Linux kernel), so gate them to full mode — moderate
@@ -1560,11 +1922,24 @@ int cbm_pipeline_run(cbm_pipeline_t *p) {
goto cleanup;
}
/* Snapshot every semantic input once before routing/extraction. The same
* bytes drive exact no-op comparison and are checked against a fresh
* rediscovery immediately before any replacement is published. */
rc = cbm_pipeline_build_semantic_manifest(p->project_name, p->repo_path, files, file_count,
p->excluded_dirs, p->excluded_count, &p->git_ctx,
p->userconfig, &baseline_manifest, &baseline_count);
if (rc != 0) {
rc = CBM_PIPELINE_ABORT_PRESERVE_DB;
goto cleanup;
}
/* Check for existing DB → try incremental or delete for reindex */
rc = try_incremental_or_delete_db(p, files, file_count);
if (rc == CBM_PIPELINE_ABORT_PRESERVE_DB || rc >= 0) {
cbm_discover_free(files, file_count);
return rc;
rc = try_incremental_or_delete_db(p, files, file_count, baseline_manifest, baseline_count);
if (rc >= 0 || rc == CBM_PIPELINE_ABORT_PRESERVE_DB || rc == CBM_PIPELINE_PERSIST_FAILED) {
goto cleanup;
}
if (rc != CBM_PIPELINE_FORCE_FULL_REINDEX) {
goto cleanup;
}
cbm_log_info("pipeline.route", "path", "full");
@@ -1596,7 +1971,7 @@ int cbm_pipeline_run(cbm_pipeline_t *p) {
goto cleanup;
}
rc = run_post_extraction(p, &ctx, files, file_count);
rc = run_post_extraction(p, &ctx, files, file_count, baseline_manifest, baseline_count);
if (rc != 0) {
goto cleanup;
}
@@ -1610,6 +1985,7 @@ cleanup:
cbm_pkgmap_free(cbm_pipeline_get_pkgmap());
cbm_pipeline_set_pkgmap(NULL);
cbm_discover_free(files, file_count);
cbm_pipeline_free_semantic_manifest(baseline_manifest, baseline_count);
cbm_gbuf_free(p->gbuf);
p->gbuf = NULL;
cbm_registry_free(p->registry);
File diff suppressed because it is too large Load Diff
+111 -1
View File
@@ -11,7 +11,10 @@
#include "pipeline/pipeline.h"
#include "pipeline/path_alias.h"
#include "graph_buffer/graph_buffer.h"
#include "store/store.h"
#include "discover/discover.h"
#include "discover/userconfig.h"
#include "git/git_context.h"
#include "foundation/hash_table.h"
#include "cbm.h"
#include "lsp/go_lsp.h" /* CBMLSPDef for cbm_parallel_resolve cross-LSP inputs */
@@ -30,6 +33,8 @@
* Distinct from CBM_NOT_FOUND, which the orchestrator uses as the normal
* "no incremental route; continue with a full index" sentinel. */
#define CBM_PIPELINE_ABORT_PRESERVE_DB (-2)
#define CBM_PIPELINE_FORCE_FULL_REINDEX (-3)
#define CBM_PIPELINE_PERSIST_FAILED (-4)
/* Canonicalize route-path parameter placeholders (":id", "{id}", "<id>",
* "${...}") to a single "{}" token so that client call sites and server
@@ -125,6 +130,18 @@ typedef struct {
const CBMReturnTypeTable *return_type_table;
} cbm_pipeline_ctx_t;
/* Transcode an ObjectScript Studio Export XML file and compose every generated
* UDL class into one cacheable result. The returned result owns all child
* extraction arenas and is released with the ordinary cbm_free_result(). */
CBMFileResult *cbm_pipeline_extract_objectscript_export(
const char *source, int source_len, const char *project_name, const char *rel_path,
const CBMMacroTable *macro_table, const CBMReturnTypeTable *return_type_table);
/* Materialize CONFIGURES edges from one extracted file's env-access carriers.
* Shared by sequential definition processing and the parallel cache registry. */
int cbm_pipeline_create_env_configures_for_file(cbm_pipeline_ctx_t *ctx,
const CBMFileResult *result, const char *rel);
static inline int cbm_pipeline_relpath_is_excluded(const char *rel_path, char *const *excluded_dirs,
int excluded_count) {
if (!rel_path || rel_path[0] == '\0' || !excluded_dirs || excluded_count <= 0) {
@@ -622,7 +639,56 @@ int cbm_scan_project_env_urls_excluded(const char *root_path, cbm_env_binding_t
* Classifies files by mtime+size, deletes changed nodes, re-parses changed
* files, merges into disk DB. Returns 0 on success. */
int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_file_info_t *files,
int file_count);
int file_count, const cbm_file_hash_t *baseline_manifest,
int baseline_count);
/* Exact semantic inputs for no-op/forced-full routing. The manifest contains
* every discovered source plus repository controls actually consumed by
* discovery, package mapping, path aliases, and extension overrides. */
#define CBM_SEMANTIC_INPUT_PREFIX ".codebase-memory/.semantic-input/"
#define CBM_SEMANTIC_INPUT_GIT_CONTEXT CBM_SEMANTIC_INPUT_PREFIX "git-context-v1"
#define CBM_SEMANTIC_INPUT_GLOBAL_CONFIG CBM_SEMANTIC_INPUT_PREFIX "global-extension-config-v1"
#define CBM_SEMANTIC_INPUT_PROJECT_CONFIG CBM_SEMANTIC_INPUT_PREFIX "project-extension-config-v1"
int cbm_pipeline_build_semantic_manifest(const char *project, const char *repo_path,
const cbm_file_info_t *files, int file_count,
char **excluded_dirs, int excluded_count,
const cbm_git_context_t *git_ctx,
const cbm_userconfig_t *userconfig, cbm_file_hash_t **out,
int *out_count);
void cbm_pipeline_free_semantic_manifest(cbm_file_hash_t *manifest, int count);
bool cbm_pipeline_semantic_manifests_equal(const cbm_file_hash_t *left, int left_count,
const cbm_file_hash_t *right, int right_count);
/* Re-run discovery and hash its exact semantic inputs. Used at the publication
* boundary so late additions/deletions cannot escape a frozen file list. */
int cbm_pipeline_build_fresh_semantic_manifest(const char *project, const char *repo_path, int mode,
cbm_file_hash_t **out, int *out_count);
/* Compatibility contract persisted in coverage metadata. Increment when a
* graph/manifest semantic change makes prior exact-input indexes unsafe. */
enum { CBM_SEMANTIC_INDEX_VERSION = 3 };
typedef struct {
cbm_gbuf_t *gbuf;
const char *final_db_path;
const char *project;
atomic_int *cancelled;
const cbm_file_hash_t *manifest;
int manifest_count;
const char *adr_content;
const cbm_coverage_row_t *coverage;
int coverage_count;
cbm_coverage_meta_t coverage_meta;
} cbm_pipeline_generation_t;
/* Serialize and fully populate a sibling staging database, then atomically
* replace final_db_path. The old generation is untouched on every failure or
* cancellation observed before the rename commit point. */
int cbm_pipeline_publish_generation(const cbm_pipeline_generation_t *generation);
/* The SQLite generation is authoritative. An explicitly requested artifact is
* part of the caller-visible operation and its export error is returned;
* automatic refresh of an already-existing artifact remains best-effort. */
int cbm_pipeline_refresh_artifact(cbm_pipeline_t *p, const char *db_path);
/* Pipeline accessors for incremental use */
const char *cbm_pipeline_repo_path(const cbm_pipeline_t *p);
@@ -645,4 +711,48 @@ bool extract_grpc_service_method(const char *callee, char *service, size_t srv_s
long cbm_pp_bp_nap_cycles(void);
void cbm_pp_bp_nap_cycles_reset(void);
/* Number of resolved-call rows handed to the parallel resolver's linear LSP
* fallback since the last reset. Test observability for occurrence-index
* coverage; deterministic and independent of wall-clock timing. */
uint64_t cbm_pp_lsp_linear_fallback_rows(void);
void cbm_pp_lsp_linear_fallback_rows_reset(void);
#if defined(CBM_CALL_REFERENCE_LOOKUP_TEST_API) && CBM_CALL_REFERENCE_LOOKUP_TEST_API
/* Deterministic test-only operation count for the shared semantic-reference
* matcher used by both sequential and fused-parallel usage materialization. */
void cbm_pipeline_lsp_reference_lookup_test_reset(void);
uint64_t cbm_pipeline_lsp_reference_lookup_test_rows_examined(void);
#endif
#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API
typedef enum {
CBM_INCREMENTAL_ROUTE_NONE = 0,
CBM_INCREMENTAL_ROUTE_NOOP,
CBM_INCREMENTAL_ROUTE_FORCED_FULL,
CBM_INCREMENTAL_ROUTE_LEGACY_PARTIAL,
} cbm_incremental_route_t;
/* Deterministic one-shot fault injection for the incremental-parallel result
* cache allocation. Reset explicitly so one test cannot affect another. */
void cbm_pipeline_incremental_test_fail_result_cache_alloc_once(void);
void cbm_pipeline_incremental_test_force_legacy_partial_once(void);
void cbm_pipeline_incremental_test_fail_after_stage_dump_once(void);
void cbm_pipeline_incremental_test_cancel_after_predump_once(void);
void cbm_pipeline_incremental_test_cancel_after_destination_prepare_once(void);
void cbm_pipeline_incremental_test_fail_adr_capture_once(void);
typedef void (*cbm_pipeline_test_hook_fn)(void *userdata);
void cbm_pipeline_incremental_test_before_final_manifest_once(cbm_pipeline_test_hook_fn hook,
void *userdata);
cbm_incremental_route_t cbm_pipeline_incremental_test_last_route(void);
void cbm_pipeline_incremental_test_reset_faults(void);
/* Shared persistence-hook plumbing. Tests use the incremental facade above so
* one reset covers route, extraction, and publication faults. */
bool cbm_pipeline_persist_test_take_failure_after_stage_dump(void);
bool cbm_pipeline_persist_test_take_cancel_after_predump(void);
bool cbm_pipeline_persist_test_take_cancel_after_destination_prepare(void);
void cbm_pipeline_persist_test_run_before_final_manifest(void);
void cbm_pipeline_persist_test_reset_faults(void);
#endif
#endif /* CBM_PIPELINE_INTERNAL_H */
+207 -21
View File
@@ -437,15 +437,17 @@ static int configure_pragmas(cbm_store_t *s, bool in_memory, bool read_only) {
return rc;
}
/* #1083: bound the WAL file so a checkpoint-starved log is physically
* reclaimed the next time a checkpoint can reset it. Our checkpoints are
* all PASSIVE (they never ftruncate — see cbm_store_checkpoint's SIGBUS
* note), so without a size limit the -wal file only ever grows; a
* journal_size_limit truncates it back to N bytes on the next successful
* reclaimed the next time a checkpoint can reset it. Shared/live
* checkpoints are PASSIVE (they never ftruncate — see
* cbm_store_checkpoint's SIGBUS note), so without a size limit the -wal
* file only ever grows; journal_size_limit truncates it back to N bytes
* on the next successful
* reset. N is far above the healthy WAL (~4 MiB under the default
* 1000-page autocheckpoint), so normal indexing never triggers
* truncate/regrow churn — it only fires after abnormal growth. We do NOT
* use a TRUNCATE checkpoint: its ftruncate(fd,0) can raise SIGBUS in a
* sibling process that has the DB mmap'd on macOS. */
* truncate/regrow churn — it only fires after abnormal growth.
* Shared/live paths do NOT use a TRUNCATE checkpoint: truncating the WAL
* to zero can raise SIGBUS in a sibling process that has the DB mmap'd
* on macOS. Exclusive staging publication seals separately below. */
rc = exec_sql(s, "PRAGMA journal_size_limit = 268435456;"); /* 256 MiB */
if (rc != CBM_STORE_OK) {
return rc;
@@ -1135,6 +1137,118 @@ int64_t cbm_store_journal_size_limit(cbm_store_t *s) {
return limit;
}
int cbm_store_seal_for_atomic_publish(cbm_store_t *s) {
if (!s || !s->db) {
return CBM_STORE_ERR;
}
/* Bulk indexing deliberately relaxes synchronous writes. Restore the
* strongest durability before copying WAL pages into the database file.
* This staging connection is exclusively owned by the publisher. */
if (exec_sql(s, "PRAGMA synchronous = FULL;") != CBM_STORE_OK) {
return CBM_STORE_ERR;
}
int log_frames = 0;
int checkpointed_frames = 0;
int rc = sqlite3_wal_checkpoint_v2(s->db, NULL, SQLITE_CHECKPOINT_TRUNCATE, &log_frames,
&checkpointed_frames);
if (rc != SQLITE_OK) {
snprintf(s->errbuf, sizeof(s->errbuf), "seal checkpoint: %s (log=%d checkpointed=%d)",
sqlite3_errstr(rc), log_frames, checkpointed_frames);
return CBM_STORE_ERR;
}
/* PRAGMA journal_mode returns the mode SQLite actually entered. sqlite3_exec
* would discard that result and could falsely report a successful seal. */
sqlite3_stmt *stmt = NULL;
rc = sqlite3_prepare_v2(s->db, "PRAGMA journal_mode = DELETE;", CBM_NOT_FOUND, &stmt, NULL);
if (rc != SQLITE_OK) {
store_set_error_sqlite(s, "seal journal_mode prepare");
return CBM_STORE_ERR;
}
rc = sqlite3_step(stmt);
if (rc != SQLITE_ROW) {
store_set_error_sqlite(s, "seal journal_mode step");
sqlite3_finalize(stmt);
return CBM_STORE_ERR;
}
const char *mode = (const char *)sqlite3_column_text(stmt, 0);
bool is_delete = mode && sqlite3_stricmp(mode, "delete") == 0;
rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE) {
store_set_error_sqlite(s, "seal journal_mode completion");
sqlite3_finalize(stmt);
return CBM_STORE_ERR;
}
rc = sqlite3_finalize(stmt);
if (rc != SQLITE_OK) {
store_set_error_sqlite(s, "seal journal_mode finalize");
return CBM_STORE_ERR;
}
if (!is_delete) {
store_set_error(s, "seal journal_mode did not enter delete mode");
return CBM_STORE_ERR;
}
return CBM_STORE_OK;
}
int cbm_store_seal_existing_path_for_replace(const char *db_path) {
if (!db_path || !db_path[0]) {
return CBM_STORE_ERR;
}
cbm_store_t maintenance = {0};
int rc = sqlite3_open_v2(db_path, &maintenance.db, SQLITE_OPEN_READWRITE, NULL);
if (rc != SQLITE_OK) {
int primary = rc & 0xff;
sqlite3_close(maintenance.db);
return primary == SQLITE_CORRUPT || primary == SQLITE_NOTADB || primary == SQLITE_FORMAT
? CBM_STORE_NOT_FOUND
: CBM_STORE_ERR;
}
(void)sqlite3_busy_timeout(maintenance.db, 10000);
sqlite3_stmt *stmt = NULL;
rc = sqlite3_prepare_v2(maintenance.db, "PRAGMA quick_check(1);", CBM_NOT_FOUND, &stmt, NULL);
if (rc != SQLITE_OK) {
int primary = rc & 0xff;
sqlite3_close(maintenance.db);
return primary == SQLITE_BUSY || primary == SQLITE_LOCKED || primary == SQLITE_READONLY ||
primary == SQLITE_CANTOPEN || primary == SQLITE_IOERR
? CBM_STORE_ERR
: CBM_STORE_NOT_FOUND;
}
rc = sqlite3_step(stmt);
const char *result = rc == SQLITE_ROW ? (const char *)sqlite3_column_text(stmt, 0) : NULL;
bool structurally_valid = result && strcmp(result, "ok") == 0;
int finalize_rc = sqlite3_finalize(stmt);
if (!structurally_valid || finalize_rc != SQLITE_OK) {
int primary = rc & 0xff;
sqlite3_close(maintenance.db);
return primary == SQLITE_BUSY || primary == SQLITE_LOCKED || primary == SQLITE_READONLY ||
primary == SQLITE_CANTOPEN || primary == SQLITE_IOERR
? CBM_STORE_ERR
: CBM_STORE_NOT_FOUND;
}
/* A structurally valid SQLite file that is not recognizably a codebase
* graph is incompatible at this destination and must be preserved before
* replacement. Legacy graph schemas still pass this shallow identity
* check and can be sealed without running init_schema(). */
if (!cbm_store_check_integrity(&maintenance)) {
sqlite3_close(maintenance.db);
return CBM_STORE_NOT_FOUND;
}
int seal_rc = cbm_store_seal_for_atomic_publish(&maintenance);
sqlite3_close(maintenance.db);
return seal_rc;
}
/* ── Dump ───────────────────────────────────────────────────────── */
/* Dump entire in-memory database to a file via sqlite3_backup.
@@ -1288,8 +1402,15 @@ int cbm_store_get_project(cbm_store_t *s, const char *name, cbm_project_t *out)
out->name = heap_strdup((const char *)sqlite3_column_text(stmt, 0));
out->indexed_at = heap_strdup((const char *)sqlite3_column_text(stmt, SKIP_ONE));
out->root_path = heap_strdup((const char *)sqlite3_column_text(stmt, CBM_SZ_2));
/* Cached SELECT statements must not remain parked on SQLITE_ROW.
* Besides retaining a read transaction, an active reader prevents
* publication from switching a fully-written staging database out of
* WAL mode before the atomic rename. All result text is owned by the
* caller at this point, so release the reader immediately. */
sqlite3_reset(stmt);
return CBM_STORE_OK;
}
sqlite3_reset(stmt);
return CBM_STORE_NOT_FOUND;
}
@@ -1438,8 +1559,10 @@ int cbm_store_find_node_by_id(cbm_store_t *s, int64_t id, cbm_node_t *out) {
int rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
scan_node(stmt, out);
sqlite3_reset(stmt);
return CBM_STORE_OK;
}
sqlite3_reset(stmt);
return CBM_STORE_NOT_FOUND;
}
@@ -1462,8 +1585,10 @@ int cbm_store_find_node_by_qn(cbm_store_t *s, const char *project, const char *q
int rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
scan_node(stmt, out);
sqlite3_reset(stmt);
return CBM_STORE_OK;
}
sqlite3_reset(stmt);
return CBM_STORE_NOT_FOUND;
}
@@ -1484,8 +1609,10 @@ int cbm_store_find_node_by_qn_any(cbm_store_t *s, const char *qn, cbm_node_t *ou
int rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
scan_node(stmt, out);
sqlite3_reset(stmt);
return CBM_STORE_OK;
}
sqlite3_reset(stmt);
return CBM_STORE_NOT_FOUND;
}
@@ -1654,10 +1781,12 @@ int cbm_store_count_nodes(cbm_store_t *s, const char *project) {
}
bind_text(stmt, SKIP_ONE, project);
int count = 0;
if (sqlite3_step(stmt) == SQLITE_ROW) {
return sqlite3_column_int(stmt, 0);
count = sqlite3_column_int(stmt, 0);
}
return 0;
sqlite3_reset(stmt);
return count;
}
int cbm_store_delete_nodes_by_project(cbm_store_t *s, const char *project) {
@@ -1972,10 +2101,12 @@ int cbm_store_count_edges(cbm_store_t *s, const char *project) {
}
bind_text(stmt, SKIP_ONE, project);
int count = 0;
if (sqlite3_step(stmt) == SQLITE_ROW) {
return sqlite3_column_int(stmt, 0);
count = sqlite3_column_int(stmt, 0);
}
return 0;
sqlite3_reset(stmt);
return count;
}
int cbm_store_count_edges_by_type(cbm_store_t *s, const char *project, const char *type) {
@@ -1988,10 +2119,12 @@ int cbm_store_count_edges_by_type(cbm_store_t *s, const char *project, const cha
bind_text(stmt, SKIP_ONE, project);
bind_text(stmt, ST_COL_2, type);
int count = 0;
if (sqlite3_step(stmt) == SQLITE_ROW) {
return sqlite3_column_int(stmt, 0);
count = sqlite3_column_int(stmt, 0);
}
return 0;
sqlite3_reset(stmt);
return count;
}
int cbm_store_delete_edges_by_project(cbm_store_t *s, const char *project) {
@@ -3552,9 +3685,11 @@ int cbm_store_search(cbm_store_t *s, const cbm_search_params_t *params, cbm_sear
const char *select_cols = "SELECT n.id, n.project, n.label, n.name, n.qualified_name, "
"n.file_path, n.start_line, n.end_line, n.properties, "
"(SELECT COUNT(*) FROM edges e WHERE e.target_id = n.id AND "
"e.type IN ('CALLS', 'USAGE', 'INHERITS', 'IMPLEMENTS')) AS in_deg, "
"e.type IN ('CALLS', 'USAGE', 'CALL_REFERENCE', 'INHERITS', "
"'IMPLEMENTS')) AS in_deg, "
"(SELECT COUNT(*) FROM edges e WHERE e.source_id = n.id AND "
"e.type IN ('CALLS', 'USAGE', 'INHERITS', 'IMPLEMENTS')) AS out_deg ";
"e.type IN ('CALLS', 'USAGE', 'CALL_REFERENCE', 'INHERITS', "
"'IMPLEMENTS')) AS out_deg ";
char where[CBM_SZ_2K] = "";
search_bind_t binds[ST_SEARCH_MAX_BINDS];
@@ -7174,6 +7309,33 @@ int cbm_store_adr_store(cbm_store_t *s, const char *project, const char *content
}
int cbm_store_adr_get(cbm_store_t *s, const char *project, cbm_adr_t *out) {
if (!s || !s->db || !project || !out) {
return CBM_STORE_ERR;
}
memset(out, 0, sizeof(*out));
/* ADR storage was added after the original graph schema. A readable
* legacy generation without project_summaries has no ADR to preserve; it
* is not a read failure and must remain replaceable by a full reindex. */
sqlite3_stmt *table_stmt = NULL;
int rc = sqlite3_prepare_v2(
s->db,
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='project_summaries' LIMIT 1;",
CBM_NOT_FOUND, &table_stmt, NULL);
if (rc != SQLITE_OK) {
store_set_error_sqlite(s, "adr_get table probe");
return CBM_STORE_ERR;
}
rc = sqlite3_step(table_stmt);
if (rc == SQLITE_DONE) {
sqlite3_finalize(table_stmt);
return CBM_STORE_NOT_FOUND;
}
if (rc != SQLITE_ROW || sqlite3_finalize(table_stmt) != SQLITE_OK) {
store_set_error_sqlite(s, "adr_get table probe step");
return CBM_STORE_ERR;
}
const char *sql = "SELECT project, summary, created_at, updated_at FROM project_summaries "
"WHERE project=?1";
sqlite3_stmt *stmt = NULL;
@@ -7181,18 +7343,42 @@ int cbm_store_adr_get(cbm_store_t *s, const char *project, cbm_adr_t *out) {
store_set_error_sqlite(s, "adr_get");
return CBM_STORE_ERR;
}
bind_text(stmt, SKIP_ONE, project);
int rc = sqlite3_step(stmt);
if (rc != SQLITE_ROW) {
if (bind_text(stmt, SKIP_ONE, project) != SQLITE_OK) {
store_set_error_sqlite(s, "adr_get bind");
sqlite3_finalize(stmt);
store_set_error(s, "no ADR found");
return CBM_STORE_NOT_FOUND;
return CBM_STORE_ERR;
}
rc = sqlite3_step(stmt);
if (rc != SQLITE_ROW) {
if (rc != SQLITE_DONE) {
store_set_error_sqlite(s, "adr_get step");
}
sqlite3_finalize(stmt);
if (rc == SQLITE_DONE) {
store_set_error(s, "no ADR found");
return CBM_STORE_NOT_FOUND;
}
return CBM_STORE_ERR;
}
out->project = heap_strdup((const char *)sqlite3_column_text(stmt, 0));
out->content = heap_strdup((const char *)sqlite3_column_text(stmt, SKIP_ONE));
out->created_at = heap_strdup((const char *)sqlite3_column_text(stmt, CBM_SZ_2));
out->updated_at = heap_strdup((const char *)sqlite3_column_text(stmt, CBM_SZ_3));
sqlite3_finalize(stmt);
rc = sqlite3_finalize(stmt);
if (rc != SQLITE_OK) {
cbm_store_adr_free(out);
store_set_error_sqlite(s, "adr_get finalize");
return CBM_STORE_ERR;
}
/* Every selected column is NOT NULL in the schema. A NULL here therefore
* means either allocation failure or corrupt data; never return a partial
* ADR that a full rebuild could silently drop during publication. */
if (!out->project || !out->content || !out->created_at || !out->updated_at) {
cbm_store_adr_free(out);
store_set_error(s, "adr_get: failed to copy complete ADR");
return CBM_STORE_ERR;
}
return CBM_STORE_OK;
}
+14
View File
@@ -207,6 +207,12 @@ cbm_store_t *cbm_store_open_path(const char *db_path);
* exist — never creates a new .db file. */
cbm_store_t *cbm_store_open_path_query(const char *db_path);
/* Validate and seal an existing DB for atomic replacement without creating or
* migrating its schema. Returns OK when sealed, NOT_FOUND when the bytes are
* definitely corrupt/incompatible and should be quarantined, or ERR when the
* file is busy/unavailable and must be left in place. */
int cbm_store_seal_existing_path_for_replace(const char *db_path);
/* On-disk path of a file-backed store, or NULL for an in-memory (:memory:)
* store. The returned pointer is owned by the store. */
const char *cbm_store_db_path(const cbm_store_t *s);
@@ -271,6 +277,14 @@ int64_t cbm_store_journal_size_limit(cbm_store_t *s);
* bumps on every index run. "legacy" for DBs predating store_meta. */
int cbm_store_generation(cbm_store_t *s, char *buf, size_t bufsz);
/* Seal a fully-written staging database before atomic publication.
* Raises synchronous to FULL, requires an exclusive TRUNCATE checkpoint to
* complete, then leaves the database in verified DELETE journal mode so the
* main file is self-contained (no required -wal/-shm sidecars). This is a
* fail-closed operation: SQLITE_BUSY and an unconfirmed mode transition are
* errors. The caller must own the staging database exclusively. */
int cbm_store_seal_for_atomic_publish(cbm_store_t *s);
/* Resolve the mmap_size pragma value applied to on-disk stores from the
* CBM_SQLITE_MMAP_SIZE environment variable. Defaults to 67108864 (64 MB)
* when the variable is unset, malformed, or partially numeric. Negative
+16 -7
View File
@@ -650,16 +650,24 @@ cbm_layout_result_t *cbm_layout_compute(cbm_store_t *store, const char *project,
int64_t *node_ids = malloc((size_t)n * sizeof(int64_t));
int *in_calls = calloc((size_t)n, sizeof(int));
int *in_usage = calloc((size_t)n, sizeof(int));
int *in_call_reference = calloc((size_t)n, sizeof(int));
int *deg_dummy = calloc((size_t)n, sizeof(int));
if (node_ids && in_calls && in_usage && deg_dummy) {
bool dead_degrees_valid = node_ids && in_calls && in_usage && in_call_reference && deg_dummy;
if (dead_degrees_valid) {
for (int i = 0; i < n; i++)
node_ids[i] = search_out.results[i].node.id;
for (int off = 0; off < n; off += DEAD_DEGREE_CHUNK) {
int cnt = (n - off < DEAD_DEGREE_CHUNK) ? (n - off) : DEAD_DEGREE_CHUNK;
cbm_store_batch_count_degrees(store, node_ids + off, cnt, "CALLS", in_calls + off,
deg_dummy + off);
cbm_store_batch_count_degrees(store, node_ids + off, cnt, "USAGE", in_usage + off,
deg_dummy + off);
if (cbm_store_batch_count_degrees(store, node_ids + off, cnt, "CALLS", in_calls + off,
deg_dummy + off) != CBM_STORE_OK ||
cbm_store_batch_count_degrees(store, node_ids + off, cnt, "USAGE", in_usage + off,
deg_dummy + off) != CBM_STORE_OK ||
cbm_store_batch_count_degrees(store, node_ids + off, cnt, "CALL_REFERENCE",
in_call_reference + off,
deg_dummy + off) != CBM_STORE_OK) {
dead_degrees_valid = false;
break;
}
}
}
@@ -720,8 +728,8 @@ cbm_layout_result_t *cbm_layout_compute(cbm_store_t *store, const char *project,
bool is_fn =
sn->label && (strcmp(sn->label, "Function") == 0 || strcmp(sn->label, "Method") == 0);
bool testish = nf.is_test || (sn->file_path && cbm_is_test_file_path(sn->file_path));
int ic = in_calls ? in_calls[i] : 1;
int iu = in_usage ? in_usage[i] : 1;
int ic = dead_degrees_valid ? in_calls[i] : 1;
int iu = dead_degrees_valid ? in_usage[i] + in_call_reference[i] : 1;
const char *status;
if (!is_fn)
status = "structural";
@@ -770,6 +778,7 @@ cbm_layout_result_t *cbm_layout_compute(cbm_store_t *store, const char *project,
free(node_ids);
free(in_calls);
free(in_usage);
free(in_call_reference);
free(deg_dummy);
free_edge_array(all_edges, mapped);
cbm_store_search_free(&search_out);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+984
View File
@@ -0,0 +1,984 @@
/*
* repro_call_node_behaviors.c — raw CALL/USAGE ownership for overloaded and
* call-shaped expressions.
*
* C++ and CUDA deliberately register several expression containers as call
* nodes. A container may own one synthetic operator call, one semantic-only
* implicit-call candidate, or no call at all; none of those roles allows it to erase
* operand references. Scala and Elixir operator syntax follows the same rule.
* Puppet resource declarations belong to the DSL extractor and must not be
* reinterpreted as generic code calls.
*
* Every fixture has exactly one target AST node. Every raw count is exact and
* joined to the exact qualified name of its caller (or exact module QN for the
* top-level Puppet DSL case), so duplicate emissions and scope drift stay RED.
*/
#include "test_framework.h"
#include "cbm.h"
#include "lang_specs.h"
#include "macro_table.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
const char *tag;
CBMLanguage language;
const char *filename;
const char *source;
const char *ast_kind;
const char *reason;
} BehaviorCase;
static int terminal_name_matches(const char *raw, const char *expected) {
if (!raw || !expected)
return 0;
if (strcmp(raw, expected) == 0)
return 1;
size_t raw_len = strlen(raw);
size_t expected_len = strlen(expected);
if (raw_len < expected_len)
return 0;
const char *tail = raw + raw_len - expected_len;
if (strcmp(tail, expected) != 0)
return 0;
if (tail == raw)
return 1;
unsigned char boundary = (unsigned char)tail[-1];
return !isalnum(boundary) && boundary != '_';
}
static void check_exact(const BehaviorCase *test_case, const char *invariant, int actual,
int expected, int *failures) {
if (actual == expected)
return;
fprintf(stderr, " [call-node-behavior] case=%s invariant=%s expected=%d actual=%d reason=%s\n",
test_case->tag, invariant, expected, actual, test_case->reason);
(*failures)++;
}
static int ast_kind_count(TSNode node, const char *kind) {
int count = strcmp(ts_node_type(node), kind) == 0 ? 1 : 0;
uint32_t child_count = ts_node_child_count(node);
for (uint32_t i = 0; i < child_count; i++)
count += ast_kind_count(ts_node_child(node, i), kind);
return count;
}
static void check_exact_ast_kind(const BehaviorCase *test_case, int *failures) {
const TSLanguage *language = cbm_ts_language(test_case->language);
TSParser *parser = ts_parser_new();
if (!language || !parser || !ts_parser_set_language(parser, language)) {
fprintf(stderr,
" [call-node-behavior] case=%s invariant=ast_parser_ready expected=1 "
"actual=0 reason=%s\n",
test_case->tag, test_case->reason);
(*failures)++;
if (parser)
ts_parser_delete(parser);
return;
}
TSTree *tree = ts_parser_parse_string(parser, NULL, test_case->source,
(uint32_t)strlen(test_case->source));
int count = 0;
int parse_error = 1;
if (tree) {
TSNode root = ts_tree_root_node(tree);
parse_error = ts_node_has_error(root) ? 1 : 0;
count = ast_kind_count(root, test_case->ast_kind);
}
check_exact(test_case, "ast_clean_parse", parse_error, 0, failures);
check_exact(test_case, "exact_ast_kind_count", count, 1, failures);
if (tree)
ts_tree_delete(tree);
ts_parser_delete(parser);
}
static CBMFileResult *extract_case_with_macros(const BehaviorCase *test_case,
const CBMMacroTable *macro_table, int *failures) {
CBMFileResult *result =
cbm_extract_file_ex(test_case->source, (int)strlen(test_case->source), test_case->language,
"repro", test_case->filename, 0, NULL, NULL, macro_table, NULL);
if (!result) {
fprintf(stderr,
" [call-node-behavior] case=%s invariant=extract_result expected=non-null "
"reason=%s\n",
test_case->tag, test_case->reason);
(*failures)++;
return NULL;
}
check_exact(test_case, "clean_parse", (result->has_error || result->parse_incomplete) ? 1 : 0,
0, failures);
return result;
}
static CBMFileResult *extract_case(const BehaviorCase *test_case, int *failures) {
return extract_case_with_macros(test_case, NULL, failures);
}
static int call_count_exact(const CBMFileResult *result, const char *caller_qn,
const char *callee) {
int count = 0;
for (int i = 0; i < result->calls.count; i++) {
const CBMCall *call = &result->calls.items[i];
if (call->callee_name && call->enclosing_func_qn &&
strcmp(call->callee_name, callee) == 0 &&
strcmp(call->enclosing_func_qn, caller_qn) == 0)
count++;
}
return count;
}
static int usage_count_exact(const CBMFileResult *result, const char *caller_qn,
const char *reference) {
int count = 0;
for (int i = 0; i < result->usages.count; i++) {
const CBMUsage *usage = &result->usages.items[i];
if (usage->ref_name && usage->enclosing_func_qn &&
strcmp(usage->ref_name, reference) == 0 &&
strcmp(usage->enclosing_func_qn, caller_qn) == 0)
count++;
}
return count;
}
static int value_usage_count_exact(const CBMFileResult *result, const char *caller_qn,
const char *reference) {
int count = 0;
for (int i = 0; i < result->usages.count; i++) {
const CBMUsage *usage = &result->usages.items[i];
if (usage->kind == CBM_USAGE_VALUE && usage->ref_name && usage->enclosing_func_qn &&
strcmp(usage->ref_name, reference) == 0 &&
strcmp(usage->enclosing_func_qn, caller_qn) == 0) {
count++;
}
}
return count;
}
static int callable_definition_count_exact(const CBMFileResult *result, const char *name,
const char *qualified_name) {
int count = 0;
for (int i = 0; i < result->defs.count; i++) {
const CBMDefinition *definition = &result->defs.items[i];
int callable_label = definition->label && (strcmp(definition->label, "Function") == 0 ||
strcmp(definition->label, "Method") == 0);
if (callable_label && definition->name && definition->qualified_name &&
strcmp(definition->name, name) == 0 &&
strcmp(definition->qualified_name, qualified_name) == 0) {
count++;
}
}
return count;
}
static int call_count_in_routine(const CBMFileResult *result, const char *caller,
const char *callee) {
int count = 0;
for (int i = 0; i < result->calls.count; i++) {
const CBMCall *call = &result->calls.items[i];
if (call->callee_name && strcmp(call->callee_name, callee) == 0 &&
terminal_name_matches(call->enclosing_func_qn, caller))
count++;
}
return count;
}
static int exact_semantic_candidate_count_in_routine(const BehaviorCase *test_case,
const CBMFileResult *result,
const char *caller, const char *callee,
const char *site_text) {
int count = 0;
size_t source_len = strlen(test_case->source);
size_t site_len = strlen(site_text);
for (int i = 0; i < result->calls.count; i++) {
const CBMCall *call = &result->calls.items[i];
if (!call->requires_lsp_resolution || !call->callee_name ||
strcmp(call->callee_name, callee) != 0 ||
!terminal_name_matches(call->enclosing_func_qn, caller) ||
call->site_end_byte <= call->site_start_byte || call->site_end_byte > source_len ||
(size_t)(call->site_end_byte - call->site_start_byte) != site_len ||
strncmp(test_case->source + call->site_start_byte, site_text, site_len) != 0) {
continue;
}
count++;
}
return count;
}
static int usage_count_in_routine(const CBMFileResult *result, const char *caller,
const char *reference) {
int count = 0;
for (int i = 0; i < result->usages.count; i++) {
const CBMUsage *usage = &result->usages.items[i];
if (usage->kind == CBM_USAGE_VALUE && usage->ref_name &&
strcmp(usage->ref_name, reference) == 0 &&
terminal_name_matches(usage->enclosing_func_qn, caller))
count++;
}
return count;
}
static int callable_reference_count_in_routine(const CBMFileResult *result, const char *caller,
const char *reference) {
int count = 0;
for (int i = 0; i < result->usages.count; i++) {
const CBMUsage *usage = &result->usages.items[i];
if (usage->kind == CBM_USAGE_CALL_REFERENCE && usage->ref_name &&
strcmp(usage->ref_name, reference) == 0 &&
terminal_name_matches(usage->enclosing_func_qn, caller))
count++;
}
return count;
}
static int semantic_reference_candidate_count_in_routine(const CBMFileResult *result,
const char *caller,
const char *reference) {
int count = 0;
for (int i = 0; i < result->usages.count; i++) {
const CBMUsage *usage = &result->usages.items[i];
if (usage->kind == CBM_USAGE_VALUE && usage->may_be_call_reference && usage->ref_name &&
strcmp(usage->ref_name, reference) == 0 &&
terminal_name_matches(usage->enclosing_func_qn, caller)) {
count++;
}
}
return count;
}
static int resolved_call_count_in_routine(const CBMFileResult *result, const char *caller,
const char *callee) {
int count = 0;
for (int i = 0; i < result->resolved_calls.count; i++) {
const CBMResolvedCall *call = &result->resolved_calls.items[i];
if (call->kind == CBM_RESOLVED_INVOCATION &&
terminal_name_matches(call->caller_qn, caller) &&
terminal_name_matches(call->callee_qn, callee)) {
count++;
}
}
return count;
}
static int resolved_call_total_in_routine(const CBMFileResult *result, const char *caller) {
int count = 0;
for (int i = 0; i < result->resolved_calls.count; i++) {
if (result->resolved_calls.items[i].kind == CBM_RESOLVED_INVOCATION &&
terminal_name_matches(result->resolved_calls.items[i].caller_qn, caller)) {
count++;
}
}
return count;
}
static int resolved_reference_count_in_routine(const CBMFileResult *result, const char *caller,
const char *callee) {
int count = 0;
for (int i = 0; i < result->resolved_calls.count; i++) {
const CBMResolvedCall *reference = &result->resolved_calls.items[i];
if (reference->kind == CBM_RESOLVED_CALL_REFERENCE &&
terminal_name_matches(reference->caller_qn, caller) &&
terminal_name_matches(reference->callee_qn, callee)) {
count++;
}
}
return count;
}
static void check_caller_definition(const BehaviorCase *test_case, const CBMFileResult *result,
const char *caller, int *failures) {
int count = 0;
for (int i = 0; i < result->defs.count; i++) {
const CBMDefinition *definition = &result->defs.items[i];
int callable_label = definition->label && (strcmp(definition->label, "Function") == 0 ||
strcmp(definition->label, "Method") == 0);
if (callable_label && definition->name && strcmp(definition->name, caller) == 0 &&
terminal_name_matches(definition->qualified_name, caller)) {
count++;
}
}
check_exact(test_case, "caller_callable_definition", count, 1, failures);
}
static const char *checked_module_qn(const BehaviorCase *test_case, const CBMFileResult *result,
int *failures) {
if (result->module_qn && result->module_qn[0])
return result->module_qn;
fprintf(stderr,
" [call-node-behavior] case=%s invariant=module_qn expected=non-empty reason=%s\n",
test_case->tag, test_case->reason);
(*failures)++;
return "<missing-module-qn>";
}
static int begin_routine_case(const BehaviorCase *test_case, CBMFileResult **result_out,
int *failures) {
check_exact_ast_kind(test_case, failures);
CBMFileResult *result = extract_case(test_case, failures);
*result_out = result;
if (!result)
return 0;
check_caller_definition(test_case, result, "run", failures);
return 1;
}
static int finish_case(CBMFileResult *result, int failures) {
if (result)
cbm_free_result(result);
return failures == 0 ? 0 : 1;
}
static int run_binary_behavior(const BehaviorCase *test_case) {
int failures = 0;
CBMFileResult *result = NULL;
if (begin_routine_case(test_case, &result, &failures)) {
check_exact(test_case, "operator_call_in_run",
call_count_in_routine(result, "run", "operator+"), 1, &failures);
check_exact(test_case, "lhs_usage_in_run", usage_count_in_routine(result, "run", "lhs"), 1,
&failures);
check_exact(test_case, "rhs_usage_in_run", usage_count_in_routine(result, "run", "rhs"), 1,
&failures);
check_exact(test_case, "lhs_not_call_in_run", call_count_in_routine(result, "run", "lhs"),
0, &failures);
check_exact(test_case, "rhs_not_call_in_run", call_count_in_routine(result, "run", "rhs"),
0, &failures);
check_exact(test_case, "total_calls", result->calls.count, 1, &failures);
}
return finish_case(result, failures);
}
static int run_subscript_behavior(const BehaviorCase *test_case) {
int failures = 0;
CBMFileResult *result = NULL;
if (begin_routine_case(test_case, &result, &failures)) {
check_exact(test_case, "operator_call_in_run",
call_count_in_routine(result, "run", "operator[]"), 1, &failures);
check_exact(test_case, "receiver_usage_in_run",
usage_count_in_routine(result, "run", "receiver"), 1, &failures);
check_exact(test_case, "index_usage_in_run", usage_count_in_routine(result, "run", "index"),
1, &failures);
check_exact(test_case, "receiver_not_call_in_run",
call_count_in_routine(result, "run", "receiver"), 0, &failures);
check_exact(test_case, "index_not_call_in_run",
call_count_in_routine(result, "run", "index"), 0, &failures);
check_exact(test_case, "total_calls", result->calls.count, 1, &failures);
}
return finish_case(result, failures);
}
static int run_unary_behavior(const BehaviorCase *test_case) {
int failures = 0;
CBMFileResult *result = NULL;
if (begin_routine_case(test_case, &result, &failures)) {
check_exact(test_case, "operator_call_in_run",
call_count_in_routine(result, "run", "operator-"), 1, &failures);
check_exact(test_case, "operand_usage_in_run",
usage_count_in_routine(result, "run", "operand"), 1, &failures);
check_exact(test_case, "operand_not_call_in_run",
call_count_in_routine(result, "run", "operand"), 0, &failures);
check_exact(test_case, "total_calls", result->calls.count, 1, &failures);
}
return finish_case(result, failures);
}
static int run_update_behavior(const BehaviorCase *test_case) {
int failures = 0;
CBMFileResult *result = NULL;
if (begin_routine_case(test_case, &result, &failures)) {
check_exact(test_case, "operator_call_in_run",
call_count_in_routine(result, "run", "operator++"), 1, &failures);
check_exact(test_case, "operand_usage_in_run",
usage_count_in_routine(result, "run", "operand"), 1, &failures);
check_exact(test_case, "operand_not_call_in_run",
call_count_in_routine(result, "run", "operand"), 0, &failures);
check_exact(test_case, "total_calls", result->calls.count, 1, &failures);
}
return finish_case(result, failures);
}
/* A delete expression has no textual `~Type` token. The raw call therefore uses
* a non-textual `~` candidate at the exact expression span. It is semantic-only:
* without a materialized destructor the LSP emits no target and the graph pass
* drops the candidate instead of falling back to the operand's spelling. */
static int run_delete_behavior(const BehaviorCase *test_case) {
int failures = 0;
CBMFileResult *result = NULL;
if (begin_routine_case(test_case, &result, &failures)) {
check_exact(test_case, "raw_delete_semantic_candidate_in_run",
call_count_in_routine(result, "run", "~"), 1, &failures);
check_exact(test_case, "raw_delete_candidate_exact_site",
exact_semantic_candidate_count_in_routine(test_case, result, "run", "~",
"delete victim"),
1, &failures);
check_exact(test_case, "operand_usage_in_run",
usage_count_in_routine(result, "run", "victim"), 1, &failures);
check_exact(test_case, "operand_not_call_in_run",
call_count_in_routine(result, "run", "victim"), 0, &failures);
check_exact(test_case, "semantic_operator_not_minted_raw",
call_count_in_routine(result, "run", "operator delete"), 0, &failures);
check_exact(test_case, "unmaterialized_destructor_not_resolved",
resolved_call_count_in_routine(result, "run", "~Value"), 0, &failures);
check_exact(test_case, "total_calls", result->calls.count, 1, &failures);
}
return finish_case(result, failures);
}
static int run_field_behavior(const BehaviorCase *test_case) {
int failures = 0;
CBMFileResult *result = NULL;
if (begin_routine_case(test_case, &result, &failures)) {
check_exact(test_case, "receiver_usage_in_run",
usage_count_in_routine(result, "run", "receiver"), 1, &failures);
check_exact(test_case, "member_usage_in_run",
usage_count_in_routine(result, "run", "member"), 1, &failures);
check_exact(test_case, "receiver_not_call_in_run",
call_count_in_routine(result, "run", "receiver"), 0, &failures);
check_exact(test_case, "member_not_call_in_run",
call_count_in_routine(result, "run", "member"), 0, &failures);
check_exact(test_case, "total_calls", result->calls.count, 0, &failures);
}
return finish_case(result, failures);
}
static int run_callable_value_behavior(const BehaviorCase *test_case,
int expect_raw_typed_reference,
int expect_semantic_reference) {
int failures = 0;
CBMFileResult *result = NULL;
if (begin_routine_case(test_case, &result, &failures)) {
check_caller_definition(test_case, result, "handler", &failures);
check_exact(test_case, "outer_accept_call_in_run",
call_count_in_routine(result, "run", "accept"), 1, &failures);
check_exact(test_case, "handler_value_usage_in_run",
usage_count_in_routine(result, "run", "handler"),
expect_raw_typed_reference ? 0 : 1, &failures);
check_exact(test_case, "handler_callable_reference_in_run",
callable_reference_count_in_routine(result, "run", "handler"),
expect_raw_typed_reference ? 1 : 0, &failures);
check_exact(test_case, "handler_semantic_reference_candidate_in_run",
semantic_reference_candidate_count_in_routine(result, "run", "handler"),
!expect_raw_typed_reference && expect_semantic_reference ? 1 : 0, &failures);
check_exact(test_case, "handler_value_not_invoked_in_run",
call_count_in_routine(result, "run", "handler"), 0, &failures);
check_exact(test_case, "total_calls", result->calls.count, 1, &failures);
if (expect_semantic_reference) {
check_exact(test_case, "lsp_outer_accept_call_in_run",
resolved_call_count_in_routine(result, "run", "accept"), 1, &failures);
check_exact(test_case, "lsp_handler_value_not_invoked_in_run",
resolved_call_count_in_routine(result, "run", "handler"), 0, &failures);
check_exact(test_case, "lsp_handler_callable_reference_in_run",
resolved_reference_count_in_routine(result, "run", "handler"), 1,
&failures);
check_exact(test_case, "lsp_total_calls_in_run",
resolved_call_total_in_routine(result, "run"), 1, &failures);
}
}
return finish_case(result, failures);
}
static int run_arrow_behavior(const BehaviorCase *test_case) {
int failures = 0;
CBMFileResult *result = NULL;
if (begin_routine_case(test_case, &result, &failures)) {
check_exact(test_case, "operator_arrow_call_in_run",
call_count_in_routine(result, "run", "operator->"), 1, &failures);
check_exact(test_case, "receiver_usage_in_run",
usage_count_in_routine(result, "run", "receiver"), 1, &failures);
check_exact(test_case, "member_usage_in_run",
usage_count_in_routine(result, "run", "member"), 1, &failures);
check_exact(test_case, "receiver_not_call_in_run",
call_count_in_routine(result, "run", "receiver"), 0, &failures);
check_exact(test_case, "member_not_call_in_run",
call_count_in_routine(result, "run", "member"), 0, &failures);
check_exact(test_case, "total_calls", result->calls.count, 1, &failures);
}
return finish_case(result, failures);
}
static int run_objectscript_call_behavior(const BehaviorCase *test_case, const char *caller_qn,
const char *callee, const char *callee_reference,
const CBMMacroTable *macro_table) {
int failures = 0;
CBMFileResult *result = NULL;
check_exact_ast_kind(test_case, &failures);
result = extract_case_with_macros(test_case, macro_table, &failures);
if (result) {
check_exact(test_case, "exact_caller_definition",
callable_definition_count_exact(result, "run", caller_qn), 1, &failures);
check_exact(test_case, "exact_call_owner_and_target",
call_count_exact(result, caller_qn, callee), 1, &failures);
check_exact(test_case, "argument_usage_in_exact_caller",
value_usage_count_exact(result, caller_qn, "watched"), 1, &failures);
check_exact(test_case, "argument_not_call_in_exact_caller",
call_count_exact(result, caller_qn, "watched"), 0, &failures);
check_exact(test_case, "callee_not_usage_in_exact_caller",
value_usage_count_exact(result, caller_qn, callee_reference), 0, &failures);
check_exact(test_case, "total_calls", result->calls.count, 1, &failures);
}
return finish_case(result, failures);
}
static const char CPP_BINARY_SOURCE[] = "struct Value {};\n"
"Value operator+(Value lhs, Value rhs) { return lhs; }\n"
"Value run(Value lhs, Value rhs) { return lhs + rhs; }\n";
static const char CPP_SUBSCRIPT_SOURCE[] =
"struct Buffer {\n"
" int storage;\n"
" int& operator[](int index) { return storage; }\n"
"};\n"
"int run(Buffer& receiver, int index) { return receiver[index]; }\n";
static const char CPP_UNARY_SOURCE[] = "struct Value {};\n"
"Value operator-(Value operand) { return operand; }\n"
"Value run(Value operand) { return -operand; }\n";
static const char CPP_UPDATE_SOURCE[] = "struct Counter {\n"
" Counter& operator++() { return *this; }\n"
"};\n"
"Counter& run(Counter& operand) { return ++operand; }\n";
static const char CPP_DELETE_SOURCE[] = "struct Value {};\n"
"void run(Value* victim) { delete victim; }\n";
static const char CPP_FIELD_SOURCE[] = "struct Record { int member; };\n"
"int run(Record receiver) { return receiver.member; }\n";
static const char CPP_ARROW_SOURCE[] = "struct Target { int member; };\n"
"struct Proxy {\n"
" Target* target;\n"
" Target* operator->() { return target; }\n"
"};\n"
"int run(Proxy receiver) { return receiver->member; }\n";
static const char CPP_DELETE_COLLISION_SOURCE[] = "struct Value { ~Value() {} };\n"
"void victim() {}\n"
"void run(Value* victim) { delete victim; }\n";
static const char JAVA_METHOD_REFERENCE_SOURCE[] = "interface Task { void invoke(); }\n"
"class Sample {\n"
" void handler() {}\n"
" void accept(Task callback) {}\n"
" void run() { accept(this::handler); }\n"
"}\n";
static const char KOTLIN_CALLABLE_REFERENCE_SOURCE[] = "fun handler() {}\n"
"fun accept(callback: () -> Unit) {}\n"
"fun run() { accept(::handler) }\n";
static const char CSHARP_METHOD_GROUP_SOURCE[] = "delegate void Task();\n"
"class Sample {\n"
" static void handler() {}\n"
" static void accept(Task callback) {}\n"
" static void run() { accept(handler); }\n"
"}\n";
static const char CPP_FUNCTION_VALUE_SOURCE[] = "void handler() {}\n"
"void accept(void (*callback)()) {}\n"
"void run() { accept(handler); }\n";
static const char RUST_FUNCTION_ITEM_SOURCE[] = "fn handler() {}\n"
"fn accept(_callback: fn()) {}\n"
"fn run() { accept(handler); }\n";
static const char OBJECTSCRIPT_CLASS_METHOD_SOURCE[] =
"Class Sample.Behavior Extends %RegisteredObject\n"
"{\n"
"Method run(watched As %String) As %Status\n"
"{\n"
" Do ##class(Sample.Target).accept(watched)\n"
" Quit\n"
"}\n"
"}\n";
static const char OBJECTSCRIPT_METHOD_SOURCE[] =
"Class Sample.Behavior Extends %RegisteredObject\n"
"{\n"
"Method run(target As Sample.Target, watched As %String) As %Status\n"
"{\n"
" Do target.accept(watched)\n"
" Quit\n"
"}\n"
"}\n";
static const char OBJECTSCRIPT_RELATIVE_DOT_METHOD_SOURCE[] =
"Class Sample.Behavior Extends %RegisteredObject\n"
"{\n"
"Method accept(value As %String) As %Status\n"
"{\n"
" Quit\n"
"}\n"
"Method run(watched As %String) As %Status\n"
"{\n"
" Do ..accept(watched)\n"
" Quit\n"
"}\n"
"}\n";
static const char OBJECTSCRIPT_MACRO_SOURCE[] = "Class Sample.Behavior Extends %RegisteredObject\n"
"{\n"
"Method run(watched As %Status) As %Status\n"
"{\n"
" Quit $$$ISERR(watched)\n"
"}\n"
"}\n";
static const char OBJECTSCRIPT_EXTRINSIC_SOURCE[] = "run(watched)\n"
" Quit $$accept(watched)\n"
"accept(value)\n"
" Quit value\n";
static const char OBJECTSCRIPT_ROUTINE_TAG_SOURCE[] = "run(watched)\n"
" Do accept(watched)\n"
" Quit\n"
"accept(value)\n"
" Quit value\n";
static const char SCALA_INFIX_SOURCE[] = "class Box {\n"
" def merge(other: Box): Box = this\n"
"}\n"
"object Sample {\n"
" def run(lhs: Box, rhs: Box): Box = lhs merge rhs\n"
"}\n";
static const char ELIXIR_BINARY_SOURCE[] = "defmodule Sample do\n"
" def run(watched), do: watched + 1\n"
"end\n";
static const char PUPPET_RESOURCE_SOURCE[] = "$watched = 'ready'\n"
"notify { 'call-node-behavior':\n"
" message => $watched,\n"
"}\n";
#define DEFINE_CPP_CUDA_BEHAVIOR_TESTS(name, source_value, ast_value, runner, reason_value) \
TEST(repro_call_node_behavior_cpp_##name) { \
static const BehaviorCase test_case = {"cpp/" #name, CBM_LANG_CPP, "behavior.cpp", \
source_value, ast_value, reason_value}; \
return runner(&test_case); \
} \
TEST(repro_call_node_behavior_cuda_##name) { \
static const BehaviorCase test_case = {"cuda/" #name, CBM_LANG_CUDA, "behavior.cu", \
source_value, ast_value, reason_value}; \
return runner(&test_case); \
}
DEFINE_CPP_CUDA_BEHAVIOR_TESTS(
binary, CPP_BINARY_SOURCE, "binary_expression", run_binary_behavior,
"binary operator call exists, but operands remain independently visible usages")
DEFINE_CPP_CUDA_BEHAVIOR_TESTS(
subscript, CPP_SUBSCRIPT_SOURCE, "subscript_expression", run_subscript_behavior,
"subscript operator needs one synthetic call without consuming receiver or index usages")
DEFINE_CPP_CUDA_BEHAVIOR_TESTS(
unary, CPP_UNARY_SOURCE, "unary_expression", run_unary_behavior,
"unary overload needs its exact synthetic operator name and operand usage")
DEFINE_CPP_CUDA_BEHAVIOR_TESTS(
update, CPP_UPDATE_SOURCE, "update_expression", run_update_behavior,
"update overload needs one operator++ call while retaining its operand usage")
DEFINE_CPP_CUDA_BEHAVIOR_TESTS(
delete, CPP_DELETE_SOURCE, "delete_expression", run_delete_behavior,
"delete keeps an exact-span semantic candidate plus the independent operand usage")
DEFINE_CPP_CUDA_BEHAVIOR_TESTS(
field, CPP_FIELD_SOURCE, "field_expression", run_field_behavior,
"ordinary member access is a callee wrapper, not a call, and both names are usages")
DEFINE_CPP_CUDA_BEHAVIOR_TESTS(
overloaded_arrow, CPP_ARROW_SOURCE, "field_expression", run_arrow_behavior,
"overloaded operator-> is an actual invocation while receiver and member remain usages")
#undef DEFINE_CPP_CUDA_BEHAVIOR_TESTS
TEST(repro_call_node_behavior_objectscript_udl_class_method_call) {
static const BehaviorCase test_case = {
"objectscript-udl/class-method-call",
CBM_LANG_OBJECTSCRIPT_UDL,
"Behavior.cls",
OBJECTSCRIPT_CLASS_METHOD_SOURCE,
"class_method_call",
"##class(Sample.Target).accept(watched) has one exact class-method callee while its "
"argument remains a value usage"};
return run_objectscript_call_behavior(&test_case, "repro.Behavior.Sample.Behavior.run",
"Sample.Target.accept", "accept", NULL);
}
TEST(repro_call_node_behavior_objectscript_udl_method_call) {
static const BehaviorCase test_case = {
"objectscript-udl/method-call",
CBM_LANG_OBJECTSCRIPT_UDL,
"Behavior.cls",
OBJECTSCRIPT_METHOD_SOURCE,
"method_call",
"a typed ObjectScript receiver resolves one exact instance-method call without "
"consuming its argument usage"};
return run_objectscript_call_behavior(&test_case, "repro.Behavior.Sample.Behavior.run",
"Sample.Target.accept", "accept", NULL);
}
TEST(repro_call_node_behavior_objectscript_udl_relative_dot_method) {
static const BehaviorCase test_case = {
"objectscript-udl/relative-dot-method",
CBM_LANG_OBJECTSCRIPT_UDL,
"Behavior.cls",
OBJECTSCRIPT_RELATIVE_DOT_METHOD_SOURCE,
"relative_dot_method",
"..accept(watched) resolves against the exact enclosing class while watched remains a "
"value usage"};
return run_objectscript_call_behavior(&test_case, "repro.Behavior.Sample.Behavior.run",
"repro.Behavior.Sample.Behavior.accept", "accept", NULL);
}
TEST(repro_call_node_behavior_objectscript_udl_macro) {
static const BehaviorCase test_case = {
"objectscript-udl/macro",
CBM_LANG_OBJECTSCRIPT_UDL,
"Behavior.cls",
OBJECTSCRIPT_MACRO_SOURCE,
"macro",
"the system $$$ISERR(watched) macro maps to %SYSTEM.Status.IsError while watched "
"remains a value usage"};
CBMMacroTable *macro_table = calloc(1, sizeof(*macro_table));
ASSERT_NOT_NULL(macro_table);
cbm_macro_table_init_system(macro_table);
int status = run_objectscript_call_behavior(&test_case, "repro.Behavior.Sample.Behavior.run",
"%SYSTEM.Status.IsError", "ISERR", macro_table);
cbm_macro_table_free(macro_table);
return status;
}
TEST(repro_call_node_behavior_objectscript_routine_extrinsic_function) {
static const BehaviorCase test_case = {
"objectscript-routine/extrinsic-function",
CBM_LANG_OBJECTSCRIPT_ROUTINE,
"Behavior.mac",
OBJECTSCRIPT_EXTRINSIC_SOURCE,
"extrinsic_function",
"$$accept(watched) belongs to the exact run tag and retains watched as its argument "
"usage"};
return run_objectscript_call_behavior(&test_case, "repro.Behavior.run", "accept", "accept",
NULL);
}
TEST(repro_call_node_behavior_objectscript_routine_tag_call) {
static const BehaviorCase test_case = {
"objectscript-routine/routine-tag-call",
CBM_LANG_OBJECTSCRIPT_ROUTINE,
"Behavior.mac",
OBJECTSCRIPT_ROUTINE_TAG_SOURCE,
"routine_tag_call",
"Do accept(watched) belongs to the exact run tag and retains watched as its argument "
"usage"};
return run_objectscript_call_behavior(&test_case, "repro.Behavior.run", "accept", "accept",
NULL);
}
TEST(repro_call_node_behavior_scala_user_infix) {
static const BehaviorCase test_case = {
"scala/user-infix",
CBM_LANG_SCALA,
"Behavior.scala",
SCALA_INFIX_SOURCE,
"infix_expression",
"user-defined infix merge is the callee while lhs and rhs remain value usages"};
int failures = 0;
CBMFileResult *result = NULL;
if (begin_routine_case(&test_case, &result, &failures)) {
check_exact(&test_case, "merge_call_in_run", call_count_in_routine(result, "run", "merge"),
1, &failures);
check_exact(&test_case, "lhs_usage_in_run", usage_count_in_routine(result, "run", "lhs"), 1,
&failures);
check_exact(&test_case, "rhs_usage_in_run", usage_count_in_routine(result, "run", "rhs"), 1,
&failures);
check_exact(&test_case, "lhs_not_call_in_run", call_count_in_routine(result, "run", "lhs"),
0, &failures);
check_exact(&test_case, "rhs_not_call_in_run", call_count_in_routine(result, "run", "rhs"),
0, &failures);
check_exact(&test_case, "total_calls", result->calls.count, 1, &failures);
}
return finish_case(result, failures);
}
TEST(repro_call_node_behavior_elixir_binary_plus) {
static const BehaviorCase test_case = {
"elixir/binary-plus",
CBM_LANG_ELIXIR,
"behavior.ex",
ELIXIR_BINARY_SOURCE,
"binary_operator",
"binary + belongs to the operator callee, not to its watched operand"};
int failures = 0;
CBMFileResult *result = NULL;
if (begin_routine_case(&test_case, &result, &failures)) {
check_exact(&test_case, "plus_call_in_run", call_count_in_routine(result, "run", "+"), 1,
&failures);
check_exact(&test_case, "operand_usage_in_run",
usage_count_in_routine(result, "run", "watched"), 1, &failures);
check_exact(&test_case, "operand_not_call_in_run",
call_count_in_routine(result, "run", "watched"), 0, &failures);
check_exact(&test_case, "total_calls", result->calls.count, 1, &failures);
}
return finish_case(result, failures);
}
TEST(repro_call_node_behavior_puppet_resource_ownership) {
static const BehaviorCase test_case = {
"puppet/resource",
CBM_LANG_PUPPET,
"behavior.pp",
PUPPET_RESOURCE_SOURCE,
"resource_declaration",
"resource declarations are Puppet DSL ownership and must not fabricate generic calls"};
int failures = 0;
check_exact_ast_kind(&test_case, &failures);
CBMFileResult *result = extract_case(&test_case, &failures);
if (result) {
const char *module_qn = checked_module_qn(&test_case, result, &failures);
check_exact(&test_case, "resource_value_usage_in_module",
usage_count_exact(result, module_qn, "$watched"), 1, &failures);
check_exact(&test_case, "variable_not_call_in_module",
call_count_exact(result, module_qn, "$watched"), 0, &failures);
check_exact(&test_case, "total_calls", result->calls.count, 0, &failures);
}
return finish_case(result, failures);
}
TEST(repro_call_node_behavior_java_method_reference_value) {
static const BehaviorCase test_case = {
"java/method-reference-value",
CBM_LANG_JAVA,
"Behavior.java",
JAVA_METHOD_REFERENCE_SOURCE,
"method_reference",
"a Java method reference is a value, not an invocation at the reference site"};
return run_callable_value_behavior(&test_case, 1, 1);
}
TEST(repro_call_node_behavior_kotlin_callable_reference_value) {
static const BehaviorCase test_case = {
"kotlin/callable-reference-value",
CBM_LANG_KOTLIN,
"Behavior.kt",
KOTLIN_CALLABLE_REFERENCE_SOURCE,
"callable_reference",
"a Kotlin callable reference is a value, not an invocation at the reference site"};
return run_callable_value_behavior(&test_case, 0, 1);
}
TEST(repro_call_node_behavior_csharp_method_group_value) {
static const BehaviorCase test_case = {
"csharp/method-group-value",
CBM_LANG_CSHARP,
"Behavior.cs",
CSHARP_METHOD_GROUP_SOURCE,
"invocation_expression",
"a C# method group passed as an argument is a value, not a handler invocation"};
return run_callable_value_behavior(&test_case, 0, 1);
}
TEST(repro_call_node_behavior_cpp_function_value) {
static const BehaviorCase test_case = {
"cpp/function-value",
CBM_LANG_CPP,
"behavior.cpp",
CPP_FUNCTION_VALUE_SOURCE,
"call_expression",
"a C++ function value passed as an argument is referenced but not invoked"};
return run_callable_value_behavior(&test_case, 0, 1);
}
TEST(repro_call_node_behavior_rust_function_item_value) {
static const BehaviorCase test_case = {
"rust/function-item-value",
CBM_LANG_RUST,
"behavior.rs",
RUST_FUNCTION_ITEM_SOURCE,
"call_expression",
"a Rust function item passed as an argument is referenced but not invoked"};
return run_callable_value_behavior(&test_case, 0, 1);
}
TEST(repro_call_node_behavior_cpp_delete_name_collision) {
static const BehaviorCase test_case = {
"cpp/delete-name-collision",
CBM_LANG_CPP,
"behavior.cpp",
CPP_DELETE_COLLISION_SOURCE,
"delete_expression",
"delete must key the implicit ~Value destructor, not operand text that collides with "
"victim()"};
int failures = 0;
CBMFileResult *result = NULL;
if (begin_routine_case(&test_case, &result, &failures)) {
check_caller_definition(&test_case, result, "victim", &failures);
check_exact(&test_case, "destructor_call_in_run",
call_count_in_routine(result, "run", "~Value"), 1, &failures);
check_exact(&test_case, "destructor_candidate_exact_site",
exact_semantic_candidate_count_in_routine(&test_case, result, "run", "~Value",
"delete victim"),
1, &failures);
check_exact(&test_case, "operand_usage_in_run",
usage_count_in_routine(result, "run", "victim"), 1, &failures);
check_exact(&test_case, "unrelated_victim_not_called_in_run",
call_count_in_routine(result, "run", "victim"), 0, &failures);
check_exact(&test_case, "lsp_destructor_call_in_run",
resolved_call_count_in_routine(result, "run", "~Value"), 1, &failures);
check_exact(&test_case, "lsp_unrelated_victim_not_called_in_run",
resolved_call_count_in_routine(result, "run", "victim"), 0, &failures);
check_exact(&test_case, "lsp_total_calls_in_run",
resolved_call_total_in_routine(result, "run"), 1, &failures);
check_exact(&test_case, "total_calls", result->calls.count, 1, &failures);
}
return finish_case(result, failures);
}
enum {
CPP_BEHAVIOR_COUNT = 9,
CUDA_BEHAVIOR_COUNT = 7,
OTHER_BEHAVIOR_COUNT = 7,
OBJECTSCRIPT_BEHAVIOR_COUNT = 6,
CALL_NODE_BEHAVIOR_COUNT = CPP_BEHAVIOR_COUNT + CUDA_BEHAVIOR_COUNT + OTHER_BEHAVIOR_COUNT +
OBJECTSCRIPT_BEHAVIOR_COUNT,
};
_Static_assert(CALL_NODE_BEHAVIOR_COUNT == 29,
"call-node behavior suite must retain 23 contracts and add 6 ObjectScript "
"call-role guards");
SUITE(repro_call_node_behaviors) {
RUN_TEST(repro_call_node_behavior_cpp_binary);
RUN_TEST(repro_call_node_behavior_cpp_subscript);
RUN_TEST(repro_call_node_behavior_cpp_unary);
RUN_TEST(repro_call_node_behavior_cpp_update);
RUN_TEST(repro_call_node_behavior_cpp_delete);
RUN_TEST(repro_call_node_behavior_cpp_field);
RUN_TEST(repro_call_node_behavior_cpp_overloaded_arrow);
RUN_TEST(repro_call_node_behavior_cpp_function_value);
RUN_TEST(repro_call_node_behavior_cpp_delete_name_collision);
RUN_TEST(repro_call_node_behavior_cuda_binary);
RUN_TEST(repro_call_node_behavior_cuda_subscript);
RUN_TEST(repro_call_node_behavior_cuda_unary);
RUN_TEST(repro_call_node_behavior_cuda_update);
RUN_TEST(repro_call_node_behavior_cuda_delete);
RUN_TEST(repro_call_node_behavior_cuda_field);
RUN_TEST(repro_call_node_behavior_cuda_overloaded_arrow);
RUN_TEST(repro_call_node_behavior_objectscript_udl_class_method_call);
RUN_TEST(repro_call_node_behavior_objectscript_udl_method_call);
RUN_TEST(repro_call_node_behavior_objectscript_udl_relative_dot_method);
RUN_TEST(repro_call_node_behavior_objectscript_udl_macro);
RUN_TEST(repro_call_node_behavior_objectscript_routine_extrinsic_function);
RUN_TEST(repro_call_node_behavior_objectscript_routine_tag_call);
RUN_TEST(repro_call_node_behavior_scala_user_infix);
RUN_TEST(repro_call_node_behavior_elixir_binary_plus);
RUN_TEST(repro_call_node_behavior_puppet_resource_ownership);
RUN_TEST(repro_call_node_behavior_java_method_reference_value);
RUN_TEST(repro_call_node_behavior_kotlin_callable_reference_value);
RUN_TEST(repro_call_node_behavior_csharp_method_group_value);
RUN_TEST(repro_call_node_behavior_rust_function_item_value);
}
+638
View File
@@ -0,0 +1,638 @@
/*
* Exhaustive contract for call-node metadata.
*
* This ledger is intentionally literal. Semantic role and extraction ownership
* are independent review decisions; neither field supplies a default for the
* other. Every row also states whether it belonged to the historical metadata
* snapshot and whether it belongs in the primary call-node registry.
*
* This metadata contract is not proof of extraction behavior. Changes must also
* pass the focused repro_call_scope_usages, repro_call_argument_matrix_a,
* repro_call_argument_matrix_b, repro_lsp_c_cpp, repro_grammar_build, and
* repro_grammar_shells suites before production metadata is changed.
* C++/CUDA operator rows require raw-call-to-LSP assertions in repro_lsp_c_cpp;
* Scala/Elixir direct operators require operand-scope assertions in the call
* argument suites; Puppet resource ownership remains gated by repro_grammar_build.
* TLA+ bound_op is historical direct-call metadata. Bounded quantification is
* not a historical ledger row; repro_call_argument_matrix_b is its separate
* negative behavior guard.
*/
#include "test_framework.h"
#include "lang_specs.h"
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
typedef enum {
DIRECT_CALL = 0,
CONSTRUCTOR_CALL,
OPERATOR_CALL,
IMPLICIT_CALL,
DSL_INVOCATION,
BUILD_DEPENDENCY,
CALLEE_WRAPPER,
ARGUMENT_WRAPPER,
CONTROL_OR_DOCUMENT_NONCALL,
CALL_SEMANTIC_ROLE_COUNT
} CallSemanticRole;
typedef enum {
PRIMARY_EXTRACTOR = 0,
SYNTHETIC_EXTRACTOR,
NO_CALL_EDGE,
CALL_EXTRACTION_OWNER_COUNT
} CallExtractionOwner;
typedef struct {
CBMLanguage language;
const char *node_kind;
CallSemanticRole semantic_role;
CallExtractionOwner extraction_owner;
bool historically_registered;
bool expected_primary_registered;
} CallNodeManifestEntry;
enum {
EXPECTED_HISTORICAL_CALL_NODE_TOTAL = 219,
EXPECTED_ACTIVE_PRIMARY_TOTAL = 189,
EXPECTED_DIRECT_CALLS = 155,
EXPECTED_CONSTRUCTOR_CALLS = 20,
EXPECTED_OPERATOR_CALLS = 12,
EXPECTED_IMPLICIT_CALLS = 2,
EXPECTED_DSL_INVOCATIONS = 14,
EXPECTED_BUILD_DEPENDENCIES = 1,
EXPECTED_CALLEE_WRAPPERS = 8,
EXPECTED_ARGUMENT_WRAPPERS = 1,
EXPECTED_CONTROL_OR_DOCUMENT_NONCALLS = 6,
EXPECTED_PRIMARY_OWNERS = 189,
EXPECTED_SYNTHETIC_OWNERS = 10,
EXPECTED_NO_CALL_EDGE_OWNERS = 20,
EXPECTED_PRIMARY_OPERATOR_CALLS = 4,
EXPECTED_SYNTHETIC_OPERATOR_CALLS = 8,
};
#define ENTRY(language, kind, role, owner, historical, expected_primary) \
{language, kind, role, owner, historical, expected_primary}
static const CallNodeManifestEntry CALL_NODE_MANIFEST[] = {
ENTRY(CBM_LANG_GO, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_PYTHON, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_PYTHON, "with_statement", CONTROL_OR_DOCUMENT_NONCALL, NO_CALL_EDGE, true,
false),
ENTRY(CBM_LANG_JAVASCRIPT, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_JAVASCRIPT, "new_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_TYPESCRIPT, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_TYPESCRIPT, "new_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_TSX, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_TSX, "new_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_RUST, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_RUST, "macro_invocation", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_JAVA, "method_invocation", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_JAVA, "object_creation_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true,
true),
ENTRY(CBM_LANG_CPP, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_CPP, "field_expression", CALLEE_WRAPPER, NO_CALL_EDGE, true, false),
ENTRY(CBM_LANG_CPP, "subscript_expression", OPERATOR_CALL, SYNTHETIC_EXTRACTOR, true, false),
ENTRY(CBM_LANG_CPP, "new_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_CPP, "delete_expression", IMPLICIT_CALL, SYNTHETIC_EXTRACTOR, true, false),
ENTRY(CBM_LANG_CPP, "binary_expression", OPERATOR_CALL, SYNTHETIC_EXTRACTOR, true, false),
ENTRY(CBM_LANG_CPP, "unary_expression", OPERATOR_CALL, SYNTHETIC_EXTRACTOR, true, false),
ENTRY(CBM_LANG_CPP, "update_expression", OPERATOR_CALL, SYNTHETIC_EXTRACTOR, true, false),
ENTRY(CBM_LANG_CSHARP, "invocation_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_CSHARP, "object_creation_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true,
true),
ENTRY(CBM_LANG_PHP, "member_call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_PHP, "scoped_call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_PHP, "function_call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_PHP, "object_creation_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true,
true),
ENTRY(CBM_LANG_PHP, "nullsafe_member_call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true,
true),
ENTRY(CBM_LANG_LUA, "function_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_SCALA, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_SCALA, "generic_function", CALLEE_WRAPPER, NO_CALL_EDGE, true, false),
ENTRY(CBM_LANG_SCALA, "field_expression", CALLEE_WRAPPER, NO_CALL_EDGE, true, false),
ENTRY(CBM_LANG_SCALA, "infix_expression", OPERATOR_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_SCALA, "instance_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_KOTLIN, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_KOTLIN, "navigation_expression", CALLEE_WRAPPER, NO_CALL_EDGE, true, false),
ENTRY(CBM_LANG_RUBY, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_RUBY, "command_call", DIRECT_CALL, NO_CALL_EDGE, true, false),
ENTRY(CBM_LANG_C, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_BASH, "command", DSL_INVOCATION, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_ZIG, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_ZIG, "builtin_function", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_ELIXIR, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_ELIXIR, "dot", CALLEE_WRAPPER, NO_CALL_EDGE, true, false),
ENTRY(CBM_LANG_ELIXIR, "binary_operator", OPERATOR_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_HASKELL, "infix", OPERATOR_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_HASKELL, "apply", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_OCAML, "application_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_OCAML, "infix_expression", OPERATOR_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_OCAML, "method_invocation", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_OCAML, "module_application", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_OCAML, "new_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_OBJC, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_OBJC, "message_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_SWIFT, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_SWIFT, "constructor_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true,
true),
ENTRY(CBM_LANG_SWIFT, "macro_invocation", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_SWIFT, "navigation_expression", CALLEE_WRAPPER, NO_CALL_EDGE, true, false),
ENTRY(CBM_LANG_DART, "selector", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_DART, "new_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_PERL, "ambiguous_function_call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true,
true),
ENTRY(CBM_LANG_PERL, "function_call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_PERL, "func1op_call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_PERL, "method_call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_GROOVY, "function_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_GROOVY, "juxt_function_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_ERLANG, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_R, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_CSS, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_SCSS, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_SCSS, "include_statement", DSL_INVOCATION, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_HCL, "function_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_SQL, "function_call", DIRECT_CALL, NO_CALL_EDGE, true, false),
ENTRY(CBM_LANG_SQL, "invocation", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_SQL, "command", CONTROL_OR_DOCUMENT_NONCALL, NO_CALL_EDGE, true, false),
ENTRY(CBM_LANG_CLOJURE, "list_lit", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_FSHARP, "application_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_FSHARP, "dot_expression", CALLEE_WRAPPER, NO_CALL_EDGE, true, false),
ENTRY(CBM_LANG_JULIA, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_JULIA, "broadcast_call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_VIMSCRIPT, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_VIMSCRIPT, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_VIMSCRIPT, "command", DSL_INVOCATION, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_NIX, "apply_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_COMMONLISP, "list_lit", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_ELM, "function_call", DIRECT_CALL, NO_CALL_EDGE, true, false),
ENTRY(CBM_LANG_ELM, "function_call_expr", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_FORTRAN, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_FORTRAN, "keyword_argument", ARGUMENT_WRAPPER, NO_CALL_EDGE, true, false),
ENTRY(CBM_LANG_FORTRAN, "subroutine_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_CUDA, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_CUDA, "field_expression", CALLEE_WRAPPER, NO_CALL_EDGE, true, false),
ENTRY(CBM_LANG_CUDA, "subscript_expression", OPERATOR_CALL, SYNTHETIC_EXTRACTOR, true, false),
ENTRY(CBM_LANG_CUDA, "new_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_CUDA, "delete_expression", IMPLICIT_CALL, SYNTHETIC_EXTRACTOR, true, false),
ENTRY(CBM_LANG_CUDA, "binary_expression", OPERATOR_CALL, SYNTHETIC_EXTRACTOR, true, false),
ENTRY(CBM_LANG_CUDA, "unary_expression", OPERATOR_CALL, SYNTHETIC_EXTRACTOR, true, false),
ENTRY(CBM_LANG_CUDA, "update_expression", OPERATOR_CALL, SYNTHETIC_EXTRACTOR, true, false),
ENTRY(CBM_LANG_COBOL, "call_statement", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_VERILOG, "system_tf_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_VERILOG, "subroutine_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_VERILOG, "function_subroutine_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_VERILOG, "method_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_EMACSLISP, "list", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_MAKEFILE, "function_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_MAKEFILE, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_MAKEFILE, "shell_function", DSL_INVOCATION, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_CMAKE, "normal_command", DSL_INVOCATION, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_MESON, "normal_command", DSL_INVOCATION, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_GLSL, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_MATLAB, "function_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_MATLAB, "command", DSL_INVOCATION, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_LEAN, "apply", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_LEAN, "command", CONTROL_OR_DOCUMENT_NONCALL, NO_CALL_EDGE, true, false),
ENTRY(CBM_LANG_FORM, "call_statement", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_MAGMA, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_WOLFRAM, "apply", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_SOLIDITY, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_SOLIDITY, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_SOLIDITY, "new_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_TYPST, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_GDSCRIPT, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_GDSCRIPT, "attribute_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_GDSCRIPT, "base_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_QML, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_QML, "new_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_CFSCRIPT, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_CFSCRIPT, "new_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_CFML, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_GLEAM, "function_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_POWERSHELL, "invokation_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_POWERSHELL, "command", DSL_INVOCATION, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_PASCAL, "exprCall", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_DLANG, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_DLANG, "function_call_expression", DIRECT_CALL, NO_CALL_EDGE, true, false),
ENTRY(CBM_LANG_DLANG, "new_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_SCHEME, "list", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_FENNEL, "list", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_FISH, "command", DSL_INVOCATION, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_AWK, "func_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_AWK, "command", CONTROL_OR_DOCUMENT_NONCALL, NO_CALL_EDGE, true, false),
ENTRY(CBM_LANG_ZSH, "command", DSL_INVOCATION, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_ZSH, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_TCL, "command", DSL_INVOCATION, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_ADA, "function_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_ADA, "procedure_call_statement", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_AGDA, "module_application", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_AGDA, "expr", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_RACKET, "list", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_ODIN, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_ODIN, "selector_call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_RESCRIPT, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_PURESCRIPT, "exp_apply", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_NICKEL, "applicative", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_CRYSTAL, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_CRYSTAL, "command", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_CRYSTAL, "implicit_object_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_TEAL, "function_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_HARE, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_PONY, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_LUAU, "function_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_SWAY, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_SWAY, "abi_call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_NASM, "call_syntax_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_NASM, "actual_instruction", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_JUST, "function_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_JUST, "dependency", BUILD_DEPENDENCY, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_GOTEMPLATE, "function_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_GOTEMPLATE, "method_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_GOTEMPLATE, "template_action", DSL_INVOCATION, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_TEMPL, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_PRISMA, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_DIFF, "command", CONTROL_OR_DOCUMENT_NONCALL, NO_CALL_EDGE, true, false),
ENTRY(CBM_LANG_WGSL, "type_constructor_or_function_call_expression", DIRECT_CALL,
PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_JSONNET, "functioncall", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_BIBTEX, "command", CONTROL_OR_DOCUMENT_NONCALL, NO_CALL_EDGE, true, false),
ENTRY(CBM_LANG_STARLARK, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_BICEP, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_HLSL, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_HLSL, "new_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_VHDL, "function_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_VHDL, "procedure_call_statement", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_VHDL, "component_instantiation_statement", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR,
true, true),
ENTRY(CBM_LANG_VHDL, "parenthesis_group", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_SYSTEMVERILOG, "function_subroutine_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true,
true),
ENTRY(CBM_LANG_SYSTEMVERILOG, "system_tf_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_SYSTEMVERILOG, "method_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_DEVICETREE, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_LINKERSCRIPT, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_GN, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_BITBAKE, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_ISPC, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_ISPC, "new_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_CAIRO, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_CAIRO, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_MOVE, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_SQUIRREL, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_FUNC, "method_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_FUNC, "function_application", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_PUPPET, "function_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_PUPPET, "resource_declaration", DSL_INVOCATION, NO_CALL_EDGE, true, false),
ENTRY(CBM_LANG_PUPPET, "include_statement", DSL_INVOCATION, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_SLANG, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_SLANG, "new_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_LLVM_IR, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_LLVM_IR, "invoke", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_LLVM_IR, "instruction_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_TLAPLUS, "function_evaluation", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_TLAPLUS, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_TLAPLUS, "bound_op", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_APEX, "method_invocation", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_PINE, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_MOJO, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_OBJECTSCRIPT_UDL, "class_method_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true,
true),
ENTRY(CBM_LANG_OBJECTSCRIPT_UDL, "method_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_OBJECTSCRIPT_UDL, "relative_dot_method", DIRECT_CALL, PRIMARY_EXTRACTOR, true,
true),
ENTRY(CBM_LANG_OBJECTSCRIPT_UDL, "macro", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_OBJECTSCRIPT_ROUTINE, "extrinsic_function", DIRECT_CALL, PRIMARY_EXTRACTOR, true,
true),
ENTRY(CBM_LANG_OBJECTSCRIPT_ROUTINE, "routine_tag_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true,
true),
};
#undef ENTRY
#define MANIFEST_COUNT (sizeof(CALL_NODE_MANIFEST) / sizeof(CALL_NODE_MANIFEST[0]))
_Static_assert(MANIFEST_COUNT == EXPECTED_HISTORICAL_CALL_NODE_TOTAL,
"call-node manifest must contain exactly 219 entries");
_Static_assert(EXPECTED_DIRECT_CALLS + EXPECTED_CONSTRUCTOR_CALLS + EXPECTED_OPERATOR_CALLS +
EXPECTED_IMPLICIT_CALLS + EXPECTED_DSL_INVOCATIONS +
EXPECTED_BUILD_DEPENDENCIES + EXPECTED_CALLEE_WRAPPERS +
EXPECTED_ARGUMENT_WRAPPERS + EXPECTED_CONTROL_OR_DOCUMENT_NONCALLS ==
EXPECTED_HISTORICAL_CALL_NODE_TOTAL,
"semantic-role partition must cover the historical manifest");
_Static_assert(EXPECTED_PRIMARY_OWNERS + EXPECTED_SYNTHETIC_OWNERS + EXPECTED_NO_CALL_EDGE_OWNERS ==
EXPECTED_HISTORICAL_CALL_NODE_TOTAL,
"extraction-owner partition must cover the historical manifest");
_Static_assert(EXPECTED_PRIMARY_OWNERS == EXPECTED_ACTIVE_PRIMARY_TOTAL,
"primary-owner and active-primary totals must agree");
_Static_assert(EXPECTED_PRIMARY_OPERATOR_CALLS + EXPECTED_SYNTHETIC_OPERATOR_CALLS ==
EXPECTED_OPERATOR_CALLS,
"operator calls must have an explicit direct or synthetic owner");
static int manifest_match_count(CBMLanguage language, const char *node_kind) {
int matches = 0;
for (size_t i = 0; i < MANIFEST_COUNT; i++) {
const CallNodeManifestEntry *entry = &CALL_NODE_MANIFEST[i];
if (entry->language == language && strcmp(entry->node_kind, node_kind) == 0) {
matches++;
}
}
return matches;
}
static int spec_match_count(const CBMLangSpec *spec, const char *node_kind) {
int matches = 0;
if (!spec || !spec->call_node_types) {
return 0;
}
for (int i = 0; spec->call_node_types[i]; i++) {
if (strcmp(spec->call_node_types[i], node_kind) == 0) {
matches++;
}
}
return matches;
}
TEST(repro_call_node_manifest_is_unique_and_partitioned) {
int failures = 0;
int role_counts[CALL_SEMANTIC_ROLE_COUNT] = {0};
int owner_counts[CALL_EXTRACTION_OWNER_COUNT] = {0};
int historical_total = 0;
int expected_primary_total = 0;
int primary_operator_calls = 0;
int synthetic_operator_calls = 0;
if (MANIFEST_COUNT != EXPECTED_HISTORICAL_CALL_NODE_TOTAL) {
fprintf(stderr, " [call-node-manifest] invariant=table_total expected=%d actual=%zu\n",
EXPECTED_HISTORICAL_CALL_NODE_TOTAL, MANIFEST_COUNT);
failures++;
}
for (size_t i = 0; i < MANIFEST_COUNT; i++) {
const CallNodeManifestEntry *entry = &CALL_NODE_MANIFEST[i];
if (entry->language < 0 || entry->language >= CBM_LANG_COUNT ||
entry->language == CBM_LANG_NIM) {
fprintf(stderr,
" [call-node-manifest] invariant=invalid_language index=%zu language=%d\n", i,
(int)entry->language);
failures++;
}
if (!entry->node_kind || !entry->node_kind[0]) {
fprintf(stderr,
" [call-node-manifest] invariant=empty_node_kind index=%zu language=%d\n", i,
(int)entry->language);
failures++;
}
if (entry->semantic_role < DIRECT_CALL ||
entry->semantic_role >= CALL_SEMANTIC_ROLE_COUNT) {
fprintf(stderr,
" [call-node-manifest] invariant=invalid_role index=%zu language=%d role=%d\n",
i, (int)entry->language, (int)entry->semantic_role);
failures++;
} else {
role_counts[entry->semantic_role]++;
}
if (entry->extraction_owner < PRIMARY_EXTRACTOR ||
entry->extraction_owner >= CALL_EXTRACTION_OWNER_COUNT) {
fprintf(stderr,
" [call-node-manifest] invariant=invalid_owner index=%zu language=%d "
"owner=%d\n",
i, (int)entry->language, (int)entry->extraction_owner);
failures++;
} else {
owner_counts[entry->extraction_owner]++;
}
if (!entry->historically_registered) {
fprintf(stderr,
" [call-node-manifest] invariant=historical_snapshot_gap index=%zu "
"language=%d node=%s\n",
i, (int)entry->language, entry->node_kind);
failures++;
} else {
historical_total++;
}
if (entry->expected_primary_registered) {
expected_primary_total++;
}
/*
* Direct operator models are PRIMARY_EXTRACTOR entries. C++/CUDA
* operator triggers owned by synthetic extraction must not also create
* generic primary scopes or raw calls.
*/
if (entry->extraction_owner == SYNTHETIC_EXTRACTOR && entry->expected_primary_registered) {
fprintf(stderr,
" [call-node-manifest] invariant=synthetic_owner_registered_primary "
"language=%d node=%s role=%d\n",
(int)entry->language, entry->node_kind, (int)entry->semantic_role);
failures++;
}
if (entry->extraction_owner == NO_CALL_EDGE && entry->expected_primary_registered) {
fprintf(stderr,
" [call-node-manifest] invariant=no_edge_owner_registered_primary "
"language=%d node=%s role=%d\n",
(int)entry->language, entry->node_kind, (int)entry->semantic_role);
failures++;
}
if ((entry->extraction_owner == PRIMARY_EXTRACTOR) != entry->expected_primary_registered) {
fprintf(stderr,
" [call-node-manifest] invariant=primary_owner_membership_mismatch "
"language=%d node=%s owner=%d expected_primary=%d\n",
(int)entry->language, entry->node_kind, (int)entry->extraction_owner,
entry->expected_primary_registered);
failures++;
}
if (entry->semantic_role == OPERATOR_CALL) {
if (entry->extraction_owner == PRIMARY_EXTRACTOR) {
primary_operator_calls++;
} else if (entry->extraction_owner == SYNTHETIC_EXTRACTOR) {
synthetic_operator_calls++;
} else {
fprintf(stderr,
" [call-node-manifest] invariant=operator_without_extractor "
"language=%d node=%s\n",
(int)entry->language, entry->node_kind);
failures++;
}
}
for (size_t j = i + 1; j < MANIFEST_COUNT; j++) {
const CallNodeManifestEntry *other = &CALL_NODE_MANIFEST[j];
if (entry->language == other->language && entry->node_kind && other->node_kind &&
strcmp(entry->node_kind, other->node_kind) == 0) {
fprintf(stderr,
" [call-node-manifest] invariant=duplicate_table_entry language=%d "
"node=%s first=%zu second=%zu\n",
(int)entry->language, entry->node_kind, i, j);
failures++;
}
}
}
if (historical_total != EXPECTED_HISTORICAL_CALL_NODE_TOTAL ||
expected_primary_total != EXPECTED_ACTIVE_PRIMARY_TOTAL) {
fprintf(stderr,
" [call-node-manifest] invariant=registration_partition historical=%d "
"expected_primary=%d\n",
historical_total, expected_primary_total);
failures++;
}
if (role_counts[DIRECT_CALL] != EXPECTED_DIRECT_CALLS ||
role_counts[CONSTRUCTOR_CALL] != EXPECTED_CONSTRUCTOR_CALLS ||
role_counts[OPERATOR_CALL] != EXPECTED_OPERATOR_CALLS ||
role_counts[IMPLICIT_CALL] != EXPECTED_IMPLICIT_CALLS ||
role_counts[DSL_INVOCATION] != EXPECTED_DSL_INVOCATIONS ||
role_counts[BUILD_DEPENDENCY] != EXPECTED_BUILD_DEPENDENCIES ||
role_counts[CALLEE_WRAPPER] != EXPECTED_CALLEE_WRAPPERS ||
role_counts[ARGUMENT_WRAPPER] != EXPECTED_ARGUMENT_WRAPPERS ||
role_counts[CONTROL_OR_DOCUMENT_NONCALL] != EXPECTED_CONTROL_OR_DOCUMENT_NONCALLS) {
fprintf(stderr,
" [call-node-manifest] invariant=semantic_role_partition direct=%d "
"constructor=%d operator=%d implicit=%d dsl=%d build=%d callee_wrapper=%d "
"argument_wrapper=%d control_or_document=%d\n",
role_counts[DIRECT_CALL], role_counts[CONSTRUCTOR_CALL], role_counts[OPERATOR_CALL],
role_counts[IMPLICIT_CALL], role_counts[DSL_INVOCATION],
role_counts[BUILD_DEPENDENCY], role_counts[CALLEE_WRAPPER],
role_counts[ARGUMENT_WRAPPER], role_counts[CONTROL_OR_DOCUMENT_NONCALL]);
failures++;
}
if (owner_counts[PRIMARY_EXTRACTOR] != EXPECTED_PRIMARY_OWNERS ||
owner_counts[SYNTHETIC_EXTRACTOR] != EXPECTED_SYNTHETIC_OWNERS ||
owner_counts[NO_CALL_EDGE] != EXPECTED_NO_CALL_EDGE_OWNERS) {
fprintf(stderr,
" [call-node-manifest] invariant=extraction_owner_partition primary=%d "
"synthetic=%d no_call_edge=%d\n",
owner_counts[PRIMARY_EXTRACTOR], owner_counts[SYNTHETIC_EXTRACTOR],
owner_counts[NO_CALL_EDGE]);
failures++;
}
if (primary_operator_calls != EXPECTED_PRIMARY_OPERATOR_CALLS ||
synthetic_operator_calls != EXPECTED_SYNTHETIC_OPERATOR_CALLS) {
fprintf(stderr,
" [call-node-manifest] invariant=operator_owner_partition primary=%d "
"synthetic=%d\n",
primary_operator_calls, synthetic_operator_calls);
failures++;
}
ASSERT_EQ(failures, 0);
PASS();
}
TEST(repro_call_node_manifest_live_specs_contain_only_expected_primary_entries) {
int failures = 0;
int runtime_total = 0;
for (int value = 0; value < CBM_LANG_COUNT; value++) {
CBMLanguage language = (CBMLanguage)value;
const CBMLangSpec *spec = cbm_lang_spec(language);
/* The registry reproduction owns Nim's stale enum and Studio Export's
* transform-only, intentionally grammar-free classification. */
if (language == CBM_LANG_NIM || language == CBM_LANG_OBJECTSCRIPT_EXPORT) {
continue;
}
if (!spec || spec->language != language) {
fprintf(stderr,
" [call-node-manifest] invariant=live_spec_missing language=%d actual=%d\n",
value, spec ? (int)spec->language : -1);
failures++;
continue;
}
if (!spec->call_node_types) {
continue;
}
for (int i = 0; spec->call_node_types[i]; i++) {
const char *node_kind = spec->call_node_types[i];
runtime_total++;
int table_matches = manifest_match_count(language, node_kind);
if (table_matches != 1) {
fprintf(stderr,
" [call-node-manifest] invariant=live_to_table language=%d node=%s "
"matches=%d\n",
value, node_kind, table_matches);
failures++;
} else {
for (size_t manifest_index = 0; manifest_index < MANIFEST_COUNT; manifest_index++) {
const CallNodeManifestEntry *entry = &CALL_NODE_MANIFEST[manifest_index];
if (entry->language == language && strcmp(entry->node_kind, node_kind) == 0 &&
!entry->expected_primary_registered) {
fprintf(stderr,
" [call-node-manifest] invariant=unexpected_primary_registration "
"language=%d node=%s role=%d owner=%d\n",
value, node_kind, (int)entry->semantic_role,
(int)entry->extraction_owner);
failures++;
break;
}
}
}
for (int j = 0; j < i; j++) {
if (strcmp(spec->call_node_types[j], node_kind) == 0) {
fprintf(stderr,
" [call-node-manifest] invariant=duplicate_live_kind language=%d "
"node=%s first=%d second=%d\n",
value, node_kind, j, i);
failures++;
}
}
}
}
if (runtime_total != EXPECTED_ACTIVE_PRIMARY_TOTAL) {
fprintf(stderr, " [call-node-manifest] invariant=runtime_total expected=%d actual=%d\n",
EXPECTED_ACTIVE_PRIMARY_TOTAL, runtime_total);
failures++;
}
ASSERT_EQ(failures, 0);
PASS();
}
TEST(repro_call_node_manifest_live_membership_matches_expected_primary_boolean) {
int failures = 0;
for (size_t i = 0; i < MANIFEST_COUNT; i++) {
const CallNodeManifestEntry *entry = &CALL_NODE_MANIFEST[i];
const CBMLangSpec *spec = cbm_lang_spec(entry->language);
if (!spec || spec->language != entry->language) {
fprintf(stderr,
" [call-node-manifest] invariant=table_spec_missing index=%zu language=%d "
"actual=%d\n",
i, (int)entry->language, spec ? (int)spec->language : -1);
failures++;
continue;
}
int live_matches = spec_match_count(spec, entry->node_kind);
int expected_matches = entry->expected_primary_registered ? 1 : 0;
if (live_matches != expected_matches) {
fprintf(stderr,
" [call-node-manifest] invariant=table_to_live index=%zu language=%d node=%s "
"expected=%d matches=%d role=%d owner=%d historical=%d\n",
i, (int)entry->language, entry->node_kind, expected_matches, live_matches,
(int)entry->semantic_role, (int)entry->extraction_owner,
entry->historically_registered);
failures++;
}
}
ASSERT_EQ(failures, 0);
PASS();
}
SUITE(repro_call_node_manifest) {
RUN_TEST(repro_call_node_manifest_is_unique_and_partitioned);
RUN_TEST(repro_call_node_manifest_live_specs_contain_only_expected_primary_entries);
RUN_TEST(repro_call_node_manifest_live_membership_matches_expected_primary_boolean);
}
+678
View File
@@ -0,0 +1,678 @@
/*
* repro_call_scope_usages.c — exact reference semantics inside call-shaped ASTs.
*
* A CALLS edge replaces only the exact invoked callee occurrence. Receivers,
* computed keys, arguments, and nested callback bodies remain ordinary value
* references. Several language specs also classify non-invocation containers
* as call nodes; those containers must neither erase their whole subtree nor
* fabricate calls from an arbitrary child. Every assertion uses exact counts
* so duplicate CALLS/USAGE emissions cannot masquerade as a successful fix.
*/
#include "test_framework.h"
#include "cbm.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int qn_ends_with(const char *qn, const char *name) {
if (!qn || !name)
return 0;
const char *last = strrchr(qn, '.');
return strcmp(last ? last + 1 : qn, name) == 0;
}
static const char *call_short_name(const char *name) {
if (!name)
return NULL;
const char *short_name = strrchr(name, '.');
short_name = short_name ? short_name + 1 : name;
const char *scope = strrchr(short_name, ':');
return scope ? scope + 1 : short_name;
}
static int usage_count(const CBMFileResult *r, const char *caller, const char *target) {
int count = 0;
for (int i = 0; i < r->usages.count; i++) {
const CBMUsage *usage = &r->usages.items[i];
if (usage->kind == CBM_USAGE_VALUE && usage->ref_name &&
strcmp(usage->ref_name, target) == 0 &&
(!caller || qn_ends_with(usage->enclosing_func_qn, caller))) {
count++;
}
}
return count;
}
static int call_reference_count(const CBMFileResult *r, const char *caller, const char *target) {
int count = 0;
for (int i = 0; i < r->usages.count; i++) {
const CBMUsage *usage = &r->usages.items[i];
if (usage->kind == CBM_USAGE_CALL_REFERENCE && usage->ref_name &&
strcmp(usage->ref_name, target) == 0 &&
(!caller || qn_ends_with(usage->enclosing_func_qn, caller))) {
count++;
}
}
return count;
}
static int call_count(const CBMFileResult *r, const char *caller, const char *target) {
int count = 0;
for (int i = 0; i < r->calls.count; i++) {
const CBMCall *call = &r->calls.items[i];
const char *callee = call_short_name(call->callee_name);
if (callee && strcmp(callee, target) == 0 &&
(!caller || qn_ends_with(call->enclosing_func_qn, caller))) {
count++;
}
}
return count;
}
static int exact_usage_count(const CBMFileResult *r, const char *caller_qn, const char *target) {
int count = 0;
for (int i = 0; i < r->usages.count; i++) {
const CBMUsage *usage = &r->usages.items[i];
if (usage->kind == CBM_USAGE_VALUE && usage->ref_name && usage->enclosing_func_qn &&
strcmp(usage->ref_name, target) == 0 &&
strcmp(usage->enclosing_func_qn, caller_qn) == 0) {
count++;
}
}
return count;
}
static int exact_call_count(const CBMFileResult *r, const char *caller_qn, const char *callee) {
int count = 0;
for (int i = 0; i < r->calls.count; i++) {
const CBMCall *call = &r->calls.items[i];
if (call->callee_name && call->enclosing_func_qn &&
strcmp(call->callee_name, callee) == 0 &&
strcmp(call->enclosing_func_qn, caller_qn) == 0) {
count++;
}
}
return count;
}
static int exact_definition_count(const CBMFileResult *r, const char *name,
const char *qualified_name) {
int count = 0;
for (int i = 0; i < r->defs.count; i++) {
const CBMDefinition *def = &r->defs.items[i];
if (def->name && def->qualified_name && strcmp(def->name, name) == 0 &&
strcmp(def->qualified_name, qualified_name) == 0) {
count++;
}
}
return count;
}
static int definition_count(const CBMFileResult *r, const char *name) {
int count = 0;
for (int i = 0; i < r->defs.count; i++) {
const CBMDefinition *def = &r->defs.items[i];
if (def->name && strcmp(def->name, name) == 0)
count++;
}
return count;
}
static CBMFileResult *extract_case(const char *tag, const char *source, CBMLanguage language,
const char *filename) {
CBMFileResult *r =
cbm_extract_file(source, (int)strlen(source), language, "repro", filename, 0, NULL, NULL);
if (!r)
fprintf(stderr, " [call-scope] case=%s invariant=extract_result expected=non-null\n", tag);
return r;
}
#define CHECK_COUNT(tag, invariant, expression, expected) \
do { \
int actual_count = (expression); \
if (actual_count != (expected)) { \
fprintf(stderr, " [call-scope] case=%s invariant=%s expected=%d actual=%d\n", (tag), \
(invariant), (expected), actual_count); \
failures++; \
} \
} while (0)
#define CHECK_CLEAN(tag, result) \
do { \
if ((result)->has_error || (result)->parse_incomplete) { \
fprintf(stderr, " [call-scope] case=%s invariant=valid_fixture expected=clean\n", \
(tag)); \
failures++; \
} \
} while (0)
/* The two textual `foo` occurrences have different roles: the first is the
* invoked callee, while the second is a function value passed as an argument. */
TEST(repro_call_scope_same_spelling_callee_and_argument) {
static const char source[] = "function foo() {}\n"
"function run() { foo(foo); }\n";
CBMFileResult *r = extract_case("same_spelling", source, CBM_LANG_JAVASCRIPT, "main.js");
ASSERT_NOT_NULL(r);
int failures = 0;
CHECK_CLEAN("same_spelling", r);
CHECK_COUNT("same_spelling", "foo_definition", definition_count(r, "foo"), 1);
CHECK_COUNT("same_spelling", "callee_call", call_count(r, "run", "foo"), 1);
CHECK_COUNT("same_spelling", "argument_usage_only", usage_count(r, "run", "foo"), 1);
cbm_free_result(r);
ASSERT_EQ(failures, 0);
PASS();
}
/* Nested calls need independent callee identity. The outer call must not hide
* either its direct function-value argument or the inner call's value argument. */
TEST(repro_call_scope_nested_calls_retain_each_argument) {
static const char source[] = "const value = 1;\n"
"function handler(): void {}\n"
"function inner(input: number): number { return input; }\n"
"function outer(cb: () => void, input: number): void {}\n"
"function run(): void { outer(handler, inner(value)); }\n";
CBMFileResult *r = extract_case("nested_calls", source, CBM_LANG_TYPESCRIPT, "main.ts");
ASSERT_NOT_NULL(r);
int failures = 0;
CHECK_CLEAN("nested_calls", r);
CHECK_COUNT("nested_calls", "outer_call", call_count(r, "run", "outer"), 1);
CHECK_COUNT("nested_calls", "inner_call", call_count(r, "run", "inner"), 1);
CHECK_COUNT("nested_calls", "handler_argument_usage", usage_count(r, "run", "handler"), 1);
CHECK_COUNT("nested_calls", "value_argument_usage", usage_count(r, "run", "value"), 1);
CHECK_COUNT("nested_calls", "outer_not_usage", usage_count(r, "run", "outer"), 0);
CHECK_COUNT("nested_calls", "inner_not_usage", usage_count(r, "run", "inner"), 0);
cbm_free_result(r);
ASSERT_EQ(failures, 0);
PASS();
}
/* A callback body is executable code, not part of the registrar's callee. Its
* references remain visible even though the arrow function is an argument. */
TEST(repro_call_scope_inline_callback_body_usage) {
static const char source[] = "const watched = 1;\n"
"function register(callback) {}\n"
"function run() { register(() => { watched; }); }\n";
CBMFileResult *r = extract_case("inline_callback", source, CBM_LANG_JAVASCRIPT, "main.js");
ASSERT_NOT_NULL(r);
int failures = 0;
CHECK_CLEAN("inline_callback", r);
CHECK_COUNT("inline_callback", "registrar_call", call_count(r, "run", "register"), 1);
CHECK_COUNT("inline_callback", "callback_body_usage", usage_count(r, NULL, "watched"), 1);
CHECK_COUNT("inline_callback", "registrar_not_usage", usage_count(r, "run", "register"), 0);
cbm_free_result(r);
ASSERT_EQ(failures, 0);
PASS();
}
/* For a member call only `method` is the direct callee. The receiver and
* arguments are values. For a computed call, both receiver and key are values. */
TEST(repro_call_scope_member_and_computed_components) {
static const char source[] =
"class Receiver { method(value: number): void {} }\n"
"const receiver = new Receiver();\n"
"const key = 'method';\n"
"const payload = 1;\n"
"function run(): void { receiver.method(payload); receiver[key](payload); }\n";
CBMFileResult *r = extract_case("member_computed", source, CBM_LANG_TYPESCRIPT, "main.ts");
ASSERT_NOT_NULL(r);
int failures = 0;
CHECK_CLEAN("member_computed", r);
CHECK_COUNT("member_computed", "method_call", call_count(r, "run", "method"), 1);
CHECK_COUNT("member_computed", "receiver_usages", usage_count(r, "run", "receiver"), 2);
CHECK_COUNT("member_computed", "computed_key_usage", usage_count(r, "run", "key"), 1);
CHECK_COUNT("member_computed", "argument_usages", usage_count(r, "run", "payload"), 2);
CHECK_COUNT("member_computed", "terminal_method_not_usage", usage_count(r, "run", "method"), 0);
cbm_free_result(r);
ASSERT_EQ(failures, 0);
PASS();
}
/* Constructor and generic-call syntax must not change argument semantics. The
* type argument belongs to type-reference extraction; both handler expressions
* are ordinary value usages. */
TEST(repro_call_scope_constructor_and_generic_arguments) {
static const char source[] =
"interface Payload {}\n"
"class Box { constructor(callback: () => void) {} }\n"
"function handler(): void {}\n"
"function accept<T>(callback: () => void): void {}\n"
"function run(): void { new Box(handler); accept<Payload>(handler); }\n";
CBMFileResult *r = extract_case("constructor_generic", source, CBM_LANG_TYPESCRIPT, "main.ts");
ASSERT_NOT_NULL(r);
int failures = 0;
CHECK_CLEAN("constructor_generic", r);
CHECK_COUNT("constructor_generic", "constructor_call", call_count(r, "run", "Box"), 1);
CHECK_COUNT("constructor_generic", "generic_call", call_count(r, "run", "accept"), 1);
CHECK_COUNT("constructor_generic", "handler_argument_usages", usage_count(r, "run", "handler"),
2);
CHECK_COUNT("constructor_generic", "constructor_not_usage", usage_count(r, "run", "Box"), 0);
CHECK_COUNT("constructor_generic", "generic_callee_not_usage", usage_count(r, "run", "accept"),
0);
cbm_free_result(r);
ASSERT_EQ(failures, 0);
PASS();
}
TEST(repro_call_scope_c_function_pointer_invocation_and_arguments) {
static const char source[] = "typedef void (*Callback)(void);\n"
"void handler(void) {}\n"
"void invoke(Callback first, Callback second) {}\n"
"Callback shared = 0;\n"
"void run(void) { invoke(shared, handler); shared(); }\n";
CBMFileResult *r = extract_case("c_function_pointer", source, CBM_LANG_C, "main.c");
ASSERT_NOT_NULL(r);
int failures = 0;
CHECK_CLEAN("c_function_pointer", r);
CHECK_COUNT("c_function_pointer", "invoke_call", call_count(r, "run", "invoke"), 1);
CHECK_COUNT("c_function_pointer", "pointer_call", call_count(r, "run", "shared"), 1);
CHECK_COUNT("c_function_pointer", "pointer_argument_usage", usage_count(r, "run", "shared"), 1);
CHECK_COUNT("c_function_pointer", "function_argument_usage", usage_count(r, "run", "handler"),
1);
CHECK_COUNT("c_function_pointer", "invoke_not_usage", usage_count(r, "run", "invoke"), 0);
cbm_free_result(r);
ASSERT_EQ(failures, 0);
PASS();
}
TEST(repro_call_scope_cpp_function_pointer_invocation_and_arguments) {
static const char source[] = "using Callback = void (*)();\n"
"void handler() {}\n"
"void invoke(Callback first, Callback second) {}\n"
"Callback shared = nullptr;\n"
"void run() { invoke(shared, handler); shared(); }\n";
CBMFileResult *r = extract_case("cpp_function_pointer", source, CBM_LANG_CPP, "main.cpp");
ASSERT_NOT_NULL(r);
int failures = 0;
CHECK_CLEAN("cpp_function_pointer", r);
CHECK_COUNT("cpp_function_pointer", "invoke_call", call_count(r, "run", "invoke"), 1);
CHECK_COUNT("cpp_function_pointer", "pointer_call", call_count(r, "run", "shared"), 1);
CHECK_COUNT("cpp_function_pointer", "pointer_argument_usage", usage_count(r, "run", "shared"),
1);
CHECK_COUNT("cpp_function_pointer", "function_argument_usage", usage_count(r, "run", "handler"),
1);
CHECK_COUNT("cpp_function_pointer", "invoke_not_usage", usage_count(r, "run", "invoke"), 0);
cbm_free_result(r);
ASSERT_EQ(failures, 0);
PASS();
}
TEST(repro_call_scope_python_keyword_argument) {
static const char source[] = "def handler():\n pass\n"
"def accept(callback=None):\n pass\n"
"def run():\n accept(callback=handler)\n";
CBMFileResult *r = extract_case("python_keyword", source, CBM_LANG_PYTHON, "main.py");
ASSERT_NOT_NULL(r);
int failures = 0;
CHECK_CLEAN("python_keyword", r);
CHECK_COUNT("python_keyword", "accept_call", call_count(r, "run", "accept"), 1);
CHECK_COUNT("python_keyword", "keyword_value_usage", usage_count(r, "run", "handler"), 1);
CHECK_COUNT("python_keyword", "keyword_label_not_usage", usage_count(r, "run", "callback"), 0);
CHECK_COUNT("python_keyword", "callee_not_usage", usage_count(r, "run", "accept"), 0);
cbm_free_result(r);
ASSERT_EQ(failures, 0);
PASS();
}
/* Python registers `with_statement` as a call node. The actual manager()
* invocation is a call, but the with body is not part of that callee. */
TEST(repro_call_scope_python_with_body_usage) {
static const char source[] = "watched = 1\n"
"def manager():\n pass\n"
"def run():\n with manager():\n watched\n";
CBMFileResult *r = extract_case("python_with", source, CBM_LANG_PYTHON, "main.py");
ASSERT_NOT_NULL(r);
int failures = 0;
CHECK_CLEAN("python_with", r);
CHECK_COUNT("python_with", "run_definition", definition_count(r, "run"), 1);
CHECK_COUNT("python_with", "manager_call", call_count(r, "run", "manager"), 1);
CHECK_COUNT("python_with", "with_body_usage", usage_count(r, "run", "watched"), 1);
CHECK_COUNT("python_with", "manager_not_usage", usage_count(r, "run", "manager"), 0);
cbm_free_result(r);
ASSERT_EQ(failures, 0);
PASS();
}
/* Rust macro_invocation is call-like, but its token-tree arguments remain
* value references and must survive the macro callee's scope. */
TEST(repro_call_scope_rust_macro_argument) {
static const char source[] = "static WATCHED: i32 = 1;\n"
"macro_rules! probe { ($value:expr) => { $value }; }\n"
"fn run() { probe!(WATCHED); }\n";
CBMFileResult *r = extract_case("rust_macro", source, CBM_LANG_RUST, "main.rs");
ASSERT_NOT_NULL(r);
int failures = 0;
CHECK_CLEAN("rust_macro", r);
CHECK_COUNT("rust_macro", "run_definition", definition_count(r, "run"), 1);
CHECK_COUNT("rust_macro", "macro_call", call_count(r, "run", "probe"), 1);
CHECK_COUNT("rust_macro", "macro_argument_usage", usage_count(r, "run", "WATCHED"), 1);
CHECK_COUNT("rust_macro", "macro_callee_not_usage", usage_count(r, "run", "probe"), 0);
cbm_free_result(r);
ASSERT_EQ(failures, 0);
PASS();
}
/* Elixir binary_operator is registered as a call node, but `watched + 1` is
* not a call to watched. This uses caller-independent counts because Elixir's
* callable attribution has a separately documented Module-scope drift. */
TEST(repro_call_scope_elixir_binary_operator_operand) {
static const char source[] = "defmodule Sample do\n"
" def run(), do: watched + 1\n"
"end\n";
CBMFileResult *r = extract_case("elixir_binary", source, CBM_LANG_ELIXIR, "sample.ex");
ASSERT_NOT_NULL(r);
int failures = 0;
CHECK_CLEAN("elixir_binary", r);
CHECK_COUNT("elixir_binary", "run_definition", definition_count(r, "run"), 1);
CHECK_COUNT("elixir_binary", "operator_call", call_count(r, NULL, "+"), 1);
CHECK_COUNT("elixir_binary", "operand_usage", usage_count(r, NULL, "watched"), 1);
CHECK_COUNT("elixir_binary", "operand_not_call", call_count(r, NULL, "watched"), 0);
cbm_free_result(r);
ASSERT_EQ(failures, 0);
PASS();
}
/* Fortran keyword_argument is an argument container, not another invocation.
* The keyword label is metadata; only its value is a usage. */
TEST(repro_call_scope_fortran_keyword_argument) {
static const char source[] = "module sample\n"
" integer :: watched\n"
"contains\n"
" integer function consume(value)\n"
" integer, intent(in) :: value\n"
" consume = value\n"
" end function consume\n"
" integer function run()\n"
" run = consume(value=watched)\n"
" end function run\n"
"end module sample\n";
CBMFileResult *r = extract_case("fortran_keyword", source, CBM_LANG_FORTRAN, "main.f90");
ASSERT_NOT_NULL(r);
int failures = 0;
CHECK_CLEAN("fortran_keyword", r);
CHECK_COUNT("fortran_keyword", "run_definition", definition_count(r, "run"), 1);
CHECK_COUNT("fortran_keyword", "consume_call", call_count(r, NULL, "consume"), 1);
CHECK_COUNT("fortran_keyword", "keyword_value_usage", usage_count(r, "run", "watched"), 1);
CHECK_COUNT("fortran_keyword", "keyword_label_not_usage", usage_count(r, "run", "value"), 0);
CHECK_COUNT("fortran_keyword", "keyword_label_not_call", call_count(r, "run", "value"), 0);
cbm_free_result(r);
ASSERT_EQ(failures, 0);
PASS();
}
/* A separate gap discovered while building the keyword-argument control:
* Fortran's `call` statement is registered as call metadata, yet the direct
* subroutine invocation currently emits no CBMCall. Keep this independent from
* keyword labels so its future fix cannot hide behind another emitted call. */
TEST(repro_fortran_subroutine_call_is_extracted) {
static const char source[] = "subroutine consume(value)\n"
" integer :: value\n"
"end subroutine consume\n"
"subroutine run()\n"
" call consume(watched)\n"
"end subroutine run\n";
CBMFileResult *r =
extract_case("fortran_subroutine_call", source, CBM_LANG_FORTRAN, "main.f90");
ASSERT_NOT_NULL(r);
int failures = 0;
CHECK_CLEAN("fortran_subroutine_call", r);
CHECK_COUNT("fortran_subroutine_call", "consume_definition", definition_count(r, "consume"), 1);
CHECK_COUNT("fortran_subroutine_call", "run_definition", definition_count(r, "run"), 1);
CHECK_COUNT("fortran_subroutine_call", "total_calls", r->calls.count, 1);
CHECK_COUNT("fortran_subroutine_call", "consume_call_from_run", call_count(r, "run", "consume"),
1);
cbm_free_result(r);
ASSERT_EQ(failures, 0);
PASS();
}
/* Kotlin navigation_expression also represents ordinary property access. A
* property read must retain both reference components and emit no call. */
TEST(repro_call_scope_kotlin_property_navigation) {
static const char source[] = "class Holder(val value: Int)\n"
"val box = Holder(1)\n"
"fun run(): Int = box.value\n";
CBMFileResult *r = extract_case("kotlin_navigation", source, CBM_LANG_KOTLIN, "Main.kt");
ASSERT_NOT_NULL(r);
int failures = 0;
CHECK_CLEAN("kotlin_navigation", r);
CHECK_COUNT("kotlin_navigation", "run_definition", definition_count(r, "run"), 1);
CHECK_COUNT("kotlin_navigation", "receiver_usage", usage_count(r, "run", "box"), 1);
CHECK_COUNT("kotlin_navigation", "property_usage", usage_count(r, "run", "value"), 1);
CHECK_COUNT("kotlin_navigation", "receiver_not_call", call_count(r, "run", "box"), 0);
cbm_free_result(r);
ASSERT_EQ(failures, 0);
PASS();
}
/* PHP 8.1 first-class callable syntax evaluates a function reference without
* invoking it. The pinned grammar exposes the ordinary bare, member,
* scoped/static, and nullsafe call-expression nodes plus variadic_placeholder;
* CHECK_CLEAN guards the combinations used below. A real variadic_unpacking
* argument is still an invocation, so keep it as a positive discriminator. */
TEST(repro_call_scope_php_first_class_callable) {
static const char source[] = "<?php\n"
"function bare_handler(): void {}\n"
"function foo(...$values): void {}\n"
"function accept(callable $callback): void {}\n"
"class CallbackTarget {\n"
" public function member_handler(): void {}\n"
" public function nullable_handler(): void {}\n"
" public static function static_handler(): void {}\n"
"}\n"
"function run($receiver, $nullable, array $args): void {\n"
" accept(bare_handler(...));\n"
" accept($receiver->member_handler(...));\n"
" accept(CallbackTarget::static_handler(...));\n"
" accept($nullable?->nullable_handler(...));\n"
" foo(...$args);\n"
"}\n";
CBMFileResult *r = extract_case("php_first_class", source, CBM_LANG_PHP, "main.php");
ASSERT_NOT_NULL(r);
int failures = 0;
CHECK_CLEAN("php_first_class", r);
CHECK_COUNT("php_first_class", "total_calls", r->calls.count, 5);
CHECK_COUNT("php_first_class", "accept_calls", call_count(r, "run", "accept"), 4);
CHECK_COUNT("php_first_class", "bare_reference_usage", usage_count(r, "run", "bare_handler"),
0);
CHECK_COUNT("php_first_class", "bare_call_reference",
call_reference_count(r, "run", "bare_handler"), 1);
CHECK_COUNT("php_first_class", "bare_reference_not_invoked",
call_count(r, "run", "bare_handler"), 0);
CHECK_COUNT("php_first_class", "member_receiver_usage", usage_count(r, "run", "$receiver"), 1);
CHECK_COUNT("php_first_class", "member_reference_usage",
usage_count(r, "run", "member_handler"), 0);
CHECK_COUNT("php_first_class", "member_call_reference",
call_reference_count(r, "run", "member_handler"), 1);
CHECK_COUNT("php_first_class", "member_reference_not_invoked",
call_count(r, "run", "member_handler"), 0);
CHECK_COUNT("php_first_class", "static_scope_usage", usage_count(r, "run", "CallbackTarget"),
1);
CHECK_COUNT("php_first_class", "static_reference_usage",
usage_count(r, "run", "static_handler"), 0);
CHECK_COUNT("php_first_class", "static_call_reference",
call_reference_count(r, "run", "static_handler"), 1);
CHECK_COUNT("php_first_class", "static_reference_not_invoked",
call_count(r, "run", "static_handler"), 0);
CHECK_COUNT("php_first_class", "nullsafe_receiver_usage", usage_count(r, "run", "$nullable"),
1);
CHECK_COUNT("php_first_class", "nullsafe_reference_usage",
usage_count(r, "run", "nullable_handler"), 0);
CHECK_COUNT("php_first_class", "nullsafe_call_reference",
call_reference_count(r, "run", "nullable_handler"), 1);
CHECK_COUNT("php_first_class", "nullsafe_reference_not_invoked",
call_count(r, "run", "nullable_handler"), 0);
CHECK_COUNT("php_first_class", "unpacking_argument_usage", usage_count(r, "run", "$args"), 1);
CHECK_COUNT("php_first_class", "unpacking_is_invocation", call_count(r, "run", "foo"), 1);
CHECK_COUNT("php_first_class", "invoked_callee_not_usage", usage_count(r, "run", "foo"), 0);
cbm_free_result(r);
ASSERT_EQ(failures, 0);
PASS();
}
/* MAX_SCOPES is currently a fixed 64-entry stack. The 65th nested function is
* silently discarded, so calls in its body inherit the wrong enclosing QN.
* The later sibling is an independent pop-recovery control: it must remain
* attributed to itself rather than inheriting any deep or stale scope. */
TEST(repro_scope_stack_preserves_deepest_function) {
enum { FUNCTION_DEPTH = 65, SOURCE_CAPACITY = 8192 };
char *source = calloc(SOURCE_CAPACITY, 1);
ASSERT_NOT_NULL(source);
size_t used = (size_t)snprintf(source, SOURCE_CAPACITY, "function target() {}\n");
for (int depth = 0; depth < FUNCTION_DEPTH && used < SOURCE_CAPACITY; depth++) {
used +=
(size_t)snprintf(source + used, SOURCE_CAPACITY - used, "function f%d() {\n", depth);
}
used += (size_t)snprintf(source + used, SOURCE_CAPACITY - used, "target();\n");
for (int depth = 0; depth < FUNCTION_DEPTH && used < SOURCE_CAPACITY; depth++) {
used += (size_t)snprintf(source + used, SOURCE_CAPACITY - used, "}\n");
}
used += (size_t)snprintf(source + used, SOURCE_CAPACITY - used,
"function sibling() { target(); }\n");
ASSERT_TRUE(used < SOURCE_CAPACITY);
CBMFileResult *r = extract_case("deep_scope", source, CBM_LANG_JAVASCRIPT, "deep.js");
free(source);
ASSERT_NOT_NULL(r);
int failures = 0;
CHECK_CLEAN("deep_scope", r);
CHECK_COUNT("deep_scope", "deepest_definition", definition_count(r, "f64"), 1);
CHECK_COUNT("deep_scope", "sibling_definition", definition_count(r, "sibling"), 1);
CHECK_COUNT("deep_scope", "target_call_total", call_count(r, NULL, "target"), 2);
CHECK_COUNT("deep_scope", "penultimate_scope_not_inherited", call_count(r, "f63", "target"), 0);
CHECK_COUNT("deep_scope", "deepest_call_attribution", call_count(r, "f64", "target"), 1);
CHECK_COUNT("deep_scope", "sibling_pop_recovery_control", call_count(r, "sibling", "target"),
1);
cbm_free_result(r);
ASSERT_EQ(failures, 0);
PASS();
}
/* Declarations and simple assignment targets are not references. True reads
* remain usages, including the read side of compound assignments and updates.
* Distinct names prevent one role from falsely satisfying another assertion. */
TEST(repro_usage_declarations_are_not_references) {
static const char source[] = "function run(unusedParameter: number,\n"
" usedParameter: number): number {\n"
" let assignmentOnly: number;\n"
" assignmentOnly = 1;\n"
" let localValue = usedParameter + usedParameter;\n"
" let compoundValue = 0;\n"
" compoundValue += 1;\n"
" let updatedValue = 0;\n"
" updatedValue++;\n"
" return localValue + localValue;\n"
"}\n";
CBMFileResult *r = extract_case("declaration_usage", source, CBM_LANG_TYPESCRIPT, "main.ts");
ASSERT_NOT_NULL(r);
int failures = 0;
CHECK_CLEAN("declaration_usage", r);
CHECK_COUNT("declaration_usage", "run_definition", definition_count(r, "run"), 1);
CHECK_COUNT("declaration_usage", "unused_parameter_not_usage",
usage_count(r, "run", "unusedParameter"), 0);
CHECK_COUNT("declaration_usage", "assignment_only_binding_not_usage",
usage_count(r, "run", "assignmentOnly"), 0);
CHECK_COUNT("declaration_usage", "used_parameter_reads", usage_count(r, "run", "usedParameter"),
2);
CHECK_COUNT("declaration_usage", "local_value_reads", usage_count(r, "run", "localValue"), 2);
CHECK_COUNT("declaration_usage", "compound_assignment_reads_value",
usage_count(r, "run", "compoundValue"), 1);
CHECK_COUNT("declaration_usage", "update_reads_value", usage_count(r, "run", "updatedValue"),
1);
cbm_free_result(r);
ASSERT_EQ(failures, 0);
PASS();
}
/* ObjectScript's typed receiver map must not change either ownership or
* CALL/USAGE roles: the method is the exact callee and the parameter passed to
* it remains one value usage in Sample.Behavior.run. */
TEST(repro_call_scope_objectscript_typed_receiver_exact_owner) {
static const char source[] =
"Class Sample.Behavior Extends %RegisteredObject\n"
"{\n"
"Method run(target As Sample.Target, watched As %String) As %Status\n"
"{\n"
" Do target.accept(watched)\n"
" Quit\n"
"}\n"
"}\n";
static const char caller_qn[] = "repro.Behavior.Sample.Behavior.run";
static const char callee[] = "Sample.Target.accept";
CBMFileResult *r = extract_case("objectscript_typed_receiver", source,
CBM_LANG_OBJECTSCRIPT_UDL, "Behavior.cls");
ASSERT_NOT_NULL(r);
int failures = 0;
CHECK_CLEAN("objectscript_typed_receiver", r);
CHECK_COUNT("objectscript_typed_receiver", "exact_run_definition",
exact_definition_count(r, "run", caller_qn), 1);
CHECK_COUNT("objectscript_typed_receiver", "exact_call_owner_and_target",
exact_call_count(r, caller_qn, callee), 1);
CHECK_COUNT("objectscript_typed_receiver", "argument_usage",
exact_usage_count(r, caller_qn, "watched"), 1);
CHECK_COUNT("objectscript_typed_receiver", "argument_not_call",
exact_call_count(r, caller_qn, "watched"), 0);
CHECK_COUNT("objectscript_typed_receiver", "callee_not_usage",
exact_usage_count(r, caller_qn, "accept"), 0);
CHECK_COUNT("objectscript_typed_receiver", "one_call", r->calls.count, 1);
cbm_free_result(r);
ASSERT_EQ(failures, 0);
PASS();
}
/* A parameterized ObjectScript routine is represented by a procedure wrapper
* around its tag. Calls in the body still belong to the tag's Function node,
* not to the module and not to the following tag. */
TEST(repro_call_scope_objectscript_routine_tag_exact_owner) {
static const char source[] = "run(watched)\n"
" Do accept(watched)\n"
" Quit\n"
"accept(value)\n"
" Quit value\n";
static const char caller_qn[] = "repro.Behavior.run";
CBMFileResult *r = extract_case("objectscript_routine_scope", source,
CBM_LANG_OBJECTSCRIPT_ROUTINE, "Behavior.mac");
ASSERT_NOT_NULL(r);
int failures = 0;
CHECK_CLEAN("objectscript_routine_scope", r);
CHECK_COUNT("objectscript_routine_scope", "exact_run_definition",
exact_definition_count(r, "run", caller_qn), 1);
CHECK_COUNT("objectscript_routine_scope", "exact_call_owner_and_target",
exact_call_count(r, caller_qn, "accept"), 1);
CHECK_COUNT("objectscript_routine_scope", "argument_usage",
exact_usage_count(r, caller_qn, "watched"), 1);
CHECK_COUNT("objectscript_routine_scope", "argument_not_call",
exact_call_count(r, caller_qn, "watched"), 0);
CHECK_COUNT("objectscript_routine_scope", "callee_not_usage",
exact_usage_count(r, caller_qn, "accept"), 0);
CHECK_COUNT("objectscript_routine_scope", "one_call", r->calls.count, 1);
cbm_free_result(r);
ASSERT_EQ(failures, 0);
PASS();
}
SUITE(repro_call_scope_usages) {
RUN_TEST(repro_call_scope_same_spelling_callee_and_argument);
RUN_TEST(repro_call_scope_nested_calls_retain_each_argument);
RUN_TEST(repro_call_scope_inline_callback_body_usage);
RUN_TEST(repro_call_scope_member_and_computed_components);
RUN_TEST(repro_call_scope_constructor_and_generic_arguments);
RUN_TEST(repro_call_scope_c_function_pointer_invocation_and_arguments);
RUN_TEST(repro_call_scope_cpp_function_pointer_invocation_and_arguments);
RUN_TEST(repro_call_scope_python_keyword_argument);
RUN_TEST(repro_call_scope_python_with_body_usage);
RUN_TEST(repro_call_scope_rust_macro_argument);
RUN_TEST(repro_call_scope_elixir_binary_operator_operand);
RUN_TEST(repro_call_scope_fortran_keyword_argument);
RUN_TEST(repro_fortran_subroutine_call_is_extracted);
RUN_TEST(repro_call_scope_kotlin_property_navigation);
RUN_TEST(repro_call_scope_php_first_class_callable);
RUN_TEST(repro_scope_stack_preserves_deepest_function);
RUN_TEST(repro_usage_declarations_are_not_references);
RUN_TEST(repro_call_scope_objectscript_typed_receiver_exact_owner);
RUN_TEST(repro_call_scope_objectscript_routine_tag_exact_owner);
}
#undef CHECK_CLEAN
#undef CHECK_COUNT
+19 -31
View File
@@ -59,9 +59,8 @@
* languages with non-empty call_types AND a fixture that
* produces a resolvable callee_name (TYPST call, QML JS
* call_expression, PURESCRIPT exp_apply). BIBTEX/DIFF
* have call_types ("command") but the nodes are not
* function-application sites with a stable callee_name;
* dim 6 is SKIPPED there and noted.
* intentionally have no call metadata because their
* `command` nodes are document records, not applications.
*
* FULL-PIPELINE (rh_index_files -> cbm_store_t*, via inv_count_* store helpers):
* 7. callable-sourcing : inv_count_calls_by_source(store,project,&mod,&call).
@@ -91,15 +90,15 @@
* Dims 1-8. Dim 5 asserts "Function" (a let-bound lambda).
* Dim 7 may RED if the lambda is anonymous and the enclosing-func
* walk attributes the call at Module.
* BIBTEX -- DOCS. call_types = {command} only; entries (@article{...}) are
* BIBTEX -- DOCS. call_types is empty; entries (@article{...}) are
* NOT mapped to any def label, and "command" nodes are LaTeX-style
* commands, not callee-named application sites. Dims 1-4 + R
* (dim 5 skipped -- no def types; dim 6 skipped -- no stable callee).
* MERMAID -- structural-only. module_types only. Dims 1-4 + R.
* PO -- DOCS/structural-only. module_types only (gettext msgid/msgstr
* entries are not mapped to a def label). Dims 1-4 + R.
* DIFF -- structural. call_types = {command} only (a "command" line in a
* git-style diff header, not a function call); no def types.
* DIFF -- structural. call_types is empty because a "command" line in a
* git-style diff header is not a function call; no def types.
* Dims 1-4 + R (dims 5-6 skipped).
* REGEX -- structural-only. module_types = {pattern}. Dims 1-4 + R.
* CAPNP -- SCHEMA. func_types = {method} -> "Function";
@@ -518,28 +517,18 @@ TEST(repro_grammar_markup_rst) {
* Dims asserted: 1-8 (full battery).
* Dim 5 expected GREEN: "Function" def for the let-bound lambda.
* Dim 6 expected GREEN: call to "greet" via the call node.
* Dim 7 expected RED if the lambda binding name does not flow to the enclosing-
* func walk and the call is attributed at Module. RED documents the gap.
* Dim 7 expected GREEN: `render` calls `greet` from its function body, so the
* call must source to the `render` Function rather than the Module.
* Dim 8 expected GREEN: no dangling CALLS endpoints.
*/
TEST(repro_grammar_markup_typst) {
/* DISABLED — RARE LANGUAGE (maintainer-approved, 2026-06-28): Typst (markup).
* The `#greet("world")` is a genuinely top-level (module-level) application
* that production CORRECTLY sources to the Module, but pipeline_battery counts
* any non-Function-sourced edge as drift (the nix-pattern). A simple in-
* function wrap conflicts with markup_callable_battery, which needs that very
* call. Murky markup/fixture interaction in a niche language; deferred. */
printf("%sSKIP%s rare language (Typst top-level-call sourcing)\n", tf_dim(), tf_reset());
return -1; /* skip — not counted as pass or fail */
static const char src[] =
"#let title = \"Codebase Memory\"\n"
"#let greet(name) = [Hello, #name!]\n"
"\n"
"= #title\n"
"\n"
"#greet(\"world\")\n"
"\n"
"Some body text with a #strong[bold] run.\n";
static const char src[] = "#let title = \"Codebase Memory\"\n"
"#let greet(name) = [Hello, #name!]\n"
"#let render() = greet(\"world\")\n"
"\n"
"= #title\n"
"\n"
"Some body text with a #strong[bold] run.\n";
static const char bad[] = "#let greet(name) = [Hello, #name";
if (markup_callable_battery("Typst", src, CBM_LANG_TYPST, "doc.typ",
"Function", "greet") != 0)
@@ -551,14 +540,13 @@ TEST(repro_grammar_markup_typst) {
/* -- BIBTEX ------------------------------------------------------------------
* Idiomatic BibTeX bibliography with an @article and an @book entry. The spec
* has bibtex_module_types = {document} and bibtex_call_types = {command}; entry
* declarations are NOT mapped to any def label, and "command" nodes are
* LaTeX-style commands without a stable function callee_name.
* has bibtex_module_types = {document} and an empty bibtex_call_types array;
* entry declarations are NOT mapped to any def label, and "command" nodes are
* LaTeX-style document commands rather than function applications.
*
* Dims asserted: 1-4 + R.
* Dim 5 SKIPPED: no def-minting types (entries unmapped).
* Dim 6 SKIPPED: call_types exists but "command" nodes have no resolvable
* callee_name to assert against; asserting would be brittle.
* Dim 6 SKIPPED: call_types is empty by design.
* Dims 7-8 SKIPPED: no func_types to anchor a call.
* Expected GREEN: dims 1-4. extract-clean RED would indicate the BibTeX grammar
* misparses standard @entry{...} records.
@@ -639,7 +627,7 @@ TEST(repro_grammar_markup_po) {
/* -- DIFF --------------------------------------------------------------------
* Idiomatic unified diff (git-style) with file headers and a hunk. The spec has
* diff_module_types = {source} and diff_call_types = {command}; there are no
* diff_module_types = {source} and an empty diff_call_types array; there are no
* def-minting types and "command" nodes are diff command lines, not function
* application sites with a stable callee_name.
*
+76 -4
View File
@@ -2,8 +2,10 @@
* repro_grammar_misc.c -- FINAL per-grammar INVARIANT battery covering the
* remaining MISCELLANEOUS language family (hardware-description, CFML dialects,
* niche scripting, structural assembly/linker/tablegen/ledger/IaC). This file
* completes the all-159-grammar reproduce-first coverage: every CBM_LANG_* now
* has a per-language RED/GREEN row on the bug-repro board.
* completes the all-161-grammar reproduce-first coverage: every grammar-backed
* CBM_LANG_* now has a per-language RED/GREEN row on the bug-repro board.
* CBM_LANG_OBJECTSCRIPT_EXPORT is intentionally excluded from the grammar
* count because Studio Export XML is transformed to ObjectScript UDL first.
*
* One TEST() per language so per-language RED/GREEN shows on the board. Each
* test runs the battery dimension appropriate to what the language's lang_spec
@@ -31,6 +33,12 @@
* call: call_expression)
* SQL -> CBM_LANG_SQL (func: create_function; call:
* function_call/invocation/command)
* OBJECTSCRIPT_UDL -> CBM_LANG_OBJECTSCRIPT_UDL
* (func: method/classmethod/query;
* call: four registered UDL forms)
* OBJECTSCRIPT_ROUTINE -> CBM_LANG_OBJECTSCRIPT_ROUTINE
* (func: tag; call: extrinsic_function/
* routine_tag_call)
*
* STRUCTURAL family (asm / linker / data / IaC) -> extract-clean +
* labels/fqn/ranges valid + defs-present (the entities each should extract) +
@@ -91,8 +99,9 @@
*
* HONEST RED CONTRACT (the point of this file): dimension 7 (callable-sourcing) is
* expected RED for the non-LSP callable languages here. None of VERILOG /
* SYSTEMVERILOG / VHDL / CFML / CFSCRIPT / RESCRIPT / SQUIRREL / PINE / TEMPL / SQL
* has a dedicated cross-LSP rescue, so attribution depends solely on the
* SYSTEMVERILOG / VHDL / CFML / CFSCRIPT / RESCRIPT / SQUIRREL / PINE / TEMPL / SQL /
* OBJECTSCRIPT_UDL / OBJECTSCRIPT_ROUTINE has a dedicated cross-LSP rescue, so
* attribution depends solely on the
* tree-sitter enclosing-func walk (cbm_find_enclosing_func + func_kinds_for_lang in
* helpers.c). When that mapping does not match the grammar's emitted func node
* types, the in-body call falls back to the Module QN -- exactly the enclosing-func
@@ -468,6 +477,67 @@ TEST(repro_grammar_misc_linkerscript) {
return misc_robustness("LINKERSCRIPT", bad, CBM_LANG_LINKERSCRIPT, "link.ld");
}
/* ── OBJECTSCRIPT UDL (callable) ─────────────────────────────────────────────
* A class method calls another class method with a local value argument. The
* call is the registered class_method_call form; the other three UDL call forms
* remain enumerated by repro_call_node_manifest.
*
* Dims asserted: 1-8 + R. Studio Export XML is not a grammar row because it is
* transformed to this UDL language before extraction.
*/
TEST(repro_grammar_misc_objectscript_udl) {
static const char src[] = "Class Sample.Callbacks Extends %RegisteredObject\n"
"{\n"
"ClassMethod Accept(value As %String) As %String\n"
"{\n"
" Quit value\n"
"}\n"
"ClassMethod Run(watched As %String) As %String\n"
"{\n"
" Quit ##class(Sample.Callbacks).Accept(watched)\n"
"}\n"
"}\n";
static const char bad[] = "Class Sample.Callbacks Extends %RegisteredObject\n"
"{\n"
"ClassMethod Run(watched As %String) As %String\n"
"{\n"
" Quit ##class(Sample.Callbacks).Accept(\n";
if (misc_single_file_battery("OBJECTSCRIPT_UDL", src, CBM_LANG_OBJECTSCRIPT_UDL,
"Callbacks.cls", "Method", NULL, "Accept") != 0)
return 1;
if (misc_robustness("OBJECTSCRIPT_UDL", bad, CBM_LANG_OBJECTSCRIPT_UDL, "Callbacks.cls") != 0)
return 1;
return misc_pipeline_battery("OBJECTSCRIPT_UDL", "Callbacks.cls", src);
}
/* ── OBJECTSCRIPT ROUTINE (callable) ─────────────────────────────────────────
* A parameterized routine tag invokes a sibling through the registered
* extrinsic_function form. routine_tag_call membership is covered separately by
* the literal call-node manifest.
*
* Dims asserted: 1-8 + R.
*/
TEST(repro_grammar_misc_objectscript_routine) {
static const char src[] = "SAMPLE\n"
" Quit\n"
"Accept(value)\n"
" Quit value\n"
"Run(watched)\n"
" Set result = $$Accept(watched)\n"
" Quit result\n";
static const char bad[] = "SAMPLE\n"
" Quit\n"
"Run(watched)\n"
" Set result = $$Accept(\n";
if (misc_single_file_battery("OBJECTSCRIPT_ROUTINE", src, CBM_LANG_OBJECTSCRIPT_ROUTINE,
"Sample.mac", "Function", NULL, "Accept") != 0)
return 1;
if (misc_robustness("OBJECTSCRIPT_ROUTINE", bad, CBM_LANG_OBJECTSCRIPT_ROUTINE, "Sample.mac") !=
0)
return 1;
return misc_pipeline_battery("OBJECTSCRIPT_ROUTINE", "Sample.mac", src);
}
/* ── PINE (callable) ─────────────────────────────────────────────────────────
* Idiomatic Pine Script v5 indicator: a user function `ema2` defined with
* function_declaration_statement, and a call to the built-in `plot()` plus an
@@ -790,6 +860,8 @@ SUITE(repro_grammar_misc) {
RUN_TEST(repro_grammar_misc_cfml);
RUN_TEST(repro_grammar_misc_cfscript);
RUN_TEST(repro_grammar_misc_linkerscript);
RUN_TEST(repro_grammar_misc_objectscript_udl);
RUN_TEST(repro_grammar_misc_objectscript_routine);
RUN_TEST(repro_grammar_misc_pine);
RUN_TEST(repro_grammar_misc_rescript);
RUN_TEST(repro_grammar_misc_sql);
+9 -34
View File
@@ -12,7 +12,7 @@
* Languages covered (12) and the CBM_LANG_* enum each uses (every enum verified
* present in internal/cbm/cbm.h; none missing, none skipped):
* Zig -> CBM_LANG_ZIG
* Nim -> CBM_LANG_NIM
* Nim -> CBM_LANG_NIM (explicitly unsupported; no grammar is vendored)
* Crystal -> CBM_LANG_CRYSTAL
* Hare -> CBM_LANG_HARE
* Odin -> CBM_LANG_ODIN
@@ -65,8 +65,8 @@
* - Crystal (method_def), Odin (procedure_declaration), Pony (method),
* Ada (subprogram_body), Fortran (function/subroutine),
* COBOL (program_definition), Pascal (defProc), Move (function_item).
* Nim has NO lang_spec / grammar entry at all, so it extracts zero defs and zero
* calls today: dims 5/6/7 are RED for Nim and the fixture documents that gap.
* Nim has no lang_spec / grammar entry. Its row is an explicit registry contract,
* not a skipped extraction fixture.
*
* When a language extracts NO in-body call today, dimension 6 (calls-extracted)
* is asserted anyway -- the language SHOULD capture the call -- so the RED row
@@ -78,6 +78,7 @@
*/
#include "test_framework.h"
#include "lang_specs.h"
#include "repro_invariant_lib.h"
#include <store/store.h>
@@ -248,39 +249,13 @@ TEST(repro_grammar_systems_zig) {
}
/* -- Nim --------------------------------------------------------------------
* Idiomatic: import, an object type, two `proc`s with the callee called inside
* the caller body. Nim has NO lang_spec row and NO grammar_nim.c -- there is no
* func/class/call node-type table for it. Expected: dim 1 (extract-clean) GREEN
* (cbm_extract_file returns a result), but dims 5/6 RED (zero defs, zero calls)
* and dim 7 RED (zero CALLS edges to attribute). These RED rows document the
* missing Nim support; the fixture asserts it SHOULD extract a "Function" and a
* call to "add".
* The enum is retained for API compatibility, but no Nim grammar is linked.
* Unsupported must mean NULL; it must never alias the zero-initialized Go row.
*/
TEST(repro_grammar_systems_nim) {
/* DISABLED — GRAMMAR ISSUE (maintainer-approved, 2026-06-28): extraction of
* standard Nim (`proc add(a, b: int): int = ...`) fails extract-clean (NULL
* result or has_error set) — tree-sitter-nim mis-parses the indentation-
* sensitive layout (Nim was a deferred/problematic grammar in the sweep). A
* grammar/parser defect, not a cbm extraction bug. Original assertions below
* are preserved (unreachable) for re-enable when the grammar is fixed. */
printf("%sSKIP%s grammar issue (tree-sitter-nim parse failure)\n", tf_dim(), tf_reset());
return -1; /* skip — not counted as pass or fail */
static const char src[] =
"import std/strutils\n"
"\n"
"type\n"
" Calc = object\n"
" base: int\n"
"\n"
"proc add(a, b: int): int =\n"
" return a + b\n"
"\n"
"proc compute(x: int): int =\n"
" return add(x, 1)\n";
if (single_file_battery("Nim", src, CBM_LANG_NIM, "calc.nim",
"Function", NULL, "add") != 0)
return 1;
return pipeline_battery("Nim", "calc.nim", src);
ASSERT_NULL(cbm_lang_spec(CBM_LANG_NIM));
ASSERT_NULL(cbm_ts_language(CBM_LANG_NIM));
PASS();
}
/* -- Crystal ----------------------------------------------------------------
+51 -23
View File
@@ -31,6 +31,7 @@
typedef struct {
char tmpdir[256];
char cachedir[256];
char dbpath[512];
char *project;
cbm_mcp_server_t *srv;
@@ -54,25 +55,43 @@ static inline cbm_store_t *rh_open_indexed(RProj *lp) {
lp->project = cbm_project_name_from_path(lp->tmpdir);
if (!lp->project)
return NULL;
const char *home = getenv("HOME");
if (!home)
home = "/tmp";
char cache_dir[512];
snprintf(cache_dir, sizeof(cache_dir), "%s/.cache/codebase-memory-mcp", home);
cbm_mkdir(cache_dir);
snprintf(lp->dbpath, sizeof(lp->dbpath), "%s/%s.db", cache_dir, lp->project);
if (!lp->cachedir[0]) {
snprintf(lp->cachedir, sizeof(lp->cachedir), "/tmp/cbm_repro_cache_XXXXXX");
if (!cbm_mkdtemp(lp->cachedir))
return NULL;
rh_to_fwd_slashes(lp->cachedir);
}
snprintf(lp->dbpath, sizeof(lp->dbpath), "%s/%s.db", lp->cachedir, lp->project);
unlink(lp->dbpath);
const char *prior_cache_dir = getenv("CBM_CACHE_DIR");
char *saved_cache_dir = prior_cache_dir ? cbm_strdup(prior_cache_dir) : NULL;
cbm_setenv("CBM_CACHE_DIR", lp->cachedir, 1);
cbm_store_t *store = NULL;
lp->srv = cbm_mcp_server_new(NULL);
if (!lp->srv)
return NULL;
char args[700];
snprintf(args, sizeof(args), "{\"repo_path\":\"%s\"}", lp->tmpdir);
char *resp = cbm_mcp_handle_tool(lp->srv, "index_repository", args);
if (resp)
free(resp);
return cbm_store_open_path(lp->dbpath);
if (lp->srv) {
char args[700];
snprintf(args, sizeof(args), "{\"repo_path\":\"%s\"}", lp->tmpdir);
char *resp = cbm_mcp_handle_tool(lp->srv, "index_repository", args);
if (resp)
free(resp);
/* Repro consumers only inspect the graph. Keep this connection query-only
* so the harness does not mutate the publisher's sealed journal mode. */
store = cbm_store_open_path_query(lp->dbpath);
}
if (saved_cache_dir) {
cbm_setenv("CBM_CACHE_DIR", saved_cache_dir, 1);
free(saved_cache_dir);
} else {
cbm_unsetenv("CBM_CACHE_DIR");
}
return store;
}
static inline void rh_cleanup(RProj *lp, cbm_store_t *store);
/* Write each fixture file into a fresh temp project, index it via the MCP
* production flow, and open the resulting graph DB. Returns store (NULL on fail). */
static inline cbm_store_t *rh_index_files(RProj *lp, const RFile *files, int nfiles) {
@@ -91,12 +110,18 @@ static inline cbm_store_t *rh_index_files(RProj *lp, const RFile *files, int nfi
*slash = '/';
}
FILE *f = fopen(path, "wb"); /* binary: keep "\n" exact */
if (!f)
if (!f) {
rh_cleanup(lp, NULL);
return NULL;
}
fputs(files[i].content, f);
fclose(f);
}
return rh_open_indexed(lp);
cbm_store_t *store = rh_open_indexed(lp);
if (!store) {
rh_cleanup(lp, NULL);
}
return store;
}
static inline cbm_store_t *rh_index(RProj *lp, const char *filename, const char *content) {
@@ -114,12 +139,15 @@ static inline void rh_cleanup(RProj *lp, cbm_store_t *store) {
free(lp->project);
lp->project = NULL;
th_rmtree(lp->tmpdir);
unlink(lp->dbpath);
char wal[600], shm[600];
snprintf(wal, sizeof(wal), "%s-wal", lp->dbpath);
unlink(wal);
snprintf(shm, sizeof(shm), "%s-shm", lp->dbpath);
unlink(shm);
th_rmtree(lp->cachedir);
if (lp->dbpath[0]) {
unlink(lp->dbpath);
char wal[600], shm[600];
snprintf(wal, sizeof(wal), "%s-wal", lp->dbpath);
unlink(wal);
snprintf(shm, sizeof(shm), "%s-shm", lp->dbpath);
unlink(shm);
}
}
/* Count edges of a given type in the project graph. Returns -1 on query error. */
+140
View File
@@ -0,0 +1,140 @@
/* Reproduction for temporary directories leaked by repro_harness failures. */
#include "test_framework.h"
#include "repro_harness.h"
#include <stdio.h>
#include <sys/stat.h>
static int cleanup_path_exists(const char *path) {
struct stat st;
return path && path[0] && stat(path, &st) == 0;
}
static int cleanup_write_sentinel(const char *path) {
FILE *file = cbm_fopen(path, "wb");
if (!file) {
return 0;
}
int wrote = fputs("sentinel\n", file) >= 0;
int closed = fclose(file) == 0;
return wrote && closed;
}
TEST(repro_harness_cleans_temp_project_after_fixture_write_failure) {
RProj project;
const RFile invalid_file = {
"", /* Resolves to the fresh project directory, so fopen must fail. */
"fixture content",
};
cbm_store_t *store = rh_index_files(&project, &invalid_file, 1);
char tmpdir[sizeof(project.tmpdir)];
char cachedir[sizeof(project.cachedir)];
snprintf(tmpdir, sizeof(tmpdir), "%s", project.tmpdir);
snprintf(cachedir, sizeof(cachedir), "%s", project.cachedir);
int temp_project_was_created = tmpdir[0] != '\0';
int tmpdir_remained = cleanup_path_exists(tmpdir);
int cachedir_remained = cleanup_path_exists(cachedir);
/* This failure happens before dbpath is initialized. Keep rh_cleanup's WAL
* cleanup absolute so the RED guard cannot unlink a relative workspace file. */
if (!project.dbpath[0]) {
snprintf(project.dbpath, sizeof(project.dbpath), "/tmp/cbm_repro_cleanup_guard_%ld.db",
(long)getpid());
}
rh_cleanup(&project, store);
int explicit_cleanup_left_tmpdir = cleanup_path_exists(tmpdir);
int explicit_cleanup_left_cachedir = cleanup_path_exists(cachedir);
if (store != NULL) {
fprintf(stderr, " [repro-harness-cleanup] invariant=invalid_fixture_was_indexed\n");
FAIL("invalid fixture path must make rh_index_files return NULL");
}
if (!temp_project_was_created) {
fprintf(stderr,
" [repro-harness-cleanup] invariant=temp_project_not_created_before_failure\n");
FAIL("test did not reach the post-mkdtemp fixture-write failure path");
}
if (explicit_cleanup_left_tmpdir || explicit_cleanup_left_cachedir) {
fprintf(stderr,
" [repro-harness-cleanup] invariant=test_cleanup_failed tmpdir=%d cachedir=%d\n",
explicit_cleanup_left_tmpdir, explicit_cleanup_left_cachedir);
FAIL("rh_cleanup did not remove the test's temporary directories");
}
if (tmpdir_remained || cachedir_remained) {
fprintf(stderr,
" [repro-harness-cleanup] invariant=helper_returned_with_temp_state "
"tmpdir=%d cachedir=%d\n",
tmpdir_remained, cachedir_remained);
FAIL("rh_index_files must clean temporary directories before returning NULL");
}
PASS();
}
TEST(repro_harness_cleanup_empty_dbpath_preserves_relative_sidecars) {
/* Empty dbpath must be a no-op for sidecars, never a cwd-relative unlink. */
char original_cwd[1024];
char private_dir[256] = "/tmp/cbm_repro_empty_dbpath_XXXXXX";
_Static_assert(sizeof(private_dir) >= 256,
"cbm_mkdtemp requires a 256-byte caller buffer on Windows");
int entered_private_dir = 0;
int wal_created = 0;
int shm_created = 0;
int wal_survived = 0;
int shm_survived = 0;
if (!getcwd(original_cwd, sizeof(original_cwd))) {
FAIL("test could not record the original working directory");
}
if (!cbm_mkdtemp(private_dir)) {
FAIL("test could not create a private temporary working directory");
}
entered_private_dir = chdir(private_dir) == 0;
if (entered_private_dir) {
wal_created = cleanup_write_sentinel("-wal");
shm_created = cleanup_write_sentinel("-shm");
if (wal_created && shm_created) {
RProj project = {0};
rh_cleanup(&project, NULL);
wal_survived = cleanup_path_exists("-wal");
shm_survived = cleanup_path_exists("-shm");
}
}
int restored_cwd = chdir(original_cwd) == 0;
int removed_private_dir = th_rmtree(private_dir) == 0;
if (!entered_private_dir || !wal_created || !shm_created) {
fprintf(stderr,
" [repro-harness-cleanup] invariant=sentinel_setup_failed entered=%d "
"wal_created=%d shm_created=%d\n",
entered_private_dir, wal_created, shm_created);
FAIL("test could not establish the empty-dbpath sidecar preconditions");
}
if (!restored_cwd || !removed_private_dir) {
fprintf(stderr,
" [repro-harness-cleanup] invariant=test_cleanup_failed restored_cwd=%d "
"removed_private_dir=%d\n",
restored_cwd, removed_private_dir);
FAIL("test did not restore and remove its private working directory");
}
if (!wal_survived || !shm_survived) {
fprintf(stderr,
" [repro-harness-cleanup] invariant=empty_dbpath_unlinked_relative_sidecar "
"wal_survived=%d shm_survived=%d\n",
wal_survived, shm_survived);
FAIL("rh_cleanup with an empty dbpath must not unlink relative -wal/-shm files");
}
PASS();
}
SUITE(repro_harness_cleanup) {
RUN_TEST(repro_harness_cleans_temp_project_after_fixture_write_failure);
RUN_TEST(repro_harness_cleanup_empty_dbpath_preserves_relative_sidecars);
}
+574
View File
@@ -0,0 +1,574 @@
/* Reproductions for CBMLanguage-to-spec registry invariants.
*
* This ledger classifies raw call-node metadata and generic identifier/reference
* vocabulary. It does not claim that every call-capable language can produce an
* exact graph CALL_REFERENCE edge; that narrower semantic contract is covered
* by the reference-precision suites. */
#include "test_framework.h"
#include "lang_specs.h"
#include "tree_sitter/api.h"
#include <stdio.h>
#include <string.h>
typedef enum {
CAP_UNSET = 0,
CAP_CALL_WITH_REFERENCE_VOCAB,
CAP_CALL_WITHOUT_REFERENCE_VOCAB,
CAP_NO_CALL,
CAP_TRANSFORM_ONLY,
CAP_UNSUPPORTED,
CAP_COUNT
} LanguageCapability;
typedef struct {
LanguageCapability capability;
const char *name;
const char *reason;
} LanguageCapabilityEntry;
size_t repro_call_argument_matrix_a_copy_language_ids(CBMLanguage *language_ids, size_t capacity);
size_t repro_call_argument_matrix_b_copy_language_ids(CBMLanguage *language_ids, size_t capacity);
#define CALL_WITH_REFERENCE_VOCAB(lang) \
[CBM_LANG_##lang] = {CAP_CALL_WITH_REFERENCE_VOCAB, #lang, \
"call metadata and a currently recognized reference node"}
#define CALL_WITHOUT_REFERENCE_VOCAB(lang) \
[CBM_LANG_##lang] = {CAP_CALL_WITHOUT_REFERENCE_VOCAB, #lang, \
"call metadata but no reference node recognized by extract_usages"}
#define NO_CALL(lang) \
[CBM_LANG_##lang] = {CAP_NO_CALL, #lang, "no call-node metadata; callback case inapplicable"}
#define TRANSFORM_ONLY(lang) \
[CBM_LANG_##lang] = {CAP_TRANSFORM_ONLY, #lang, \
"source is transformed into another registered language first"}
#define UNSUPPORTED(lang) \
[CBM_LANG_##lang] = {CAP_UNSUPPORTED, #lang, "enum retained without a registered grammar"}
/* Explicit ledger: no default initializer is intentional. A new enum remains
* CAP_UNSET until its call-metadata/reference-vocabulary behavior is audited. */
static const LanguageCapabilityEntry LANGUAGE_CAPABILITIES[CBM_LANG_COUNT] = {
CALL_WITH_REFERENCE_VOCAB(GO),
CALL_WITH_REFERENCE_VOCAB(PYTHON),
CALL_WITH_REFERENCE_VOCAB(JAVASCRIPT),
CALL_WITH_REFERENCE_VOCAB(TYPESCRIPT),
CALL_WITH_REFERENCE_VOCAB(TSX),
CALL_WITH_REFERENCE_VOCAB(RUST),
CALL_WITH_REFERENCE_VOCAB(JAVA),
CALL_WITH_REFERENCE_VOCAB(CPP),
CALL_WITH_REFERENCE_VOCAB(CSHARP),
CALL_WITHOUT_REFERENCE_VOCAB(PHP),
CALL_WITH_REFERENCE_VOCAB(LUA),
CALL_WITH_REFERENCE_VOCAB(SCALA),
CALL_WITH_REFERENCE_VOCAB(KOTLIN),
CALL_WITH_REFERENCE_VOCAB(RUBY),
CALL_WITH_REFERENCE_VOCAB(C),
CALL_WITHOUT_REFERENCE_VOCAB(BASH),
CALL_WITH_REFERENCE_VOCAB(ZIG),
CALL_WITH_REFERENCE_VOCAB(ELIXIR),
CALL_WITH_REFERENCE_VOCAB(HASKELL),
CALL_WITH_REFERENCE_VOCAB(OCAML),
CALL_WITH_REFERENCE_VOCAB(OBJC),
CALL_WITH_REFERENCE_VOCAB(SWIFT),
CALL_WITH_REFERENCE_VOCAB(DART),
CALL_WITHOUT_REFERENCE_VOCAB(PERL),
CALL_WITH_REFERENCE_VOCAB(GROOVY),
CALL_WITH_REFERENCE_VOCAB(ERLANG),
CALL_WITH_REFERENCE_VOCAB(R),
NO_CALL(HTML),
CALL_WITH_REFERENCE_VOCAB(CSS),
CALL_WITH_REFERENCE_VOCAB(SCSS),
NO_CALL(YAML),
NO_CALL(TOML),
CALL_WITH_REFERENCE_VOCAB(HCL),
CALL_WITH_REFERENCE_VOCAB(SQL),
NO_CALL(DOCKERFILE),
CALL_WITHOUT_REFERENCE_VOCAB(CLOJURE),
CALL_WITH_REFERENCE_VOCAB(FSHARP),
CALL_WITH_REFERENCE_VOCAB(JULIA),
CALL_WITH_REFERENCE_VOCAB(VIMSCRIPT),
CALL_WITH_REFERENCE_VOCAB(NIX),
CALL_WITHOUT_REFERENCE_VOCAB(COMMONLISP),
CALL_WITHOUT_REFERENCE_VOCAB(ELM),
CALL_WITH_REFERENCE_VOCAB(FORTRAN),
CALL_WITH_REFERENCE_VOCAB(CUDA),
CALL_WITHOUT_REFERENCE_VOCAB(COBOL),
CALL_WITH_REFERENCE_VOCAB(VERILOG),
CALL_WITHOUT_REFERENCE_VOCAB(EMACSLISP),
NO_CALL(JSON),
NO_CALL(XML),
NO_CALL(MARKDOWN),
CALL_WITHOUT_REFERENCE_VOCAB(MAKEFILE),
CALL_WITH_REFERENCE_VOCAB(CMAKE),
NO_CALL(PROTOBUF),
NO_CALL(GRAPHQL),
NO_CALL(VUE),
NO_CALL(SVELTE),
CALL_WITH_REFERENCE_VOCAB(MESON),
CALL_WITH_REFERENCE_VOCAB(GLSL),
NO_CALL(INI),
CALL_WITH_REFERENCE_VOCAB(MATLAB),
CALL_WITH_REFERENCE_VOCAB(LEAN),
CALL_WITH_REFERENCE_VOCAB(FORM),
CALL_WITH_REFERENCE_VOCAB(MAGMA),
CALL_WITHOUT_REFERENCE_VOCAB(WOLFRAM),
CALL_WITH_REFERENCE_VOCAB(SOLIDITY),
CALL_WITHOUT_REFERENCE_VOCAB(TYPST),
CALL_WITH_REFERENCE_VOCAB(GDSCRIPT),
CALL_WITH_REFERENCE_VOCAB(GLEAM),
CALL_WITHOUT_REFERENCE_VOCAB(POWERSHELL),
CALL_WITH_REFERENCE_VOCAB(PASCAL),
CALL_WITH_REFERENCE_VOCAB(DLANG),
UNSUPPORTED(NIM),
CALL_WITHOUT_REFERENCE_VOCAB(SCHEME),
CALL_WITHOUT_REFERENCE_VOCAB(FENNEL),
CALL_WITHOUT_REFERENCE_VOCAB(FISH),
CALL_WITH_REFERENCE_VOCAB(AWK),
CALL_WITHOUT_REFERENCE_VOCAB(ZSH),
CALL_WITHOUT_REFERENCE_VOCAB(TCL),
CALL_WITH_REFERENCE_VOCAB(ADA),
CALL_WITHOUT_REFERENCE_VOCAB(AGDA),
CALL_WITHOUT_REFERENCE_VOCAB(RACKET),
CALL_WITH_REFERENCE_VOCAB(ODIN),
CALL_WITH_REFERENCE_VOCAB(RESCRIPT),
CALL_WITHOUT_REFERENCE_VOCAB(PURESCRIPT),
CALL_WITHOUT_REFERENCE_VOCAB(NICKEL),
CALL_WITH_REFERENCE_VOCAB(CRYSTAL),
CALL_WITH_REFERENCE_VOCAB(TEAL),
CALL_WITH_REFERENCE_VOCAB(HARE),
CALL_WITH_REFERENCE_VOCAB(PONY),
CALL_WITH_REFERENCE_VOCAB(LUAU),
NO_CALL(JANET),
CALL_WITH_REFERENCE_VOCAB(SWAY),
CALL_WITHOUT_REFERENCE_VOCAB(NASM),
NO_CALL(ASSEMBLY),
NO_CALL(ASTRO),
NO_CALL(BLADE),
CALL_WITH_REFERENCE_VOCAB(JUST),
CALL_WITH_REFERENCE_VOCAB(GOTEMPLATE),
CALL_WITH_REFERENCE_VOCAB(TEMPL),
NO_CALL(LIQUID),
NO_CALL(JINJA2),
CALL_WITH_REFERENCE_VOCAB(PRISMA),
NO_CALL(HYPRLANG),
NO_CALL(DOTENV),
NO_CALL(DIFF),
CALL_WITH_REFERENCE_VOCAB(WGSL),
NO_CALL(KDL),
NO_CALL(JSON5),
CALL_WITH_REFERENCE_VOCAB(JSONNET),
NO_CALL(RON),
NO_CALL(THRIFT),
NO_CALL(CAPNP),
NO_CALL(PROPERTIES),
NO_CALL(SSHCONFIG),
NO_CALL(BIBTEX),
CALL_WITH_REFERENCE_VOCAB(STARLARK),
CALL_WITH_REFERENCE_VOCAB(BICEP),
NO_CALL(CSV),
NO_CALL(REQUIREMENTS),
CALL_WITH_REFERENCE_VOCAB(HLSL),
CALL_WITH_REFERENCE_VOCAB(VHDL),
CALL_WITH_REFERENCE_VOCAB(SYSTEMVERILOG),
CALL_WITH_REFERENCE_VOCAB(DEVICETREE),
CALL_WITHOUT_REFERENCE_VOCAB(LINKERSCRIPT),
CALL_WITH_REFERENCE_VOCAB(GN),
NO_CALL(KCONFIG),
CALL_WITH_REFERENCE_VOCAB(BITBAKE),
NO_CALL(SMALI),
NO_CALL(TABLEGEN),
CALL_WITH_REFERENCE_VOCAB(ISPC),
CALL_WITH_REFERENCE_VOCAB(CAIRO),
CALL_WITH_REFERENCE_VOCAB(MOVE),
CALL_WITH_REFERENCE_VOCAB(SQUIRREL),
CALL_WITH_REFERENCE_VOCAB(FUNC),
NO_CALL(REGEX),
NO_CALL(JSDOC),
NO_CALL(RST),
NO_CALL(BEANCOUNT),
NO_CALL(MERMAID),
CALL_WITHOUT_REFERENCE_VOCAB(PUPPET),
NO_CALL(PO),
NO_CALL(GITATTRIBUTES),
NO_CALL(GITIGNORE),
CALL_WITH_REFERENCE_VOCAB(SLANG),
CALL_WITHOUT_REFERENCE_VOCAB(LLVM_IR),
NO_CALL(SMITHY),
NO_CALL(WIT),
CALL_WITH_REFERENCE_VOCAB(TLAPLUS),
NO_CALL(PKL),
NO_CALL(GOMOD),
CALL_WITH_REFERENCE_VOCAB(APEX),
NO_CALL(SOQL),
NO_CALL(SOSL),
NO_CALL(KUSTOMIZE),
NO_CALL(K8S),
CALL_WITH_REFERENCE_VOCAB(PINE),
CALL_WITH_REFERENCE_VOCAB(QML),
CALL_WITH_REFERENCE_VOCAB(CFSCRIPT),
CALL_WITH_REFERENCE_VOCAB(CFML),
CALL_WITH_REFERENCE_VOCAB(MOJO),
CALL_WITH_REFERENCE_VOCAB(OBJECTSCRIPT_UDL),
CALL_WITH_REFERENCE_VOCAB(OBJECTSCRIPT_ROUTINE),
TRANSFORM_ONLY(OBJECTSCRIPT_EXPORT),
};
#undef CALL_WITH_REFERENCE_VOCAB
#undef CALL_WITHOUT_REFERENCE_VOCAB
#undef NO_CALL
#undef TRANSFORM_ONLY
#undef UNSUPPORTED
_Static_assert(sizeof(LANGUAGE_CAPABILITIES) / sizeof(LANGUAGE_CAPABILITIES[0]) == CBM_LANG_COUNT,
"language capability ledger must track CBM_LANG_COUNT");
static int grammar_has_symbol(const TSLanguage *language, const char *expected_name) {
uint32_t symbol_count = ts_language_symbol_count(language);
for (TSSymbol symbol = 0; symbol < symbol_count; symbol++) {
const char *actual_name = ts_language_symbol_name(language, symbol);
if (actual_name && strcmp(actual_name, expected_name) == 0) {
return 1;
}
}
return 0;
}
/* Every enum value must either be unsupported (NULL) or map to a spec tagged
* with that exact language. A missing designated initializer must never alias
* the zero-initialized CBM_LANG_GO slot. */
TEST(repro_language_specs_do_not_alias_other_languages) {
int failures = 0;
for (int value = 0; value < CBM_LANG_COUNT; value++) {
CBMLanguage language = (CBMLanguage)value;
const CBMLangSpec *spec = cbm_lang_spec(language);
if (spec && spec->language != language) {
fprintf(
stderr,
" [language-registry] invariant=spec_language_mismatch requested=%d actual=%d\n",
value, (int)spec->language);
failures++;
}
}
ASSERT_EQ(failures, 0);
PASS();
}
TEST(repro_language_capability_ledger_covers_every_enum) {
int failures = 0;
int counts[CAP_COUNT] = {0};
for (int value = 0; value < CBM_LANG_COUNT; value++) {
const LanguageCapabilityEntry *entry = &LANGUAGE_CAPABILITIES[value];
const CBMLangSpec *spec = cbm_lang_spec((CBMLanguage)value);
if (entry->capability <= CAP_UNSET || entry->capability >= CAP_COUNT || !entry->name ||
!entry->reason) {
fprintf(stderr, " [language-registry] invariant=capability_unset requested=%d\n",
value);
failures++;
continue;
}
counts[entry->capability]++;
if (entry->capability == CAP_UNSUPPORTED || entry->capability == CAP_TRANSFORM_ONLY) {
if (spec) {
fprintf(stderr,
" [language-registry] lang=%s "
"invariant=non_grammar_capability_spec_nonnull capability=%d actual=%d\n",
entry->name, (int)entry->capability, (int)spec->language);
failures++;
}
continue;
}
if (!spec || spec->language != (CBMLanguage)value) {
fprintf(stderr, " [language-registry] lang=%s invariant=registered_spec_missing\n",
entry->name);
failures++;
continue;
}
int has_call_metadata = spec->call_node_types && spec->call_node_types[0];
int expects_call_metadata = entry->capability == CAP_CALL_WITH_REFERENCE_VOCAB ||
entry->capability == CAP_CALL_WITHOUT_REFERENCE_VOCAB;
if (has_call_metadata != expects_call_metadata) {
fprintf(stderr,
" [language-registry] lang=%s invariant=call_capability_mismatch expected=%d "
"actual=%d\n",
entry->name, expects_call_metadata, has_call_metadata);
failures++;
}
}
if (counts[CAP_CALL_WITH_REFERENCE_VOCAB] != 86 ||
counts[CAP_CALL_WITHOUT_REFERENCE_VOCAB] != 25 || counts[CAP_NO_CALL] != 50 ||
counts[CAP_TRANSFORM_ONLY] != 1 || counts[CAP_UNSUPPORTED] != 1) {
fprintf(stderr,
" [language-registry] invariant=capability_partition call_ref_vocab=%d ref_gap=%d "
"no_call=%d transform_only=%d unsupported=%d\n",
counts[CAP_CALL_WITH_REFERENCE_VOCAB], counts[CAP_CALL_WITHOUT_REFERENCE_VOCAB],
counts[CAP_NO_CALL], counts[CAP_TRANSFORM_ONLY], counts[CAP_UNSUPPORTED]);
failures++;
}
ASSERT_EQ(failures, 0);
PASS();
}
TEST(repro_call_argument_matrices_equal_call_capability_ledger) {
enum {
EXPECTED_MATRIX_A_ROWS = 67,
EXPECTED_MATRIX_B_ROWS = 46,
EXPECTED_CALL_CAPABLE_LANGUAGES = 111,
EXPECTED_NON_CALL_LANGUAGES = 52,
EXPECTED_NON_CALL_DOMAIN_CONTROLS = 2,
};
CBMLanguage matrix_a_ids[CBM_LANG_COUNT];
CBMLanguage matrix_b_ids[CBM_LANG_COUNT];
unsigned int matrix_a_counts[CBM_LANG_COUNT] = {0};
unsigned int matrix_b_counts[CBM_LANG_COUNT] = {0};
size_t matrix_a_rows = repro_call_argument_matrix_a_copy_language_ids(
matrix_a_ids, sizeof(matrix_a_ids) / sizeof(matrix_a_ids[0]));
size_t matrix_b_rows = repro_call_argument_matrix_b_copy_language_ids(
matrix_b_ids, sizeof(matrix_b_ids) / sizeof(matrix_b_ids[0]));
int failures = 0;
if (matrix_a_rows != EXPECTED_MATRIX_A_ROWS) {
fprintf(stderr,
" [language-registry] matrix=A invariant=row_count expected=%d actual=%zu\n",
EXPECTED_MATRIX_A_ROWS, matrix_a_rows);
failures++;
}
if (matrix_b_rows != EXPECTED_MATRIX_B_ROWS) {
fprintf(stderr,
" [language-registry] matrix=B invariant=row_count expected=%d actual=%zu\n",
EXPECTED_MATRIX_B_ROWS, matrix_b_rows);
failures++;
}
size_t matrix_a_copied =
matrix_a_rows < CBM_LANG_COUNT ? matrix_a_rows : (size_t)CBM_LANG_COUNT;
for (size_t row = 0; row < matrix_a_copied; row++) {
int language_id = (int)matrix_a_ids[row];
if (language_id < 0 || language_id >= CBM_LANG_COUNT) {
fprintf(stderr,
" [language-registry] matrix=A row=%zu invariant=language_id_range id=%d\n",
row, language_id);
failures++;
continue;
}
matrix_a_counts[language_id]++;
}
size_t matrix_b_copied =
matrix_b_rows < CBM_LANG_COUNT ? matrix_b_rows : (size_t)CBM_LANG_COUNT;
for (size_t row = 0; row < matrix_b_copied; row++) {
int language_id = (int)matrix_b_ids[row];
if (language_id < 0 || language_id >= CBM_LANG_COUNT) {
fprintf(stderr,
" [language-registry] matrix=B row=%zu invariant=language_id_range id=%d\n",
row, language_id);
failures++;
continue;
}
matrix_b_counts[language_id]++;
}
int call_capable_languages = 0;
int covered_call_capable_languages = 0;
int explicitly_non_call_languages = 0;
int covered_non_call_domain_controls = 0;
for (int language_id = 0; language_id < CBM_LANG_COUNT; language_id++) {
const LanguageCapabilityEntry *entry = &LANGUAGE_CAPABILITIES[language_id];
const char *language_name = entry->name ? entry->name : "<unclassified>";
unsigned int matrix_a_count = matrix_a_counts[language_id];
unsigned int matrix_b_count = matrix_b_counts[language_id];
unsigned int total_count = matrix_a_count + matrix_b_count;
int call_capable = entry->capability == CAP_CALL_WITH_REFERENCE_VOCAB ||
entry->capability == CAP_CALL_WITHOUT_REFERENCE_VOCAB;
if (matrix_a_count > 1) {
fprintf(stderr,
" [language-registry] matrix=A lang=%s id=%d invariant=duplicate_language "
"actual=%u\n",
language_name, language_id, matrix_a_count);
failures++;
}
if (matrix_b_count > 1) {
fprintf(stderr,
" [language-registry] matrix=B lang=%s id=%d invariant=duplicate_language "
"actual=%u\n",
language_name, language_id, matrix_b_count);
failures++;
}
if (matrix_a_count && matrix_b_count) {
fprintf(stderr,
" [language-registry] lang=%s id=%d invariant=matrix_overlap matrix_a=%u "
"matrix_b=%u\n",
language_name, language_id, matrix_a_count, matrix_b_count);
failures++;
}
if (call_capable) {
call_capable_languages++;
if (total_count == 0) {
fprintf(stderr,
" [language-registry] lang=%s id=%d "
"invariant=call_capability_missing_from_matrices\n",
language_name, language_id);
failures++;
} else {
covered_call_capable_languages++;
if (total_count != 1) {
fprintf(stderr,
" [language-registry] lang=%s id=%d "
"invariant=call_capability_matrix_count expected=1 actual=%u\n",
language_name, language_id, total_count);
failures++;
}
}
continue;
}
if (entry->capability != CAP_NO_CALL && entry->capability != CAP_TRANSFORM_ONLY &&
entry->capability != CAP_UNSUPPORTED) {
fprintf(stderr,
" [language-registry] lang=%s id=%d "
"invariant=remaining_capability_explicit "
"expected=NO_CALL_or_TRANSFORM_ONLY_or_UNSUPPORTED "
"actual=%d\n",
language_name, language_id, (int)entry->capability);
failures++;
} else {
explicitly_non_call_languages++;
}
int expected_domain_control =
language_id == CBM_LANG_DIFF || language_id == CBM_LANG_BIBTEX;
if (expected_domain_control && total_count == 1) {
covered_non_call_domain_controls++;
} else if (total_count != 0 || expected_domain_control) {
fprintf(stderr,
" [language-registry] lang=%s id=%d "
"invariant=non_call_domain_control_coverage expected=%d "
"matrix_a=%u matrix_b=%u\n",
language_name, language_id, expected_domain_control, matrix_a_count,
matrix_b_count);
failures++;
}
}
if (call_capable_languages != EXPECTED_CALL_CAPABLE_LANGUAGES ||
covered_call_capable_languages != EXPECTED_CALL_CAPABLE_LANGUAGES ||
explicitly_non_call_languages != EXPECTED_NON_CALL_LANGUAGES ||
covered_non_call_domain_controls != EXPECTED_NON_CALL_DOMAIN_CONTROLS) {
fprintf(stderr,
" [language-registry] invariant=matrix_ledger_partition "
"call_capable_expected=%d call_capable_actual=%d covered_actual=%d "
"non_call_expected=%d non_call_actual=%d "
"non_call_controls_expected=%d non_call_controls_actual=%d\n",
EXPECTED_CALL_CAPABLE_LANGUAGES, call_capable_languages,
covered_call_capable_languages, EXPECTED_NON_CALL_LANGUAGES,
explicitly_non_call_languages, EXPECTED_NON_CALL_DOMAIN_CONTROLS,
covered_non_call_domain_controls);
failures++;
}
ASSERT_EQ(failures, 0);
PASS();
}
/* A call-node registration that does not exist in the compiled grammar can
* never match an AST node. Check the compiled language instead of trusting
* hand-maintained spec strings or the graph index. */
TEST(repro_primary_call_metadata_names_exist_in_grammars) {
int failures = 0;
for (int value = 0; value < CBM_LANG_COUNT; value++) {
const LanguageCapabilityEntry *entry = &LANGUAGE_CAPABILITIES[value];
const CBMLangSpec *spec = cbm_lang_spec((CBMLanguage)value);
const TSLanguage *language = cbm_ts_language((CBMLanguage)value);
if (!spec || !spec->call_node_types || !spec->call_node_types[0]) {
continue;
}
if (!language) {
fprintf(stderr,
" [language-registry] lang=%s invariant=call_metadata_grammar_missing\n",
entry->name);
failures++;
continue;
}
for (int i = 0; spec->call_node_types[i]; i++) {
const char *call_node_type = spec->call_node_types[i];
if (!grammar_has_symbol(language, call_node_type)) {
fprintf(stderr,
" [language-registry] lang=%s "
"invariant=call_node_type_exists node=%s\n",
entry->name, call_node_type);
failures++;
}
}
}
ASSERT_EQ(failures, 0);
PASS();
}
/* LinkerScript's compiled grammar root is `linkerscript`, while the language
* spec currently registers `source_file`. Path-derived module QNs hide the
* mismatch, so exercise the AST-to-metadata contract directly. */
TEST(repro_linkerscript_root_is_registered_as_module) {
static const char source[] = "SECTIONS { .text : { *(.text) } }\n";
const CBMLangSpec *spec = cbm_lang_spec(CBM_LANG_LINKERSCRIPT);
const TSLanguage *language = cbm_ts_language(CBM_LANG_LINKERSCRIPT);
TSParser *parser = ts_parser_new();
int failures = 0;
if (!spec || !language || !parser || !ts_parser_set_language(parser, language)) {
fprintf(stderr, " [language-registry] lang=LINKERSCRIPT invariant=parser_setup\n");
failures++;
} else {
TSTree *tree = ts_parser_parse_string(parser, NULL, source, (uint32_t)strlen(source));
if (!tree) {
fprintf(stderr,
" [language-registry] lang=LINKERSCRIPT invariant=parse_tree_nonnull\n");
failures++;
} else {
TSNode root = ts_tree_root_node(tree);
const char *root_kind = ts_node_type(root);
int registered = 0;
if (spec->module_node_types) {
for (int i = 0; spec->module_node_types[i]; i++) {
if (strcmp(spec->module_node_types[i], root_kind) == 0) {
registered = 1;
break;
}
}
}
if (ts_node_has_error(root)) {
fprintf(stderr,
" [language-registry] lang=LINKERSCRIPT invariant=valid_fixture\n");
failures++;
}
if (!registered) {
fprintf(stderr,
" [language-registry] lang=LINKERSCRIPT "
"invariant=root_registered_as_module root=%s\n",
root_kind);
failures++;
}
ts_tree_delete(tree);
}
}
if (parser) {
ts_parser_delete(parser);
}
ASSERT_EQ(failures, 0);
PASS();
}
SUITE(repro_language_registry) {
RUN_TEST(repro_language_specs_do_not_alias_other_languages);
RUN_TEST(repro_language_capability_ledger_covers_every_enum);
RUN_TEST(repro_call_argument_matrices_equal_call_capability_ledger);
RUN_TEST(repro_primary_call_metadata_names_exist_in_grammars);
RUN_TEST(repro_linkerscript_root_is_registered_as_module);
}
File diff suppressed because it is too large Load Diff
+64 -11
View File
@@ -57,9 +57,8 @@
* lsp_static_call (1904) ClassName.staticMethod()
* lsp_interface_resolve (1985) iface-typed call, SOLE concrete impl
* lsp_interface_dispatch (1990) iface-typed call, no sole impl
* lsp_method_ref_ctor (2591) ClassName::new, ctor indexed
* lsp_method_ref_ctor_synth(2594) ClassName::new, ctor NOT in registry
* lsp_method_ref (2614) Type::instanceMethod reference
* lsp_method_ref_ctor (2758) ClassName::new, ctor indexed
* lsp_method_ref (2783) Type::instanceMethod reference
* lsp_constructor (2787) new Foo(), ctor indexed
* lsp_constructor_synth (2792) new Foo(), ctor NOT in registry
* lsp_unresolved (1801) fallback marker for an unresolved call
@@ -153,6 +152,63 @@ static int assert_lsp_strategy(const char *filename, const char *src,
return rc;
}
static int count_edges_from_callable(cbm_store_t *store, const char *project, const char *edge_type,
const char *callable_name) {
cbm_edge_t *edges = NULL;
int edge_count = 0;
if (cbm_store_find_edges_by_type(store, project, edge_type, &edges, &edge_count) !=
CBM_STORE_OK) {
return 0;
}
int matches = 0;
for (int i = 0; i < edge_count; i++) {
cbm_node_t source = {0};
if (cbm_store_find_node_by_id(store, edges[i].source_id, &source) == CBM_STORE_OK &&
source.name && strcmp(source.name, callable_name) == 0 && source.label &&
(strcmp(source.label, "Function") == 0 || strcmp(source.label, "Method") == 0)) {
matches++;
}
cbm_node_free_fields(&source);
}
cbm_store_free_edges(edges, edge_count);
return matches;
}
static int assert_method_reference_semantics(const char *filename, const char *src,
int target_is_materialized) {
RProj project;
cbm_store_t *store = rh_index(&project, filename, src);
if (!store) {
printf(" %sFAIL%s %s:%d: method-reference fixture failed to index\n", tf_red(), tf_reset(),
__FILE__, __LINE__);
rh_cleanup(&project, store);
return 1;
}
int references = count_edges_from_callable(store, project.project, "CALL_REFERENCE", "run");
int calls = count_edges_from_callable(store, project.project, "CALLS", "run");
int usages = count_edges_from_callable(store, project.project, "USAGE", "run");
int rc = 0;
if (references != (target_is_materialized ? 1 : 0)) {
printf(" %sFAIL%s %s:%d: method reference count expected=%d actual=%d\n", tf_red(),
tf_reset(), __FILE__, __LINE__, target_is_materialized ? 1 : 0, references);
rc = 1;
}
if (calls != 0) {
printf(" %sFAIL%s %s:%d: method reference fabricated %d CALLS edges\n", tf_red(),
tf_reset(), __FILE__, __LINE__, calls);
rc = 1;
}
if (!target_is_materialized && usages < 1) {
printf(" %sFAIL%s %s:%d: unmaterialized constructor reference lost its USAGE\n", tf_red(),
tf_reset(), __FILE__, __LINE__);
rc = 1;
}
rh_cleanup(&project, store);
return rc;
}
/*
* assert_no_resolvable_edge — the ACCURATE invariant for a call whose callee is
* genuinely UNRESOLVABLE: undeclared (totallyUnknownFn), an external symbol
@@ -336,9 +392,8 @@ static const char kJavaMethodRefCtor[] =
" Maker run() { return Foo::new; }\n"
"}\n";
/* lsp_method_ref_ctor_synth — a constructor reference ClassName::new whose ctor
* is NOT in the registry, so the resolver synthesizes the ctor QN
* (java_lsp.c:2592-2594). Foo declares no explicit constructor. */
/* A constructor reference ClassName::new whose ctor is NOT materialized. The
* reference must remain ordinary USAGE; a class node is not a callable target. */
static const char kJavaMethodRefCtorSynth[] =
"interface Maker {\n"
" Foo make();\n"
@@ -607,17 +662,15 @@ TEST(repro_lsp_java_interface_dispatch) {
}
TEST(repro_lsp_java_method_ref_ctor) {
return assert_lsp_strategy("Client.java", kJavaMethodRefCtor,
"lsp_method_ref_ctor");
return assert_method_reference_semantics("Client.java", kJavaMethodRefCtor, 1);
}
TEST(repro_lsp_java_method_ref_ctor_synth) {
return assert_lsp_strategy("Client.java", kJavaMethodRefCtorSynth,
"lsp_method_ref_ctor_synth");
return assert_method_reference_semantics("Client.java", kJavaMethodRefCtorSynth, 0);
}
TEST(repro_lsp_java_method_ref) {
return assert_lsp_strategy("Client.java", kJavaMethodRef, "lsp_method_ref");
return assert_method_reference_semantics("Client.java", kJavaMethodRef, 1);
}
TEST(repro_lsp_java_constructor) {
+152 -25
View File
@@ -7,7 +7,8 @@
* same two invariants per (lang,strategy) — (a) inv_count_calls_by_source
* module_sourced == 0 and a callable-sourced CALLS edge exists, and (b)
* inv_edge_has_strategy(store, project, "<strategy>"). One TEST per
* (lang,strategy); SUITE(repro_lsp_kt_php_rust) at the bottom.
* (lang,strategy), plus two Rust dispatch-provenance guards;
* SUITE(repro_lsp_kt_php_rust) is at the bottom.
*
* WHAT THIS ASSERTS — the LSP RESOLUTION CONTRACT, one invariant per strategy.
* Each hybrid LSP resolves a call via a specific STRATEGY and tags the
@@ -36,16 +37,14 @@
* - RED = the strategy is dropped, lands Module-sourced, or never reaches
* the graph. The TEST documents the exact gap for the fixer.
*
* RUST CROSS-LSP IS NOT WIRED (documented gap). src/pipeline/pass_lsp_cross.c
* has NO CBM_LANG_RUST case in either cbm_pxc_has_cross_lsp (lines 282-298)
* or the cbm_pxc_run_one dispatch (lines 372-407). Go/C/C++/Python/PHP/Java/
* Kotlin are wired; Rust is absent. So rust_lsp.c can EMIT every strategy
* below, but those resolved calls never reach pass_lsp_cross → never become
* tagged CALLS edges in the graph. Every Rust strategy test is therefore
* expected RED until rust_lsp.c is wired into the pipeline. We assert the
* CORRECT (resolved) outcome anyway, per the reproduce-first contract: the
* red test is the durable record of the gap and turns GREEN the moment Rust
* is wired and resolving correctly.
* RUST CROSS-LSP CONTRACT. src/pipeline/pass_lsp_cross.c dispatches
* CBM_LANG_RUST through the project-wide Rust registry, including exact
* impl-block provenance and independent `impl Trait for Type` relations for
* default-only impls. The Rust cases below are intended GREEN regression
* guards. Dispatch follows Rust precedence: an inherent method wins over a
* same-named trait method; otherwise an exact trait override or unambiguous
* trait default may resolve. Missing or conflicting provenance fails closed:
* the resolver must not guess a trait or method from a shared leaf name.
*
* SKIPPED STRATEGIES (documented, not tested):
* Kotlin:
@@ -64,14 +63,15 @@
* synthetic module scope (a `mod foo;` declaration has no
* enclosing callable). It would violate invariant (a)
* (module_sourced == 0) by construction, so the shared
* runner cannot express it. Also blocked by the unwired-
* Rust gap above.
* runner cannot express it.
* - lsp_deref_dispatch / lsp_bound_dispatch / lsp_prelude_trait /
* lsp_short_name_unique / lsp_trait_ufcs_amb — emitted on harder-to-
* fixture paths (Deref chains, type-param bounds, prelude best-effort,
* crate-prefix short-name scan, multi-impl ambiguity). They are all also
* blocked by the unwired-Rust gap, so adding fragile fixtures for them
* buys nothing over the representative dispatch tests below; skipped.
* lsp_short_name_unique — emitted on harder-to-fixture paths (Deref
* chains, type-param bounds, prelude best-effort, crate-prefix short-name
* scan). Representative exact-provenance dispatch is covered below;
* these specialized strategies remain skipped. Multi-trait ambiguity is
* deliberately not a resolved strategy: without exact qualification,
* competing trait methods/defaults fail closed instead of emitting a
* guessed CALLS edge.
*
* STRATEGY INVENTORIES — every strategy literal grepped from each source:
* Kotlin (kotlin_lsp.c, grep '"lsp_kt_'):
@@ -113,7 +113,7 @@
* lsp_operator_trait (2443) a + b where T : Add (operator overload)
* lsp_macro (3832) known std macro (println!/vec!/panic!)
* lsp_deref_dispatch / lsp_bound_dispatch / lsp_prelude_trait /
* lsp_short_name_unique / lsp_trait_ufcs_amb / lsp_mod_decl (skipped, see above)
* lsp_short_name_unique / lsp_mod_decl (skipped, see above)
* lsp_unresolved (3393) fallback marker
*
* NOTE: line comments only inside this header (no nested block comments, per
@@ -188,6 +188,92 @@ static int assert_lsp_strategy(const char *filename, const char *src,
return rc;
}
/* Assert required/forbidden strategy provenance on the SAME Rust CALLS edge.
* Restricting by caller and callee avoids a false green from an unrelated edge
* elsewhere in a multi-file fixture (for example `mod contract;`). */
static int assert_rust_dispatch_files(const RFile *files, int nfiles, const char *caller_substr,
const char *callee_suffix, const char *required_strategy,
const char *forbidden_strategy) {
RProj lp;
cbm_store_t *store = rh_index_files(&lp, files, nfiles);
if (!store) {
printf(" %sFAIL%s %s:%d: index failed for Rust %s -> %s dispatch\n", tf_red(), tf_reset(),
__FILE__, __LINE__, caller_substr, callee_suffix);
rh_cleanup(&lp, store);
return 1;
}
cbm_edge_t *edges = NULL;
int edge_count = 0;
if (cbm_store_find_edges_by_type(store, lp.project, "CALLS", &edges, &edge_count) !=
CBM_STORE_OK) {
printf(" %sFAIL%s %s:%d: CALLS query failed for Rust %s -> %s dispatch\n", tf_red(),
tf_reset(), __FILE__, __LINE__, caller_substr, callee_suffix);
rh_cleanup(&lp, store);
return 1;
}
int matching_edges = 0;
int required_edges = 0;
int forbidden_edges = 0;
const char *observed_properties = NULL;
size_t suffix_len = strlen(callee_suffix);
for (int i = 0; i < edge_count; i++) {
cbm_node_t source;
cbm_node_t target;
if (cbm_store_find_node_by_id(store, edges[i].source_id, &source) != CBM_STORE_OK ||
cbm_store_find_node_by_id(store, edges[i].target_id, &target) != CBM_STORE_OK) {
continue;
}
const char *source_label = source.label ? source.label : "";
if (strcmp(source_label, "Function") != 0 && strcmp(source_label, "Method") != 0) {
continue;
}
if (!source.qualified_name || !strstr(source.qualified_name, caller_substr) ||
!target.qualified_name) {
continue;
}
size_t target_len = strlen(target.qualified_name);
if (target_len < suffix_len ||
strcmp(target.qualified_name + target_len - suffix_len, callee_suffix) != 0) {
continue;
}
matching_edges++;
const char *properties = edges[i].properties_json ? edges[i].properties_json : "{}";
if (!observed_properties) {
observed_properties = properties;
}
if (strstr(properties, required_strategy)) {
required_edges++;
}
if (forbidden_strategy && strstr(properties, forbidden_strategy)) {
forbidden_edges++;
}
}
int rc = 0;
if (matching_edges == 0 || required_edges == 0) {
printf(" %sFAIL%s %s:%d: Rust %s -> *%s has %d matching CALLS edges, "
"but none with required strategy %s (observed %s)\n",
tf_red(), tf_reset(), __FILE__, __LINE__, caller_substr, callee_suffix,
matching_edges, required_strategy,
observed_properties ? observed_properties : "<none>");
rc = 1;
}
if (forbidden_edges != 0) {
printf(" %sFAIL%s %s:%d: Rust %s -> *%s has %d CALLS edges with "
"forbidden strategy %s\n",
tf_red(), tf_reset(), __FILE__, __LINE__, caller_substr, callee_suffix,
forbidden_edges, forbidden_strategy);
rc = 1;
}
cbm_store_free_edges(edges, edge_count);
rh_cleanup(&lp, store);
return rc;
}
/* ════════════════════════════════════════════════════════════════════════════
* KOTLIN FIXTURES (main.kt) — every fixture keeps the call inside a callable
* (a top-level fun or a method) so callable-sourcing is testable, and the
@@ -399,10 +485,9 @@ static const char kPhpSelfStatic[] =
"}\n";
/* ════════════════════════════════════════════════════════════════════════════
* RUST FIXTURES (main.rs) — Rust cross-LSP is NOT wired into pass_lsp_cross
* (see header), so ALL of these are expected RED until rust_lsp.c is wired.
* Each fixture still exercises exactly the keyed construct so the test turns
* GREEN the moment Rust resolution reaches the graph.
* RUST FIXTURES (main.rs) — Rust cross-LSP is wired through pass_lsp_cross.
* These are intended GREEN guards for callable attribution, strategy tags,
* exact inherent/trait provenance, and default-only cross-file trait impls.
* ═══════════════════════════════════════════════════════════════════════════ */
/* lsp_direct — plain free-function call (rust_lsp.c:3580: path resolves to a
@@ -439,6 +524,35 @@ static const char kRustTraitDispatch[] =
" d.speak(2)\n"
"}\n";
/* Inherent methods take precedence over same-named trait methods. The shared
* Rust registry must preserve that distinction even though both concrete
* definitions have receiver S and the type also implements Pinger. */
static const char kRustInherentTraitCollision[] = "trait Pinger {\n"
" fn ping(&self) -> i32;\n"
"}\n"
"struct S;\n"
"impl Pinger for S {\n"
" fn ping(&self) -> i32 { 2 }\n"
"}\n"
"impl S {\n"
" fn ping(&self) -> i32 { 1 }\n"
"}\n"
"fn caller(s: &S) -> i32 { s.ping() }\n";
/* Cross-file default-only trait dispatch. S has no concrete `ping` Method def;
* the project-wide registry must therefore carry `impl Pinger for S` itself,
* rather than relying on provenance attached to an overriding method. */
static const RFile kRustDefaultTraitDispatch[] = {
{"contract.rs", "pub trait Pinger {\n"
" fn ping(&self) -> i32 { 7 }\n"
"}\n"
"pub struct S;\n"
"impl Pinger for S {}\n"},
{"main.rs", "mod contract;\n"
"use contract::Pinger;\n"
"fn caller(s: &contract::S) -> i32 { s.ping() }\n"},
};
/* lsp_constructor — Type::new() UFCS constructor (rust_lsp.c:3607: UFCS head is
* a type, short_name == "new"). */
static const char kRustConstructor[] =
@@ -594,7 +708,7 @@ TEST(repro_lsp_php_self_static) {
return assert_lsp_strategy("main.php", kPhpSelfStatic, "php_self_static");
}
/* Rust — all expected RED (cross-LSP not wired; see header). */
/* Rust — intended GREEN, including exact dispatch-provenance guards. */
TEST(repro_lsp_rust_direct) {
return assert_lsp_strategy("main.rs", kRustDirect, "lsp_direct");
}
@@ -606,6 +720,17 @@ TEST(repro_lsp_rust_trait_dispatch) {
return assert_lsp_strategy("main.rs", kRustTraitDispatch,
"lsp_trait_dispatch");
}
TEST(repro_lsp_rust_inherent_trait_collision) {
RFile file = {"main.rs", kRustInherentTraitCollision};
return assert_rust_dispatch_files(&file, 1, "caller", ".ping", "lsp_method_dispatch",
"lsp_trait_dispatch");
}
TEST(repro_lsp_rust_default_trait_dispatch_cross_file) {
return assert_rust_dispatch_files(
kRustDefaultTraitDispatch,
(int)(sizeof(kRustDefaultTraitDispatch) / sizeof(kRustDefaultTraitDispatch[0])), "caller",
".ping", "lsp_trait_dispatch", "lsp_method_dispatch");
}
TEST(repro_lsp_rust_constructor) {
return assert_lsp_strategy("main.rs", kRustConstructor, "lsp_constructor");
}
@@ -673,10 +798,12 @@ SUITE(repro_lsp_kt_php_rust) {
RUN_TEST(repro_lsp_php_static_resolved);
RUN_TEST(repro_lsp_php_self_static);
/* Rust — expected RED (cross-LSP not wired). */
/* Rust — eight strategy inventories plus two dispatch-provenance guards. */
RUN_TEST(repro_lsp_rust_direct);
RUN_TEST(repro_lsp_rust_method_dispatch);
RUN_TEST(repro_lsp_rust_trait_dispatch);
RUN_TEST(repro_lsp_rust_inherent_trait_collision);
RUN_TEST(repro_lsp_rust_default_trait_dispatch_cross_file);
RUN_TEST(repro_lsp_rust_constructor);
RUN_TEST(repro_lsp_rust_ufcs);
RUN_TEST(repro_lsp_rust_trait_ufcs);
+400
View File
@@ -0,0 +1,400 @@
/*
* repro_lsp_ordered_local.c — Behaviour-level REDs for positional signatures
* after a language's local/post-extraction registry refinement.
*
* These cases deliberately assert a downstream call target, not merely the
* extracted metadata. A GREEN transition therefore means the ordered slots
* survived far enough to change resolver selection/type inference. Strategy
* and target checks prevent a raw unique-name fallback from satisfying the
* oracle accidentally.
*/
#include "test_framework.h"
#include "arena.h"
#include "cbm.h"
#include "lsp/java_lsp.h"
#include "lsp/ts_lsp.h"
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
static bool ends_with(const char *text, const char *suffix) {
if (!text || !suffix)
return false;
size_t text_len = strlen(text);
size_t suffix_len = strlen(suffix);
return suffix_len <= text_len && strcmp(text + text_len - suffix_len, suffix) == 0;
}
static void print_resolved_calls(const CBMResolvedCallArray *calls) {
if (!calls)
return;
for (int i = 0; i < calls->count; i++) {
const CBMResolvedCall *call = &calls->items[i];
printf(" %s -> %s [%s %.2f kind=%d]\n", call->caller_qn ? call->caller_qn : "(null)",
call->callee_qn ? call->callee_qn : "(null)",
call->strategy ? call->strategy : "(null)", call->confidence, (int)call->kind);
}
}
static bool has_resolved_suffix(const CBMResolvedCallArray *calls, const char *caller_sub,
const char *callee_suffix, const char *strategy) {
if (!calls)
return false;
for (int i = 0; i < calls->count; i++) {
const CBMResolvedCall *call = &calls->items[i];
if (call->kind != CBM_RESOLVED_INVOCATION || call->confidence < 0.5f || !call->caller_qn ||
!strstr(call->caller_qn, caller_sub) || !ends_with(call->callee_qn, callee_suffix) ||
!call->strategy || strcmp(call->strategy, strategy) != 0) {
continue;
}
return true;
}
return false;
}
static bool has_resolved_exact(const CBMResolvedCallArray *calls, const char *caller_sub,
const char *callee_qn, const char *strategy) {
if (!calls)
return false;
for (int i = 0; i < calls->count; i++) {
const CBMResolvedCall *call = &calls->items[i];
if (call->kind == CBM_RESOLVED_INVOCATION && call->confidence >= 0.5f && call->caller_qn &&
strstr(call->caller_qn, caller_sub) && call->callee_qn &&
strcmp(call->callee_qn, callee_qn) == 0 && call->strategy &&
strcmp(call->strategy, strategy) == 0) {
return true;
}
}
return false;
}
static int assert_local_chained_target(CBMLanguage language, const char *rel_path,
const char *source, const char *callee_suffix,
const char *strategy) {
CBMFileResult *result = cbm_extract_file(source, (int)strlen(source), language,
"repro_ordered_local", rel_path, 0, NULL, NULL);
if (!result) {
printf(" ordered-local: extraction returned NULL for %s\n", rel_path);
return 1;
}
if (result->has_error) {
printf(" ordered-local: fixture parse failed for %s\n", rel_path);
cbm_free_result(result);
return 1;
}
bool found = has_resolved_suffix(&result->resolved_calls, "run", callee_suffix, strategy);
if (!found) {
printf(" ordered-local: missing run -> *%s via %s for %s (have %d)\n", callee_suffix,
strategy, rel_path, result->resolved_calls.count);
print_resolved_calls(&result->resolved_calls);
}
cbm_free_result(result);
return found ? 0 : 1;
}
/* Go's legacy type-usage projection stores [T,U] for `(first, second T,
* last U)`. Correct implicit inference requires the ordered [T,T,U] carrier,
* including preservation through the AST generic-refinement pass. */
static const char kGoGroupedGeneric[] =
"package main\n"
"type Left struct{}\n"
"type Right struct{}\n"
"func (Right) RightOnly() {}\n"
"func PickLast[T any, U any](first, second T, last U) U { return last }\n"
"func run(left Left, right Right) {\n"
" got := PickLast(left, left, right)\n"
" got.RightOnly()\n"
"}\n";
TEST(repro_lsp_ordered_local_go_grouped_generic) {
/* Go method nodes intentionally use a flat `module.Method` graph QN;
* receiver ownership is carried separately by receiver_type/parent_class.
* Only Right defines RightOnly, and lsp_type_dispatch therefore proves the
* positional generic result was inferred as Right. */
return assert_local_chained_target(CBM_LANG_GO, "main.go", kGoGroupedGeneric, "RightOnly",
"lsp_type_dispatch");
}
/* A C++ reference return wraps the function declarator in a
* reference_declarator. The local signature extractor must still reach the
* nested parameter list; otherwise `operator[](int)` collapses to arity zero
* and the exact-arity operator resolver correctly refuses it. */
static const char kCppReferenceReturnOperator[] =
"struct Item {};\n"
"struct Vec {\n"
" Item& operator[](int index) { static Item item; return item; }\n"
"};\n"
"void run(Vec& values) { values[0]; }\n";
static int assert_reference_return_operator_arity(CBMLanguage language, const char *rel_path,
const char *language_name) {
CBMFileResult *result =
cbm_extract_file(kCppReferenceReturnOperator, (int)strlen(kCppReferenceReturnOperator),
language, "repro_ordered_local", rel_path, 0, NULL, NULL);
if (!result || result->has_error) {
printf(" ordered-local: %s reference-return fixture extraction failed\n", language_name);
if (result)
cbm_free_result(result);
return 1;
}
bool counted_signature = false;
for (int i = 0; i < result->defs.count; i++) {
const CBMDefinition *definition = &result->defs.items[i];
if (definition->name && strcmp(definition->name, "operator[]") == 0 &&
definition->signature_param_count == 1 && definition->signature_param_types &&
definition->signature_param_types[0] &&
strcmp(definition->signature_param_types[0], "int") == 0) {
counted_signature = true;
break;
}
}
bool resolved =
has_resolved_suffix(&result->resolved_calls, "run", "operator[]", "lsp_operator");
if (!counted_signature || !resolved) {
printf(" ordered-local: %s reference-return operator counted=%d resolved=%d\n",
language_name, counted_signature, resolved);
print_resolved_calls(&result->resolved_calls);
}
cbm_free_result(result);
return counted_signature && resolved ? 0 : 1;
}
TEST(repro_lsp_ordered_local_cpp_reference_return_operator_arity) {
return assert_reference_return_operator_arity(CBM_LANG_CPP, "main.cpp", "C++");
}
/* CUDA uses its own vendored grammar. It aliases this syntax to the same
* reference_declarator node shape today, but must retain an independent guard
* so a future grammar update cannot silently reintroduce the arity collapse. */
TEST(repro_lsp_ordered_local_cuda_reference_return_operator_arity) {
return assert_reference_return_operator_arity(CBM_LANG_CUDA, "main.cu", "CUDA");
}
/* The shared C-family name resolver permits eight declarator wrappers, but the
* parameter walk historically stopped after five. A five-level pointer return
* therefore produced a named function whose signature silently had arity zero.
* C, C++, and CUDA each get a real grammar guard; GLSL has no pointer syntax. */
static const char kDeepPointerReturn[] = "int *****deep(int value) { return 0; }\n";
static int assert_deep_pointer_return_arity(CBMLanguage language, const char *rel_path,
const char *language_name) {
CBMFileResult *result =
cbm_extract_file(kDeepPointerReturn, (int)strlen(kDeepPointerReturn), language,
"repro_ordered_local", rel_path, 0, NULL, NULL);
if (!result || result->has_error) {
printf(" ordered-local: %s deep-pointer fixture extraction failed\n", language_name);
if (result)
cbm_free_result(result);
return 1;
}
bool found = false;
for (int i = 0; i < result->defs.count; i++) {
const CBMDefinition *definition = &result->defs.items[i];
if (definition->name && strcmp(definition->name, "deep") == 0 &&
definition->signature_param_count == 1 && definition->signature_param_types &&
definition->signature_param_types[0] &&
strcmp(definition->signature_param_types[0], "int") == 0) {
found = true;
break;
}
}
if (!found) {
printf(" ordered-local: %s deep-pointer return lost parameter arity\n", language_name);
}
cbm_free_result(result);
return found ? 0 : 1;
}
TEST(repro_lsp_ordered_local_c_deep_pointer_return_arity) {
return assert_deep_pointer_return_arity(CBM_LANG_C, "main.c", "C");
}
TEST(repro_lsp_ordered_local_cpp_deep_pointer_return_arity) {
return assert_deep_pointer_return_arity(CBM_LANG_CPP, "main.cpp", "C++");
}
TEST(repro_lsp_ordered_local_cuda_deep_pointer_return_arity) {
return assert_deep_pointer_return_arity(CBM_LANG_CUDA, "main.cu", "CUDA");
}
/* GREEN controls for the local TypeScript AST-rebuild path. The pseudo `this`
* parameter is not an argument and is excluded from the ordered carrier. The
* AST rebuild also skips it because it is not an identifier pattern, then
* reconstructs [T,T,U], so these pass even before ordered-carrier transport is
* adopted. They verify post-AST positional behavior, not transport itself. */
static const char kTsThisGeneric[] =
"class Left {}\n"
"class Right { rightOnly(): void {} }\n"
"function pickLast<T, U>(this: void, first: T, second: T, last: U): U {\n"
" return last;\n"
"}\n"
"function run(left: Left, right: Right): void {\n"
" const got = pickLast(left, left, right);\n"
" got.rightOnly();\n"
"}\n";
TEST(repro_lsp_ordered_local_control_typescript_this_receiver) {
return assert_local_chained_target(CBM_LANG_TYPESCRIPT, "main.ts", kTsThisGeneric,
"Right.rightOnly", "lsp_ts_method");
}
TEST(repro_lsp_ordered_local_control_tsx_this_receiver) {
return assert_local_chained_target(CBM_LANG_TSX, "main.tsx", kTsThisGeneric, "Right.rightOnly",
"lsp_ts_method");
}
/* Interface method signatures are synthesized by the TS AST shape sweep rather
* than supplied as graph defs. Put the same two parameter types in the wrong
* order first, then require the reversed two-argument overload plus a non-first
* zero-argument overload. Arity alone cannot pass this: the selected edge and
* return-chain type must agree on ordered parameter types, and the post-sweep
* overloads must remain visible through a finalized registry. */
static const char kTsInterfaceOverloadReturnChains[] =
"interface Reversed { reversedOnly(): void; }\n"
"interface Empty { emptyOnly(): void; }\n"
"interface Pair { pairOnly(): void; }\n"
"interface Factory {\n"
" pick(s: string, n: number): Reversed;\n"
" pick(): Empty;\n"
" pick(n: number, s: string): Pair;\n"
"}\n"
"function run(f: Factory): void {\n"
" f.pick().emptyOnly();\n"
" f.pick(1, 'x').pairOnly();\n"
"}\n";
static int assert_ts_interface_overload_return_chains(CBMLanguage language, bool jsx_mode,
const char *language_name) {
static const char *const type_names[] = {"Reversed", "Empty", "Pair", "Factory"};
CBMLSPDef defs[5];
memset(defs, 0, sizeof(defs));
for (int i = 0; i < 4; i++) {
defs[i].qualified_name = type_names[i];
defs[i].short_name = type_names[i];
defs[i].label = "Interface";
defs[i].def_module_qn = "repro.ts";
defs[i].is_interface = true;
defs[i].lang = language;
}
defs[0].qualified_name = "repro.ts.Reversed";
defs[1].qualified_name = "repro.ts.Empty";
defs[2].qualified_name = "repro.ts.Pair";
defs[3].qualified_name = "repro.ts.Factory";
defs[4].qualified_name = "repro.ts.run";
defs[4].short_name = "run";
defs[4].label = "Function";
defs[4].def_module_qn = "repro.ts";
defs[4].return_types = "void";
defs[4].lang = language;
CBMArena arena;
cbm_arena_init(&arena);
CBMTypeRegistry *registry = cbm_ts_build_cross_registry(&arena, defs, 5);
CBMResolvedCallArray calls = {0};
if (registry) {
cbm_run_ts_lsp_cross_with_registry(
&arena, kTsInterfaceOverloadReturnChains, (int)strlen(kTsInterfaceOverloadReturnChains),
"repro.ts", false, jsx_mode, false, registry, defs, 5, NULL, NULL, 0, NULL, &calls);
}
bool empty = has_resolved_suffix(&calls, "run", "Empty.emptyOnly", "lsp_ts_method");
bool pair = has_resolved_suffix(&calls, "run", "Pair.pairOnly", "lsp_ts_method");
if (!registry || !empty || !pair) {
printf(" ordered-local: %s interface overload chains empty=%d pair=%d registry=%d\n",
language_name, empty, pair, registry != NULL);
print_resolved_calls(&calls);
}
cbm_arena_destroy(&arena);
return registry && empty && pair ? 0 : 1;
}
TEST(repro_lsp_ordered_local_typescript_interface_overload_return_chains) {
return assert_ts_interface_overload_return_chains(CBM_LANG_TYPESCRIPT, false, "TypeScript");
}
TEST(repro_lsp_ordered_local_tsx_interface_overload_return_chains) {
return assert_ts_interface_overload_return_chains(CBM_LANG_TSX, true, "TSX");
}
/* Java overloads normally share one graph QN, which hides which overload won.
* The synthetic suffixes below make the registry choice observable while both
* entries retain the source-level short name `pick`. With both signatures
* collapsed to zero slots, the three-argument call falls back to the first
* registration ($zero); materializing its counted carrier must select $three. */
TEST(repro_lsp_ordered_local_java_overload_arity) {
static const char source[] =
"package demo;\n"
"public class Caller {\n"
" public void callZero(demo.Target t) { t.pick(); }\n"
" public void callThree(demo.Target t) { t.pick(1, \"x\", true); }\n"
"}\n";
const char *three_params[] = {"int", "java.lang.String", "boolean", NULL};
CBMLSPDef defs[3];
memset(defs, 0, sizeof(defs));
defs[0].qualified_name = "demo.Target";
defs[0].short_name = "Target";
defs[0].label = "Class";
defs[0].def_module_qn = "demo";
defs[0].lang = CBM_LANG_JAVA;
defs[1].qualified_name = "demo.Target.pick$zero";
defs[1].short_name = "pick";
defs[1].label = "Method";
defs[1].receiver_type = "demo.Target";
defs[1].def_module_qn = "demo";
defs[1].return_types = "void";
defs[1].signature_param_types = NULL;
defs[1].signature_param_count = 0;
defs[1].lang = CBM_LANG_JAVA;
defs[2].qualified_name = "demo.Target.pick$three";
defs[2].short_name = "pick";
defs[2].label = "Method";
defs[2].receiver_type = "demo.Target";
defs[2].def_module_qn = "demo";
defs[2].return_types = "void";
defs[2].signature_param_types = three_params;
defs[2].signature_param_count = 3;
defs[2].lang = CBM_LANG_JAVA;
CBMArena arena;
cbm_arena_init(&arena);
CBMResolvedCallArray calls = {0};
cbm_run_java_lsp_cross(&arena, source, (int)strlen(source), "demo", defs, 3, NULL, NULL, 0,
NULL, &calls);
bool zero =
has_resolved_exact(&calls, "callZero", "demo.Target.pick$zero", "lsp_type_dispatch");
bool three =
has_resolved_exact(&calls, "callThree", "demo.Target.pick$three", "lsp_type_dispatch");
bool wrong =
has_resolved_exact(&calls, "callThree", "demo.Target.pick$zero", "lsp_type_dispatch");
if (!zero || !three || wrong) {
printf(" ordered-local: Java overload selection zero=%d three=%d wrong-zero=%d\n", zero,
three, wrong);
print_resolved_calls(&calls);
}
cbm_arena_destroy(&arena);
return zero && three && !wrong ? 0 : 1;
}
SUITE(repro_lsp_ordered_local) {
RUN_TEST(repro_lsp_ordered_local_go_grouped_generic);
RUN_TEST(repro_lsp_ordered_local_cpp_reference_return_operator_arity);
RUN_TEST(repro_lsp_ordered_local_cuda_reference_return_operator_arity);
RUN_TEST(repro_lsp_ordered_local_c_deep_pointer_return_arity);
RUN_TEST(repro_lsp_ordered_local_cpp_deep_pointer_return_arity);
RUN_TEST(repro_lsp_ordered_local_cuda_deep_pointer_return_arity);
RUN_TEST(repro_lsp_ordered_local_control_typescript_this_receiver);
RUN_TEST(repro_lsp_ordered_local_control_tsx_this_receiver);
RUN_TEST(repro_lsp_ordered_local_typescript_interface_overload_return_chains);
RUN_TEST(repro_lsp_ordered_local_tsx_interface_overload_return_chains);
RUN_TEST(repro_lsp_ordered_local_java_overload_arity);
}
+980
View File
@@ -0,0 +1,980 @@
/*
* repro_lsp_ordered_signatures.c — Counted positional signature transport into
* the project-wide LSP registries.
*
* The public CBMDefinition.param_types array is a legacy type-usage projection:
* it is not positional. Cross-LSP registration instead receives the internal
* CBMLSPDef.signature_param_types + signature_param_count carrier. These tests
* start at the production registry-population boundary so a GREEN result means
* the registry consumer, not merely extraction, retained every argument slot.
*
* Contracts exercised per typed language:
* - non-palindromic [integer, UNKNOWN, string] catches reordering;
* - [integer, integer] catches accidental deduplication;
* - [integer, string, UNKNOWN] catches NULL-termination at an unknown slot;
* - only the exact text "?" means UNKNOWN; a language-valid nullable type is
* still a real, non-UNKNOWN type;
* - signature_param_count is authoritative. The non-palindromic carrier has
* a fourth, out-of-count bool entry which must not be scanned or merged, and
* a non-NULL carrier with count zero must materialize zero parameters.
*
* JavaScript has no source annotations in its primary carrier, so its contract
* is three explicit UNKNOWN slots (rather than silently collapsing to zero).
* Every listed frontend now consumes the counted carrier. These cases are
* permanent gating guards against reordering, deduplication, or truncation.
*/
#include "test_framework.h"
#include "arena.h"
#include "cbm.h"
#include "lang_specs.h"
#include "lsp/c_lsp.h"
#include "lsp/cs_lsp.h"
#include "lsp/go_lsp.h"
#include "lsp/java_lsp.h"
#include "lsp/kotlin_lsp.h"
#include "lsp/php_lsp.h"
#include "lsp/py_lsp.h"
#include "lsp/rust_lsp.h"
#include "lsp/ts_lsp.h"
#include "lsp/type_registry.h"
#include "lsp/type_rep.h"
#include "tree_sitter/api.h"
#include <string.h>
typedef CBMTypeRegistry *(*CrossRegistryBuilder)(CBMArena *, CBMLSPDef *, int);
typedef struct {
const char *tag;
CBMLanguage lang;
CrossRegistryBuilder build;
const char *integer_text;
const char *string_text;
const char *nullable_text;
const char *return_text;
} OrderedSignatureLanguage;
static TSTree *parse_lsp_fixture(CBMLanguage language, const char *source) {
const TSLanguage *grammar = cbm_ts_language(language);
TSParser *parser = ts_parser_new();
if (!grammar || !parser || !ts_parser_set_language(parser, grammar)) {
if (parser)
ts_parser_delete(parser);
return NULL;
}
TSTree *tree = ts_parser_parse_string(parser, NULL, source, (uint32_t)strlen(source));
ts_parser_delete(parser);
return tree;
}
static TSNode find_first_lsp_node_of_kind(TSNode node, const char *kind) {
if (strcmp(ts_node_type(node), kind) == 0)
return node;
uint32_t child_count = ts_node_child_count(node);
for (uint32_t i = 0; i < child_count; i++) {
TSNode found = find_first_lsp_node_of_kind(ts_node_child(node, i), kind);
if (!ts_node_is_null(found))
return found;
}
TSNode missing = {0};
return missing;
}
static CBMTypeRegistry *alloc_test_registry(CBMArena *arena) {
CBMTypeRegistry *registry = (CBMTypeRegistry *)cbm_arena_alloc(arena, sizeof(*registry));
if (!registry)
return NULL;
cbm_registry_init(registry, arena);
return registry;
}
static CBMTypeRegistry *build_java_cross_registry_for_contract(CBMArena *arena, CBMLSPDef *defs,
int def_count) {
CBMTypeRegistry *registry = alloc_test_registry(arena);
if (registry)
cbm_java_register_lsp_defs(arena, registry, defs, def_count);
return registry;
}
static CBMTypeRegistry *build_kotlin_cross_registry_for_contract(CBMArena *arena, CBMLSPDef *defs,
int def_count) {
CBMTypeRegistry *registry = alloc_test_registry(arena);
if (registry)
cbm_kotlin_register_lsp_defs(arena, registry, defs, def_count);
return registry;
}
static CBMTypeRegistry *build_php_cross_registry_for_contract(CBMArena *arena, CBMLSPDef *defs,
int def_count) {
CBMTypeRegistry *registry = alloc_test_registry(arena);
if (registry)
cbm_php_register_lsp_defs(arena, NULL, registry, defs, def_count);
return registry;
}
static int func_param_count(const CBMType *signature) {
if (!signature || signature->kind != CBM_TYPE_FUNC || !signature->data.func.param_types)
return 0;
int count = 0;
while (signature->data.func.param_types[count])
count++;
return count;
}
static const char *last_type_component(const char *name) {
if (!name)
return NULL;
const char *leaf = name;
for (const char *cursor = name; *cursor; cursor++) {
if (*cursor == '.' || *cursor == ':')
leaf = cursor + 1;
}
return leaf;
}
static const char *type_atom_name(const CBMType *type) {
if (!type)
return NULL;
switch (type->kind) {
case CBM_TYPE_BUILTIN:
return type->data.builtin.name;
case CBM_TYPE_NAMED:
return type->data.named.qualified_name;
case CBM_TYPE_TYPE_PARAM:
return type->data.type_param.name;
case CBM_TYPE_TEMPLATE:
return type->data.template_type.template_name;
case CBM_TYPE_ALIAS:
return type->data.alias.alias_qn;
default:
return NULL;
}
}
static int param_matches(const CBMType *actual, const char *expected) {
if (strcmp(expected, "?") == 0)
return cbm_type_is_unknown(actual);
if (!actual || cbm_type_is_unknown(actual))
return 0;
const char *name = type_atom_name(actual);
const char *leaf = last_type_component(name);
return leaf && strcmp(leaf, expected) == 0;
}
static int check_signature(const CBMTypeRegistry *registry, const char *qualified_name,
const char *case_name, const char *const *expected, int expected_count) {
const CBMRegisteredFunc *func = cbm_registry_lookup_func(registry, qualified_name);
if (!func) {
fprintf(stderr, " [ordered-signature] case=%s missing function=%s\n", case_name,
qualified_name);
return 1;
}
if (!func->signature || func->signature->kind != CBM_TYPE_FUNC) {
fprintf(stderr, " [ordered-signature] case=%s function=%s has no FUNC signature\n",
case_name, qualified_name);
return 1;
}
int actual_count = func_param_count(func->signature);
if (actual_count != expected_count) {
fprintf(stderr,
" [ordered-signature] case=%s function=%s expected-count=%d actual-count=%d\n",
case_name, qualified_name, expected_count, actual_count);
return 1;
}
for (int i = 0; i < expected_count; i++) {
const CBMType *actual = func->signature->data.func.param_types[i];
if (!param_matches(actual, expected[i])) {
const char *actual_name = type_atom_name(actual);
fprintf(stderr,
" [ordered-signature] case=%s function=%s slot=%d expected=%s "
"actual=%s kind=%d\n",
case_name, qualified_name, i, expected[i],
actual_name ? actual_name
: (cbm_type_is_unknown(actual) ? "?" : "<structured>"),
actual ? (int)actual->kind : -1);
return 1;
}
}
return 0;
}
static int check_single_return(const CBMTypeRegistry *registry, const char *qualified_name,
const char *case_name, const char *expected) {
const CBMRegisteredFunc *func = cbm_registry_lookup_func(registry, qualified_name);
if (!func || !func->signature || func->signature->kind != CBM_TYPE_FUNC ||
!func->signature->data.func.return_types || !func->signature->data.func.return_types[0]) {
fprintf(stderr, " [ordered-signature] case=%s function=%s missing return type\n",
case_name, qualified_name);
return 1;
}
const CBMType *actual = func->signature->data.func.return_types[0];
if (!param_matches(actual, expected)) {
const char *actual_name = type_atom_name(actual);
fprintf(stderr,
" [ordered-signature] case=%s function=%s expected-return=%s actual=%s kind=%d\n",
case_name, qualified_name, expected,
actual_name ? actual_name : (cbm_type_is_unknown(actual) ? "?" : "<structured>"),
(int)actual->kind);
return 1;
}
return 0;
}
static int check_nullable_signature(const CBMTypeRegistry *registry, const char *qualified_name,
const char *case_name) {
const CBMRegisteredFunc *func = cbm_registry_lookup_func(registry, qualified_name);
if (!func || !func->signature || func->signature->kind != CBM_TYPE_FUNC) {
fprintf(stderr, " [ordered-signature] case=%s missing FUNC signature=%s\n", case_name,
qualified_name);
return 1;
}
int actual_count = func_param_count(func->signature);
if (actual_count != 1) {
fprintf(stderr,
" [ordered-signature] case=%s function=%s expected-count=1 actual-count=%d\n",
case_name, qualified_name, actual_count);
return 1;
}
if (cbm_type_is_unknown(func->signature->data.func.param_types[0])) {
fprintf(stderr,
" [ordered-signature] case=%s function=%s nullable type collapsed to UNKNOWN\n",
case_name, qualified_name);
return 1;
}
return 0;
}
static void init_function_def(CBMLSPDef *def, CBMArena *arena,
const OrderedSignatureLanguage *language, const char *case_name,
const char **param_types, int param_count) {
memset(def, 0, sizeof(*def));
def->qualified_name = cbm_arena_sprintf(arena, "cbm.ordered.%s.%s", language->tag, case_name);
def->short_name = case_name;
def->label = "Function";
def->def_module_qn = cbm_arena_sprintf(arena, "cbm.ordered.%s", language->tag);
def->return_types = language->return_text;
def->signature_param_types = param_types;
def->signature_param_count = param_count;
def->lang = language->lang;
}
static int run_typed_language_contract(const OrderedSignatureLanguage *language) {
CBMArena arena;
cbm_arena_init(&arena);
/* The out-of-count bool is deliberate: count=3, so it must never surface. */
const char *nonpal_carrier[] = {language->integer_text, "?", language->string_text, "bool",
NULL};
const char *duplicate_carrier[] = {language->integer_text, language->integer_text, NULL};
const char *trailing_carrier[] = {language->integer_text, language->string_text, "?", NULL};
const char *nullable_carrier[] = {language->nullable_text, NULL};
/* A stale pointer with authoritative count zero must not be scanned. */
const char *count_zero_carrier[] = {language->integer_text, language->string_text, NULL};
CBMLSPDef defs[5];
init_function_def(&defs[0], &arena, language, "nonpal", nonpal_carrier, 3);
init_function_def(&defs[1], &arena, language, "duplicate", duplicate_carrier, 2);
init_function_def(&defs[2], &arena, language, "trailing_unknown", trailing_carrier, 3);
init_function_def(&defs[3], &arena, language, "nullable", nullable_carrier, 1);
init_function_def(&defs[4], &arena, language, "count_zero", count_zero_carrier, 0);
CBMTypeRegistry *registry = language->build(&arena, defs, 5);
if (!registry) {
fprintf(stderr, " [ordered-signature] lang=%s registry build failed\n", language->tag);
cbm_arena_destroy(&arena);
return 1;
}
/* C# deliberately canonicalizes source aliases in its semantic registry.
* Keep feeding valid source spellings (`int`, `string`) while checking the
* canonical leaf names, rather than weakening production parsing to satisfy
* a spelling-only assertion. */
const bool csharp_canonical = strcmp(language->tag, "csharp") == 0;
const char *integer_expected = csharp_canonical ? "Int32" : language->integer_text;
const char *string_expected = csharp_canonical ? "String" : language->string_text;
const char *nonpal_expected[] = {integer_expected, "?", string_expected};
const char *duplicate_expected[] = {integer_expected, integer_expected};
const char *trailing_expected[] = {integer_expected, string_expected, "?"};
int failures = 0;
failures += check_signature(registry, defs[0].qualified_name, "nonpal", nonpal_expected, 3);
failures +=
check_signature(registry, defs[1].qualified_name, "duplicate", duplicate_expected, 2);
failures +=
check_signature(registry, defs[2].qualified_name, "trailing_unknown", trailing_expected, 3);
failures += check_nullable_signature(registry, defs[3].qualified_name, "nullable");
failures += check_signature(registry, defs[4].qualified_name, "count_zero", NULL, 0);
cbm_arena_destroy(&arena);
return failures;
}
static int run_javascript_unknown_contract(const OrderedSignatureLanguage *language) {
CBMArena arena;
cbm_arena_init(&arena);
const char *unknown_carrier[] = {"?", "?", "?", "number", NULL};
const char *count_zero_carrier[] = {"number", NULL};
CBMLSPDef defs[2];
init_function_def(&defs[0], &arena, language, "three_unknowns", unknown_carrier, 3);
init_function_def(&defs[1], &arena, language, "count_zero", count_zero_carrier, 0);
CBMTypeRegistry *registry = language->build(&arena, defs, 2);
if (!registry) {
fprintf(stderr, " [ordered-signature] lang=%s registry build failed\n", language->tag);
cbm_arena_destroy(&arena);
return 1;
}
const char *expected[] = {"?", "?", "?"};
int failures = check_signature(registry, defs[0].qualified_name, "three_unknowns", expected, 3);
failures += check_signature(registry, defs[1].qualified_name, "count_zero", NULL, 0);
cbm_arena_destroy(&arena);
return failures;
}
static const OrderedSignatureLanguage C_LANGUAGE = {
"c", CBM_LANG_C, cbm_c_build_cross_registry, "int", "string", "Thing *", "void"};
static const OrderedSignatureLanguage CPP_LANGUAGE = {
"cpp", CBM_LANG_CPP, cbm_c_build_cross_registry, "int", "string", "std::optional<Thing>",
"void"};
static const OrderedSignatureLanguage CUDA_LANGUAGE = {
"cuda", CBM_LANG_CUDA, cbm_c_build_cross_registry, "int", "string", "Thing *", "void"};
static const OrderedSignatureLanguage GO_LANGUAGE = {
"go", CBM_LANG_GO, cbm_go_build_cross_registry, "int", "string", "*Thing", "void"};
static const OrderedSignatureLanguage JS_LANGUAGE = {
"javascript", CBM_LANG_JAVASCRIPT, cbm_ts_build_cross_registry, NULL, NULL, NULL, "void"};
static const OrderedSignatureLanguage TS_LANGUAGE = {"typescript",
CBM_LANG_TYPESCRIPT,
cbm_ts_build_cross_registry,
"number",
"string",
"Thing | null",
"void"};
static const OrderedSignatureLanguage TSX_LANGUAGE = {
"tsx", CBM_LANG_TSX, cbm_ts_build_cross_registry, "number", "string", "Thing | null", "void"};
static const OrderedSignatureLanguage PYTHON_LANGUAGE = {
"python", CBM_LANG_PYTHON, cbm_py_build_cross_registry, "int", "str", "Thing | None", "None"};
static const OrderedSignatureLanguage CSHARP_LANGUAGE = {
"csharp", CBM_LANG_CSHARP, cbm_cs_build_cross_registry, "int", "string", "Thing?", "void"};
static const OrderedSignatureLanguage RUST_LANGUAGE = {
"rust", CBM_LANG_RUST, cbm_rust_build_cross_registry, "i32", "String", "Option<Thing>", "()"};
static const OrderedSignatureLanguage JAVA_LANGUAGE = {
"java", CBM_LANG_JAVA, build_java_cross_registry_for_contract,
"int", "String", "java.util.List<?>",
"void"};
static const OrderedSignatureLanguage KOTLIN_LANGUAGE = {
"kotlin", CBM_LANG_KOTLIN, build_kotlin_cross_registry_for_contract, "Int", "String",
"Thing?", "Unit"};
static const OrderedSignatureLanguage PHP_LANGUAGE = {
"php", CBM_LANG_PHP, build_php_cross_registry_for_contract, "int", "string", "?Thing", "void"};
static void add_fixture_type(CBMTypeRegistry *registry, const char *qualified_name,
const char *short_name) {
CBMRegisteredType type = {0};
type.qualified_name = qualified_name;
type.short_name = short_name;
cbm_registry_add_type(registry, type);
}
static void add_php_seeded_method(CBMArena *arena, CBMTypeRegistry *registry,
const char *qualified_name, const char *short_name,
const char *receiver_type, const char *thing_qn,
const CBMType *return_type) {
const char *param_names[] = {"first", "middle", "last", NULL};
const CBMType *param_types[] = {cbm_type_builtin(arena, "int"), cbm_type_named(arena, thing_qn),
cbm_type_builtin(arena, "int"), NULL};
const CBMType *return_types[] = {return_type, NULL};
CBMRegisteredFunc function = {0};
function.qualified_name = qualified_name;
function.short_name = short_name;
function.receiver_type = receiver_type;
function.min_params = 3;
function.signature = cbm_type_func(arena, param_names, param_types, return_types);
cbm_registry_add_func(registry, function);
}
static void add_csharp_seeded_method(CBMArena *arena, CBMTypeRegistry *registry,
const char *qualified_name, const char *short_name,
const char *receiver_type, const char *thing_qn) {
const CBMType *param_types[] = {cbm_type_builtin(arena, "int"), cbm_type_named(arena, thing_qn),
cbm_type_builtin(arena, "int"), NULL};
const CBMType *return_types[] = {cbm_type_unknown(), NULL};
CBMRegisteredFunc function = {0};
function.qualified_name = qualified_name;
function.short_name = short_name;
function.receiver_type = receiver_type;
function.min_params = 3;
function.signature = cbm_type_func(arena, NULL, param_types, return_types);
cbm_registry_add_func(registry, function);
}
typedef struct {
int call_count;
const char *seen[8];
} SignatureParserProbe;
static const CBMType *parse_signature_probe_type(CBMArena *arena, const char *text,
void *parser_ctx) {
SignatureParserProbe *probe = (SignatureParserProbe *)parser_ctx;
probe->seen[probe->call_count++] = text;
if (strcmp(text, "parser-null") == 0)
return NULL;
return cbm_type_builtin(arena, text);
}
TEST(repro_lsp_signature_param_materializer_contract) {
CBMArena arena;
cbm_arena_init(&arena);
SignatureParserProbe probe = {0};
const char *ordered_texts[] = {"int", "?", "string", "outside-count"};
const CBMType **ordered = cbm_type_materialize_signature_params(
&arena, ordered_texts, 3, parse_signature_probe_type, &probe);
ASSERT_NOT_NULL(ordered);
ASSERT_EQ(probe.call_count, 2);
ASSERT_STR_EQ(probe.seen[0], "int");
ASSERT_STR_EQ(probe.seen[1], "string");
ASSERT_EQ(ordered[0]->kind, CBM_TYPE_BUILTIN);
ASSERT_STR_EQ(ordered[0]->data.builtin.name, "int");
ASSERT_TRUE(cbm_type_is_unknown(ordered[1]));
ASSERT_EQ(ordered[2]->kind, CBM_TYPE_BUILTIN);
ASSERT_STR_EQ(ordered[2]->data.builtin.name, "string");
ASSERT_NULL(ordered[3]);
memset(&probe, 0, sizeof(probe));
const char *duplicate_texts[] = {"int", "int"};
const CBMType **duplicates = cbm_type_materialize_signature_params(
&arena, duplicate_texts, 2, parse_signature_probe_type, &probe);
ASSERT_NOT_NULL(duplicates);
ASSERT_EQ(probe.call_count, 2);
ASSERT_STR_EQ(duplicates[0]->data.builtin.name, "int");
ASSERT_STR_EQ(duplicates[1]->data.builtin.name, "int");
ASSERT_NULL(duplicates[2]);
memset(&probe, 0, sizeof(probe));
const char *trailing_texts[] = {"int", "string", "?"};
const CBMType **trailing = cbm_type_materialize_signature_params(
&arena, trailing_texts, 3, parse_signature_probe_type, &probe);
ASSERT_NOT_NULL(trailing);
ASSERT_EQ(probe.call_count, 2);
ASSERT_STR_EQ(trailing[0]->data.builtin.name, "int");
ASSERT_STR_EQ(trailing[1]->data.builtin.name, "string");
ASSERT_TRUE(cbm_type_is_unknown(trailing[2]));
ASSERT_NULL(trailing[3]);
memset(&probe, 0, sizeof(probe));
const char *sentinel_texts[] = {"?", "?Foo", "Foo?", "parser-null", "", NULL};
const CBMType **sentinels = cbm_type_materialize_signature_params(
&arena, sentinel_texts, 6, parse_signature_probe_type, &probe);
ASSERT_NOT_NULL(sentinels);
ASSERT_EQ(probe.call_count, 3);
ASSERT_STR_EQ(probe.seen[0], "?Foo");
ASSERT_STR_EQ(probe.seen[1], "Foo?");
ASSERT_STR_EQ(probe.seen[2], "parser-null");
ASSERT_TRUE(cbm_type_is_unknown(sentinels[0]));
ASSERT_EQ(sentinels[1]->kind, CBM_TYPE_BUILTIN);
ASSERT_STR_EQ(sentinels[1]->data.builtin.name, "?Foo");
ASSERT_EQ(sentinels[2]->kind, CBM_TYPE_BUILTIN);
ASSERT_STR_EQ(sentinels[2]->data.builtin.name, "Foo?");
ASSERT_TRUE(cbm_type_is_unknown(sentinels[3]));
ASSERT_TRUE(cbm_type_is_unknown(sentinels[4]));
ASSERT_TRUE(cbm_type_is_unknown(sentinels[5]));
ASSERT_NULL(sentinels[6]);
memset(&probe, 0, sizeof(probe));
const CBMType **missing_texts =
cbm_type_materialize_signature_params(&arena, NULL, 3, parse_signature_probe_type, &probe);
ASSERT_NOT_NULL(missing_texts);
ASSERT_EQ(probe.call_count, 0);
ASSERT_TRUE(cbm_type_is_unknown(missing_texts[0]));
ASSERT_TRUE(cbm_type_is_unknown(missing_texts[1]));
ASSERT_TRUE(cbm_type_is_unknown(missing_texts[2]));
ASSERT_NULL(missing_texts[3]);
ASSERT_NULL(cbm_type_materialize_signature_params(&arena, ordered_texts, 0,
parse_signature_probe_type, &probe));
cbm_arena_destroy(&arena);
PASS();
}
TEST(repro_lsp_func_replace_returns_contract) {
CBMArena arena;
cbm_arena_init(&arena);
const CBMType *integer = cbm_type_builtin(&arena, "int");
const CBMType *string = cbm_type_builtin(&arena, "string");
const CBMType *old_return = cbm_type_builtin(&arena, "old-return");
const CBMType *new_return = cbm_type_builtin(&arena, "new-return");
const char *param_names[] = {"first", "middle", "last", NULL};
const CBMType *param_types[] = {integer, cbm_type_unknown(), string, NULL};
const CBMType *old_returns[] = {old_return, NULL};
const CBMType *old_signature = cbm_type_func(&arena, param_names, param_types, old_returns);
ASSERT_NOT_NULL(old_signature);
ASSERT_EQ(old_signature->kind, CBM_TYPE_FUNC);
const CBMType *new_returns[] = {new_return, NULL};
const CBMType *replaced = cbm_type_func_replace_returns(&arena, old_signature, new_returns);
ASSERT_NOT_NULL(replaced);
ASSERT_EQ(replaced->kind, CBM_TYPE_FUNC);
ASSERT(replaced != old_signature);
ASSERT_STR_EQ(replaced->data.func.param_names[0], "first");
ASSERT_STR_EQ(replaced->data.func.param_names[1], "middle");
ASSERT_STR_EQ(replaced->data.func.param_names[2], "last");
ASSERT_NULL(replaced->data.func.param_names[3]);
ASSERT(replaced->data.func.param_types[0] == integer);
ASSERT_TRUE(cbm_type_is_unknown(replaced->data.func.param_types[1]));
ASSERT(replaced->data.func.param_types[2] == string);
ASSERT_NULL(replaced->data.func.param_types[3]);
ASSERT(replaced->data.func.return_types[0] == new_return);
ASSERT_NULL(replaced->data.func.return_types[1]);
/* The source signature remains untouched. */
ASSERT(old_signature->data.func.return_types[0] == old_return);
ASSERT_NULL(old_signature->data.func.return_types[1]);
ASSERT_STR_EQ(old_signature->data.func.param_names[1], "middle");
ASSERT_TRUE(cbm_type_is_unknown(old_signature->data.func.param_types[1]));
cbm_arena_destroy(&arena);
PASS();
}
static int run_c_explicit_template_value_signature_contract(const char *source,
const char *registered_qn,
const char *function_node_kind) {
TSTree *tree = parse_lsp_fixture(CBM_LANG_CPP, source);
ASSERT_NOT_NULL(tree);
TSNode call = find_first_lsp_node_of_kind(ts_tree_root_node(tree), "call_expression");
ASSERT_FALSE(ts_node_is_null(call));
TSNode function = ts_node_child_by_field_name(call, "function", 8);
ASSERT_FALSE(ts_node_is_null(function));
ASSERT_STR_EQ(ts_node_type(function), function_node_kind);
CBMArena arena;
cbm_arena_init(&arena);
CBMTypeRegistry registry;
cbm_registry_init(&registry, &arena);
const char *param_names[] = {"value", "enabled", NULL};
const CBMType *param_types[] = {cbm_type_builtin(&arena, "long"),
cbm_type_builtin(&arena, "bool"), NULL};
const CBMType *return_types[] = {cbm_type_type_param(&arena, "T"), NULL};
const char *type_params[] = {"T", NULL};
CBMRegisteredFunc registered = {0};
registered.qualified_name = registered_qn;
registered.short_name = "materialize";
registered.signature = cbm_type_func(&arena, param_names, param_types, return_types);
registered.type_param_names = type_params;
registered.min_params = 2;
cbm_registry_add_func(&registry, registered);
CBMResolvedCallArray out = {0};
CLSPContext ctx;
c_lsp_init(&ctx, &arena, source, (int)strlen(source), &registry, NULL,
/*cpp_mode=*/true, &out);
const CBMType *value_type = c_eval_expr_type(&ctx, function);
ASSERT_NOT_NULL(value_type);
ASSERT_EQ(value_type->kind, CBM_TYPE_FUNC);
ASSERT_NOT_NULL(value_type->data.func.return_types);
ASSERT_NOT_NULL(value_type->data.func.return_types[0]);
ASSERT_EQ(value_type->data.func.return_types[0]->kind, CBM_TYPE_BUILTIN);
ASSERT_STR_EQ(value_type->data.func.return_types[0]->data.builtin.name, "int");
ASSERT_NOT_NULL(value_type->data.func.param_names);
ASSERT_STR_EQ(value_type->data.func.param_names[0], "value");
ASSERT_STR_EQ(value_type->data.func.param_names[1], "enabled");
ASSERT_NULL(value_type->data.func.param_names[2]);
ASSERT_NOT_NULL(value_type->data.func.param_types);
ASSERT_EQ(value_type->data.func.param_types[0]->kind, CBM_TYPE_BUILTIN);
ASSERT_STR_EQ(value_type->data.func.param_types[0]->data.builtin.name, "long");
ASSERT_EQ(value_type->data.func.param_types[1]->kind, CBM_TYPE_BUILTIN);
ASSERT_STR_EQ(value_type->data.func.param_types[1]->data.builtin.name, "bool");
ASSERT_NULL(value_type->data.func.param_types[2]);
ts_tree_delete(tree);
cbm_arena_destroy(&arena);
return 0;
}
TEST(repro_lsp_c_unqualified_explicit_template_value_preserves_signature) {
return run_c_explicit_template_value_signature_contract(
"void caller(){ materialize<int>(1L, true); }", "materialize", "template_function");
}
TEST(repro_lsp_c_qualified_explicit_template_value_preserves_signature) {
return run_c_explicit_template_value_signature_contract(
"void caller(){ ns::materialize<int>(1L, true); }", "ns.materialize",
"qualified_identifier");
}
TEST(repro_lsp_ordered_signatures_c_control) {
ASSERT_EQ(run_typed_language_contract(&C_LANGUAGE), 0);
PASS();
}
TEST(repro_lsp_ordered_signatures_cpp_control) {
ASSERT_EQ(run_typed_language_contract(&CPP_LANGUAGE), 0);
PASS();
}
TEST(repro_lsp_ordered_signatures_cuda_control) {
ASSERT_EQ(run_typed_language_contract(&CUDA_LANGUAGE), 0);
PASS();
}
TEST(repro_lsp_ordered_signatures_go) {
ASSERT_EQ(run_typed_language_contract(&GO_LANGUAGE), 0);
PASS();
}
TEST(repro_lsp_ordered_signatures_javascript) {
ASSERT_EQ(run_javascript_unknown_contract(&JS_LANGUAGE), 0);
PASS();
}
TEST(repro_lsp_ordered_signatures_typescript) {
ASSERT_EQ(run_typed_language_contract(&TS_LANGUAGE), 0);
PASS();
}
TEST(repro_lsp_ordered_signatures_tsx) {
ASSERT_EQ(run_typed_language_contract(&TSX_LANGUAGE), 0);
PASS();
}
TEST(repro_lsp_ordered_signatures_python) {
ASSERT_EQ(run_typed_language_contract(&PYTHON_LANGUAGE), 0);
PASS();
}
TEST(repro_lsp_ordered_signatures_csharp) {
ASSERT_EQ(run_typed_language_contract(&CSHARP_LANGUAGE), 0);
PASS();
}
TEST(repro_lsp_ordered_signatures_csharp_cross_return) {
CBMArena arena;
cbm_arena_init(&arena);
CBMLSPDef def;
init_function_def(&def, &arena, &CSHARP_LANGUAGE, "cross_return", NULL, 0);
def.return_types = "System.String";
CBMTypeRegistry *registry = cbm_cs_build_cross_registry(&arena, &def, 1);
int failures = registry ? check_single_return(registry, def.qualified_name,
"csharp-cross-return", "String")
: 1;
cbm_arena_destroy(&arena);
ASSERT_EQ(failures, 0);
PASS();
}
TEST(repro_lsp_ordered_signatures_rust) {
ASSERT_EQ(run_typed_language_contract(&RUST_LANGUAGE), 0);
PASS();
}
TEST(repro_lsp_ordered_signatures_java) {
ASSERT_EQ(run_typed_language_contract(&JAVA_LANGUAGE), 0);
PASS();
}
TEST(repro_lsp_ordered_signatures_kotlin) {
ASSERT_EQ(run_typed_language_contract(&KOTLIN_LANGUAGE), 0);
PASS();
}
TEST(repro_lsp_ordered_signatures_php) {
ASSERT_EQ(run_typed_language_contract(&PHP_LANGUAGE), 0);
PASS();
}
TEST(repro_lsp_ordered_signatures_csharp_ast_return_refinement) {
const char *module_qn = "cbm.ordered.csharp.local";
const char *subject_qn = "cbm.ordered.csharp.local.Subject";
const char *thing_qn = "cbm.ordered.csharp.local.Thing";
const char *result_qn = "cbm.ordered.csharp.local.Result";
const char *method_qn = "cbm.ordered.csharp.local.Subject.method";
const char *source =
"class Thing {}\n"
"class Result {}\n"
"class Subject {\n"
" Result method(int first, Thing middle, int last) { return new Result(); }\n"
"}\n";
TSTree *tree = parse_lsp_fixture(CBM_LANG_CSHARP, source);
ASSERT_NOT_NULL(tree);
TSNode root = ts_tree_root_node(tree);
ASSERT_FALSE(ts_node_has_error(root));
CBMArena arena;
cbm_arena_init(&arena);
CBMTypeRegistry registry;
cbm_registry_init(&registry, &arena);
add_fixture_type(&registry, thing_qn, "Thing");
add_fixture_type(&registry, result_qn, "Result");
add_fixture_type(&registry, subject_qn, "Subject");
add_csharp_seeded_method(&arena, &registry, method_qn, "method", subject_qn, thing_qn);
CBMResolvedCallArray out = {0};
CSLSPContext ctx;
cs_lsp_init(&ctx, &arena, source, (int)strlen(source), &registry, module_qn, &out);
cbm_cs_refine_ast_return_types(&ctx, &registry, root);
const char *expected[] = {"int", "Thing", "int"};
int failures =
check_signature(&registry, method_qn, "csharp-ast-return-refinement", expected, 3);
failures += check_single_return(&registry, method_qn, "csharp-ast-return-refinement", "Result");
ts_tree_delete(tree);
cbm_arena_destroy(&arena);
ASSERT_EQ(failures, 0);
PASS();
}
TEST(repro_lsp_ordered_signatures_rust_ast_return_refinement) {
const char *module_qn = "cbm.ordered.rust.local";
const char *thing_qn = "cbm.ordered.rust.local.Thing";
const char *result_qn = "cbm.ordered.rust.local.Result";
const char *subject_qn = "cbm.ordered.rust.local.Subject";
const char *top_qn = "cbm.ordered.rust.local.top";
const char *method_qn = "cbm.ordered.rust.local.Subject.method";
const char *source =
"struct Thing;\n"
"struct Result;\n"
"struct Subject;\n"
"fn top(first: i32, middle: Thing, last: i32) -> Result { Result }\n"
"impl Subject {\n"
" fn method(&self, first: i32, middle: Thing, last: i32) -> Result { Result }\n"
"}\n";
TSTree *tree = parse_lsp_fixture(CBM_LANG_RUST, source);
ASSERT_NOT_NULL(tree);
TSNode root = ts_tree_root_node(tree);
ASSERT_FALSE(ts_node_has_error(root));
const char *params[] = {"i32", "Thing", "i32", NULL};
CBMDefinition defs[5] = {0};
defs[0].name = "Thing";
defs[0].qualified_name = thing_qn;
defs[0].label = "Struct";
defs[1].name = "Result";
defs[1].qualified_name = result_qn;
defs[1].label = "Struct";
defs[2].name = "Subject";
defs[2].qualified_name = subject_qn;
defs[2].label = "Struct";
defs[3].name = "top";
defs[3].qualified_name = top_qn;
defs[3].label = "Function";
defs[3].param_types = params;
defs[3].signature_param_types = params;
defs[3].signature_param_count = 3;
defs[4].name = "method";
defs[4].qualified_name = method_qn;
defs[4].label = "Method";
defs[4].parent_class = subject_qn;
defs[4].param_types = params;
defs[4].signature_param_types = params;
defs[4].signature_param_count = 3;
CBMFileResult result = {0};
result.module_qn = module_qn;
result.defs.items = defs;
result.defs.count = 5;
CBMArena arena;
cbm_arena_init(&arena);
CBMTypeRegistry registry;
cbm_rust_build_local_registry(&arena, &registry, &result, module_qn, root, source);
const char *expected[] = {"i32", "Thing", "i32"};
int failures = check_signature(&registry, top_qn, "rust-top-return-refinement", expected, 3);
failures += check_single_return(&registry, top_qn, "rust-top-return-refinement", "Result");
failures += check_signature(&registry, method_qn, "rust-method-return-refinement", expected, 3);
failures +=
check_single_return(&registry, method_qn, "rust-method-return-refinement", "Result");
ts_tree_delete(tree);
cbm_arena_destroy(&arena);
ASSERT_EQ(failures, 0);
PASS();
}
TEST(repro_lsp_ordered_signatures_kotlin_local_registry) {
const char *source =
"class Thing {}\n"
"class Result {}\n"
"class Holder {\n"
" fun method(first: Int, middle: Thing?, last: Int): Result { return Result() }\n"
"}\n"
"fun top(first: Int, middle: Thing?, last: Int): Result { return Result() }\n";
TSTree *tree = parse_lsp_fixture(CBM_LANG_KOTLIN, source);
ASSERT_NOT_NULL(tree);
TSNode root = ts_tree_root_node(tree);
ASSERT_FALSE(ts_node_has_error(root));
CBMArena arena;
cbm_arena_init(&arena);
CBMTypeRegistry registry;
cbm_registry_init(&registry, &arena);
CBMResolvedCallArray out = {0};
KotlinLSPContext ctx;
kotlin_lsp_init(&ctx, &arena, source, (int)strlen(source), &registry,
"cbm.ordered.kotlin.local", "cbm.ordered.kotlin.local", "cbm", "Local.kt",
&out);
kotlin_lsp_process_file(&ctx, root);
/* Kotlin intentionally erases nullability in the registry model, but
* Thing? must still become the real named Thing rather than UNKNOWN. */
const char *expected[] = {"Int", "Thing", "Int"};
int failures = check_signature(&registry, "cbm.ordered.kotlin.local.Holder.method",
"kotlin-local-method", expected, 3);
failures += check_single_return(&registry, "cbm.ordered.kotlin.local.Holder.method",
"kotlin-local-method", "Result");
failures +=
check_signature(&registry, "cbm.ordered.kotlin.local.top", "kotlin-local-top", expected, 3);
failures += check_single_return(&registry, "cbm.ordered.kotlin.local.top", "kotlin-local-top",
"Result");
ts_tree_delete(tree);
cbm_arena_destroy(&arena);
ASSERT_EQ(failures, 0);
PASS();
}
TEST(repro_lsp_ordered_signatures_php_return_refinement) {
const char *module_qn = "cbm.ordered.php.local";
const char *subject_qn = "cbm.ordered.php.local.Subject";
const char *thing_qn = "cbm.ordered.php.local.Thing";
const char *result_qn = "cbm.ordered.php.local.Result";
const char *source =
"<?php\n"
"class Thing {}\n"
"class Result {}\n"
"class Subject {\n"
" public function declared(int $first, ?Thing $middle, int $last): Result {\n"
" return new Result();\n"
" }\n"
" /** @return Result */\n"
" public function documented(int $first, ?Thing $middle, int $last) {\n"
" return new Result();\n"
" }\n"
"}\n";
TSTree *tree = parse_lsp_fixture(CBM_LANG_PHP, source);
ASSERT_NOT_NULL(tree);
TSNode root = ts_tree_root_node(tree);
ASSERT_FALSE(ts_node_has_error(root));
CBMArena arena;
cbm_arena_init(&arena);
CBMTypeRegistry registry;
cbm_registry_init(&registry, &arena);
add_fixture_type(&registry, thing_qn, "Thing");
add_fixture_type(&registry, result_qn, "Result");
add_fixture_type(&registry, subject_qn, "Subject");
/* Seed the state expected immediately before AST return refinement. This
* isolates whether a declared/PHPDoc return rewrite preserves parameters;
* it does not depend on the still-RED PHP registration materializer. */
add_php_seeded_method(&arena, &registry, "cbm.ordered.php.local.Subject.declared", "declared",
subject_qn, thing_qn, cbm_type_unknown());
add_php_seeded_method(&arena, &registry, "cbm.ordered.php.local.Subject.documented",
"documented", subject_qn, thing_qn, cbm_type_unknown());
CBMResolvedCallArray out = {0};
PHPLSPContext ctx;
php_lsp_init(&ctx, &arena, source, (int)strlen(source), &registry, module_qn, &out);
cbm_php_refine_lsp_registry(&ctx, &registry, root);
const char *expected[] = {"int", "Thing", "int"};
int failures = check_signature(&registry, "cbm.ordered.php.local.Subject.declared",
"php-declared-return-refinement", expected, 3);
failures += check_single_return(&registry, "cbm.ordered.php.local.Subject.declared",
"php-declared-return-refinement", "Result");
failures += check_signature(&registry, "cbm.ordered.php.local.Subject.documented",
"phpdoc-return-refinement", expected, 3);
failures += check_single_return(&registry, "cbm.ordered.php.local.Subject.documented",
"phpdoc-return-refinement", "Result");
ts_tree_delete(tree);
cbm_arena_destroy(&arena);
ASSERT_EQ(failures, 0);
PASS();
}
TEST(repro_lsp_ordered_signatures_php_trait_self_refinement) {
const char *module_qn = "cbm.ordered.php.trait";
const char *thing_qn = "cbm.ordered.php.trait.Thing";
const char *trait_qn = "cbm.ordered.php.trait.Fluent";
const char *consumer_qn = "cbm.ordered.php.trait.Consumer";
const char *source =
"<?php\n"
"class Thing {}\n"
"trait Fluent {\n"
" public function tap(int $first, ?Thing $middle, int $last) { return $this; }\n"
"}\n"
"class Consumer { use Fluent; }\n";
TSTree *tree = parse_lsp_fixture(CBM_LANG_PHP, source);
ASSERT_NOT_NULL(tree);
TSNode root = ts_tree_root_node(tree);
ASSERT_FALSE(ts_node_has_error(root));
CBMArena arena;
cbm_arena_init(&arena);
CBMTypeRegistry registry;
cbm_registry_init(&registry, &arena);
add_fixture_type(&registry, thing_qn, "Thing");
add_fixture_type(&registry, trait_qn, "Fluent");
add_fixture_type(&registry, consumer_qn, "Consumer");
/* No AST return annotation is deliberate: only trait self-substitution
* may rebuild this seeded signature, so parameter loss has one cause. */
add_php_seeded_method(&arena, &registry, "cbm.ordered.php.trait.Fluent.tap", "tap", trait_qn,
thing_qn, cbm_type_named(&arena, trait_qn));
CBMResolvedCallArray out = {0};
PHPLSPContext ctx;
php_lsp_init(&ctx, &arena, source, (int)strlen(source), &registry, module_qn, &out);
cbm_php_refine_lsp_registry(&ctx, &registry, root);
const char *expected[] = {"int", "Thing", "int"};
int failures = check_signature(&registry, "cbm.ordered.php.trait.Consumer.tap",
"php-trait-self-refinement", expected, 3);
failures += check_single_return(&registry, "cbm.ordered.php.trait.Consumer.tap",
"php-trait-self-refinement", "Consumer");
ts_tree_delete(tree);
cbm_arena_destroy(&arena);
ASSERT_EQ(failures, 0);
PASS();
}
SUITE(repro_lsp_ordered_signatures) {
RUN_TEST(repro_lsp_signature_param_materializer_contract);
RUN_TEST(repro_lsp_func_replace_returns_contract);
RUN_TEST(repro_lsp_c_unqualified_explicit_template_value_preserves_signature);
RUN_TEST(repro_lsp_c_qualified_explicit_template_value_preserves_signature);
RUN_TEST(repro_lsp_ordered_signatures_c_control);
RUN_TEST(repro_lsp_ordered_signatures_cpp_control);
RUN_TEST(repro_lsp_ordered_signatures_cuda_control);
RUN_TEST(repro_lsp_ordered_signatures_go);
RUN_TEST(repro_lsp_ordered_signatures_javascript);
RUN_TEST(repro_lsp_ordered_signatures_typescript);
RUN_TEST(repro_lsp_ordered_signatures_tsx);
RUN_TEST(repro_lsp_ordered_signatures_python);
RUN_TEST(repro_lsp_ordered_signatures_csharp);
RUN_TEST(repro_lsp_ordered_signatures_csharp_cross_return);
RUN_TEST(repro_lsp_ordered_signatures_rust);
RUN_TEST(repro_lsp_ordered_signatures_java);
RUN_TEST(repro_lsp_ordered_signatures_kotlin);
RUN_TEST(repro_lsp_ordered_signatures_php);
RUN_TEST(repro_lsp_ordered_signatures_csharp_ast_return_refinement);
RUN_TEST(repro_lsp_ordered_signatures_rust_ast_return_refinement);
RUN_TEST(repro_lsp_ordered_signatures_kotlin_local_registry);
RUN_TEST(repro_lsp_ordered_signatures_php_return_refinement);
RUN_TEST(repro_lsp_ordered_signatures_php_trait_self_refinement);
}
+5 -9
View File
@@ -64,11 +64,8 @@
* registry).
* lsp_ts_jsx_import (ts_lsp.c:2657) <Comp/> JSX element whose tag is an
* imported component (TSX only; tag matches
* an import local name → synthetic
* "<module>.<Comp>" QN). NOTE: this site
* builds the callee QN WITHOUT verifying the
* symbol exists in the registry, so it can
* emit even when the import target is absent.
* an import local name and the exact
* component exists in the registry.
* lsp_ts (ts_lsp.c:116) DEFAULT fallback inside ts_emit_resolved_call
* used only when a caller passes a NULL
* strategy. Every concrete emit site passes a
@@ -311,10 +308,9 @@ static const char kTsxJsx[] =
" return <Widget />;\n"
"}\n";
/* lsp_ts_jsx_import — <Comp/> JSX element whose tag is an imported component
* (ts_lsp.c:2652-2657). TSX only; the tag matches an import local name → a
* synthetic "<module>.<Comp>" callee QN is emitted (this site does NOT verify
* the symbol is in the registry). Cross-file: widget.tsx exports Widget,
/* lsp_ts_jsx_import — <Comp/> JSX element whose tag is an imported component.
* TSX only; the tag matches an import local name and the cross-file registry
* proves the exact component symbol. Cross-file: widget.tsx exports Widget,
* app.tsx imports it and renders <Widget/>. */
static const RFile kTsxJsxImport[] = {
{"widget.tsx",
+51 -24
View File
@@ -2,11 +2,11 @@
* repro_main.c — Entry point for the cumulative BUG-REPRODUCTION suite.
*
* This runner is SEPARATE from the gating `make test` (test-runner). It exists
* to hold reproduce-first cases for every OPEN bug issue. Each case asserts the
* CORRECT behaviour, so it is **RED until the bug is fixed** — the redness is the
* deliverable (proof the bug is real + the permanent regression guard).
* to hold reproduce-first cases for OPEN bugs plus the controls needed to prove
* their fixtures and oracles are meaningful. Reproduction cases assert the
* correct behaviour and stay RED until fixed; controls remain GREEN.
*
* Because these cases are red by design, they MUST NOT live in `ALL_TEST_SRCS`
* Because open reproductions are red by design, these suites MUST NOT live in `ALL_TEST_SRCS`
* (that would turn the PR gate `ci-ok` red and wedge every merge). They are built
* + run only via `make test-repro` and the `bug-repro.yml` workflow, neither of
* which gates branch protection.
@@ -26,33 +26,41 @@ int tf_fail_count = 0;
int tf_skip_count = 0;
#include "test_framework.h"
#include "repro_runner.h"
#include "foundation/compat.h" /* cbm_setenv — #845 supervisor kill switch */
/* Per-suite summary + filter. RUN_SUITE prints a one-line
* "[SUITE] <name> P passed, F failed" report (greppable for which suites still
* have reds). When CBM_REPRO_ONLY is set (comma/space list of suite-name
* substrings), only matching suites run — for fast targeted validation of a
* single fix without rebuilding intent. */
static int cbm_suite_enabled(const char *name) {
const char *only = getenv("CBM_REPRO_ONLY");
if (!only || !*only)
return 1;
return strstr(only, name) != NULL;
}
* have reds). The shared selector implementation lives with its gating tests in
* repro_runner_filter.c. */
#undef RUN_SUITE
#define RUN_SUITE(name) \
do { \
if (!cbm_suite_enabled(#name)) \
break; \
int _p0 = tf_pass_count, _f0 = tf_fail_count; \
printf("\n%s=== %s ===%s\n", tf_dim(), #name, tf_reset()); \
suite_##name(); \
printf("[SUITE] %-38s %d passed, %d failed\n", #name, tf_pass_count - _p0, \
tf_fail_count - _f0); \
#define RUN_SUITE(name) \
do { \
if (!cbm_suite_enabled(#name)) \
break; \
int _p0 = tf_pass_count, _f0 = tf_fail_count; \
printf("\n%s=== %s ===%s\n", tf_dim(), #name, tf_reset()); \
suite_##name(); \
printf("[SUITE] %-38s %d passed, %d failed\n", #name, tf_pass_count - _p0, \
tf_fail_count - _f0); \
} while (0)
/* ── Repro suites (one per bug cluster / issue) ─────────────────── */
extern void suite_repro_extraction(void);
extern void suite_repro_runner_filter(void);
extern void suite_repro_harness_cleanup(void);
extern void suite_repro_language_registry(void);
extern void suite_repro_call_node_manifest(void);
extern void suite_repro_call_scope_usages(void);
extern void suite_repro_call_argument_usages(void);
extern void suite_repro_lsp_ordered_signatures(void);
extern void suite_repro_lsp_ordered_local(void);
extern void suite_repro_ts_overload_return_chains(void);
extern void suite_repro_reference_precision(void);
extern void suite_repro_lexical_binding_precision(void);
extern void suite_repro_call_argument_matrix_a(void);
extern void suite_repro_call_argument_matrix_b(void);
extern void suite_repro_call_node_behaviors(void);
extern void suite_repro_parallel_determinism(void);
extern void suite_repro_issue495(void);
extern void suite_repro_issue521(void);
@@ -132,11 +140,25 @@ int main(void) {
printf("════════════════════════════════════════════════════════════\n");
printf(" CUMULATIVE BUG-REPRODUCTION SUITE\n");
printf(" RED rows are EXPECTED — each is an open bug reproduced.\n");
printf(" A row that PASSES means that bug appears FIXED → flip it\n");
printf(" into the gating suite and close the issue with the guard.\n");
printf(" PASS rows may be controls or candidate fixes. Verify each\n");
printf(" RED→GREEN transition before promotion or issue closure.\n");
printf("════════════════════════════════════════════════════════════\n");
RUN_SUITE(repro_extraction);
RUN_SUITE(repro_runner_filter);
RUN_SUITE(repro_harness_cleanup);
RUN_SUITE(repro_language_registry);
RUN_SUITE(repro_call_node_manifest);
RUN_SUITE(repro_call_scope_usages);
RUN_SUITE(repro_call_argument_usages);
RUN_SUITE(repro_lsp_ordered_signatures);
RUN_SUITE(repro_lsp_ordered_local);
RUN_SUITE(repro_ts_overload_return_chains);
RUN_SUITE(repro_reference_precision);
RUN_SUITE(repro_lexical_binding_precision);
RUN_SUITE(repro_call_argument_matrix_a);
RUN_SUITE(repro_call_argument_matrix_b);
RUN_SUITE(repro_call_node_behaviors);
RUN_SUITE(repro_parallel_determinism);
RUN_SUITE(repro_issue495);
RUN_SUITE(repro_issue521);
@@ -194,5 +216,10 @@ int main(void) {
RUN_SUITE(repro_lsp_java_cs);
RUN_SUITE(repro_lsp_kt_php_rust);
if (tf_pass_count + tf_fail_count + tf_skip_count == 0) {
fprintf(stderr, "::error::bug-repro runner executed zero tests\n");
return 2;
}
TEST_SUMMARY();
}
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
# Reproduce linked targets ignoring shared test or production-header changes,
# which can execute a stale binary after a header-only semantic fix.
set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
MAKE_DB="$(mktemp "${TMPDIR:-/tmp}/cbm-repro-make-db.XXXXXX")"
trap 'rm -f "$MAKE_DB"' EXIT
cd "$PROJECT_ROOT"
make -f Makefile.cbm -pn build/c/test-repro-runner >"$MAKE_DB"
repro_target_line="$(grep '^build/c/test-repro-runner:' "$MAKE_DB" | head -1 || true)"
test_target_line="$(grep '^build/c/test-runner:' "$MAKE_DB" | head -1 || true)"
tsan_target_line="$(grep '^build/c/test-runner-tsan:' "$MAKE_DB" | head -1 || true)"
production_target_line="$(grep '^build/c/codebase-memory-mcp:' "$MAKE_DB" | head -1 || true)"
if [[ "$repro_target_line" != *'tests/repro/repro_harness.h'* ||
"$repro_target_line" != *'tests/test_framework.h'* ||
"$repro_target_line" != *'tests/test_helpers.h'* ]]; then
printf '[repro-make-headers] invariant=shared_headers_missing_from_target\n' >&2
exit 1
fi
missing_test_header=0
for target_name in build/c/test-runner build/c/test-runner-tsan; do
target_line="$test_target_line"
if [[ "$target_name" == 'build/c/test-runner-tsan' ]]; then
target_line="$tsan_target_line"
fi
for header in tests/test_framework.h tests/test_helpers.h tests/grammar_cases.h; do
if [[ "$target_line" != *"$header"* ]]; then
printf '[repro-make-headers] invariant=test_header_missing_from_target target=%s header=%s\n' \
"$target_name" "$header" >&2
missing_test_header=1
fi
done
done
missing_production_header=0
for target_name in build/c/test-runner build/c/test-repro-runner build/c/test-runner-tsan \
build/c/codebase-memory-mcp; do
case "$target_name" in
build/c/test-runner) target_line="$test_target_line" ;;
build/c/test-repro-runner) target_line="$repro_target_line" ;;
build/c/test-runner-tsan) target_line="$tsan_target_line" ;;
*) target_line="$production_target_line" ;;
esac
for header in src/pipeline/lsp_resolve.h internal/cbm/lsp/type_registry.h; do
if [[ "$target_line" != *"$header"* ]]; then
printf '[repro-make-headers] invariant=production_header_missing_from_target target=%s header=%s\n' \
"$target_name" "$header" >&2
missing_production_header=1
fi
done
done
if [[ "$missing_test_header" -ne 0 || "$missing_production_header" -ne 0 ]]; then
exit 1
fi
printf '[repro-make-headers] shared headers tracked by runner and production targets\n'
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
#ifndef CBM_TESTS_REPRO_RUNNER_H
#define CBM_TESTS_REPRO_RUNNER_H
int cbm_suite_enabled(const char *name);
#endif
+130
View File
@@ -0,0 +1,130 @@
/* Reproductions for the cumulative repro runner itself. */
#include "test_framework.h"
#include "repro_runner.h"
#include "foundation/compat.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int selector_delimiter(char value) {
return value == ',' || value == ' ' || value == '\t' || value == '\r' || value == '\n';
}
static int suite_name_contains_token(const char *name, const char *token, size_t token_len) {
size_t name_len = strlen(name);
if (token_len > name_len) {
return 0;
}
for (size_t offset = 0; offset + token_len <= name_len; offset++) {
if (strncmp(name + offset, token, token_len) == 0) {
return 1;
}
}
return 0;
}
int cbm_suite_enabled(const char *name) {
const char *only = getenv("CBM_REPRO_ONLY");
if (!only || !*only) {
return 1;
}
const char *cursor = only;
while (*cursor) {
while (*cursor && selector_delimiter(*cursor)) {
cursor++;
}
const char *token = cursor;
while (*cursor && !selector_delimiter(*cursor)) {
cursor++;
}
size_t token_len = (size_t)(cursor - token);
if (token_len > 0 && suite_name_contains_token(name, token, token_len)) {
return 1;
}
}
return 0;
}
static char *save_selector(void) {
const char *prior = getenv("CBM_REPRO_ONLY");
return prior ? cbm_strdup(prior) : NULL;
}
static void restore_selector(char *saved) {
if (saved) {
cbm_setenv("CBM_REPRO_ONLY", saved, 1);
free(saved);
} else {
cbm_unsetenv("CBM_REPRO_ONLY");
}
}
/* The documented selector accepts suite-name substrings. */
TEST(repro_runner_filter_accepts_suite_substring) {
char *saved = save_selector();
cbm_setenv("CBM_REPRO_ONLY", "call_argument", 1);
int enabled = cbm_suite_enabled("repro_call_argument_usages");
restore_selector(saved);
if (!enabled)
fprintf(stderr, " [repro-filter] invariant=suite_substring_not_matched\n");
ASSERT_TRUE(enabled);
PASS();
}
TEST(repro_runner_filter_accepts_comma_list) {
char *saved = save_selector();
cbm_setenv("CBM_REPRO_ONLY", "language_registry,call_argument", 1);
int first = cbm_suite_enabled("repro_language_registry");
int second = cbm_suite_enabled("repro_call_argument_usages");
restore_selector(saved);
if (!first || !second)
fprintf(stderr, " [repro-filter] invariant=comma_list_not_matched\n");
ASSERT_TRUE(first && second);
PASS();
}
TEST(repro_runner_filter_accepts_space_list) {
char *saved = save_selector();
cbm_setenv("CBM_REPRO_ONLY", "language_registry call_argument", 1);
int first = cbm_suite_enabled("repro_language_registry");
int second = cbm_suite_enabled("repro_call_argument_usages");
restore_selector(saved);
if (!first || !second)
fprintf(stderr, " [repro-filter] invariant=space_list_not_matched\n");
ASSERT_TRUE(first && second);
PASS();
}
TEST(repro_runner_filter_rejects_nonmatch) {
char *saved = save_selector();
cbm_setenv("CBM_REPRO_ONLY", "language_registry", 1);
int enabled = cbm_suite_enabled("repro_call_argument_usages");
restore_selector(saved);
if (enabled)
fprintf(stderr, " [repro-filter] invariant=nonmatch_enabled\n");
ASSERT_FALSE(enabled);
PASS();
}
TEST(repro_runner_filter_unset_or_empty_enables_all) {
char *saved = save_selector();
cbm_unsetenv("CBM_REPRO_ONLY");
int unset_enabled = cbm_suite_enabled("repro_call_argument_usages");
cbm_setenv("CBM_REPRO_ONLY", "", 1);
int empty_enabled = cbm_suite_enabled("repro_call_argument_usages");
restore_selector(saved);
if (!unset_enabled || !empty_enabled)
fprintf(stderr, " [repro-filter] invariant=unset_or_empty_disabled\n");
ASSERT_TRUE(unset_enabled && empty_enabled);
PASS();
}
SUITE(repro_runner_filter) {
RUN_TEST(repro_runner_filter_accepts_suite_substring);
RUN_TEST(repro_runner_filter_accepts_comma_list);
RUN_TEST(repro_runner_filter_accepts_space_list);
RUN_TEST(repro_runner_filter_rejects_nonmatch);
RUN_TEST(repro_runner_filter_unset_or_empty_enables_all);
}
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# The compiled repro runner must not claim every passing control is a fixed bug.
set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
RUNNER="$PROJECT_ROOT/build/c/test-repro-runner"
if [[ ! -x "$RUNNER" ]]; then
printf '[repro-runner-semantics] invariant=runner_missing path=%s\n' "$RUNNER" >&2
exit 1
fi
set +e
output="$(CBM_REPRO_ONLY=repro_runner_filter "$RUNNER" 2>&1)"
status=$?
set -e
if [[ $status -ne 0 ]]; then
printf '[repro-runner-semantics] invariant=control_suite_failed status=%d\n' "$status" >&2
exit 1
fi
old_claim='A row that PASSES means that bug appears FIXED'
if [[ "$output" == *"$old_claim"* ]]; then
printf '[repro-runner-semantics] invariant=passing_controls_blanket_fixed old_claim=true\n' >&2
exit 1
fi
neutral_claim='PASS rows may be controls or candidate fixes'
if [[ "$output" != *"$neutral_claim"* ]]; then
printf '[repro-runner-semantics] invariant=neutral_passing_semantics_missing\n' >&2
exit 1
fi
printf '[repro-runner-semantics] compiled runner reports neutral passing semantics\n'
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Reproduce the compiled repro runner returning success when a selector matches
# no suite. This protects direct `make test-repro`, not only scripts/repro.sh.
set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
RUNNER="$PROJECT_ROOT/build/c/test-repro-runner"
if [[ ! -x "$RUNNER" ]]; then
printf '[repro-runner-zero] invariant=runner_missing path=%s\n' "$RUNNER" >&2
exit 1
fi
set +e
output="$(CBM_REPRO_ONLY='__cbm_no_such_repro_suite__' "$RUNNER" 2>&1)"
status=$?
set -e
if [[ $status -eq 0 ]]; then
printf '[repro-runner-zero] invariant=compiled_runner_accepted_zero_tests status=%d\n' \
"$status" >&2
exit 1
fi
expected='bug-repro runner executed zero tests'
if [[ "$output" != *"$expected"* ]]; then
printf '[repro-runner-zero] invariant=zero_test_diagnostic_missing status=%d\n' \
"$status" >&2
exit 1
fi
printf '[repro-runner-zero] compiled runner rejected zero tests correctly\n'
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
# Reproduce scripts/repro.sh exiting through `set -e -o pipefail` before its
# explicit build/runner diagnostic when the transcript has no aggregate line.
set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/cbm-repro-missing-summary.XXXXXX")"
cleanup() {
rm -f "$TMP_ROOT/scripts/repro.sh" "$TMP_ROOT/scripts/env.sh"
rm -f "$TMP_ROOT/repro-out.txt" "$TMP_ROOT/summary.txt"
rmdir "$TMP_ROOT/scripts" "$TMP_ROOT" 2>/dev/null || true
}
trap cleanup EXIT
mkdir -p "$TMP_ROOT/scripts"
cp "$PROJECT_ROOT/scripts/repro.sh" "$TMP_ROOT/scripts/repro.sh"
cp "$PROJECT_ROOT/scripts/env.sh" "$TMP_ROOT/scripts/env.sh"
# Model a compile/link failure: make emits a useful error but the test runner
# never reaches its final aggregate summary.
make() {
printf '%s\n' 'clang: error: linker command failed with exit code 1'
return 2
}
export -f make
set +e
output="$(TMPDIR="$TMP_ROOT" CC=true CXX=true GITHUB_STEP_SUMMARY="$TMP_ROOT/summary.txt" \
bash "$TMP_ROOT/scripts/repro.sh" 2>&1)"
status=$?
set -e
if [[ $status -eq 0 ]]; then
printf '[repro-missing-summary] invariant=build_failure_accepted status=%d\n' "$status" >&2
exit 1
fi
linker_marker='clang: error: linker command failed with exit code 1'
if [[ "$output" != *"$linker_marker"* ]]; then
printf '[repro-missing-summary] invariant=mocked_linker_failure_not_observed status=%d\n' \
"$status" >&2
exit 1
fi
expected='::error::bug-repro runner did not execute — build or link failure'
if [[ "$output" != *"$expected"* ]]; then
printf '[repro-missing-summary] invariant=explicit_diagnostic_missing status=%d\n' \
"$status" >&2
exit 1
fi
printf '[repro-missing-summary] missing aggregate rejected with explicit diagnostic\n'
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Reproduce scripts/repro.sh leaving its generated runner transcript in the
# repository root after a completed board run.
set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/cbm-repro-artifact.XXXXXX")"
cleanup() {
rm -f "$TMP_ROOT/scripts/repro.sh" "$TMP_ROOT/scripts/env.sh"
rm -f "$TMP_ROOT/repro-out.txt" "$TMP_ROOT/summary.txt"
rmdir "$TMP_ROOT/scripts" "$TMP_ROOT" 2>/dev/null || true
}
trap cleanup EXIT
mkdir -p "$TMP_ROOT/scripts"
cp "$PROJECT_ROOT/scripts/repro.sh" "$TMP_ROOT/scripts/repro.sh"
cp "$PROJECT_ROOT/scripts/env.sh" "$TMP_ROOT/scripts/env.sh"
make() {
printf '%s\n' \
'────────────────────────────────────────────' \
' 1 passed'
return 0
}
export -f make
TMPDIR="$TMP_ROOT" CC=true CXX=true GITHUB_STEP_SUMMARY="$TMP_ROOT/summary.txt" \
bash "$TMP_ROOT/scripts/repro.sh" >/dev/null 2>&1
if [[ -e "$TMP_ROOT/repro-out.txt" ]]; then
printf '[repro-artifact] invariant=workspace_transcript_left_behind path=repro-out.txt\n' >&2
exit 1
fi
printf '[repro-artifact] workspace transcript cleaned correctly\n'
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
# Reproduce scripts/repro.sh treating the board as successful even though the
# test framework's valid final aggregate reports one or more skipped tests.
set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/cbm-repro-skipped.XXXXXX")"
cleanup() {
rm -f "$TMP_ROOT/scripts/repro.sh" "$TMP_ROOT/scripts/env.sh"
rm -f "$TMP_ROOT/repro-out.txt" "$TMP_ROOT/summary.txt"
rmdir "$TMP_ROOT/scripts" "$TMP_ROOT" 2>/dev/null || true
}
trap cleanup EXIT
mkdir -p "$TMP_ROOT/scripts"
cp "$PROJECT_ROOT/scripts/repro.sh" "$TMP_ROOT/scripts/repro.sh"
cp "$PROJECT_ROOT/scripts/env.sh" "$TMP_ROOT/scripts/env.sh"
# TEST_SUMMARY appends the skipped count after pass/fail counts. The board must
# parse and report the aggregate faithfully, but it must exit nonzero because a
# board with skipped cases is incomplete.
make() {
printf '%s\n' \
'[SUITE] repro_parallel_determinism 0 passed, 2 skipped' \
'[SUITE] repro_call_argument_usages 1 passed, 17 failed' \
'────────────────────────────────────────────' \
' 1 passed, 17 failed, 2 skipped'
return 1
}
export -f make
set +e
output="$(TMPDIR="$TMP_ROOT" CC=true CXX=true GITHUB_STEP_SUMMARY="$TMP_ROOT/summary.txt" \
bash "$TMP_ROOT/scripts/repro.sh" 2>&1)"
status=$?
set -e
expected_prefix='=== bug-repro board: 17 reproduced (RED), 1 '
if [[ "$output" != *"$expected_prefix"* ]]; then
actual="$(printf '%s\n' "$output" | grep '^=== bug-repro board:' || true)"
printf '[repro-skipped] invariant=skipped_aggregate_misreported expected=17/1 actual=%s\n' \
"${actual:-missing}" >&2
exit 1
fi
skip_summary='- **2** case(s) SKIPPED — board is incomplete'
if ! grep -Fq -- "$skip_summary" "$TMP_ROOT/summary.txt"; then
printf '[repro-skipped] invariant=skipped_count_hidden expected=2\n' >&2
exit 1
fi
skip_warning='::warning::bug-repro board incomplete — 2 case(s) skipped'
if [[ "$output" != *"$skip_warning"* ]]; then
printf '[repro-skipped] invariant=skipped_warning_hidden expected=2\n' >&2
exit 1
fi
if [[ $status -eq 0 ]]; then
printf '[repro-skipped] invariant=incomplete_board_exited_zero expected=nonzero actual=0\n' >&2
exit 1
fi
printf '[repro-skipped] skipped aggregate reported and rejected correctly\n'
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# Reproduce the no-GITHUB_STEP_SUMMARY fallback writing through /dev/stderr,
# which can be denied even though the process already has a valid stderr FD.
set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/cbm-repro-stderr.XXXXXX")"
cleanup() {
rm -f "$TMP_ROOT/scripts/repro.sh" "$TMP_ROOT/scripts/env.sh"
rm -f "$TMP_ROOT/repro-out.txt"
rmdir "$TMP_ROOT/scripts" "$TMP_ROOT" 2>/dev/null || true
}
trap cleanup EXIT
mkdir -p "$TMP_ROOT/scripts"
cp "$PROJECT_ROOT/scripts/repro.sh" "$TMP_ROOT/scripts/repro.sh"
cp "$PROJECT_ROOT/scripts/env.sh" "$TMP_ROOT/scripts/env.sh"
make() {
printf '%s\n' \
'────────────────────────────────────────────' \
' 1 passed, 1 failed'
return 1
}
export -f make
set +e
output="$(
unset GITHUB_STEP_SUMMARY
TMPDIR="$TMP_ROOT" CC=true CXX=true bash "$TMP_ROOT/scripts/repro.sh" 2>&1
)"
status=$?
set -e
if [[ $status -ne 0 ]]; then
printf '[repro-stderr] invariant=fallback_run_failed status=%d\n' "$status" >&2
exit 1
fi
if [[ "$output" == *'Operation not permitted'* ]]; then
printf '[repro-stderr] invariant=stderr_fallback_permission_denied\n' >&2
exit 1
fi
if [[ "$output" != *'## Bug-reproduction board'* ||
"$output" != *'**1** open bug(s) still reproduced'* ]]; then
printf '[repro-stderr] invariant=stderr_summary_missing\n' >&2
exit 1
fi
printf '[repro-stderr] summary written through stderr correctly\n'
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# Reproduce scripts/repro.sh selecting the first per-suite count instead of the
# final cumulative total when more than one repro suite runs, while ensuring
# known-green controls are not blanket-labeled as candidate fixes.
set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/cbm-repro-board.XXXXXX")"
cleanup() {
rm -f "$TMP_ROOT/scripts/repro.sh" "$TMP_ROOT/scripts/env.sh"
rm -f "$TMP_ROOT/repro-out.txt" "$TMP_ROOT/summary.txt"
rmdir "$TMP_ROOT/scripts" "$TMP_ROOT" 2>/dev/null || true
}
trap cleanup EXIT
mkdir -p "$TMP_ROOT/scripts"
cp "$PROJECT_ROOT/scripts/repro.sh" "$TMP_ROOT/scripts/repro.sh"
cp "$PROJECT_ROOT/scripts/env.sh" "$TMP_ROOT/scripts/env.sh"
# The copied production script calls make. This test-local function supplies a
# deterministic runner transcript while exercising the production board parser.
make() {
printf '%s\n' \
'[SUITE] repro_known_green_controls 2 passed' \
'[SUITE] repro_runner_filter 0 passed, 1 failed' \
'[SUITE] repro_call_argument_usages 0 passed, 17 failed' \
'────────────────────────────────────────────' \
' 2 passed, 18 failed'
return 1
}
export -f make
set +e
output="$(TMPDIR="$TMP_ROOT" CC=true CXX=true GITHUB_STEP_SUMMARY="$TMP_ROOT/summary.txt" \
bash "$TMP_ROOT/scripts/repro.sh" 2>&1)"
status=$?
set -e
if [[ $status -ne 0 ]]; then
printf '[repro-board] invariant=production_script_did_not_complete status=%d\n' "$status" >&2
exit 1
fi
expected_prefix='=== bug-repro board: 18 reproduced (RED), 2 '
if [[ "$output" != *"$expected_prefix"* ]]; then
actual="$(printf '%s\n' "$output" | grep '^=== bug-repro board:' || true)"
printf '[repro-board] invariant=cumulative_total_not_reported expected=18/2 actual=%s\n' \
"${actual:-missing}" >&2
exit 1
fi
neutral_label='passing control(s) or candidate-fix candidate(s)'
if [[ "$output" != *"$neutral_label"* ]] ||
! grep -Fq -- "$neutral_label" "$TMP_ROOT/summary.txt"; then
actual="$(printf '%s\n' "$output" | grep '^=== bug-repro board:' || true)"
printf '[repro-board] invariant=passing_controls_blanket_candidate_fixed expected=%q actual=%s\n' \
"$neutral_label" "${actual:-missing}" >&2
exit 1
fi
if [[ "$output" == *'passing (candidate-fixed)'* ]] ||
grep -Fq -- 'PASSING — candidate-fixed' "$TMP_ROOT/summary.txt"; then
printf '[repro-board] invariant=passing_controls_blanket_candidate_fixed old_label_present=true\n' >&2
exit 1
fi
printf '[repro-board] cumulative total and neutral passing semantics reported correctly\n'
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
# Reproduce scripts/repro.sh accepting a selector/build transcript in which the
# runner executes zero tests. A zero-test board proves neither red nor green and
# must be treated as a runner failure.
set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/cbm-repro-zero.XXXXXX")"
cleanup() {
rm -f "$TMP_ROOT/scripts/repro.sh" "$TMP_ROOT/scripts/env.sh"
rm -f "$TMP_ROOT/repro-out.txt" "$TMP_ROOT/summary.txt"
rmdir "$TMP_ROOT/scripts" "$TMP_ROOT" 2>/dev/null || true
}
trap cleanup EXIT
mkdir -p "$TMP_ROOT/scripts"
cp "$PROJECT_ROOT/scripts/repro.sh" "$TMP_ROOT/scripts/repro.sh"
cp "$PROJECT_ROOT/scripts/env.sh" "$TMP_ROOT/scripts/env.sh"
# Supply the exact transcript produced when CBM_REPRO_ONLY matches no suite.
make() {
printf '%s\n' \
'────────────────────────────────────────────' \
' 0 passed'
return 0
}
export -f make
set +e
output="$(TMPDIR="$TMP_ROOT" CC=true CXX=true GITHUB_STEP_SUMMARY="$TMP_ROOT/summary.txt" \
bash "$TMP_ROOT/scripts/repro.sh" 2>&1)"
status=$?
set -e
if [[ $status -eq 0 ]]; then
board="$(printf '%s\n' "$output" | grep '^=== bug-repro board:' || true)"
printf '[repro-zero] invariant=zero_tests_accepted status=%d board=%s\n' \
"$status" "${board:-missing}" >&2
exit 1
fi
if [[ "$output" != *'bug-repro runner executed zero tests'* ]]; then
printf '[repro-zero] invariant=zero_test_error_missing status=%d\n' "$status" >&2
exit 1
fi
printf '[repro-zero] zero-test transcript rejected correctly\n'
@@ -0,0 +1,514 @@
/*
* repro_ts_overload_return_chains.c — REDs for argument-aware return typing
* of TypeScript/TSX overloads outside method dispatch.
*
* Both fixtures register two overloads with the same QN and arity but reversed
* positional types. They invoke both orders and assert distinct downstream
* method targets. A declaration-order, last-definition, or arity-only fallback
* can satisfy at most one chain; both become GREEN only when the call arguments
* select the return signature used by expression and contextual callback typing.
* Shadow and exact-import cases guard the selector's fail-closed boundaries.
*
* The shadowing fixtures use untyped parameters on purpose. An UNKNOWN local
* binding still owns the lexical name and must block both call-edge promotion
* and return-type inheritance from a same-named registry symbol.
*/
#include "test_framework.h"
#include "arena.h"
#include "cbm.h"
#include "lsp/ts_lsp.h"
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
enum { OVERLOAD_DEF_COUNT = 7 };
typedef struct {
const char *symbol_module_qn;
const char *caller_module_qn;
const char *reversed_type_qn;
const char *pair_type_qn;
const char *reversed_method_qn;
const char *pair_method_qn;
const char *choose_qn;
const char *run_qn;
} TSOverloadFixtureNames;
static const char *kNumberStringParams[] = {"number", "string"};
static const char *kStringNumberParams[] = {"string", "number"};
static const char *kNumberStringPairCallbackParams[] = {"number", "string",
"(value: Pair) => void"};
static const char *kStringNumberReversedCallbackParams[] = {"string", "number",
"(value: Reversed) => void"};
static bool ends_with(const char *text, const char *suffix) {
if (!text || !suffix)
return false;
size_t text_len = strlen(text);
size_t suffix_len = strlen(suffix);
return suffix_len <= text_len && strcmp(text + text_len - suffix_len, suffix) == 0;
}
static int count_resolved_suffix(const CBMResolvedCallArray *calls, const char *callee_suffix,
const char *strategy) {
if (!calls)
return 0;
int count = 0;
for (int i = 0; i < calls->count; i++) {
const CBMResolvedCall *call = &calls->items[i];
if (call->kind == CBM_RESOLVED_INVOCATION && call->confidence >= 0.5f && call->caller_qn &&
strstr(call->caller_qn, "run") && ends_with(call->callee_qn, callee_suffix) &&
call->strategy && strcmp(call->strategy, strategy) == 0) {
count++;
}
}
return count;
}
static int count_any_callee_suffix(const CBMResolvedCallArray *calls, const char *callee_suffix) {
if (!calls)
return 0;
int count = 0;
for (int i = 0; i < calls->count; i++) {
const CBMResolvedCall *call = &calls->items[i];
if (call->caller_qn && strstr(call->caller_qn, "run") &&
ends_with(call->callee_qn, callee_suffix)) {
count++;
}
}
return count;
}
static void print_resolved_calls(const CBMResolvedCallArray *calls) {
if (!calls)
return;
for (int i = 0; i < calls->count; i++) {
const CBMResolvedCall *call = &calls->items[i];
printf(" %s -> %s [%s %.2f kind=%d]\n", call->caller_qn ? call->caller_qn : "(null)",
call->callee_qn ? call->callee_qn : "(null)",
call->strategy ? call->strategy : "(null)", call->confidence, (int)call->kind);
}
}
static void init_overload_defs(CBMLSPDef defs[OVERLOAD_DEF_COUNT], CBMLanguage language,
const TSOverloadFixtureNames *names) {
memset(defs, 0, (size_t)OVERLOAD_DEF_COUNT * sizeof(*defs));
defs[0].qualified_name = names->reversed_type_qn;
defs[0].short_name = "Reversed";
defs[0].label = "Interface";
defs[0].def_module_qn = names->symbol_module_qn;
defs[0].is_interface = true;
defs[0].lang = language;
defs[1].qualified_name = names->pair_type_qn;
defs[1].short_name = "Pair";
defs[1].label = "Interface";
defs[1].def_module_qn = names->symbol_module_qn;
defs[1].is_interface = true;
defs[1].lang = language;
defs[2].qualified_name = names->reversed_method_qn;
defs[2].short_name = "reversedOnly";
defs[2].label = "Method";
defs[2].receiver_type = names->reversed_type_qn;
defs[2].def_module_qn = names->symbol_module_qn;
defs[2].return_types = "void";
defs[2].lang = language;
defs[3].qualified_name = names->pair_method_qn;
defs[3].short_name = "pairOnly";
defs[3].label = "Method";
defs[3].receiver_type = names->pair_type_qn;
defs[3].def_module_qn = names->symbol_module_qn;
defs[3].return_types = "void";
defs[3].lang = language;
/* Same QN and arity; only ordered argument types distinguish these. */
defs[4].qualified_name = names->choose_qn;
defs[4].short_name = "choose";
defs[4].label = "Function";
defs[4].def_module_qn = names->symbol_module_qn;
defs[4].return_types = "Pair";
defs[4].signature_param_types = kNumberStringParams;
defs[4].signature_param_count = 2;
defs[4].lang = language;
defs[5].qualified_name = names->choose_qn;
defs[5].short_name = "choose";
defs[5].label = "Function";
defs[5].def_module_qn = names->symbol_module_qn;
defs[5].return_types = "Reversed";
defs[5].signature_param_types = kStringNumberParams;
defs[5].signature_param_count = 2;
defs[5].lang = language;
defs[6].qualified_name = names->run_qn;
defs[6].short_name = "run";
defs[6].label = "Function";
defs[6].def_module_qn = names->caller_module_qn;
defs[6].return_types = "void";
defs[6].lang = language;
}
static int assert_both_return_chains(const CBMResolvedCallArray *calls, const char *language_name,
const char *fixture_name, const char *inner_strategy) {
int inner_calls = count_resolved_suffix(calls, "choose", inner_strategy);
bool pair = count_resolved_suffix(calls, "Pair.pairOnly", "lsp_ts_method") > 0;
bool reversed = count_resolved_suffix(calls, "Reversed.reversedOnly", "lsp_ts_method") > 0;
if (inner_calls < 2 || !pair || !reversed) {
printf(" ts-overload-return: %s %s inner=%d pair=%d reversed=%d\n", language_name,
fixture_name, inner_calls, pair, reversed);
print_resolved_calls(calls);
}
return inner_calls >= 2 && pair && reversed ? 0 : 1;
}
static const char kFreeFunctionSource[] = "function run(): void {\n"
" choose(1, 'x').pairOnly();\n"
" choose('x', 1).reversedOnly();\n"
"}\n";
static int assert_free_function_overload_returns(CBMLanguage language, bool jsx_mode,
const char *language_name) {
static const TSOverloadFixtureNames names = {
"repro.free",
"repro.free",
"repro.free.Reversed",
"repro.free.Pair",
"repro.free.Reversed.reversedOnly",
"repro.free.Pair.pairOnly",
"repro.free.choose",
"repro.free.run",
};
CBMLSPDef defs[OVERLOAD_DEF_COUNT];
init_overload_defs(defs, language, &names);
CBMArena arena;
cbm_arena_init(&arena);
CBMTypeRegistry *registry = cbm_ts_build_cross_registry(&arena, defs, OVERLOAD_DEF_COUNT);
CBMResolvedCallArray calls = {0};
if (registry) {
cbm_run_ts_lsp_cross_with_registry(&arena, kFreeFunctionSource,
(int)strlen(kFreeFunctionSource), names.caller_module_qn,
false, jsx_mode, false, registry, defs,
OVERLOAD_DEF_COUNT, NULL, NULL, 0, NULL, &calls);
}
int rc = registry
? assert_both_return_chains(&calls, language_name, "free-function", "lsp_ts_local")
: 1;
if (!registry)
printf(" ts-overload-return: %s free-function registry build failed\n", language_name);
cbm_arena_destroy(&arena);
return rc;
}
static const char kNamespaceSource[] = "import * as Lib from './provider';\n"
"function run(): void {\n"
" Lib.choose(1, 'x').pairOnly();\n"
" Lib.choose('x', 1).reversedOnly();\n"
"}\n";
static int assert_namespace_overload_returns(CBMLanguage language, bool jsx_mode,
const char *language_name) {
static const TSOverloadFixtureNames names = {
"repro.provider",
"repro.consumer",
"repro.provider.Reversed",
"repro.provider.Pair",
"repro.provider.Reversed.reversedOnly",
"repro.provider.Pair.pairOnly",
"repro.provider.choose",
"repro.consumer.run",
};
CBMLSPDef defs[OVERLOAD_DEF_COUNT];
init_overload_defs(defs, language, &names);
const char *import_names[] = {"Lib"};
const char *import_qns[] = {names.symbol_module_qn};
CBMArena arena;
cbm_arena_init(&arena);
CBMTypeRegistry *registry = cbm_ts_build_cross_registry(&arena, defs, OVERLOAD_DEF_COUNT);
CBMResolvedCallArray calls = {0};
if (registry) {
cbm_run_ts_lsp_cross_with_registry(&arena, kNamespaceSource, (int)strlen(kNamespaceSource),
names.caller_module_qn, false, jsx_mode, false, registry,
defs, OVERLOAD_DEF_COUNT, import_names, import_qns, 1,
NULL, &calls);
}
int rc = registry ? assert_both_return_chains(&calls, language_name, "namespace-import",
"lsp_ts_namespace")
: 1;
if (!registry)
printf(" ts-overload-return: %s namespace registry build failed\n", language_name);
cbm_arena_destroy(&arena);
return rc;
}
static const char kExactSymbolImportSource[] = "import { choose } from './provider';\n"
"function run(): void {\n"
" choose(1, 'x').pairOnly();\n"
" choose('x', 1).reversedOnly();\n"
"}\n";
static int assert_exact_symbol_import_overload_returns(CBMLanguage language, bool jsx_mode,
const char *language_name) {
static const TSOverloadFixtureNames names = {
"repro.exact.provider",
"repro.exact.consumer",
"repro.exact.provider.Reversed",
"repro.exact.provider.Pair",
"repro.exact.provider.Reversed.reversedOnly",
"repro.exact.provider.Pair.pairOnly",
"repro.exact.provider.choose",
"repro.exact.consumer.run",
};
CBMLSPDef defs[OVERLOAD_DEF_COUNT];
init_overload_defs(defs, language, &names);
const char *import_names[] = {"choose"};
/* Some production import maps already carry the exact exported-symbol QN.
* The call lookup must not append the local name a second time. */
const char *import_qns[] = {names.choose_qn};
CBMArena arena;
cbm_arena_init(&arena);
CBMTypeRegistry *registry = cbm_ts_build_cross_registry(&arena, defs, OVERLOAD_DEF_COUNT);
CBMResolvedCallArray calls = {0};
if (registry) {
cbm_run_ts_lsp_cross_with_registry(
&arena, kExactSymbolImportSource, (int)strlen(kExactSymbolImportSource),
names.caller_module_qn, false, jsx_mode, false, registry, defs, OVERLOAD_DEF_COUNT,
import_names, import_qns, 1, NULL, &calls);
}
int chain_rc = registry ? assert_both_return_chains(&calls, language_name,
"exact-symbol-import", "lsp_ts_import")
: 1;
int doubled = count_any_callee_suffix(&calls, "choose.choose");
if (!registry || doubled != 0) {
printf(" ts-overload-return: %s exact-symbol-import registry=%d doubled=%d\n",
language_name, registry != NULL, doubled);
print_resolved_calls(&calls);
}
cbm_arena_destroy(&arena);
return registry && chain_rc == 0 && doubled == 0 ? 0 : 1;
}
static const char kContextualCallbackOverloadSource[] =
"function run(): void {\n"
" choose(1, 'x', value => value.pairOnly());\n"
" choose('x', 1, value => value.reversedOnly());\n"
"}\n";
static int assert_contextual_callback_uses_selected_overload(CBMLanguage language, bool jsx_mode,
const char *language_name) {
static const TSOverloadFixtureNames names = {
"repro.callback",
"repro.callback",
"repro.callback.Reversed",
"repro.callback.Pair",
"repro.callback.Reversed.reversedOnly",
"repro.callback.Pair.pairOnly",
"repro.callback.choose",
"repro.callback.run",
};
CBMLSPDef defs[OVERLOAD_DEF_COUNT];
init_overload_defs(defs, language, &names);
defs[4].return_types = "void";
defs[4].signature_param_types = kNumberStringPairCallbackParams;
defs[4].signature_param_count = 3;
defs[5].return_types = "void";
defs[5].signature_param_types = kStringNumberReversedCallbackParams;
defs[5].signature_param_count = 3;
CBMArena arena;
cbm_arena_init(&arena);
CBMTypeRegistry *registry = cbm_ts_build_cross_registry(&arena, defs, OVERLOAD_DEF_COUNT);
CBMResolvedCallArray calls = {0};
if (registry) {
cbm_run_ts_lsp_cross_with_registry(&arena, kContextualCallbackOverloadSource,
(int)strlen(kContextualCallbackOverloadSource),
names.caller_module_qn, false, jsx_mode, false, registry,
defs, OVERLOAD_DEF_COUNT, NULL, NULL, 0, NULL, &calls);
}
int outer = count_resolved_suffix(&calls, "choose", "lsp_ts_local");
int pair = count_resolved_suffix(&calls, "Pair.pairOnly", "lsp_ts_method");
int reversed = count_resolved_suffix(&calls, "Reversed.reversedOnly", "lsp_ts_method");
if (!registry || outer < 2 || pair < 1 || reversed < 1) {
printf(" ts-overload-return: %s contextual-callback registry=%d outer=%d pair=%d "
"reversed=%d\n",
language_name, registry != NULL, outer, pair, reversed);
print_resolved_calls(&calls);
}
cbm_arena_destroy(&arena);
return registry && outer >= 2 && pair >= 1 && reversed >= 1 ? 0 : 1;
}
static int assert_no_shadowed_return_chains(const CBMResolvedCallArray *calls,
const char *language_name, const char *fixture_name,
const char *forbidden_strategy) {
int promoted = count_resolved_suffix(calls, "choose", forbidden_strategy);
int pair = count_resolved_suffix(calls, "Pair.pairOnly", "lsp_ts_method");
int reversed = count_resolved_suffix(calls, "Reversed.reversedOnly", "lsp_ts_method");
if (promoted != 0 || pair != 0 || reversed != 0) {
printf(" ts-overload-return: %s %s promoted=%d pair=%d reversed=%d\n", language_name,
fixture_name, promoted, pair, reversed);
print_resolved_calls(calls);
}
return promoted == 0 && pair == 0 && reversed == 0 ? 0 : 1;
}
static const char kShadowedFreeFunctionSource[] = "function run(choose): void {\n"
" choose(1, 'x').pairOnly();\n"
" choose('x', 1).reversedOnly();\n"
"}\n";
static int assert_parameter_shadows_free_function(CBMLanguage language, bool jsx_mode,
const char *language_name) {
static const TSOverloadFixtureNames names = {
"repro.shadow.free",
"repro.shadow.free",
"repro.shadow.free.Reversed",
"repro.shadow.free.Pair",
"repro.shadow.free.Reversed.reversedOnly",
"repro.shadow.free.Pair.pairOnly",
"repro.shadow.free.choose",
"repro.shadow.free.run",
};
CBMLSPDef defs[OVERLOAD_DEF_COUNT];
init_overload_defs(defs, language, &names);
CBMArena arena;
cbm_arena_init(&arena);
CBMTypeRegistry *registry = cbm_ts_build_cross_registry(&arena, defs, OVERLOAD_DEF_COUNT);
CBMResolvedCallArray calls = {0};
if (registry) {
cbm_run_ts_lsp_cross_with_registry(&arena, kShadowedFreeFunctionSource,
(int)strlen(kShadowedFreeFunctionSource),
names.caller_module_qn, false, jsx_mode, false, registry,
defs, OVERLOAD_DEF_COUNT, NULL, NULL, 0, NULL, &calls);
}
int rc = registry ? assert_no_shadowed_return_chains(&calls, language_name,
"parameter-shadows-free-function",
"lsp_ts_local")
: 1;
if (!registry)
printf(" ts-overload-return: %s free shadow registry build failed\n", language_name);
cbm_arena_destroy(&arena);
return rc;
}
static const char kShadowedNamespaceSource[] = "import * as Lib from './provider';\n"
"function run(Lib): void {\n"
" Lib.choose(1, 'x').pairOnly();\n"
" Lib.choose('x', 1).reversedOnly();\n"
"}\n";
static int assert_parameter_shadows_namespace(CBMLanguage language, bool jsx_mode,
const char *language_name) {
static const TSOverloadFixtureNames names = {
"repro.shadow.provider",
"repro.shadow.consumer",
"repro.shadow.provider.Reversed",
"repro.shadow.provider.Pair",
"repro.shadow.provider.Reversed.reversedOnly",
"repro.shadow.provider.Pair.pairOnly",
"repro.shadow.provider.choose",
"repro.shadow.consumer.run",
};
CBMLSPDef defs[OVERLOAD_DEF_COUNT];
init_overload_defs(defs, language, &names);
const char *import_names[] = {"Lib"};
const char *import_qns[] = {names.symbol_module_qn};
CBMArena arena;
cbm_arena_init(&arena);
CBMTypeRegistry *registry = cbm_ts_build_cross_registry(&arena, defs, OVERLOAD_DEF_COUNT);
CBMResolvedCallArray calls = {0};
if (registry) {
cbm_run_ts_lsp_cross_with_registry(
&arena, kShadowedNamespaceSource, (int)strlen(kShadowedNamespaceSource),
names.caller_module_qn, false, jsx_mode, false, registry, defs, OVERLOAD_DEF_COUNT,
import_names, import_qns, 1, NULL, &calls);
}
int rc = registry ? assert_no_shadowed_return_chains(&calls, language_name,
"parameter-shadows-namespace",
"lsp_ts_namespace")
: 1;
if (!registry)
printf(" ts-overload-return: %s namespace shadow registry build failed\n", language_name);
cbm_arena_destroy(&arena);
return rc;
}
TEST(repro_ts_free_function_overload_returns_typescript) {
return assert_free_function_overload_returns(CBM_LANG_TYPESCRIPT, false, "TypeScript");
}
TEST(repro_ts_free_function_overload_returns_tsx) {
return assert_free_function_overload_returns(CBM_LANG_TSX, true, "TSX");
}
TEST(repro_ts_namespace_overload_returns_typescript) {
return assert_namespace_overload_returns(CBM_LANG_TYPESCRIPT, false, "TypeScript");
}
TEST(repro_ts_namespace_overload_returns_tsx) {
return assert_namespace_overload_returns(CBM_LANG_TSX, true, "TSX");
}
TEST(repro_ts_exact_symbol_import_overload_returns_typescript) {
return assert_exact_symbol_import_overload_returns(CBM_LANG_TYPESCRIPT, false, "TypeScript");
}
TEST(repro_ts_exact_symbol_import_overload_returns_tsx) {
return assert_exact_symbol_import_overload_returns(CBM_LANG_TSX, true, "TSX");
}
TEST(repro_ts_contextual_callback_uses_selected_overload_typescript) {
return assert_contextual_callback_uses_selected_overload(CBM_LANG_TYPESCRIPT, false,
"TypeScript");
}
TEST(repro_ts_contextual_callback_uses_selected_overload_tsx) {
return assert_contextual_callback_uses_selected_overload(CBM_LANG_TSX, true, "TSX");
}
TEST(repro_ts_parameter_shadows_free_function_typescript) {
return assert_parameter_shadows_free_function(CBM_LANG_TYPESCRIPT, false, "TypeScript");
}
TEST(repro_ts_parameter_shadows_free_function_tsx) {
return assert_parameter_shadows_free_function(CBM_LANG_TSX, true, "TSX");
}
TEST(repro_ts_parameter_shadows_namespace_typescript) {
return assert_parameter_shadows_namespace(CBM_LANG_TYPESCRIPT, false, "TypeScript");
}
TEST(repro_ts_parameter_shadows_namespace_tsx) {
return assert_parameter_shadows_namespace(CBM_LANG_TSX, true, "TSX");
}
SUITE(repro_ts_overload_return_chains) {
RUN_TEST(repro_ts_free_function_overload_returns_typescript);
RUN_TEST(repro_ts_free_function_overload_returns_tsx);
RUN_TEST(repro_ts_namespace_overload_returns_typescript);
RUN_TEST(repro_ts_namespace_overload_returns_tsx);
RUN_TEST(repro_ts_exact_symbol_import_overload_returns_typescript);
RUN_TEST(repro_ts_exact_symbol_import_overload_returns_tsx);
RUN_TEST(repro_ts_contextual_callback_uses_selected_overload_typescript);
RUN_TEST(repro_ts_contextual_callback_uses_selected_overload_tsx);
RUN_TEST(repro_ts_parameter_shadows_free_function_typescript);
RUN_TEST(repro_ts_parameter_shadows_free_function_tsx);
RUN_TEST(repro_ts_parameter_shadows_namespace_typescript);
RUN_TEST(repro_ts_parameter_shadows_namespace_tsx);
}
+572 -16
View File
@@ -26,7 +26,9 @@
#include "lsp/ts_lsp.h"
#include "lsp/go_lsp.h"
#include "lsp/type_registry.h"
#include "../src/pipeline/lsp_resolve.h"
#include "arena.h"
#include "preprocessor.h"
#include <stdio.h>
#include <stdlib.h>
@@ -62,6 +64,13 @@ static CBMFileResult *extract_cpp(const char *src) {
return cbm_extract_file(src, (int)strlen(src), CBM_LANG_CPP, "test", "main.cpp", 0, NULL, NULL);
}
static CBMFileResult *extract_c_family(const char *src, CBMLanguage language) {
const char *path = language == CBM_LANG_C ? "main.c"
: language == CBM_LANG_CUDA ? "main.cu"
: "main.cpp";
return cbm_extract_file(src, (int)strlen(src), language, "test", path, 0, NULL, NULL);
}
TEST(clsp_simple_var_decl) {
CBMFileResult *r = extract_c("\n"
"struct Foo {\n"
@@ -572,17 +581,18 @@ TEST(clsp_nocrash_template_extra_call_args) {
}
TEST(clsp_nocrash_template_function_multi_param_nested_call) {
CBMFileResult *r = extract_cpp("\n"
"void right_aligned_text(int color, int width, const char* fmt, float value) {}\n"
"\n"
"template<typename T, typename R = float>\n"
"R format_units(T value, const char*& unit) { return value; }\n"
"\n"
"void f() {\n"
" const char* unit = nullptr;\n"
" right_aligned_text(0, 0, \"%.1f\", format_units(1, unit));\n"
"}\n"
"");
CBMFileResult *r = extract_cpp(
"\n"
"void right_aligned_text(int color, int width, const char* fmt, float value) {}\n"
"\n"
"template<typename T, typename R = float>\n"
"R format_units(T value, const char*& unit) { return value; }\n"
"\n"
"void f() {\n"
" const char* unit = nullptr;\n"
" right_aligned_text(0, 0, \"%.1f\", format_units(1, unit));\n"
"}\n"
"");
ASSERT_NOT_NULL(r);
ASSERT_GTE(find_resolved(r, "f", "right_aligned_text"), 0);
ASSERT_GTE(find_resolved(r, "f", "format_units"), 0);
@@ -6367,7 +6377,10 @@ TEST(clsp_cross_gap_new_then_method_chain) {
ASSERT_GTE(find_resolved(r, "test", "Service.Service"), 0);
ASSERT_GTE(find_resolved(r, "test", "start"), 0);
ASSERT_GTE(find_resolved(r, "test", "stop"), 0);
ASSERT_GTE(find_resolved(r, "test", "~Service"), 0);
/* Service declares no destructor node. Explicit delete retains its
* semantic-only carrier, but the resolver must not fabricate an implicit
* ~Service target that the graph cannot materialize. */
ASSERT_EQ(find_resolved(r, "test", "~Service"), -1);
cbm_free_result(r);
PASS();
}
@@ -7292,7 +7305,10 @@ TEST(clsp_pattern_auto_ptr_from_new) {
ASSERT_NOT_NULL(r);
ASSERT_GTE(find_resolved(r, "test", "draw"), 0);
ASSERT_GTE(find_resolved(r, "test", "Widget.Widget"), 0);
ASSERT_GTE(find_resolved(r, "test", "~Widget"), 0);
/* Widget has only an implicit destructor, so delete cannot resolve to a
* concrete graph target. A declared ~Widget is covered by the dedicated
* explicit-destructor test. */
ASSERT_EQ(find_resolved(r, "test", "~Widget"), -1);
cbm_free_result(r);
PASS();
}
@@ -15370,7 +15386,7 @@ TEST(seal_py_shared_registry_readonly) {
const char *src = "def helper(x):\n return x + 1\n\ndef caller():\n return helper(1)\n";
CBMResolvedCallArray out = {0};
cbm_run_py_lsp_cross_with_registry(&arena, src, (int)strlen(src), "test.mod", reg, NULL, NULL,
0, NULL, &out);
0, NULL, &out, NULL);
ASSERT_EQ(reg->func_count, fb);
ASSERT_EQ(reg->type_count, tb);
cbm_arena_destroy(&arena);
@@ -15436,8 +15452,8 @@ TEST(seal_py_shared_registry_readonly_fields) {
" def m(self):\n"
" self.x = 1\n";
CBMResolvedCallArray out = {0};
cbm_run_py_lsp_cross_with_registry(&arena, src, (int)strlen(src), "test.mod", reg, NULL, NULL, 0,
NULL, &out);
cbm_run_py_lsp_cross_with_registry(&arena, src, (int)strlen(src), "test.mod", reg, NULL, NULL,
0, NULL, &out, NULL);
/* The sealed registry entry must be untouched by resolve. */
int count_after = 0;
@@ -15502,7 +15518,547 @@ TEST(seal_go_shared_registry_readonly) {
PASS();
}
static int assert_cpp_family_repeated_method_occurrences(CBMLanguage language) {
static const char source[] = "struct Alpha { int render() { return 1; } };\n"
"struct Bravo { int render() { return 2; } };\n"
"int occurrence_probe() {\n"
" Alpha alpha;\n"
" Bravo bravo;\n"
" return alpha.render() + bravo.render();\n"
"}\n";
static const char *call_texts[2] = {"alpha.render()", "bravo.render()"};
const char *sites[2] = {strstr(source, call_texts[0]), strstr(source, call_texts[1])};
ASSERT_NOT_NULL(sites[0]);
ASSERT_NOT_NULL(sites[1]);
uint32_t starts[2] = {(uint32_t)(sites[0] - source), (uint32_t)(sites[1] - source)};
uint32_t ends[2] = {starts[0] + (uint32_t)strlen(call_texts[0]),
starts[1] + (uint32_t)strlen(call_texts[1])};
CBMFileResult *result = extract_c_family(source, language);
ASSERT_NOT_NULL(result);
const CBMCall *calls[2] = {NULL, NULL};
const CBMResolvedCall *semantics[2] = {NULL, NULL};
int carrier_count = 0;
int semantic_count = 0;
for (int i = 0; i < result->calls.count; i++) {
const CBMCall *call = &result->calls.items[i];
if (!call->enclosing_func_qn || !strstr(call->enclosing_func_qn, "occurrence_probe") ||
!call->callee_name || strcmp(cbm_lsp_bare_segment(call->callee_name), "render") != 0) {
continue;
}
carrier_count++;
for (int occurrence = 0; occurrence < 2; occurrence++) {
if (call->site_start_byte == starts[occurrence] &&
call->site_end_byte == ends[occurrence]) {
calls[occurrence] = call;
}
}
}
for (int i = 0; i < result->resolved_calls.count; i++) {
const CBMResolvedCall *resolved = &result->resolved_calls.items[i];
if (resolved->kind != CBM_RESOLVED_INVOCATION || resolved->confidence <= 0.0f ||
!resolved->caller_qn || !strstr(resolved->caller_qn, "occurrence_probe") ||
!resolved->callee_qn ||
strcmp(cbm_lsp_bare_segment(resolved->callee_qn), "render") != 0) {
continue;
}
semantic_count++;
for (int occurrence = 0; occurrence < 2; occurrence++) {
if (resolved->site_start_byte == starts[occurrence] &&
resolved->site_end_byte == ends[occurrence]) {
semantics[occurrence] = resolved;
}
}
}
ASSERT_EQ(carrier_count, 2);
ASSERT_EQ(semantic_count, 2);
ASSERT_NOT_NULL(calls[0]);
ASSERT_NOT_NULL(calls[1]);
ASSERT_NOT_NULL(semantics[0]);
ASSERT_NOT_NULL(semantics[1]);
ASSERT_NOT_NULL(strstr(semantics[0]->callee_qn, "Alpha.render"));
ASSERT_NOT_NULL(strstr(semantics[1]->callee_qn, "Bravo.render"));
ASSERT_TRUE(cbm_pipeline_find_lsp_resolution(&result->resolved_calls, calls[0], false) ==
semantics[0]);
ASSERT_TRUE(cbm_pipeline_find_lsp_resolution(&result->resolved_calls, calls[1], false) ==
semantics[1]);
cbm_free_result(result);
return 0;
}
TEST(clsp_cpp_repeated_same_leaf_calls_join_exact_occurrences) {
return assert_cpp_family_repeated_method_occurrences(CBM_LANG_CPP);
}
TEST(clsp_cuda_repeated_same_leaf_calls_join_exact_occurrences) {
return assert_cpp_family_repeated_method_occurrences(CBM_LANG_CUDA);
}
TEST(clsp_c_reassigned_function_pointer_calls_join_exact_occurrences) {
static const char source[] = "static int alpha_target(void) { return 1; }\n"
"static int bravo_target(void) { return 2; }\n"
"int occurrence_probe(void) {\n"
" int (*fp)(void) = alpha_target;\n"
" int first = fp();\n"
" fp = bravo_target;\n"
" return first + fp();\n"
"}\n";
static const char call_text[] = "fp()";
const char *first_site = strstr(source, call_text);
const char *second_site = first_site ? strstr(first_site + 1, call_text) : NULL;
ASSERT_NOT_NULL(first_site);
ASSERT_NOT_NULL(second_site);
uint32_t starts[2] = {(uint32_t)(first_site - source), (uint32_t)(second_site - source)};
uint32_t ends[2] = {starts[0] + (uint32_t)strlen(call_text),
starts[1] + (uint32_t)strlen(call_text)};
CBMFileResult *result = extract_c_family(source, CBM_LANG_C);
ASSERT_NOT_NULL(result);
const CBMCall *calls[2] = {NULL, NULL};
const CBMResolvedCall *semantics[2] = {NULL, NULL};
int carrier_count = 0;
int semantic_count = 0;
for (int i = 0; i < result->calls.count; i++) {
const CBMCall *call = &result->calls.items[i];
if (!call->enclosing_func_qn || !strstr(call->enclosing_func_qn, "occurrence_probe") ||
!call->callee_name || strcmp(call->callee_name, "fp") != 0) {
continue;
}
carrier_count++;
for (int occurrence = 0; occurrence < 2; occurrence++) {
if (call->site_start_byte == starts[occurrence] &&
call->site_end_byte == ends[occurrence]) {
calls[occurrence] = call;
}
}
}
for (int i = 0; i < result->resolved_calls.count; i++) {
const CBMResolvedCall *resolved = &result->resolved_calls.items[i];
if (resolved->kind != CBM_RESOLVED_INVOCATION || resolved->confidence <= 0.0f ||
!resolved->caller_qn || !strstr(resolved->caller_qn, "occurrence_probe") ||
!resolved->reason || strcmp(resolved->reason, "fp") != 0) {
continue;
}
semantic_count++;
for (int occurrence = 0; occurrence < 2; occurrence++) {
if (resolved->site_start_byte == starts[occurrence] &&
resolved->site_end_byte == ends[occurrence]) {
semantics[occurrence] = resolved;
}
}
}
ASSERT_EQ(carrier_count, 2);
ASSERT_EQ(semantic_count, 2);
ASSERT_NOT_NULL(calls[0]);
ASSERT_NOT_NULL(calls[1]);
ASSERT_NOT_NULL(semantics[0]);
ASSERT_NOT_NULL(semantics[1]);
ASSERT_NOT_NULL(strstr(semantics[0]->callee_qn, "alpha_target"));
ASSERT_NOT_NULL(strstr(semantics[1]->callee_qn, "bravo_target"));
ASSERT_TRUE(cbm_pipeline_find_lsp_resolution(&result->resolved_calls, calls[0], false) ==
semantics[0]);
ASSERT_TRUE(cbm_pipeline_find_lsp_resolution(&result->resolved_calls, calls[1], false) ==
semantics[1]);
cbm_free_result(result);
PASS();
}
TEST(clsp_c_nested_function_pointer_shadow_restores_outer_target) {
static const char source[] = "static int alpha_target(void) { return 1; }\n"
"static int bravo_target(void) { return 2; }\n"
"int occurrence_probe(void) {\n"
" int (*fp)(void) = alpha_target;\n"
" int inner = 0;\n"
" {\n"
" int (*fp)(void) = bravo_target;\n"
" inner = fp();\n"
" }\n"
" return inner + fp();\n"
"}\n";
const char *inner_site = strstr(source, "fp();");
const char *outer_site = inner_site ? strstr(inner_site + 1, "fp();") : NULL;
ASSERT_NOT_NULL(inner_site);
ASSERT_NOT_NULL(outer_site);
uint32_t inner_start = (uint32_t)(inner_site - source);
uint32_t outer_start = (uint32_t)(outer_site - source);
uint32_t call_len = (uint32_t)strlen("fp()");
CBMFileResult *result = extract_c_family(source, CBM_LANG_C);
ASSERT_NOT_NULL(result);
const CBMResolvedCall *inner_semantic = NULL;
const CBMResolvedCall *outer_semantic = NULL;
for (int i = 0; i < result->resolved_calls.count; i++) {
const CBMResolvedCall *resolved = &result->resolved_calls.items[i];
if (resolved->kind != CBM_RESOLVED_INVOCATION || !resolved->reason ||
strcmp(resolved->reason, "fp") != 0 || !resolved->callee_qn) {
continue;
}
if (resolved->site_start_byte == inner_start &&
resolved->site_end_byte == inner_start + call_len) {
inner_semantic = resolved;
}
if (resolved->site_start_byte == outer_start &&
resolved->site_end_byte == outer_start + call_len) {
outer_semantic = resolved;
}
}
ASSERT_NOT_NULL(inner_semantic);
ASSERT_NOT_NULL(outer_semantic);
ASSERT_NOT_NULL(strstr(inner_semantic->callee_qn, "bravo_target"));
ASSERT_NOT_NULL(strstr(outer_semantic->callee_qn, "alpha_target"));
cbm_free_result(result);
PASS();
}
TEST(clsp_cpp_new_expression_joins_exact_occurrence) {
static const char source[] = "struct Widget { Widget() {} };\n"
"Widget* occurrence_probe() { return new Widget(); }\n";
const char *site = strstr(source, "new Widget()");
ASSERT_NOT_NULL(site);
uint32_t start = (uint32_t)(site - source);
uint32_t end = start + (uint32_t)strlen("new Widget()");
CBMFileResult *result = extract_c_family(source, CBM_LANG_CPP);
ASSERT_NOT_NULL(result);
const CBMCall *carrier = NULL;
const CBMResolvedCall *semantic = NULL;
for (int i = 0; i < result->calls.count; i++) {
const CBMCall *call = &result->calls.items[i];
if (call->site_start_byte == start && call->site_end_byte == end) {
carrier = call;
break;
}
}
for (int i = 0; i < result->resolved_calls.count; i++) {
const CBMResolvedCall *resolved = &result->resolved_calls.items[i];
if (resolved->kind == CBM_RESOLVED_INVOCATION && resolved->callee_qn &&
strstr(resolved->callee_qn, "Widget.Widget") && resolved->site_start_byte == start &&
resolved->site_end_byte == end) {
semantic = resolved;
break;
}
}
ASSERT_NOT_NULL(carrier);
ASSERT_NOT_NULL(semantic);
ASSERT_TRUE(cbm_pipeline_find_lsp_resolution(&result->resolved_calls, carrier, false) ==
semantic);
cbm_free_result(result);
PASS();
}
TEST(clsp_c_global_initializer_does_not_inherit_previous_function_caller) {
static const char source[] = "int helper(void) { return 1; }\n"
"void previous(void) {}\n"
"int global_value = helper();\n";
const char *site = strrchr(source, 'h');
ASSERT_NOT_NULL(site);
ASSERT_STR_EQ(site, "helper();\n");
uint32_t start = (uint32_t)(site - source);
uint32_t end = start + (uint32_t)strlen("helper()");
CBMFileResult *result = extract_c_family(source, CBM_LANG_C);
ASSERT_NOT_NULL(result);
const CBMCall *carrier = NULL;
const CBMResolvedCall *semantic = NULL;
for (int i = 0; i < result->calls.count; i++) {
const CBMCall *call = &result->calls.items[i];
if (call->site_start_byte == start && call->site_end_byte == end) {
carrier = call;
break;
}
}
for (int i = 0; i < result->resolved_calls.count; i++) {
const CBMResolvedCall *resolved = &result->resolved_calls.items[i];
if (resolved->kind == CBM_RESOLVED_INVOCATION && resolved->callee_qn &&
strstr(resolved->callee_qn, "helper") && resolved->site_start_byte == start &&
resolved->site_end_byte == end) {
semantic = resolved;
break;
}
}
ASSERT_NOT_NULL(carrier);
ASSERT_NOT_NULL(semantic);
ASSERT_STR_EQ(semantic->caller_qn, carrier->enclosing_func_qn);
ASSERT_TRUE(cbm_pipeline_find_lsp_resolution(&result->resolved_calls, carrier, false) ==
semantic);
cbm_free_result(result);
PASS();
}
static int assert_cpp_family_preprocessed_collision_isolated(CBMLanguage language) {
/* simplecpp removes the 41-byte comment payload and normalizes tokens. As
* verified by the non-vacuity assertions below, raw alpha.render and the
* macro-hidden expanded bravo.render both land at numeric span 182:200,
* even though those offsets refer to different source texts. */
static const char source[] =
"struct Alpha { int render(){return 1;} };\n"
"struct Bravo { int render(){return 2;} };\n"
"int occurrence_probe(){\n"
" Alpha alpha; Bravo bravo;\n"
" /*xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx*/ alpha . render ( );\n"
"#define HIDDEN_RENDER(x) x.render()\n"
" return HIDDEN_RENDER(bravo);\n"
"}\n";
const uint32_t collision_start = 182;
const uint32_t collision_end = 200;
ASSERT_EQ((uint32_t)(strstr(source, "alpha . render ( )") - source), collision_start);
CBMFileResult *result = extract_c_family(source, language);
ASSERT_NOT_NULL(result);
const CBMCall *raw_alpha = NULL;
const CBMCall *expanded_bravo = NULL;
int colliding_carriers = 0;
for (int i = 0; i < result->calls.count; i++) {
const CBMCall *call = &result->calls.items[i];
if (!call->enclosing_func_qn || !strstr(call->enclosing_func_qn, "occurrence_probe") ||
call->site_start_byte != collision_start || call->site_end_byte != collision_end ||
!call->callee_name || !strstr(call->callee_name, "render")) {
continue;
}
colliding_carriers++;
if (strstr(call->callee_name, "alpha")) {
raw_alpha = call;
}
if (strstr(call->callee_name, "bravo")) {
expanded_bravo = call;
}
}
ASSERT_EQ(colliding_carriers, 2);
ASSERT_NOT_NULL(raw_alpha);
ASSERT_NOT_NULL(expanded_bravo);
const CBMResolvedCall *raw_alpha_semantic = NULL;
const CBMResolvedCall *expanded_bravo_semantic = NULL;
for (int i = 0; i < result->resolved_calls.count; i++) {
const CBMResolvedCall *resolved = &result->resolved_calls.items[i];
if (resolved->kind != CBM_RESOLVED_INVOCATION || resolved->confidence <= 0.0f ||
resolved->site_start_byte != collision_start ||
resolved->site_end_byte != collision_end || !resolved->callee_qn) {
continue;
}
if (strstr(resolved->callee_qn, "Alpha.render")) {
raw_alpha_semantic = resolved;
}
if (strstr(resolved->callee_qn, "Bravo.render")) {
expanded_bravo_semantic = resolved;
}
}
ASSERT_NOT_NULL(raw_alpha_semantic);
ASSERT_NOT_NULL(expanded_bravo_semantic);
ASSERT_TRUE(cbm_pipeline_find_lsp_resolution(&result->resolved_calls, raw_alpha, false) ==
raw_alpha_semantic);
ASSERT_TRUE(cbm_pipeline_find_lsp_resolution(&result->resolved_calls, expanded_bravo, false) ==
expanded_bravo_semantic);
cbm_free_result(result);
return 0;
}
TEST(clsp_cpp_preprocessed_coordinate_collision_isolated) {
return assert_cpp_family_preprocessed_collision_isolated(CBM_LANG_CPP);
}
TEST(clsp_cuda_preprocessed_coordinate_collision_isolated) {
return assert_cpp_family_preprocessed_collision_isolated(CBM_LANG_CUDA);
}
TEST(clsp_c_preprocessed_coordinate_collision_isolated) {
/* The raw fp call and macro-hidden expanded fp call both occupy 206:212,
* but data flow resolves them to different targets. */
static const char source[] =
"static int alpha_target(void){return 1;}\n"
"static int bravo_target(void){return 2;}\n"
"int occurrence_probe(void){\n"
" int (*fp)(void) = alpha_target;\n"
" /*xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx*/ fp ( );\n"
"#define HIDDEN_FP_CALL() (fp = bravo_target, fp())\n"
" return HIDDEN_FP_CALL();\n"
"}\n";
const uint32_t collision_start = 206;
const uint32_t collision_end = 212;
const char *raw_probe = strstr(source, "occurrence_probe");
ASSERT_NOT_NULL(raw_probe);
ASSERT_EQ((uint32_t)(strstr(raw_probe, "fp ( )") - source), collision_start);
CBMFileResult *result = extract_c_family(source, CBM_LANG_C);
ASSERT_NOT_NULL(result);
const CBMCall *colliding_calls[2] = {NULL, NULL};
int colliding_carriers = 0;
for (int i = 0; i < result->calls.count; i++) {
const CBMCall *call = &result->calls.items[i];
if (call->enclosing_func_qn && strstr(call->enclosing_func_qn, "occurrence_probe") &&
call->callee_name && strcmp(call->callee_name, "fp") == 0 &&
call->site_start_byte == collision_start && call->site_end_byte == collision_end) {
if (colliding_carriers < 2) {
colliding_calls[colliding_carriers] = call;
}
colliding_carriers++;
}
}
ASSERT_EQ(colliding_carriers, 2);
ASSERT_NOT_NULL(colliding_calls[0]);
ASSERT_NOT_NULL(colliding_calls[1]);
const CBMResolvedCall *alpha_semantic = NULL;
const CBMResolvedCall *bravo_semantic = NULL;
for (int i = 0; i < result->resolved_calls.count; i++) {
const CBMResolvedCall *resolved = &result->resolved_calls.items[i];
if (resolved->kind != CBM_RESOLVED_INVOCATION || resolved->confidence <= 0.0f ||
resolved->site_start_byte != collision_start ||
resolved->site_end_byte != collision_end || !resolved->callee_qn || !resolved->reason ||
strcmp(resolved->reason, "fp") != 0) {
continue;
}
if (strstr(resolved->callee_qn, "alpha_target")) {
alpha_semantic = resolved;
}
if (strstr(resolved->callee_qn, "bravo_target")) {
bravo_semantic = resolved;
}
}
ASSERT_NOT_NULL(alpha_semantic);
ASSERT_NOT_NULL(bravo_semantic);
const CBMResolvedCall *first_join =
cbm_pipeline_find_lsp_resolution(&result->resolved_calls, colliding_calls[0], false);
const CBMResolvedCall *second_join =
cbm_pipeline_find_lsp_resolution(&result->resolved_calls, colliding_calls[1], false);
ASSERT_TRUE((first_join == alpha_semantic && second_join == bravo_semantic) ||
(first_join == bravo_semantic && second_join == alpha_semantic));
cbm_free_result(result);
PASS();
}
TEST(clsp_preprocessed_destructor_rewrite_respects_origin_during_rewrite) {
/* Calibrate the comment padding against simplecpp itself: comments vanish
* from the expanded buffer, so one padding length makes the raw
* `delete alpha` and macro-expanded `delete bravo` occupy the same numeric
* byte span. The names intentionally have equal length.
*
* The raw destructor carrier is rewritten during the first C-LSP walk. On
* the second walk, a preprocessed carrier must be stamped PREPROCESSED
* before c_rewrite_proved_destructor_candidates compares it with the
* already-present raw semantic rows. Otherwise the earlier raw ~Alpha row
* wins this colliding occurrence and rewrites the expanded carrier to the
* wrong destructor. */
static const char format[] = "struct Alpha { ~Alpha(){} };\n"
"struct Bravo { ~Bravo(){} };\n"
"void occurrence_probe(){\n"
" Alpha* alpha = new Alpha(); Bravo* bravo = new Bravo();\n"
" /*%s*/ delete alpha;\n"
"#define HIDDEN_DELETE(p) delete p\n"
" HIDDEN_DELETE(bravo);\n"
"}\n";
char padding[1024] = {0};
char source[4096];
int written = snprintf(source, sizeof(source), format, padding);
ASSERT_GT(written, 0);
ASSERT_LT((size_t)written, sizeof(source));
char *expanded = cbm_preprocess(source, written, "main.cpp", NULL, NULL, true);
ASSERT_NOT_NULL(expanded);
const char *raw_alpha = strstr(source, "delete alpha");
const char *expanded_bravo = strstr(expanded, "delete bravo");
ASSERT_NOT_NULL(raw_alpha);
ASSERT_NOT_NULL(expanded_bravo);
ptrdiff_t raw_offset = raw_alpha - source;
ptrdiff_t expanded_offset = expanded_bravo - expanded;
cbm_preprocess_free(expanded);
ASSERT_GTE(expanded_offset, raw_offset);
size_t padding_length = (size_t)(expanded_offset - raw_offset);
ASSERT_LT(padding_length, sizeof(padding));
memset(padding, 'x', padding_length);
padding[padding_length] = '\0';
written = snprintf(source, sizeof(source), format, padding);
ASSERT_GT(written, 0);
ASSERT_LT((size_t)written, sizeof(source));
expanded = cbm_preprocess(source, written, "main.cpp", NULL, NULL, true);
ASSERT_NOT_NULL(expanded);
raw_alpha = strstr(source, "delete alpha");
expanded_bravo = strstr(expanded, "delete bravo");
ASSERT_NOT_NULL(raw_alpha);
ASSERT_NOT_NULL(expanded_bravo);
const uint32_t collision_start = (uint32_t)(raw_alpha - source);
ASSERT_EQ((uint32_t)(expanded_bravo - expanded), collision_start);
const uint32_t collision_end = collision_start + (uint32_t)strlen("delete alpha");
cbm_preprocess_free(expanded);
CBMFileResult *result = extract_cpp(source);
ASSERT_NOT_NULL(result);
const CBMCall *raw_carrier = NULL;
const CBMCall *preprocessed_carrier = NULL;
const CBMResolvedCall *raw_semantic = NULL;
const CBMResolvedCall *preprocessed_semantic = NULL;
for (int i = 0; i < result->calls.count; i++) {
const CBMCall *call = &result->calls.items[i];
if (!call->requires_lsp_resolution || call->site_start_byte != collision_start ||
call->site_end_byte != collision_end) {
continue;
}
if (call->source_origin == CBM_SOURCE_ORIGIN_RAW) {
raw_carrier = call;
} else if (call->source_origin == CBM_SOURCE_ORIGIN_PREPROCESSED) {
preprocessed_carrier = call;
}
}
for (int i = 0; i < result->resolved_calls.count; i++) {
const CBMResolvedCall *resolved = &result->resolved_calls.items[i];
if (resolved->kind != CBM_RESOLVED_INVOCATION || !resolved->strategy ||
strcmp(resolved->strategy, "lsp_destructor") != 0 || !resolved->callee_qn ||
resolved->site_start_byte != collision_start ||
resolved->site_end_byte != collision_end) {
continue;
}
if (resolved->source_origin == CBM_SOURCE_ORIGIN_RAW &&
strstr(resolved->callee_qn, "~Alpha")) {
raw_semantic = resolved;
} else if (resolved->source_origin == CBM_SOURCE_ORIGIN_PREPROCESSED &&
strstr(resolved->callee_qn, "~Bravo")) {
preprocessed_semantic = resolved;
}
}
ASSERT_NOT_NULL(raw_carrier);
ASSERT_NOT_NULL(preprocessed_carrier);
ASSERT_NOT_NULL(raw_semantic);
ASSERT_NOT_NULL(preprocessed_semantic);
bool raw_name_ok = raw_carrier->callee_name &&
strcmp(cbm_lsp_bare_segment(raw_carrier->callee_name), "~Alpha") == 0;
bool preprocessed_name_ok =
preprocessed_carrier->callee_name &&
strcmp(cbm_lsp_bare_segment(preprocessed_carrier->callee_name), "~Bravo") == 0;
bool raw_join_ok = cbm_pipeline_find_lsp_resolution(&result->resolved_calls, raw_carrier,
false) == raw_semantic;
bool preprocessed_join_ok =
cbm_pipeline_find_lsp_resolution(&result->resolved_calls, preprocessed_carrier, false) ==
preprocessed_semantic;
if (!raw_name_ok || !preprocessed_name_ok || !raw_join_ok || !preprocessed_join_ok) {
printf(" destructor origin rewrite diagnostic: raw=%s/%d preprocessed=%s/%d\n",
raw_carrier->callee_name ? raw_carrier->callee_name : "<null>", raw_join_ok,
preprocessed_carrier->callee_name ? preprocessed_carrier->callee_name : "<null>",
preprocessed_join_ok);
}
cbm_free_result(result);
ASSERT_TRUE(raw_name_ok);
ASSERT_TRUE(preprocessed_name_ok);
ASSERT_TRUE(raw_join_ok);
ASSERT_TRUE(preprocessed_join_ok);
PASS();
}
SUITE(c_lsp) {
RUN_TEST(clsp_c_reassigned_function_pointer_calls_join_exact_occurrences);
RUN_TEST(clsp_c_nested_function_pointer_shadow_restores_outer_target);
RUN_TEST(clsp_cpp_repeated_same_leaf_calls_join_exact_occurrences);
RUN_TEST(clsp_cuda_repeated_same_leaf_calls_join_exact_occurrences);
RUN_TEST(clsp_cpp_new_expression_joins_exact_occurrence);
RUN_TEST(clsp_c_global_initializer_does_not_inherit_previous_function_caller);
RUN_TEST(clsp_c_preprocessed_coordinate_collision_isolated);
RUN_TEST(clsp_cpp_preprocessed_coordinate_collision_isolated);
RUN_TEST(clsp_cuda_preprocessed_coordinate_collision_isolated);
RUN_TEST(clsp_preprocessed_destructor_rewrite_respects_origin_during_rewrite);
RUN_TEST(clsp_tier2_shared_registry_readonly_c);
RUN_TEST(clsp_tier2_shared_registry_readonly_cpp);
RUN_TEST(seal_py_shared_registry_readonly);
+442
View File
@@ -0,0 +1,442 @@
/* Production-graph contracts for callable values. These tests intentionally
* exercise the full MCP indexing path rather than only raw extraction/LSP rows. */
#include "test_framework.h"
#include "repro_harness.h"
#include <store/store.h>
#include <string.h>
static int crc_edge_count(cbm_store_t *store, const char *project, const char *type,
const char *source_name, const char *target_label,
const char *target_name) {
cbm_edge_t *edges = NULL;
int edge_count = 0;
if (cbm_store_find_edges_by_type(store, project, type, &edges, &edge_count) != CBM_STORE_OK) {
return -1;
}
int matches = 0;
for (int i = 0; i < edge_count; i++) {
cbm_node_t source = {0};
cbm_node_t target = {0};
bool source_ok =
cbm_store_find_node_by_id(store, edges[i].source_id, &source) == CBM_STORE_OK;
bool target_ok =
cbm_store_find_node_by_id(store, edges[i].target_id, &target) == CBM_STORE_OK;
if (source_ok && target_ok && source.name && target.name &&
strcmp(source.name, source_name) == 0 && strcmp(target.name, target_name) == 0 &&
(!target_label || (target.label && strcmp(target.label, target_label) == 0))) {
matches++;
}
cbm_node_free_fields(&source);
cbm_node_free_fields(&target);
}
cbm_store_free_edges(edges, edge_count);
return matches;
}
static int crc_global_handler_edge_count(cbm_store_t *store, const char *project,
const char *caller) {
int references =
crc_edge_count(store, project, "CALL_REFERENCE", caller, "Function", "handler");
int calls = crc_edge_count(store, project, "CALLS", caller, "Function", "handler");
int usages = crc_edge_count(store, project, "USAGE", caller, "Function", "handler");
return references < 0 || calls < 0 || usages < 0 ? -1 : references + calls + usages;
}
static int crc_function_count(cbm_store_t *store, const char *project, const char *name) {
cbm_node_t *nodes = NULL;
int node_count = 0;
if (cbm_store_find_nodes_by_name(store, project, name, &nodes, &node_count) != CBM_STORE_OK) {
return -1;
}
int functions = 0;
for (int i = 0; i < node_count; i++) {
if (nodes[i].label && strcmp(nodes[i].label, "Function") == 0) {
functions++;
}
}
cbm_store_free_nodes(nodes, node_count);
return functions;
}
TEST(call_reference_typescript_direct_argument_is_exact) {
static const char source[] = "function handler(): void {}\n"
"function register(callback: () => void): void {}\n"
"function bootstrap(): void { register(handler); }\n";
RProj project;
cbm_store_t *store = rh_index(&project, "main.ts", source);
ASSERT_NOT_NULL(store);
int registrar =
crc_edge_count(store, project.project, "CALLS", "bootstrap", "Function", "register");
int reference = crc_edge_count(store, project.project, "CALL_REFERENCE", "bootstrap",
"Function", "handler");
int usage = crc_edge_count(store, project.project, "USAGE", "bootstrap", "Function", "handler");
int fabricated_call =
crc_edge_count(store, project.project, "CALLS", "bootstrap", "Function", "handler");
rh_cleanup(&project, store);
ASSERT_EQ(registrar, 1);
ASSERT_EQ(reference, 1);
ASSERT_EQ(usage, 0);
ASSERT_EQ(fabricated_call, 0);
PASS();
}
TEST(call_reference_kotlin_alias_argument_is_exact) {
static const char source[] = "fun handler(): Unit {}\n"
"fun accept(callback: () -> Unit): Unit {}\n"
"val callback: () -> Unit = ::handler\n"
"fun argument(): Unit {\n"
" accept(callback)\n"
"}\n";
RProj project;
cbm_store_t *store = rh_index(&project, "AliasArgument.kt", source);
ASSERT_NOT_NULL(store);
int reference =
crc_edge_count(store, project.project, "CALL_REFERENCE", "argument", "Function", "handler");
int usage = crc_edge_count(store, project.project, "USAGE", "argument", "Function", "handler");
int fabricated_call =
crc_edge_count(store, project.project, "CALLS", "argument", "Function", "handler");
rh_cleanup(&project, store);
ASSERT_EQ(reference, 1);
ASSERT_EQ(usage, 0);
ASSERT_EQ(fabricated_call, 0);
PASS();
}
TEST(call_reference_python_use_before_local_shadow_is_not_global) {
static const char source[] = "def handler():\n"
" pass\n"
"def accept(callback):\n"
" pass\n"
"def caller():\n"
" accept(handler)\n"
" handler = lambda: None\n";
RProj project;
cbm_store_t *store = rh_index(&project, "shadow.py", source);
ASSERT_NOT_NULL(store);
int registrar = crc_edge_count(store, project.project, "CALLS", "caller", "Function", "accept");
int handler_nodes = crc_function_count(store, project.project, "handler");
int global_edges = crc_global_handler_edge_count(store, project.project, "caller");
rh_cleanup(&project, store);
ASSERT_EQ(registrar, 1);
ASSERT_GTE(handler_nodes, 1);
ASSERT_EQ(global_edges, 0);
PASS();
}
TEST(call_reference_javascript_use_before_local_shadow_is_not_global) {
static const char source[] = "function handler() {}\n"
"function accept(callback) {}\n"
"function caller() {\n"
" accept(handler);\n"
" const handler = () => {};\n"
"}\n";
RProj project;
cbm_store_t *store = rh_index(&project, "shadow.js", source);
ASSERT_NOT_NULL(store);
int registrar = crc_edge_count(store, project.project, "CALLS", "caller", "Function", "accept");
int handler_nodes = crc_function_count(store, project.project, "handler");
int global_edges = crc_global_handler_edge_count(store, project.project, "caller");
rh_cleanup(&project, store);
ASSERT_EQ(registrar, 1);
ASSERT_GTE(handler_nodes, 1);
ASSERT_EQ(global_edges, 0);
PASS();
}
TEST(call_reference_typescript_use_before_local_shadow_is_not_global) {
static const char source[] = "function handler(): void {}\n"
"function accept(callback: () => void): void {}\n"
"function caller(): void {\n"
" accept(handler);\n"
" const handler = (): void => {};\n"
"}\n";
RProj project;
cbm_store_t *store = rh_index(&project, "shadow.ts", source);
ASSERT_NOT_NULL(store);
int registrar = crc_edge_count(store, project.project, "CALLS", "caller", "Function", "accept");
int handler_nodes = crc_function_count(store, project.project, "handler");
int global_edges = crc_global_handler_edge_count(store, project.project, "caller");
rh_cleanup(&project, store);
ASSERT_EQ(registrar, 1);
ASSERT_GTE(handler_nodes, 1);
ASSERT_EQ(global_edges, 0);
PASS();
}
TEST(call_reference_python_keyword_argument_is_exact) {
static const char source[] = "def handler():\n"
" pass\n"
"def accept(callback=None):\n"
" pass\n"
"def argument():\n"
" accept(callback=handler)\n";
RProj project;
cbm_store_t *store = rh_index(&project, "keyword.py", source);
ASSERT_NOT_NULL(store);
int reference =
crc_edge_count(store, project.project, "CALL_REFERENCE", "argument", "Function", "handler");
int usage = crc_edge_count(store, project.project, "USAGE", "argument", "Function", "handler");
int fabricated_call =
crc_edge_count(store, project.project, "CALLS", "argument", "Function", "handler");
rh_cleanup(&project, store);
ASSERT_EQ(reference, 1);
ASSERT_EQ(usage, 0);
ASSERT_EQ(fabricated_call, 0);
PASS();
}
TEST(call_reference_python_bound_method_argument_is_exact) {
static const char source[] = "class Service:\n"
" def handler(self):\n"
" pass\n"
"def accept(callback):\n"
" pass\n"
"def argument(service: Service):\n"
" accept(service.handler)\n";
RProj project;
cbm_store_t *store = rh_index(&project, "bound.py", source);
ASSERT_NOT_NULL(store);
int reference =
crc_edge_count(store, project.project, "CALL_REFERENCE", "argument", "Method", "handler");
int usage = crc_edge_count(store, project.project, "USAGE", "argument", "Method", "handler");
int fabricated_call =
crc_edge_count(store, project.project, "CALLS", "argument", "Method", "handler");
rh_cleanup(&project, store);
ASSERT_EQ(reference, 1);
ASSERT_EQ(usage, 0);
ASSERT_EQ(fabricated_call, 0);
PASS();
}
TEST(call_reference_python_parenthesized_bound_method_is_exact) {
static const char source[] = "class Service:\n"
" def handler(self):\n"
" pass\n"
"def accept(callback):\n"
" pass\n"
"def argument(service: Service):\n"
" accept((service.handler))\n";
RProj project;
cbm_store_t *store = rh_index(&project, "parenthesized.py", source);
ASSERT_NOT_NULL(store);
int reference =
crc_edge_count(store, project.project, "CALL_REFERENCE", "argument", "Method", "handler");
int usage = crc_edge_count(store, project.project, "USAGE", "argument", "Method", "handler");
int fabricated_call =
crc_edge_count(store, project.project, "CALLS", "argument", "Method", "handler");
rh_cleanup(&project, store);
ASSERT_EQ(reference, 1);
ASSERT_EQ(usage, 0);
ASSERT_EQ(fabricated_call, 0);
PASS();
}
TEST(call_reference_python_instance_field_shadow_is_not_method) {
static const char source[] = "class Service:\n"
" def __init__(self):\n"
" self.handler = 42\n"
" def handler(self):\n"
" pass\n"
"def accept(callback):\n"
" pass\n"
"def argument(service: Service):\n"
" accept(service.handler)\n";
RProj project;
cbm_store_t *store = rh_index(&project, "field_shadow.py", source);
ASSERT_NOT_NULL(store);
int method_reference =
crc_edge_count(store, project.project, "CALL_REFERENCE", "argument", "Method", "handler");
int any_reference =
crc_edge_count(store, project.project, "CALL_REFERENCE", "argument", NULL, "handler");
int usage = crc_edge_count(store, project.project, "USAGE", "argument", NULL, "handler");
int fabricated_call =
crc_edge_count(store, project.project, "CALLS", "argument", NULL, "handler");
rh_cleanup(&project, store);
ASSERT_EQ(method_reference, 0);
ASSERT_EQ(any_reference, 0);
ASSERT_EQ(usage, 1);
ASSERT_EQ(fabricated_call, 0);
PASS();
}
TEST(call_reference_python_property_value_stays_usage) {
static const char source[] = "class Service:\n"
" @property\n"
" def handler(self) -> int:\n"
" return 42\n"
"def accept(callback):\n"
" pass\n"
"def argument(service: Service):\n"
" accept(service.handler)\n";
RProj project;
cbm_store_t *store = rh_index(&project, "property.py", source);
ASSERT_NOT_NULL(store);
int accept_call =
crc_edge_count(store, project.project, "CALLS", "argument", "Function", "accept");
int reference =
crc_edge_count(store, project.project, "CALL_REFERENCE", "argument", NULL, "handler");
int usage = crc_edge_count(store, project.project, "USAGE", "argument", "Method", "handler");
int fabricated_call =
crc_edge_count(store, project.project, "CALLS", "argument", NULL, "handler");
rh_cleanup(&project, store);
ASSERT_EQ(accept_call, 1);
ASSERT_EQ(reference, 0);
ASSERT_EQ(usage, 1);
ASSERT_EQ(fabricated_call, 0);
PASS();
}
TEST(call_reference_python_unknown_decorated_value_stays_usage) {
static const char source[] = "def replace_with_value(function):\n"
" return 42\n"
"class Service:\n"
" @replace_with_value\n"
" def handler(self):\n"
" pass\n"
"def accept(callback):\n"
" pass\n"
"def argument(service: Service):\n"
" accept(service.handler)\n";
RProj project;
cbm_store_t *store = rh_index(&project, "unknown_decorator.py", source);
ASSERT_NOT_NULL(store);
int accept_call =
crc_edge_count(store, project.project, "CALLS", "argument", "Function", "accept");
int reference =
crc_edge_count(store, project.project, "CALL_REFERENCE", "argument", NULL, "handler");
int usage = crc_edge_count(store, project.project, "USAGE", "argument", "Method", "handler");
int fabricated_call =
crc_edge_count(store, project.project, "CALLS", "argument", NULL, "handler");
rh_cleanup(&project, store);
ASSERT_EQ(accept_call, 1);
ASSERT_EQ(reference, 0);
ASSERT_EQ(usage, 1);
ASSERT_EQ(fabricated_call, 0);
PASS();
}
TEST(call_reference_python_later_decorated_function_rebinding_stays_usage) {
static const char source[] = "def handler():\n"
" pass\n"
"def replace_with_value(function):\n"
" return 42\n"
"@replace_with_value\n"
"def handler():\n"
" pass\n"
"def accept(callback):\n"
" pass\n"
"def argument():\n"
" accept(handler)\n";
RProj project;
cbm_store_t *store = rh_index(&project, "function_rebinding.py", source);
ASSERT_NOT_NULL(store);
int accept_call =
crc_edge_count(store, project.project, "CALLS", "argument", "Function", "accept");
int reference =
crc_edge_count(store, project.project, "CALL_REFERENCE", "argument", NULL, "handler");
int usage = crc_edge_count(store, project.project, "USAGE", "argument", "Function", "handler");
int fabricated_call =
crc_edge_count(store, project.project, "CALLS", "argument", NULL, "handler");
rh_cleanup(&project, store);
ASSERT_EQ(accept_call, 1);
ASSERT_EQ(reference, 0);
ASSERT_EQ(usage, 1);
ASSERT_EQ(fabricated_call, 0);
PASS();
}
TEST(call_reference_python_later_decorated_method_rebinding_stays_usage) {
static const char source[] = "def replace_with_value(function):\n"
" return 42\n"
"class Service:\n"
" def handler(self):\n"
" pass\n"
" @replace_with_value\n"
" def handler(self):\n"
" pass\n"
"def accept(callback):\n"
" pass\n"
"def argument(service: Service):\n"
" accept(service.handler)\n";
RProj project;
cbm_store_t *store = rh_index(&project, "method_rebinding.py", source);
ASSERT_NOT_NULL(store);
int accept_call =
crc_edge_count(store, project.project, "CALLS", "argument", "Function", "accept");
int reference =
crc_edge_count(store, project.project, "CALL_REFERENCE", "argument", NULL, "handler");
int usage = crc_edge_count(store, project.project, "USAGE", "argument", "Method", "handler");
int fabricated_call =
crc_edge_count(store, project.project, "CALLS", "argument", NULL, "handler");
rh_cleanup(&project, store);
ASSERT_EQ(accept_call, 1);
ASSERT_EQ(reference, 0);
ASSERT_EQ(usage, 1);
ASSERT_EQ(fabricated_call, 0);
PASS();
}
TEST(call_reference_go_bound_method_argument_is_exact) {
static const char source[] = "package refs\n"
"type Service struct{}\n"
"func (Service) handler() {}\n"
"func accept(callback func()) {}\n"
"func argument(service Service) { accept(service.handler) }\n";
RProj project;
cbm_store_t *store = rh_index(&project, "bound.go", source);
ASSERT_NOT_NULL(store);
int reference =
crc_edge_count(store, project.project, "CALL_REFERENCE", "argument", "Method", "handler");
int usage = crc_edge_count(store, project.project, "USAGE", "argument", "Method", "handler");
int fabricated_call =
crc_edge_count(store, project.project, "CALLS", "argument", "Method", "handler");
rh_cleanup(&project, store);
ASSERT_EQ(reference, 1);
ASSERT_EQ(usage, 0);
ASSERT_EQ(fabricated_call, 0);
PASS();
}
TEST(call_reference_go_ambiguous_promoted_method_stays_usage) {
static const char source[] = "package refs\n"
"type Alpha struct{}\n"
"func (Alpha) handler() {}\n"
"type Beta struct{}\n"
"func (Beta) handler() {}\n"
"type Service struct { Alpha; Beta }\n"
"func accept(callback func()) {}\n"
"func argument(service Service) { accept(service.handler) }\n";
RProj project;
cbm_store_t *store = rh_index(&project, "ambiguous.go", source);
ASSERT_NOT_NULL(store);
int reference =
crc_edge_count(store, project.project, "CALL_REFERENCE", "argument", "Method", "handler");
int usage = crc_edge_count(store, project.project, "USAGE", "argument", "Method", "handler");
int fabricated_call =
crc_edge_count(store, project.project, "CALLS", "argument", "Method", "handler");
rh_cleanup(&project, store);
ASSERT_EQ(reference, 0);
ASSERT_EQ(usage, 1);
ASSERT_EQ(fabricated_call, 0);
PASS();
}
SUITE(call_reference_contract) {
RUN_TEST(call_reference_typescript_direct_argument_is_exact);
RUN_TEST(call_reference_kotlin_alias_argument_is_exact);
RUN_TEST(call_reference_python_use_before_local_shadow_is_not_global);
RUN_TEST(call_reference_javascript_use_before_local_shadow_is_not_global);
RUN_TEST(call_reference_typescript_use_before_local_shadow_is_not_global);
RUN_TEST(call_reference_python_keyword_argument_is_exact);
RUN_TEST(call_reference_python_bound_method_argument_is_exact);
RUN_TEST(call_reference_python_parenthesized_bound_method_is_exact);
RUN_TEST(call_reference_python_instance_field_shadow_is_not_method);
RUN_TEST(call_reference_python_property_value_stays_usage);
RUN_TEST(call_reference_python_unknown_decorated_value_stays_usage);
RUN_TEST(call_reference_python_later_decorated_function_rebinding_stays_usage);
RUN_TEST(call_reference_python_later_decorated_method_rebinding_stays_usage);
RUN_TEST(call_reference_go_bound_method_argument_is_exact);
RUN_TEST(call_reference_go_ambiguous_promoted_method_stays_usage);
}
+30 -8
View File
@@ -275,14 +275,36 @@ static int cp_callables(cbm_store_t *store, const char *project) {
}
/* Diagnostic edge histogram (stderr only, so test output stays clean). */
static const char *CP_ALL_EDGE_TYPES[] = {
"CALLS", "CONFIGURES", "CONTAINS_FILE", "CONTAINS_FOLDER",
"DATA_FLOWS", "DECORATES", "DEFINES", "DEFINES_METHOD",
"DEPENDS_ON", "FILE_CHANGES_WITH", "GRAPHQL_CALLS", "GRPC_CALLS",
"HANDLES", "HTTP_CALLS", "IMPLEMENTS", "IMPORTS",
"INHERITS", "INFRA_MAPS", "OVERRIDE", "READS",
"SEMANTICALLY_RELATED", "SIMILAR_TO", "TESTS_FILE", "TESTS",
"TRPC_CALLS", "USAGE", "ASYNC_CALLS", "WRITES", NULL};
static const char *CP_ALL_EDGE_TYPES[] = {"CALLS",
"CALL_REFERENCE",
"CONFIGURES",
"CONTAINS_FILE",
"CONTAINS_FOLDER",
"DATA_FLOWS",
"DECORATES",
"DEFINES",
"DEFINES_METHOD",
"DEPENDS_ON",
"FILE_CHANGES_WITH",
"GRAPHQL_CALLS",
"GRPC_CALLS",
"HANDLES",
"HTTP_CALLS",
"IMPLEMENTS",
"IMPORTS",
"INHERITS",
"INFRA_MAPS",
"OVERRIDE",
"READS",
"SEMANTICALLY_RELATED",
"SIMILAR_TO",
"TESTS_FILE",
"TESTS",
"TRPC_CALLS",
"USAGE",
"ASYNC_CALLS",
"WRITES",
NULL};
static void cp_diag(cbm_store_t *store, const char *project, const char *label) {
if (!store) { fprintf(stderr, " [CP] %s: no graph DB\n", label); return; }

Some files were not shown because too many files have changed in this diff Show More