/* * repro_invariant_enclosing_parity.c — Enclosing-function detection DRIFT * (QUALITY_ANALYSIS gap #3). * * INVARIANT (same family as repro_invariant_calls.c, broadened to the drift set): * For a fixture where EVERY call site sits strictly INSIDE a function/method * body, EVERY CALLS edge must be sourced at a node whose label is "Function" * or "Method" — never "Module". A Module-sourced CALLS edge proves the * enclosing-function walk failed. * * ROOT CAUSE (verified against the tree, 2026-06-26): * helpers.c cbm_find_enclosing_func() (helpers.c:700) walks a call node's * ancestry looking for a parent whose tree-sitter type matches a HARD-CODED * per-language list, func_kinds_for_lang() (helpers.c:644). Languages NOT in * that switch fall through to: * func_kinds_generic = {"function_declaration","function_definition", * "method_declaration","method_definition"} (helpers.c:641) * But lang_specs.c defines `*_func_types[]` (the grammar function node types) * for 100+ languages. When a language is (a) absent from the switch AND * (b) its grammar's actual enclosing-function node type is NOT one of the four * generic strings, cbm_find_enclosing_func() never matches, returns the null * node, and cbm_enclosing_func_qn() falls back to the MODULE qn. Every call * inside such a function is then attributed to Module. The LSP rescue path * (pass_lsp_cross.c) joins on exact caller_qn equality, so a Module qn from * tree-sitter can never be reconciled with a Function qn from the LSP — the * rescue is silently discarded. * * THE SWITCH (helpers.c func_kinds_for_lang) COVERS: * Go, Python, JS/TS/TSX, Rust, Java, C/C++, Ruby, PHP, Lua, Scala, Kotlin, * Elixir, Haskell, OCaml, Zig, Bash, Erlang, C#, Matlab, Lean, Form, Magma, * Wolfram. * (Perl is NOT in the switch — its drift symptom is already reproduced in * repro_invariant_graph.c INVARIANT 4; this file does NOT duplicate Perl.) * * COMPLETE VERIFIED DRIFT TABLE * Columns: lang -> function_node_types (lang_specs.c) -> in switch? -> * intersects generic? -> drift verdict. * generic = {function_declaration, function_definition, method_declaration, * method_definition}. * * FULLY-DRIFTED (in switch? NO ; generic-intersect? EMPTY -> every body drifts) * dart function_signature, method_signature, lambda_expression NO/none -> DRIFT * scss mixin_statement, function_statement NO/none -> DRIFT * nix function_expression NO/none -> DRIFT * commonlisp defun NO/none -> DRIFT * fortran function, subroutine, function_statement, * subroutine_statement NO/none -> DRIFT * cobol program_definition NO/none -> DRIFT * * PARTIAL DRIFT (in switch? NO ; generic-intersect? NON-EMPTY but the DRIFTED * node type below is NOT in generic -> only bodies of that form drift; fixture * MUST use the missing form): * julia function_definition[gen], short_function_definition[DRIFT] -> use `f(x)=...` * sql create_function[DRIFT], function_declaration -> use CREATE FUNCTION * verilog function_declaration, task_declaration[DRIFT], * function_body_declaration, function_statement -> use `task ...` * emacslisp function_definition[gen], macro_definition[DRIFT] -> use `defmacro` * cfscript function_declaration, function_expression[DRIFT], * arrow_function, method_definition -> use anon function_expression * cfml function_declaration, function_expression[DRIFT] -> use anon function_expression * * NOT DRIFTED (intersect generic via a leading generic node type; plain * function bodies resolve through the generic fallback even though absent from * the switch) — e.g. objc/swift/groovy/r/fsharp/vim/elm/d/solidity/gdscript/ * gleam/crystal/templ/... all lead with function_declaration|function_definition. * * SECOND, INDEPENDENT GAP (callee resolution) — IMPORTANT for the fixer: * Some drifted langs ALSO have no callee-resolution branch in extract_calls.c * (test_lang_contract.c marks expected_calls=false for: commonlisp, emacslisp, * dart-as-of-that-table, solidity, ada, fennel, fsharp, powershell, clojure...). * For those the fixture produces ZERO CALLS edges, so this test REDs at the * "no CALLS edges" guard, NOT at the Module-source check. That is STILL the * correct expected-RED state, but fixing gap #3 (the enclosing-func switch) * alone will NOT flip them green — the missing callee branch must also land. * The cleanest pure-#3 reproductions (a CALLS edge forms, but it is * Module-sourced) are FORTRAN, SCSS, SQL, VERILOG, JULIA, NIX. Each per-lang * comment states which failure class applies. * * FIX (single root cause for the FULLY/PARTIAL-drifted set): * Replace the hard-coded func_kinds_for_lang switch with a lookup of the * language's spec->func_types (lang_specs.c) so cbm_find_enclosing_func uses * the SAME node-type list the definition walker uses. Then add the missing * callee branches for the second-gap langs separately. * * ASSERTION (per edge): for every CALLS edge e, * cbm_store_find_node_by_id(store, e.source_id, &src) == CBM_STORE_OK AND * (src.label == "Function" || src.label == "Method"); i.e. module_sourced == 0. * PLUS: at least one CALLS edge must exist (zero edges is a no-signal fixture). * * NOTE: block comments use line-comment style internally; no nested block * comment opener appears inside this comment. */ #include "test_framework.h" #include "repro_harness.h" #include #include /* ── Table-driven model ─────────────────────────────────────────────────── */ typedef struct { CBMLanguage lang; const char *name; /* human-readable tag for failure messages */ const char *file; /* fixture filename (extension drives language detection) */ const char *src; /* fixture source: a call strictly inside a drifted function */ } parity_case_t; /* * run_parity_case * * Index the single fixture file through the production pipeline, collect all * CALLS edges, and assert each edge's source node is callable-labelled. * * Returns 0 (PASS) when >=1 CALLS edge exists and ALL are callable-sourced. * Returns 1 (FAIL) when zero CALLS edges exist OR any edge is Module-sourced. * * Both failure modes are "expected RED" for the drift set; the printed reason * distinguishes the enclosing-func drift (Module-sourced) from the co-occurring * no-edge gap (callee resolution). */ static int run_parity_case(const parity_case_t *c) { const char *RED = "\033[31m"; const char *RST = "\033[0m"; RFile files[1] = {{c->file, c->src}}; RProj lp; cbm_store_t *store = rh_index_files(&lp, files, 1); if (!store) { printf(" %sFAIL%s [%s] rh_index_files returned NULL\n", RED, RST, c->name); return 1; } cbm_edge_t *edges = NULL; int nedges = 0; int rc = cbm_store_find_edges_by_type(store, lp.project, "CALLS", &edges, &nedges); if (rc != CBM_STORE_OK) { printf(" %sFAIL%s [%s] cbm_store_find_edges_by_type rc=%d\n", RED, RST, c->name, rc); rh_cleanup(&lp, store); return 1; } if (nedges == 0) { /* RED for the right family — but via the no-edge (callee resolution) * gap, not the Module-source drift. Stated explicitly so the #3 fixer * is not misled into thinking the enclosing-func fix alone flips this. */ printf(" %sFAIL%s [%s] no CALLS edges (callee-resolution gap; gap #3 fix " "alone will not flip this)\n", RED, RST, c->name); cbm_store_free_edges(edges, nedges); rh_cleanup(&lp, store); return 1; } int module_sourced = 0; for (int i = 0; i < nedges; i++) { cbm_node_t src; if (cbm_store_find_node_by_id(store, edges[i].source_id, &src) != CBM_STORE_OK) { continue; /* dangling edge — not this invariant's concern */ } const char *lbl = src.label ? src.label : "(null)"; if (strcmp(lbl, "Function") != 0 && strcmp(lbl, "Method") != 0) { module_sourced++; } } cbm_store_free_edges(edges, nedges); rh_cleanup(&lp, store); if (module_sourced > 0) { printf(" %sFAIL%s [%s] %d/%d CALLS edge(s) Module-sourced " "(enclosing-func drift; gap #3)\n", RED, RST, c->name, module_sourced, nedges); return 1; } return 0; } /* ── Fixtures (one drifted function CONTAINING a call to another) ────────── */ /* * FORTRAN — FULLY DRIFTED. grammar type `function` is not in generic, absent * from switch. Contract table marks expected_calls=true, so a CALLS edge DOES * form: this is the CLEANEST pure-#3 reproduction — the edge is Module-sourced. */ static const parity_case_t case_fortran = { CBM_LANG_FORTRAN, "Fortran", "a.f90", "function helper(x) result(y)\n" " integer, intent(in) :: x\n" " integer :: y\n" " y = x + 1\n" "end function helper\n" "\n" "function run(n) result(total)\n" " integer, intent(in) :: n\n" " integer :: total\n" " total = helper(n)\n" "end function run\n"}; /* * SCSS — FULLY DRIFTED. function_statement / mixin_statement not in generic, * absent from switch. The call (`double(...)`) sits inside an @function body. */ static const parity_case_t case_scss = { CBM_LANG_SCSS, "SCSS", "a.scss", "@function double($x) {\n" " @return $x * 2;\n" "}\n" "\n" "@function quad($x) {\n" " @return double($x) + double($x);\n" "}\n"}; /* * SQL — PARTIAL DRIFT. create_function is the missing (DRIFT) form. The inner * call to helper() lives inside the CREATE FUNCTION body. */ static const parity_case_t case_sql = { CBM_LANG_SQL, "SQL", "a.sql", "CREATE FUNCTION helper(x INTEGER) RETURNS INTEGER AS $$\n" " SELECT x + 1;\n" "$$ LANGUAGE sql;\n" "\n" "CREATE FUNCTION run(n INTEGER) RETURNS INTEGER AS $$\n" " SELECT helper(n);\n" "$$ LANGUAGE sql;\n"}; /* * VERILOG — PARTIAL DRIFT. task_declaration is the missing (DRIFT) form. The * call to the subroutine `do_log` sits inside a `task` body. (.sv routes to * CBM_LANG_VERILOG via EXT_TABLE.) */ static const parity_case_t case_verilog = { CBM_LANG_VERILOG, "Verilog", "a.sv", "module m;\n" " task do_log(input int v);\n" " $display(\"v=%0d\", v);\n" " endtask\n" "\n" " task run(input int n);\n" " do_log(n);\n" " endtask\n" "endmodule\n"}; /* * JULIA — PARTIAL DRIFT. short_function_definition (`f(x) = ...`) is the missing * (DRIFT) form; the plain `function ... end` form would resolve via generic * `function_definition`. The call to helper() is in the short-form body. */ static const parity_case_t case_julia = { CBM_LANG_JULIA, "Julia", "a.jl", "helper(x) = x + 1\n" "run(n) = helper(n)\n"}; /* * NIX. function_expression (`x: body`) is bound in a let; the call inside the * lambda body must source to the bound function (the call-scope resolver names * a function_expression from its parent binding's attr). Every call is inside a * lambda body — the `in` body is a bare reference, not a top-level application, * so a genuinely module-level call (correctly Module-sourced) does not muddy the * in-function-drift invariant. */ static const parity_case_t case_nix = { CBM_LANG_NIX, "Nix", "a.nix", "let\n" " double = x: x * 2;\n" " run = n: double n;\n" " main = _: run 21;\n" "in main\n"}; /* * COMMONLISP — FULLY DRIFTED (defun not in generic) AND second-gap: the lisp * `list_lit` callee head is a sym_lit, so extract_calls forms NO CALLS edge * (test_lang_contract expected_calls=false). Expect RED via the no-edge guard; * gap #3 fix alone will not flip it. */ static const parity_case_t case_commonlisp = { CBM_LANG_COMMONLISP, "CommonLisp", "a.lisp", "(defun helper (x)\n" " (* x 2))\n" "\n" "(defun run ()\n" " (helper 21))\n"}; /* * EMACSLISP — PARTIAL DRIFT: defun maps to function_definition (generic, NOT * drifted), so the drift form is macro_definition (`defmacro`). ALSO second-gap: * the `list` callee head is a `symbol`, so no CALLS edge forms * (test_lang_contract expected_calls=false). The call lives inside a defmacro * body. Expect RED via the no-edge guard. */ static const parity_case_t case_emacslisp = { CBM_LANG_EMACSLISP, "EmacsLisp", "a.el", "(defmacro run (n)\n" " \"Expand to a helper call.\"\n" " (helper n))\n"}; /* * DART — FULLY DRIFTED (function_signature/method_signature not in generic). * The call to helper() is inside run()'s body. Dart additionally has a * historically-noted callee gap (test_lang_contract expected_calls=false); * if no edge forms this REDs via the no-edge guard, otherwise via Module-source. */ static const parity_case_t case_dart = { CBM_LANG_DART, "Dart", "a.dart", "void helper() {\n" " print('helper');\n" "}\n" "\n" "void run() {\n" " helper();\n" "}\n"}; /* * COBOL — FULLY DRIFTED (program_definition not in generic). The CALL statement * lives inside the PROCEDURE DIVISION of a program_definition body. */ static const parity_case_t case_cobol = { CBM_LANG_COBOL, "COBOL", "a.cob", " IDENTIFICATION DIVISION.\n" " PROGRAM-ID. RUNPROG.\n" " PROCEDURE DIVISION.\n" " CALL 'HELPER'.\n" " STOP RUN.\n"}; /* ── Per-language TEST wrappers (one each so RED/GREEN shows per lang) ───── */ TEST(repro_enclosing_parity_fortran) { return run_parity_case(&case_fortran); } TEST(repro_enclosing_parity_scss) { return run_parity_case(&case_scss); } TEST(repro_enclosing_parity_sql) { return run_parity_case(&case_sql); } /* DISABLED — GRAMMAR ISSUE (maintainer-approved, 2026-06-28): tree-sitter-verilog * mis-parses the SystemVerilog task call `do_log(n);` as a data_declaration * (variable decl: type `do_log`, instance `(n)`), not a subroutine call, so no * CALLS edge ever forms. Verified to fail identically under CBM_LANG_SYSTEMVERILOG * (function_subroutine_call). This is a tree-sitter grammar defect, not a cbm * extraction bug; re-enable when the grammar is fixed/replaced. */ TEST(repro_enclosing_parity_verilog) { (void)&case_verilog; printf("%sSKIP%s grammar issue (tree-sitter-verilog mis-parses task call)\n", tf_dim(), tf_reset()); return -1; /* skip — not counted as pass or fail */ } TEST(repro_enclosing_parity_julia) { return run_parity_case(&case_julia); } TEST(repro_enclosing_parity_nix) { return run_parity_case(&case_nix); } TEST(repro_enclosing_parity_commonlisp) { return run_parity_case(&case_commonlisp); } /* DISABLED — RARE LANGUAGE (maintainer-approved, 2026-06-28): the Emacs Lisp * `(defmacro run (n) (helper n))` body calls `helper`, which is an external/ * undefined symbol (not defined in-file), so there is no in-tree target node and * no CALLS edge. Resolving cross-file/builtin Elisp symbols is out of scope for * now; re-enable if/when Elisp gets in-file or builtin call-target resolution. */ TEST(repro_enclosing_parity_emacslisp) { (void)&case_emacslisp; printf("%sSKIP%s rare language (external/undefined callee)\n", tf_dim(), tf_reset()); return -1; /* skip — not counted as pass or fail */ } TEST(repro_enclosing_parity_dart) { return run_parity_case(&case_dart); } /* DISABLED — RARE LANGUAGE (maintainer-approved, 2026-06-28): COBOL * `CALL 'HELPER'` invokes an EXTERNAL program named by a string literal; HELPER * is not defined in this translation unit, so there is no in-tree target node and * no CALLS edge. Modelling external COBOL program targets is out of scope for now; * re-enable when external-program call targets are synthesized. */ TEST(repro_enclosing_parity_cobol) { (void)&case_cobol; printf("%sSKIP%s rare language (external program callee)\n", tf_dim(), tf_reset()); return -1; /* skip — not counted as pass or fail */ } /* ── Suite ──────────────────────────────────────────────────────────────── */ SUITE(repro_invariant_enclosing_parity) { RUN_TEST(repro_enclosing_parity_fortran); RUN_TEST(repro_enclosing_parity_scss); RUN_TEST(repro_enclosing_parity_sql); RUN_TEST(repro_enclosing_parity_verilog); RUN_TEST(repro_enclosing_parity_julia); RUN_TEST(repro_enclosing_parity_nix); RUN_TEST(repro_enclosing_parity_commonlisp); RUN_TEST(repro_enclosing_parity_emacslisp); RUN_TEST(repro_enclosing_parity_dart); RUN_TEST(repro_enclosing_parity_cobol); }