From 6b1e3a0d76574bbc0d6dc4eda76f19e57213d6b0 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Sun, 28 Jun 2026 18:47:39 +0200 Subject: [PATCH 1/5] fix(calls): extract resolvable builtin calls so LSP builtin edges form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The call extractor dropped any callee matched by cbm_is_keyword(), and the Python keyword set lists builtins (len, str, range, int, list, dict, print) to suppress them as bare usages. That also dropped the textual CALL, so the LSP resolution to the injected builtins. node (py_builtins.c) had no call to attach to and no CALLS edge formed (callable=0) for free-function and constructor builtins. Method builtins (s.upper) were unaffected because method names are not in the keyword set. Add cbm_is_resolvable_builtin() — the builtins we mint a real graph node for — and keep those callees in call extraction only; the keyword filter still applies to usage and semantic extraction (its four other call sites are unchanged). Every un-filtered builtin has a target node, so the new edge is always callable-sourced, never Module-sourced. Un-skips repro_lsp_py_builtin and repro_lsp_py_builtin_constructor. Repro suite 311/0, unit suite 5693/0. Signed-off-by: Martin Vogel --- internal/cbm/extract_calls.c | 6 +++++- internal/cbm/helpers.c | 23 +++++++++++++++++++++++ internal/cbm/helpers.h | 8 ++++++++ tests/repro/repro_lsp_go_py.c | 17 ++++------------- 4 files changed, 40 insertions(+), 14 deletions(-) diff --git a/internal/cbm/extract_calls.c b/internal/cbm/extract_calls.c index 302fee5b7..a7f4286ef 100644 --- a/internal/cbm/extract_calls.c +++ b/internal/cbm/extract_calls.c @@ -1873,7 +1873,11 @@ void handle_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, Walk if (cbm_kind_in_set(node, spec->call_node_types)) { char *callee = extract_callee_name(ctx->arena, node, ctx->source, ctx->language); - if (callee && callee[0] && !cbm_is_keyword(callee, ctx->language)) { + // Keyword-filter callees, but keep builtins we mint a node for (len, str, + // ...) so the LSP-resolved builtin call still forms a CALLS edge. + if (callee && callee[0] && + (!cbm_is_keyword(callee, ctx->language) || + cbm_is_resolvable_builtin(callee, ctx->language))) { CBMCall call = {0}; call.callee_name = callee; call.enclosing_func_qn = state->enclosing_func_qn; diff --git a/internal/cbm/helpers.c b/internal/cbm/helpers.c index 5821eefbc..3cea43ab3 100644 --- a/internal/cbm/helpers.c +++ b/internal/cbm/helpers.c @@ -210,6 +210,29 @@ bool cbm_is_keyword(const char *name, CBMLanguage lang) { return false; } +// Builtins that appear in the keyword set above (so they are suppressed as bare +// usages) but for which we mint a real graph node and an LSP resolution, so a +// CALL to them must still be extracted. MUST stay in sync with kPyBuiltinNodes +// in internal/cbm/lsp/py_builtins.c — every entry here has a "builtins." +// node, so the resulting CALLS edge always has a target (never Module-sourced). +static const char *python_resolvable_builtins[] = {"len", "print", "str", "int", + "list", "dict", "range", NULL}; + +bool cbm_is_resolvable_builtin(const char *name, CBMLanguage lang) { + if (!name || !name[0]) { + return false; + } + if (lang != CBM_LANG_PYTHON) { + return false; + } + for (const char **b = python_resolvable_builtins; *b; b++) { + if (strcmp(name, *b) == 0) { + return true; + } + } + return false; +} + // --- Export detection --- bool cbm_is_exported(const char *name, CBMLanguage lang) { diff --git a/internal/cbm/helpers.h b/internal/cbm/helpers.h index 232db84d1..6fb136294 100644 --- a/internal/cbm/helpers.h +++ b/internal/cbm/helpers.h @@ -15,6 +15,14 @@ char *cbm_node_text(CBMArena *a, TSNode node, const char *source); // Check if a string is a language keyword (should be skipped as callee/usage). bool cbm_is_keyword(const char *name, CBMLanguage lang); +// Check if a name is a builtin we mint a real graph node for, so a CALL to it +// must NOT be keyword-filtered out of call extraction (the LSP resolves it to +// the injected builtin node and forms a CALLS edge). Narrower than +// cbm_is_keyword: it only covers builtins with a target node, so un-filtering +// them cannot produce a node-less / Module-sourced edge. The Python set MUST +// stay in sync with kPyBuiltinNodes in internal/cbm/lsp/py_builtins.c. +bool cbm_is_resolvable_builtin(const char *name, CBMLanguage lang); + // Classify a string literal as URL, config, or neither. // Returns CBM_STRREF_URL (0), CBM_STRREF_CONFIG (1), or -1 for neither. int cbm_classify_string(const char *str, int len); diff --git a/tests/repro/repro_lsp_go_py.c b/tests/repro/repro_lsp_go_py.c index d83077ca6..1d037bccf 100644 --- a/tests/repro/repro_lsp_go_py.c +++ b/tests/repro/repro_lsp_go_py.c @@ -571,23 +571,14 @@ TEST(repro_lsp_py_operator_dunder) { } TEST(repro_lsp_py_builtin) { - /* PARKED for release: lsp_builtin (len(v)) needs a typeshed/builtins registry - * so builtin functions have target nodes; without it the resolution has no - * node to form a CALLS edge to (callable=0). Tracked for a future builtins - * registry. */ - printf(" %sSKIP%s parked: needs builtins/typeshed registry (len has no node)\n", tf_dim(), - tf_reset()); - return -1; /* skip — not counted as pass or fail */ + /* len(v) resolves to the injected builtins.len node (py_builtins.c) and + * emits lsp_builtin with a real CALLS edge. */ return assert_lsp_strategy("main.py", kPyBuiltin, "lsp_builtin"); } TEST(repro_lsp_py_builtin_constructor) { - /* PARKED for release: lsp_builtin_constructor (str(v)) needs a builtins/ - * typeshed registry so the builtin type str has a node to target. Tracked - * for a future builtins registry. */ - printf(" %sSKIP%s parked: needs builtins/typeshed registry (str type has no node)\n", tf_dim(), - tf_reset()); - return -1; /* skip — not counted as pass or fail */ + /* str(v) resolves to the injected builtins.str type node (py_builtins.c) + * and emits lsp_builtin_constructor with a real CALLS edge. */ return assert_lsp_strategy("main.py", kPyBuiltinConstructor, "lsp_builtin_constructor"); } From 006ea01d6092077887756f3d5693f35d1a3f11a5 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Sun, 28 Jun 2026 19:00:10 +0200 Subject: [PATCH 2/5] fix(kotlin): inject kotlin.Any nodes so lsp_kt_any edges form The Kotlin LSP resolves a method call on an unknown-typed receiver whose member is a universal kotlin.Any method (toString/equals/hashCode) to kotlin.Any. and emits lsp_kt_any, but no kotlin.Any node existed in the graph, so pass_calls had no target node and dropped the call (callable=0). Add kotlin_builtins.c (mirror of py_builtins.c): inject kotlin.Any plus its three universal methods into result->defs during cbm_run_kotlin_lsp, before the pipeline mints def nodes. The lsp_kt_any call now targets the injected node and forms a CALLS edge. Update the kotlin grammar-label golden to include the 4 injected nodes, exactly as python's golden already includes its injected builtins. Un-skips repro_lsp_kt_any. Repro suite 312/0, unit suite 5693/0. Signed-off-by: Martin Vogel --- internal/cbm/lsp/kotlin_builtins.c | 71 +++++++++++++++++++++++++++++ internal/cbm/lsp/kotlin_lsp.c | 8 ++++ tests/repro/repro_lsp_kt_php_rust.c | 10 ++-- tests/test_grammar_labels.c | 4 +- 4 files changed, 85 insertions(+), 8 deletions(-) create mode 100644 internal/cbm/lsp/kotlin_builtins.c diff --git a/internal/cbm/lsp/kotlin_builtins.c b/internal/cbm/lsp/kotlin_builtins.c new file mode 100644 index 000000000..50d596a45 --- /dev/null +++ b/internal/cbm/lsp/kotlin_builtins.c @@ -0,0 +1,71 @@ +/* + * kotlin_builtins.c — Minimal Kotlin universal builtins as real graph nodes. + * + * When a method call lands on an unknown-typed receiver and the member is one + * of the universal kotlin.Any methods (toString / equals / hashCode), the + * Kotlin LSP resolves it to "kotlin.Any." and emits the lsp_kt_any + * strategy (kotlin_lsp.c, kt_emit_resolved). Any is the supertype of every + * Kotlin reference, so this is the same target the fwcd LSP resolves to. + * + * The missing piece is downstream: pass_calls.c only writes a CALLS edge when + * cbm_pipeline_lsp_target_node() resolves the callee_qn to a graph node + * (src/pipeline/lsp_resolve.h). There is no "kotlin.Any" node in the graph, so + * the resolved call is dropped and the strategy never lands on an edge + * (callable=0). + * + * Fix: inject a small, fixed set of kotlin.Any definitions into result->defs + * during the per-file Kotlin LSP run (cbm_run_kotlin_lsp, which executes inside + * cbm_extract_file, BEFORE the parallel pipeline mints def nodes from + * result->defs). The graph therefore gains real "kotlin.Any[.]" nodes + * that the lsp_kt_any edges target. The QNs here MUST match what kt_emit_resolved + * emits as callee_qn ("kotlin.Any."). + * + * Node minting upserts by QN (cbm_gbuf_upsert_node), so injecting the same + * builtins per Kotlin file collapses to one node per QN — no duplicates. + * + * Self-contained: #included from kotlin_lsp.c only (amalgamation pattern; see + * lsp_all.c). Not a standalone translation unit. Mirror of py_builtins.c. + */ + +/* A single builtin entry to mint as a graph node. */ +typedef struct { + const char *qn; /* graph QN — MUST equal the kt_emit_resolved callee_qn */ + const char *name; /* short name (last segment of qn) */ + const char *label; /* "Class" | "Method" */ +} KtBuiltinNode; + +/* + * Universal kotlin.Any members the LSP falls back to (kt_any_methods in + * kotlin_lsp.c). The Any class node anchors the three methods. + */ +static const KtBuiltinNode kKtBuiltinNodes[] = { + {"kotlin.Any", "Any", "Class"}, + {"kotlin.Any.toString", "toString", "Method"}, + {"kotlin.Any.equals", "equals", "Method"}, + {"kotlin.Any.hashCode", "hashCode", "Method"}, +}; + +/* + * Inject the builtin definitions into result->defs so the pipeline mints them + * as graph nodes. All fields beyond name/qn/label are left zero/NULL: builtins + * have no body, so complexity/line-range/etc. are irrelevant, and a synthetic + * file_path keeps them out of any real source file's def list. + */ +static void kt_builtins_inject_defs(CBMFileResult *result, CBMArena *arena) { + if (!result || !arena) { + return; + } + const int n = (int)(sizeof(kKtBuiltinNodes) / sizeof(kKtBuiltinNodes[0])); + for (int i = 0; i < n; i++) { + const KtBuiltinNode *b = &kKtBuiltinNodes[i]; + CBMDefinition def; + memset(&def, 0, sizeof(def)); + def.name = b->name; + def.qualified_name = b->qn; + def.label = b->label; + def.file_path = ""; + def.start_line = 1; + def.end_line = 1; + cbm_defs_push(&result->defs, arena, def); + } +} diff --git a/internal/cbm/lsp/kotlin_lsp.c b/internal/cbm/lsp/kotlin_lsp.c index 84fc07cdb..7a3d843a6 100644 --- a/internal/cbm/lsp/kotlin_lsp.c +++ b/internal/cbm/lsp/kotlin_lsp.c @@ -42,6 +42,10 @@ #include #include +/* Minimal Kotlin universal builtins as real graph nodes (kt_builtins_inject_defs). + * Amalgamation-included (see lsp_all.c); mirror of py_builtins.c. */ +#include "kotlin_builtins.c" + #define KT_EVAL_MAX_DEPTH 32 #define KT_IMPORT_INITIAL_CAP 16 @@ -4117,6 +4121,10 @@ void cbm_run_kotlin_lsp(CBMArena *arena, CBMFileResult *result, const char *sour kotlin_lsp_process_file(&ctx, use_root); + /* Inject kotlin.Any universal-method nodes (toString/equals/hashCode) so the + * lsp_kt_any fallback emitted above has a target node to form a CALLS edge. */ + kt_builtins_inject_defs(result, arena); + if (patched_tree) { ts_tree_delete(patched_tree); } diff --git a/tests/repro/repro_lsp_kt_php_rust.c b/tests/repro/repro_lsp_kt_php_rust.c index e5a801773..d51d517c2 100644 --- a/tests/repro/repro_lsp_kt_php_rust.c +++ b/tests/repro/repro_lsp_kt_php_rust.c @@ -536,13 +536,9 @@ TEST(repro_lsp_kt_lambda_it) { return assert_lsp_strategy("main.kt", kKtLambdaIt, "lsp_kt_lambda_it"); } TEST(repro_lsp_kt_any) { - /* PARKED for release: `x.toString()` on an unknown-typed receiver resolves to - * kotlin.Any.toString — a builtin with no node in the project, so no CALLS - * edge can form (callable=0). Needs an Any/builtin node (a kotlin stdlib - * registry) to anchor the edge. */ - printf(" %sSKIP%s parked: needs a kotlin.Any/builtin node (toString has no target)\n", - tf_dim(), tf_reset()); - return -1; /* skip — not counted as pass or fail */ + /* x.toString() on an unknown-typed receiver resolves to kotlin.Any.toString + * (the universal-method fallback) and forms a CALLS edge to the injected + * kotlin.Any.toString node (kotlin_builtins.c). */ return assert_lsp_strategy("main.kt", kKtAny, "lsp_kt_any"); } TEST(repro_lsp_kt_destructure) { diff --git a/tests/test_grammar_labels.c b/tests/test_grammar_labels.c index 5f3bd324c..21f1a4704 100644 --- a/tests/test_grammar_labels.c +++ b/tests/test_grammar_labels.c @@ -86,7 +86,9 @@ static const LabelGolden LABEL_GOLDENS[] = { {"typescript", "Class:1,Function:1,Module:1"}, {"tsx", "Function:1,Module:1"}, {"java", "Class:1,Method:1,Module:1"}, - {"kotlin", "Class:1,Function:1,Module:1"}, + /* +Class:1 (kotlin.Any) +Method:3 (toString/equals/hashCode) injected by + * kotlin_builtins.c, as python's golden includes its injected builtins. */ + {"kotlin", "Class:2,Function:1,Method:3,Module:1"}, {"rust", "Function:1,Module:1,Struct:1"}, {"ruby", "Class:1,Function:1,Module:1"}, {"php", "Class:1,Function:1,Module:1"}, From 25d2fa8af2ba3d41ba953148e0b5931d9d051838 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Sun, 28 Jun 2026 19:53:44 +0200 Subject: [PATCH 3/5] test(repro): honest-RED #581, honest safety-net #431 (false-guard audit) repro_issue581 was a FALSE GUARD. A 3-node fixture over 150 search_graph calls cannot move process RSS (observed factor=1.00), so rss_end <= 3.0x rss_warmup passed even on the leaking build, and an explicit PASS() escape fired when RSS read 0. #581 is OPEN: there is no SQLITE_CHECKPOINT_TRUNCATE anywhere and cbm_mem_collect is never called in the query path (only after index/delete). The green was a lie. Replace the vacuous assertion with an honest FAIL that documents the leak is unfixed and not reproducible in this tier (needs a real WAL/committed-pages reproduction tier plus the fix). repro_issue431 is actually FIXED: install_vscode_profile_configs (cli.c:3211) scans Code/User/profiles/ and plans a per-profile mcp.json, and the test passes via the real assertion. Its detection-unmet branch returned a VACUOUS PASS that would hide a future regression; change it to SKIP_PLATFORM so a host where detection cannot fire is honestly inconclusive, not falsely green. Repro board: 311 passed, 1 failed (the honest #581 RED). Signed-off-by: Martin Vogel --- tests/repro/repro_issue431.c | 17 ++++++----- tests/repro/repro_issue581.c | 59 +++++++++++++++--------------------- 2 files changed, 34 insertions(+), 42 deletions(-) diff --git a/tests/repro/repro_issue431.c b/tests/repro/repro_issue431.c index 4fddecb35..17e1dc82b 100644 --- a/tests/repro/repro_issue431.c +++ b/tests/repro/repro_issue431.c @@ -109,14 +109,17 @@ TEST(repro_issue431_vscode_profile_inherits_mcp_json) { /* --- Precondition: VSCode is detected --- */ cbm_detected_agents_t agents = cbm_detect_agents(tmpdir); if (!agents.vscode) { - /* Detection failed in the temp tree — adjust path derivation. - * On non-Apple Linux the detection reads cbm_app_config_dir() which - * is process-global (not home-relative), so detection may return false - * for a synthetic tmpdir home. The bug still exists, but we cannot - * demonstrate it via the plan-based oracle without detection firing. - * Mark the test as an expected skip on this platform/config. */ + /* #431 IS FIXED: install_vscode_profile_configs() (cli.c:3211) scans + * Code/User/profiles/ and plans a per-profile mcp.json, so the assertion + * below passes as a genuine regression guard whenever detection fires + * (which it does for this fixture). This branch is only reached if + * cbm_detect_agents() cannot see the fixture home on some platform — in + * which case the fix cannot be VERIFIED here. Skip honestly rather than + * vacuously PASS (would hide a future regression) or FAIL (would red a + * fixed bug). */ th_rmtree(tmpdir); - PASS(); /* precondition unmet — non-blocking; bug still open */ + SKIP_PLATFORM("VSCode detection did not fire for the synthetic fixture " + "home; cannot verify the #431 per-profile install here"); } /* --- Run the install plan oracle (dry-run, no mutations) --- */ diff --git a/tests/repro/repro_issue581.c b/tests/repro/repro_issue581.c index a7ae514f6..ae84574cc 100644 --- a/tests/repro/repro_issue581.c +++ b/tests/repro/repro_issue581.c @@ -245,46 +245,35 @@ TEST(repro_issue581_query_rss_stable) { free(args); rh_cleanup(&lp, store); - // If RSS is not measurable (cbm_mem_rss() returns 0 and no Linux fallback), - // skip the growth assertion -- an unmeasurable RSS cannot produce a - // meaningful signal. This avoids a false PASS masking a real leak on - // platforms where our RSS API is unavailable. - if (rss_warmup == 0 || rss_end == 0) { - printf(" NOTE: RSS not measurable on this platform/build; " - "growth assertion skipped (inconclusive, not a pass)\n"); - PASS(); + if (rss_warmup > 0 && rss_end > 0) { + printf(" rss_warmup_kb=%zu rss_end_kb=%zu factor=%.2f threshold=%.1f\n", rss_warmup / 1024, + rss_end / 1024, (double)rss_end / (double)rss_warmup, LEAK_FACTOR); + } else { + printf(" NOTE: RSS not measurable on this platform/build\n"); } - printf(" rss_warmup_kb=%zu rss_end_kb=%zu factor=%.2f threshold=%.1f\n", - rss_warmup / 1024, rss_end / 1024, - (double)rss_end / (double)rss_warmup, - LEAK_FACTOR); - - // PRIMARY assertion: end RSS must not exceed LEAK_FACTOR x warmup RSS. + // HONEST RED — this guard is currently VACUOUS and #581 is OPEN. // - // RED condition (current code): - // SQLite WAL + mimalloc retained pages grow each iteration. - // Over 150 iterations the cumulative growth pushes rss_end above - // LEAK_FACTOR * rss_warmup. - // ASSERT fires -> RED. + // This fixture CANNOT reproduce the leak: a 3-node graph over 150 + // search_graph calls allocates far too little to move process RSS (observed + // factor=1.00), so the old "rss_end <= 3.0 x rss_warmup" assertion passed + // even on the leaking build. A green here would mean "leak fixed" while the + // leak is unfixed — a false guard that violates the tests-are-guards rule + // (green <=> fixed). So it stays RED. // - // GREEN condition (after fix): - // Periodic compaction (cbm_mem_collect + WAL TRUNCATE checkpoint) keeps - // rss_end near rss_warmup. factor stays <1.5 comfortably. + // Turning this GREEN legitimately requires BOTH: + // (a) a real reproduction tier — a long-running MCP session issuing + // thousands of ops against a LARGE graph, measuring the SQLite WAL + // file size and mimalloc committed pages DIRECTLY (not process-RSS + // jitter) so the monotonic growth is actually observable; AND + // (b) the fix — periodic SQLITE_CHECKPOINT_TRUNCATE + cbm_mem_collect() in + // the MCP query loop / idle eviction (see the header + #581). // - // We report the ratio in the failure message so the fixer can see the - // growth slope without needing a profiler. - size_t rss_limit = (size_t)(rss_warmup * LEAK_FACTOR); - if (rss_end > rss_limit) { - printf(" BUG #581 reproduced: RSS grew %.2fx after %d search_graph calls " - "(warmup=%zu kB end=%zu kB limit=%zu kB)\n", - (double)rss_end / (double)rss_warmup, - ITER_TOTAL - ITER_WARMUP, - rss_warmup / 1024, rss_end / 1024, rss_limit / 1024); - } - ASSERT(rss_end <= rss_limit); - - PASS(); + // Until both land this is an honest "not fixed / not provable here" RED, not + // a false green. + FAIL("bug #581 (query-path memory leak) is OPEN and cannot be reproduced in " + "this fixture (RSS factor ~1.0 even when leaking) — needs a real " + "WAL/committed-pages reproduction tier plus the fix"); } // -- Suite ------------------------------------------------------------------ From 927dffc336ffa9d9b436b763f7f1512f44ef775c Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Sun, 28 Jun 2026 20:07:59 +0200 Subject: [PATCH 4/5] test(repro): harden assert_no_resolvable_edge against vacuous pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 9 no-resolvable-edge tests asserted only that no CALLS edge targets the unresolvable callee via inv_no_calls_edge_to_qn, which returns PASS when the fixture produces ZERO edges for any reason — so a green did not prove the unresolvable call was actually extracted and correctly dropped (a vacuous-pass false-guard risk, unlike the sibling assert_lsp_strategy which checks callable_sourced > 0). Add an exercised-check to all four helper copies (c_cpp/java_cs/go_py/ts): require at least one callable-sourced CALLS edge. Give each of the 9 fixtures a resolvable in-fixture control call (known / Helper.Known / c.Inc) so a real edge forms. The no-edge-to-callee invariant is unchanged; the tests stay green but can no longer pass when extraction silently produces nothing. Repro board: 311 passed, 1 failed (the honest #581 RED) — no regressions. Signed-off-by: Martin Vogel --- tests/repro/repro_lsp_c_cpp.c | 35 ++++++++++++++++++++--------- tests/repro/repro_lsp_go_py.c | 39 +++++++++++++++++++++++---------- tests/repro/repro_lsp_java_cs.c | 25 +++++++++++++++++---- tests/repro/repro_lsp_ts.c | 18 ++++++++++++++- 4 files changed, 90 insertions(+), 27 deletions(-) diff --git a/tests/repro/repro_lsp_c_cpp.c b/tests/repro/repro_lsp_c_cpp.c index a94f2e25a..4e89f755e 100644 --- a/tests/repro/repro_lsp_c_cpp.c +++ b/tests/repro/repro_lsp_c_cpp.c @@ -156,6 +156,21 @@ static int assert_no_resolvable_edge(const char *filename, const char *src, return 1; } int rc = 0; + /* Exercised-check: the fixture MUST produce at least one callable-sourced + * CALLS edge (its in-fixture control call). Without it the "no edge to + * " invariant is VACUOUS — it also passes when extraction silently + * produced nothing, so a green would not prove the unresolvable call was + * actually processed and correctly dropped. */ + int module_sourced = -1; + int callable_sourced = -1; + inv_count_calls_by_source(store, lp.project, &module_sourced, &callable_sourced); + (void)module_sourced; + if (callable_sourced <= 0) { + printf(" %sFAIL%s %s:%d: no callable-sourced CALLS edge — fixture not " + "exercised; the no-edge invariant for %s is vacuous\n", + tf_red(), tf_reset(), __FILE__, __LINE__, callee_substr); + rc = 1; + } if (!inv_no_calls_edge_to_qn(store, lp.project, callee_substr)) { printf(" %sFAIL%s %s:%d: a CALLS edge unexpectedly targets %s " "(expected NONE — callee is unresolvable)\n", @@ -302,12 +317,12 @@ static const char kFuncPtr[] = * (RED) — it documents that the DLL-resolution path needs an external binding * the single-file harness can't synthesize. The fixture below at least exercises * a pointer assigned from an extern declaration. */ -static const char kDllResolve[] = - "extern int plugin_entry(int x);\n" - "int caller(int v) {\n" - " int (*fp)(int) = plugin_entry;\n" - " return fp(v);\n" - "}\n"; +static const char kDllResolve[] = "extern int plugin_entry(int x);\n" + "int known(int x) { return x + 1; }\n" + "int caller(int v) {\n" + " int (*fp)(int) = plugin_entry;\n" + " return known(v) + fp(v);\n" + "}\n"; /* lsp_operator — overloaded binary operator+ on a custom type (c_lsp.c:3771-3789: * binary_expression, lhs is a custom type, operator+ member found). */ @@ -387,10 +402,10 @@ static const char kAdl[] = * called with a NULL callee_qn; the more common unresolved path is * c_emit_unresolved_call (a different marker). This fixture exercises a call to * an undeclared function and documents whether "lsp_unresolved" surfaces. */ -static const char kUnresolved[] = - "int caller(int v) {\n" - " return totally_unknown_fn(v);\n" - "}\n"; +static const char kUnresolved[] = "int known(int x) { return x + 1; }\n" + "int caller(int v) {\n" + " return known(v) + totally_unknown_fn(v);\n" + "}\n"; /* ── Per-strategy tests ──────────────────────────────────────────────────── */ diff --git a/tests/repro/repro_lsp_go_py.c b/tests/repro/repro_lsp_go_py.c index 1d037bccf..fcb06903d 100644 --- a/tests/repro/repro_lsp_go_py.c +++ b/tests/repro/repro_lsp_go_py.c @@ -201,6 +201,21 @@ static int assert_no_resolvable_edge_files(const RFile *files, int nfiles, return 1; } int rc = 0; + /* Exercised-check: the fixture MUST produce at least one callable-sourced + * CALLS edge (its in-fixture control call). Without it the "no edge to + * " invariant is VACUOUS — it also passes when extraction silently + * produced nothing, so a green would not prove the unresolvable call was + * actually processed and correctly dropped. */ + int module_sourced = -1; + int callable_sourced = -1; + inv_count_calls_by_source(store, lp.project, &module_sourced, &callable_sourced); + (void)module_sourced; + if (callable_sourced <= 0) { + printf(" %sFAIL%s %s:%d: no callable-sourced CALLS edge — fixture not " + "exercised; the no-edge invariant for %s is vacuous\n", + tf_red(), tf_reset(), __FILE__, __LINE__, callee_substr); + rc = 1; + } if (!inv_no_calls_edge_to_qn(store, lp.project, callee_substr)) { printf(" %sFAIL%s %s:%d: a CALLS edge unexpectedly targets %s " "(expected NONE — callee is unresolvable)\n", @@ -309,11 +324,11 @@ static const RFile kGoCrossFile[] = { * "lsp_unresolved"). NOTE: emit_unresolved_call uses confidence 0.0, so the * pipeline may not promote it into a CALLS edge with the strategy tag — this * fixture documents whether "lsp_unresolved" surfaces in the graph. */ -static const char kGoUnresolved[] = - "package main\n" - "func caller(v int) int {\n" - " return totallyUnknownFn(v)\n" - "}\n"; +static const char kGoUnresolved[] = "package main\n" + "func known(x int) int { return x + 1 }\n" + "func caller(v int) int {\n" + " return known(v) + totallyUnknownFn(v)\n" + "}\n"; /* ── Python fixtures ───────────────────────────────────────────────────────── */ @@ -386,13 +401,13 @@ static const RFile kPyModuleAttr[] = { * symbol lookup misses → best-effort "module.attr" QN, low confidence). helpers * defines nothing named missing_fn. */ static const RFile kPyModuleAttrUnresolved[] = { - {"helpers.py", - "def do_work(x):\n" - " return x + 9\n"}, - {"main.py", - "import helpers\n" - "def caller(v):\n" - " return helpers.missing_fn(v)\n"}, + {"helpers.py", "def do_work(x):\n" + " return x + 9\n"}, + {"main.py", "import helpers\n" + "def known(x):\n" + " return x + 1\n" + "def caller(v):\n" + " return known(v) + helpers.missing_fn(v)\n"}, }; /* lsp_dict_dispatch — funcs["key"]() where funcs is a dict-literal dispatch diff --git a/tests/repro/repro_lsp_java_cs.c b/tests/repro/repro_lsp_java_cs.c index a898f8795..fed8a6121 100644 --- a/tests/repro/repro_lsp_java_cs.c +++ b/tests/repro/repro_lsp_java_cs.c @@ -174,6 +174,21 @@ static int assert_no_resolvable_edge(const char *filename, const char *src, return 1; } int rc = 0; + /* Exercised-check: the fixture MUST produce at least one callable-sourced + * CALLS edge (its in-fixture control call). Without it the "no edge to + * " invariant is VACUOUS — it also passes when extraction silently + * produced nothing, so a green would not prove the unresolvable call was + * actually processed and correctly dropped. */ + int module_sourced = -1; + int callable_sourced = -1; + inv_count_calls_by_source(store, lp.project, &module_sourced, &callable_sourced); + (void)module_sourced; + if (callable_sourced <= 0) { + printf(" %sFAIL%s %s:%d: no callable-sourced CALLS edge — fixture not " + "exercised; the no-edge invariant for %s is vacuous\n", + tf_red(), tf_reset(), __FILE__, __LINE__, callee_substr); + rc = 1; + } if (!inv_no_calls_edge_to_qn(store, lp.project, callee_substr)) { printf(" %sFAIL%s %s:%d: a CALLS edge unexpectedly targets %s " "(expected NONE — callee is unresolvable)\n", @@ -245,7 +260,8 @@ static const char kJavaStaticImport[] = static const char kJavaStaticImportText[] = "import static java.lang.Math.max;\n" "class Client {\n" - " int run(int a, int b) { return max(a, b); }\n" + " int known(int x) { return x + 1; }\n" + " int run(int a, int b) { return known(a) + max(a, b); }\n" "}\n"; /* lsp_super_dispatch — super.method() resolves on the superclass @@ -375,7 +391,8 @@ static const char kJavaConstructorSynth[] = * surfaces on a CALLS edge at all. */ static const char kJavaUnresolved[] = "class Client {\n" - " int run(int v) { return totallyUnknownFn(v); }\n" + " int known(int x) { return x + 1; }\n" + " int run(int v) { return known(v) + totallyUnknownFn(v); }\n" "}\n"; /* ── C# fixtures ───────────────────────────────────────────────────────────── @@ -402,7 +419,7 @@ static const char kCsStaticTypedUnindexed[] = " public static int Known() { return 1; }\n" "}\n" "class Client {\n" - " public int Run() { return Helper.Missing(); }\n" + " public int Run() { return Helper.Known() + Helper.Missing(); }\n" "}\n"; /* cs_method_typed — obj.Method() on the object's OWN declared type @@ -450,7 +467,7 @@ static const char kCsMethodTypedUnindexed[] = " public int Inc(int x) { return x + 1; }\n" "}\n" "class Client {\n" - " public int Run(Counter c) { return c.Missing(); }\n" + " public int Run(Counter c) { return c.Inc(1) + c.Missing(); }\n" "}\n"; /* cs_self_method — a bare Method() resolved on the enclosing class diff --git a/tests/repro/repro_lsp_ts.c b/tests/repro/repro_lsp_ts.c index 38dee95c1..509679eb6 100644 --- a/tests/repro/repro_lsp_ts.c +++ b/tests/repro/repro_lsp_ts.c @@ -193,6 +193,21 @@ static int assert_no_resolvable_edge(const char *filename, const char *src, return 1; } int rc = 0; + /* Exercised-check: the fixture MUST produce at least one callable-sourced + * CALLS edge (its in-fixture control call). Without it the "no edge to + * " invariant is VACUOUS — it also passes when extraction silently + * produced nothing, so a green would not prove the unresolvable call was + * actually processed and correctly dropped. */ + int module_sourced = -1; + int callable_sourced = -1; + inv_count_calls_by_source(store, lp.project, &module_sourced, &callable_sourced); + (void)module_sourced; + if (callable_sourced <= 0) { + printf(" %sFAIL%s %s:%d: no callable-sourced CALLS edge — fixture not " + "exercised; the no-edge invariant for %s is vacuous\n", + tf_red(), tf_reset(), __FILE__, __LINE__, callee_substr); + rc = 1; + } if (!inv_no_calls_edge_to_qn(store, lp.project, callee_substr)) { printf(" %sFAIL%s %s:%d: a CALLS edge unexpectedly targets %s " "(expected NONE — callee is unresolvable)\n", @@ -333,7 +348,8 @@ static const char kTsDefault[] = * is EXPECTED ABSENT (RED) — it documents whether "lsp_unresolved" surfaces in * the graph. */ static const char kTsUnresolved[] = - "function caller(v: number): number { return totallyUnknownFn(v); }\n"; + "function known(x: number): number { return x + 1; }\n" + "function caller(v: number): number { return known(v) + totallyUnknownFn(v); }\n"; /* ── Per-strategy tests ──────────────────────────────────────────────────── */ From 10ab2074f7b6016897cacecfffc764eaaf5042c9 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Sun, 28 Jun 2026 20:16:37 +0200 Subject: [PATCH 5/5] test(repro): mark #581 as a whitelisted known-red TODO #581 (query-path memory leak) is an OPEN bug the unit-scale fixture cannot yet reproduce, so the test stays honestly RED rather than vacuously green. The bug-repro board is a non-gating status board where reds ARE the expected state (repro_main.c), so this is a tracked known-red TODO, not a hidden skip. Tag it TODO(#581) for tracking; turning it green needs a real WAL/committed-pages reproduction tier plus the query-path compaction fix. Signed-off-by: Martin Vogel --- tests/repro/repro_issue581.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/repro/repro_issue581.c b/tests/repro/repro_issue581.c index ae84574cc..a2151da25 100644 --- a/tests/repro/repro_issue581.c +++ b/tests/repro/repro_issue581.c @@ -271,9 +271,15 @@ TEST(repro_issue581_query_rss_stable) { // // Until both land this is an honest "not fixed / not provable here" RED, not // a false green. - FAIL("bug #581 (query-path memory leak) is OPEN and cannot be reproduced in " - "this fixture (RSS factor ~1.0 even when leaking) — needs a real " - "WAL/committed-pages reproduction tier plus the fix"); + /* TODO(#581): whitelisted known-red on the non-gating bug-repro board. The + * leak is a real OPEN bug; this fixture cannot yet reproduce it, so the test + * stays RED (honest "not fixed") rather than vacuously green. Turning it + * green requires a real WAL-size / mimalloc-committed-pages reproduction tier + * plus the query-path compaction fix (see header). Tracked, not skipped. */ + FAIL("TODO(#581) whitelisted known-red: query-path memory leak is OPEN and " + "cannot be reproduced in this fixture (RSS factor ~1.0 even when " + "leaking) — needs a real WAL/committed-pages reproduction tier plus the " + "query-path compaction fix"); } // -- Suite ------------------------------------------------------------------