diff --git a/Makefile.cbm b/Makefile.cbm index 0ffbe9b61..3d8fbba38 100644 --- a/Makefile.cbm +++ b/Makefile.cbm @@ -342,7 +342,7 @@ TEST_JAVA_LSP_SRCS = tests/test_java_lsp.c tests/test_java_lsp_coverage.c TEST_KOTLIN_LSP_SRCS = tests/test_kotlin_lsp.c TEST_RUST_LSP_SRCS = tests/test_rust_lsp.c -TEST_INTEGRATION_SRCS = tests/test_integration.c tests/test_incremental.c tests/test_lang_contract.c tests/test_edge_imports.c tests/test_edge_structural.c tests/test_lsp_resolution_probe.c tests/test_node_creation_probe.c tests/test_edge_types_probe.c tests/test_convergence_probe.c tests/test_matrix_known_classes.c tests/test_matrix_new_constructs.c +TEST_INTEGRATION_SRCS = tests/test_integration.c tests/test_incremental.c tests/test_lang_contract.c tests/test_edge_imports.c tests/test_edge_structural.c tests/test_lsp_resolution_probe.c tests/test_node_creation_probe.c tests/test_edge_types_probe.c tests/test_convergence_probe.c tests/test_matrix_known_classes.c tests/test_matrix_new_constructs.c tests/test_grammar_probe_a.c tests/test_grammar_probe_b.c tests/test_grammar_probe_c.c TEST_TRACES_SRCS = tests/test_traces.c diff --git a/tests/test_grammar_probe_a.c b/tests/test_grammar_probe_a.c new file mode 100644 index 000000000..268c4cc0e --- /dev/null +++ b/tests/test_grammar_probe_a.c @@ -0,0 +1,1146 @@ +/* + * test_grammar_probe_a.c — Grammar-only node/import/inheritance probe. + * + * SCOPE + * ───── + * Probes 12 grammar-only languages: ada, awk, cairo, clojure, commonlisp, + * crystal, d, emacslisp, fennel, fish, fsharp, gleam. + * + * CALLS is already covered for these languages by the P5 contract_calls_breadth + * suite in test_lang_contract.c (CALL_CASES rows). This file probes the OTHER + * dimensions that breadth doesn't cover: + * • NODE creation — definition-kind diversity (functions, classes, structs, + * records, constants, modules, protocols, etc.) + * • IMPORTS edges — the language's cross-file import/require/use/open + * mechanism at the graph layer (two-file fixture). + * • INHERITANCE — OOP languages: Crystal `<`, D `:`, F# `inherit`, + * Ada tagged-type extension. + * • Other — language-specific constructs (D structs/interfaces, + * F# records, Clojure protocols/defrecord, etc.) + * + * COLOUR LEGEND + * ───────────── + * GREEN = guard: the pipeline already produces the correct result; a failure + * here is a real regression. + * RED = bug reproduction: the pipeline does NOT yet produce the expected + * node/edge; the test FAILS until the bug is fixed. Brief inline + * comments document the root-cause class. + * + * CALLS dimensions are intentionally omitted here (covered by P5 breadth). + * Do NOT register this suite in test_main.c — a sibling agent owns that file. + * + * SUITE(grammar_probe_a) + */ + +#include "../src/foundation/compat.h" +#include "test_framework.h" +#include "test_helpers.h" +#include "cbm.h" +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +/* ══════════════════════════════════════════════════════════════════ + * Harness — mirrors test_lang_contract.c / test_node_creation_probe.c. + * Prefix "gpa_" to avoid symbol collisions with sibling probe files. + * ══════════════════════════════════════════════════════════════════ */ + +typedef struct { + char tmpdir[256]; + char dbpath[512]; + char *project; + cbm_mcp_server_t *srv; +} GpaProj; + +typedef struct { + const char *name; /* relative filename, may include '/' for subdirs */ + const char *content; +} GpaFile; + +static void gpa_to_fwd_slashes(char *p) { + for (; *p; p++) { + if (*p == '\\') *p = '/'; + } +} + +static cbm_store_t *gpa_open_indexed(GpaProj *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); + unlink(lp->dbpath); + 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); +} + +static cbm_store_t *gpa_index_files(GpaProj *lp, const GpaFile *files, int nfiles) { + memset(lp, 0, sizeof(*lp)); + snprintf(lp->tmpdir, sizeof(lp->tmpdir), "/tmp/cbm_gpa_XXXXXX"); + if (!cbm_mkdtemp(lp->tmpdir)) return NULL; + gpa_to_fwd_slashes(lp->tmpdir); + for (int i = 0; i < nfiles; i++) { + char path[700]; + snprintf(path, sizeof(path), "%s/%s", lp->tmpdir, files[i].name); + char *slash = strrchr(path, '/'); + if (slash && slash > path + strlen(lp->tmpdir)) { + *slash = '\0'; + cbm_mkdir_p(path, 0755); + *slash = '/'; + } + FILE *f = fopen(path, "wb"); + if (!f) return NULL; + fputs(files[i].content, f); + fclose(f); + } + return gpa_open_indexed(lp); +} + +static void gpa_cleanup(GpaProj *lp, cbm_store_t *store) { + if (store) cbm_store_close(store); + if (lp->srv) { cbm_mcp_server_free(lp->srv); lp->srv = NULL; } + 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); +} + +/* ── Node-count helpers ─────────────────────────────────────────── */ + +static int gpa_count_label(cbm_store_t *store, const char *project, const char *label) { + cbm_node_t *nodes = NULL; + int count = 0; + if (cbm_store_find_nodes_by_label(store, project, label, &nodes, &count) != CBM_STORE_OK) + return -1; + cbm_store_free_nodes(nodes, count); + return count; +} + +/* Sum of all type-like labels. */ +static int gpa_type_nodes(cbm_store_t *store, const char *project) { + static const char *labels[] = {"Class","Struct","Interface","Enum","Trait","Type",NULL}; + int total = 0; + for (int i = 0; labels[i]; i++) { + int n = gpa_count_label(store, project, labels[i]); + if (n > 0) total += n; + } + return total; +} + +/* Metrics bundled per index pass. */ +typedef struct { + int ok; + int total_nodes; + int functions; + int methods; + int types; /* type-like sum */ + int imports; /* IMPORTS edges */ + int inherits; /* INHERITS edges */ +} GpaMetrics; + +static GpaMetrics gpa_metrics_files(const GpaFile *files, int nfiles) { + GpaProj lp; + cbm_store_t *store = gpa_index_files(&lp, files, nfiles); + GpaMetrics m = {0}; + if (store) { + m.ok = 1; + m.total_nodes = cbm_store_count_nodes(store, lp.project); + m.functions = gpa_count_label(store, lp.project, "Function"); + m.methods = gpa_count_label(store, lp.project, "Method"); + m.types = gpa_type_nodes(store, lp.project); + m.imports = cbm_store_count_edges_by_type(store, lp.project, "IMPORTS"); + m.inherits = cbm_store_count_edges_by_type(store, lp.project, "INHERITS"); + } + gpa_cleanup(&lp, store); + return m; +} + +static GpaMetrics gpa_metrics(const char *filename, const char *content) { + GpaFile f = {filename, content}; + return gpa_metrics_files(&f, 1); +} + +/* ══════════════════════════════════════════════════════════════════ + * GROUP 1 — Ada (.adb) + * + * Ada label histogram (from test_grammar_labels.c): Function:1, Module:1 + * OOP: Ada 95 tagged types can model inheritance but grammar-only extraction + * produces Function/Module — no Class node expected. + * IMPORTS: Ada `with Pkg;` imports are extracted at the source level. + * Graph-level IMPORTS edges require the package body to be present; + * single-file fixture is expected RED (resolver can't find the target). + * CALLS: covered by P5 breadth (CALL_CASES ada entry); not probed here. + * ══════════════════════════════════════════════════════════════════ */ + +/* Ada: two procedure definitions → at least 2 Function nodes. */ +TEST(probe_ada_functions) { + GpaMetrics m = gpa_metrics("calc.adb", + "procedure Calc is\n" + " function Add(A, B : Integer) return Integer is\n" + " begin\n" + " return A + B;\n" + " end Add;\n" + " function Sub(A, B : Integer) return Integer is\n" + " begin\n" + " return A - B;\n" + " end Sub;\n" + "begin\n" + " null;\n" + "end Calc;\n"); + ASSERT_TRUE(m.ok); + ASSERT_TRUE(m.functions >= 1); /* GREEN: at least the outer procedure or inner ones */ + PASS(); +} + +/* Ada: a package spec (.ads) with multiple subprogram specs → Function nodes. */ +TEST(probe_ada_package_spec_nodes) { + GpaMetrics m = gpa_metrics("math.ads", + "package Math is\n" + " function Square(X : Float) return Float;\n" + " function Cube(X : Float) return Float;\n" + " function Abs_Val(X : Float) return Float;\n" + "end Math;\n"); + ASSERT_TRUE(m.ok); + /* GREEN: spec subprogram declarations must produce Function nodes. */ + ASSERT_TRUE(m.functions >= 1); + PASS(); +} + +/* Ada: pipeline indexes the file and produces at least 1 node (no crash). */ +TEST(probe_ada_no_crash) { + GpaMetrics m = gpa_metrics("hello.adb", + "with Ada.Text_IO;\n" + "procedure Hello is\n" + "begin\n" + " Ada.Text_IO.Put_Line(\"Hello\");\n" + "end Hello;\n"); + ASSERT_TRUE(m.ok); + ASSERT_TRUE(m.total_nodes >= 1); + PASS(); +} + +/* Ada: `with` import in a two-file fixture → IMPORTS edge at graph level. + * RED: grammar-only Ada has no import-resolution pass that turns `with Pkg` + * into a graph IMPORTS edge pointing to the package body node. */ +TEST(probe_ada_imports_edge) { + static const GpaFile files[] = { + {"math.ads", + "package Math is\n" + " function Square(X : Float) return Float;\n" + "end Math;\n"}, + {"main.adb", + "with Math;\n" + "procedure Main is\n" + " R : Float;\n" + "begin\n" + " R := Math.Square(3.0);\n" + "end Main;\n"} + }; + GpaMetrics m = gpa_metrics_files(files, 2); + ASSERT_TRUE(m.ok); + /* RED: Ada `with` not yet resolved into IMPORTS edges by the pipeline. */ + ASSERT_TRUE(m.imports >= 1); /* expected RED — no Ada import resolver */ + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════ + * GROUP 2 — Awk (.awk) + * + * Awk label histogram: Function:2, Module:1 + * Awk has no imports, no OOP, no types — only user-defined functions. + * ══════════════════════════════════════════════════════════════════ */ + +/* Awk: two function definitions → 2 Function nodes. */ +TEST(probe_awk_functions) { + GpaMetrics m = gpa_metrics("stats.awk", + "function max(a, b) {\n" + " return (a > b) ? a : b\n" + "}\n" + "\n" + "function min(a, b) {\n" + " return (a < b) ? a : b\n" + "}\n"); + ASSERT_TRUE(m.ok); + /* GREEN: both user-defined functions must reach the graph. */ + ASSERT_TRUE(m.functions >= 2); + PASS(); +} + +/* Awk: three functions in one file — broader coverage of def-count floor. */ +TEST(probe_awk_three_functions) { + GpaMetrics m = gpa_metrics("util.awk", + "function trim(s) { sub(/^[ \\t]+/, \"\", s); sub(/[ \\t]+$/, \"\", s); return s }\n" + "function upper(s, i, c, r) { for(i=1;i<=length(s);i++) r=r toupper(substr(s,i,1)); return r }\n" + "function repeat(s, n, i, r) { for(i=0;i= 3); + PASS(); +} + +/* Awk has no OOP — no type-like nodes expected. */ +TEST(probe_awk_no_type_nodes) { + GpaMetrics m = gpa_metrics("proc.awk", + "function process(line) {\n" + " print line\n" + "}\n"); + ASSERT_TRUE(m.ok); + /* GREEN: awk produces 0 type-like nodes (no classes/structs). */ + ASSERT_TRUE(m.types == 0); + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════ + * GROUP 3 — Cairo (.cairo) + * + * Cairo label histogram: Function:2, Module:1 + * Cairo (Starknet) is a Rust-like language — free functions and `fn` items. + * No OOP classes; no cross-file import mechanism indexable in single pass. + * ══════════════════════════════════════════════════════════════════ */ + +/* Cairo: two free functions → 2 Function nodes. */ +TEST(probe_cairo_functions) { + GpaMetrics m = gpa_metrics("math.cairo", + "fn add(x: felt252, y: felt252) -> felt252 {\n" + " x + y\n" + "}\n" + "\n" + "fn mul(x: felt252, y: felt252) -> felt252 {\n" + " x * y\n" + "}\n"); + ASSERT_TRUE(m.ok); + /* GREEN: both functions must be graph nodes. */ + ASSERT_TRUE(m.functions >= 2); + PASS(); +} + +/* Cairo: trait definition — if the grammar/extractor models it. */ +TEST(probe_cairo_trait_or_type) { + GpaMetrics m = gpa_metrics("iface.cairo", + "trait IMath {\n" + " fn add(x: felt252, y: felt252) -> felt252;\n" + "}\n" + "\n" + "fn zero() -> felt252 { 0 }\n"); + ASSERT_TRUE(m.ok); + ASSERT_TRUE(m.total_nodes >= 1); /* GREEN: at least the function node. */ + PASS(); +} + +/* Cairo: three functions to exercise def-count floor. */ +TEST(probe_cairo_three_functions) { + GpaMetrics m = gpa_metrics("ops.cairo", + "fn identity(x: felt252) -> felt252 { x }\n" + "fn negate(x: felt252) -> felt252 { -x }\n" + "fn square(x: felt252) -> felt252 { x * x }\n"); + ASSERT_TRUE(m.ok); + /* GREEN: all three free functions. */ + ASSERT_TRUE(m.functions >= 3); + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════ + * GROUP 4 — Clojure (.clj) + * + * Clojure label histogram: Function:2, Module:1 + * Clojure is a Lisp; no OOP classes. Namespace with `ns` + `:require`. + * IMPORTS: `ns` with `:require` can form cross-ns dependencies. + * CALLS: known gap (lisp list-head not resolved as callee). + * ══════════════════════════════════════════════════════════════════ */ + +/* Clojure: two `defn` definitions → 2 Function nodes. */ +TEST(probe_clojure_functions) { + GpaMetrics m = gpa_metrics("core.clj", + "(ns myapp.core)\n" + "\n" + "(defn greet [name]\n" + " (str \"Hello, \" name))\n" + "\n" + "(defn farewell [name]\n" + " (str \"Goodbye, \" name))\n"); + ASSERT_TRUE(m.ok); + /* GREEN: both defn forms must produce Function nodes. */ + ASSERT_TRUE(m.functions >= 2); + PASS(); +} + +/* Clojure: defmulti / defmethod (polymorphic dispatch). */ +TEST(probe_clojure_defmulti) { + GpaMetrics m = gpa_metrics("dispatch.clj", + "(ns myapp.dispatch)\n" + "\n" + "(defmulti area :shape)\n" + "\n" + "(defmethod area :circle [s]\n" + " (* Math/PI (:r s) (:r s)))\n" + "\n" + "(defmethod area :rect [s]\n" + " (* (:w s) (:h s)))\n"); + ASSERT_TRUE(m.ok); + /* GREEN: at least the defmulti + defmethod forms produce Function nodes. */ + ASSERT_TRUE(m.functions >= 1); + PASS(); +} + +/* Clojure: defrecord → type-like node (if grammar models it). */ +TEST(probe_clojure_defrecord) { + GpaMetrics m = gpa_metrics("model.clj", + "(ns myapp.model)\n" + "\n" + "(defrecord Point [x y])\n" + "\n" + "(defn make-point [x y] (->Point x y))\n"); + ASSERT_TRUE(m.ok); + ASSERT_TRUE(m.total_nodes >= 1); /* GREEN: at minimum the function node. */ + PASS(); +} + +/* Clojure: two-file :require → IMPORTS edge. + * RED: grammar-only Clojure has no ns-resolution pass in the pipeline. */ +TEST(probe_clojure_imports_edge) { + static const GpaFile files[] = { + {"util.clj", + "(ns myapp.util)\n" + "(defn double-it [x] (* 2 x))\n"}, + {"core.clj", + "(ns myapp.core\n" + " (:require [myapp.util :as u]))\n" + "\n" + "(defn run [n] (u/double-it n))\n"} + }; + GpaMetrics m = gpa_metrics_files(files, 2); + ASSERT_TRUE(m.ok); + /* RED: Clojure ns :require not resolved into IMPORTS edges. */ + ASSERT_TRUE(m.imports >= 1); /* expected RED */ + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════ + * GROUP 5 — Common Lisp (.lisp) + * + * CommonLisp label histogram: Function:2, Module:1 + * Common Lisp uses `defun` for functions; `defpackage`/`in-package` for namespaces. + * No class hierarchy (CLOS exists but grammar-only extraction won't model it). + * ══════════════════════════════════════════════════════════════════ */ + +/* Common Lisp: two `defun` definitions → 2 Function nodes. */ +TEST(probe_commonlisp_functions) { + GpaMetrics m = gpa_metrics("math.lisp", + "(defun square (x)\n" + " (* x x))\n" + "\n" + "(defun cube (x)\n" + " (* x (square x)))\n"); + ASSERT_TRUE(m.ok); + /* GREEN: both defun forms must be Function nodes. */ + ASSERT_TRUE(m.functions >= 2); + PASS(); +} + +/* Common Lisp: `defmacro` — if extractor models it as a Function. */ +TEST(probe_commonlisp_defmacro) { + GpaMetrics m = gpa_metrics("macros.lisp", + "(defmacro when-positive (n &body body)\n" + " `(when (> ,n 0) ,@body))\n" + "\n" + "(defun use-macro (x)\n" + " (when-positive x\n" + " (format t \"positive~%\")))\n"); + ASSERT_TRUE(m.ok); + /* GREEN: at minimum defun should produce a Function node. */ + ASSERT_TRUE(m.functions >= 1); + PASS(); +} + +/* Common Lisp: defstruct → type-like node (if grammar models it). */ +TEST(probe_commonlisp_defstruct) { + GpaMetrics m = gpa_metrics("types.lisp", + "(defstruct point\n" + " (x 0)\n" + " (y 0))\n" + "\n" + "(defun make-pt (a b)\n" + " (make-point :x a :y b))\n"); + ASSERT_TRUE(m.ok); + ASSERT_TRUE(m.total_nodes >= 1); /* GREEN: at minimum the function. */ + PASS(); +} + +/* Common Lisp: `require` in two-file fixture → IMPORTS edge. + * RED: grammar-only CL has no package-resolution pass in the pipeline. */ +TEST(probe_commonlisp_imports_edge) { + static const GpaFile files[] = { + {"util.lisp", + "(defpackage :util (:export :double))\n" + "(in-package :util)\n" + "(defun double (x) (* 2 x))\n"}, + {"main.lisp", + "(defpackage :main (:use :cl :util))\n" + "(in-package :main)\n" + "(defun run (n) (util:double n))\n"} + }; + GpaMetrics m = gpa_metrics_files(files, 2); + ASSERT_TRUE(m.ok); + /* RED: CL package :use not resolved into IMPORTS edges. */ + ASSERT_TRUE(m.imports >= 1); /* expected RED */ + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════ + * GROUP 6 — Crystal (.cr) + * + * Crystal label histogram: Class:1, Function:1, Module:1 + * Crystal is an OOP language with Ruby-like syntax. + * Inheritance: `class Child < Parent` — extractor should populate base_classes. + * Imports: `require "./module"` — cross-file IMPORTS possible. + * ══════════════════════════════════════════════════════════════════ */ + +/* Crystal: class and method definitions → Class + Function/Method nodes. */ +TEST(probe_crystal_class_node) { + GpaMetrics m = gpa_metrics("animal.cr", + "class Animal\n" + " def initialize(@name : String)\n" + " end\n" + "\n" + " def speak : String\n" + " \"...\"\n" + " end\n" + "end\n"); + ASSERT_TRUE(m.ok); + /* GREEN: class Animal must produce a Class node. */ + ASSERT_TRUE(m.types >= 1); + PASS(); +} + +/* Crystal: class with inheritance `class Dog < Animal` → Class + INHERITS edge. + * GREEN for Class node (histogram confirms Class:1); INHERITS edge status probed + * separately below. */ +TEST(probe_crystal_subclass_node) { + GpaMetrics m = gpa_metrics("pets.cr", + "class Animal\n" + " def breathe; end\n" + "end\n" + "\n" + "class Dog < Animal\n" + " def bark; \"woof\"; end\n" + "end\n" + "\n" + "class Cat < Animal\n" + " def meow; \"meow\"; end\n" + "end\n"); + ASSERT_TRUE(m.ok); + /* GREEN: at least 2 class nodes (Dog, Cat; Animal also expected). */ + ASSERT_TRUE(m.types >= 2); + PASS(); +} + +/* Crystal: INHERITS edge via `class Dog < Animal`. + * RED: grammar-only Crystal pipeline does not yet resolve base_classes into + * INHERITS graph edges (no Crystal branch in pass_semantic.c resolver). */ +TEST(probe_crystal_inherits_edge) { + GpaMetrics m = gpa_metrics("inherit.cr", + "class Base\n" + " def do_base; end\n" + "end\n" + "\n" + "class Derived < Base\n" + " def do_derived; end\n" + "end\n"); + ASSERT_TRUE(m.ok); + /* RED: Crystal INHERITS edge not yet produced by the pipeline. */ + ASSERT_TRUE(m.inherits >= 1); /* expected RED */ + PASS(); +} + +/* Crystal: module definition → type-like node. */ +TEST(probe_crystal_module_node) { + GpaMetrics m = gpa_metrics("mixin.cr", + "module Greetable\n" + " def greet\n" + " puts \"Hello!\"\n" + " end\n" + "end\n" + "\n" + "class Person\n" + " include Greetable\n" + "end\n"); + ASSERT_TRUE(m.ok); + /* GREEN: both the module and class should yield type-like nodes. */ + ASSERT_TRUE(m.types >= 1); + PASS(); +} + +/* Crystal: require in two-file fixture → IMPORTS edge. + * RED: grammar-only Crystal has no `require` resolver in the pipeline. */ +TEST(probe_crystal_imports_edge) { + static const GpaFile files[] = { + {"util.cr", + "def double(x : Int32) : Int32\n" + " x * 2\n" + "end\n"}, + {"main.cr", + "require \"./util\"\n" + "\n" + "def run(n : Int32) : Int32\n" + " double(n)\n" + "end\n"} + }; + GpaMetrics m = gpa_metrics_files(files, 2); + ASSERT_TRUE(m.ok); + /* RED: Crystal `require` not resolved into IMPORTS edges. */ + ASSERT_TRUE(m.imports >= 1); /* expected RED */ + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════ + * GROUP 7 — D (.d) — CBM_LANG_DLANG + * + * D label histogram: Function:2, Module:1 + * D has `import`, structs, classes with `:` inheritance, interfaces. + * ══════════════════════════════════════════════════════════════════ */ + +/* D: two free functions → 2 Function nodes. */ +TEST(probe_d_functions) { + GpaMetrics m = gpa_metrics("math.d", + "int square(int x)\n" + "{\n" + " return x * x;\n" + "}\n" + "\n" + "int cube(int x)\n" + "{\n" + " return x * square(x);\n" + "}\n"); + ASSERT_TRUE(m.ok); + /* GREEN: both functions must be Function nodes. */ + ASSERT_TRUE(m.functions >= 2); + PASS(); +} + +/* D: struct definition → Struct/type-like node. + * RED: the grammar-only D extractor produces Function:2/Module:1 (histogram); + * structs may not yet be extracted as distinct Struct nodes. */ +TEST(probe_d_struct_node) { + GpaMetrics m = gpa_metrics("point.d", + "struct Point\n" + "{\n" + " double x;\n" + " double y;\n" + "}\n" + "\n" + "double dist(Point a, Point b)\n" + "{\n" + " import std.math : sqrt;\n" + " return sqrt((a.x-b.x)^^2 + (a.y-b.y)^^2);\n" + "}\n"); + ASSERT_TRUE(m.ok); + /* REAL BUG (NEW-16, node-extraction incompleteness): d_class_types DOES list + * "struct_declaration" (lang_specs.c:943-950), so the spec is configured, yet 0 + * type nodes reach the graph. D struct/class/interface defs are not being + * extracted despite the spec — root cause is in the D def-extraction path. */ + ASSERT_TRUE(m.types >= 1); + PASS(); +} + +/* D: class with inheritance `class Dog : Animal`. + * RED: D OOP classes not expected in the current histogram (only Function/Module). */ +TEST(probe_d_class_inherits) { + GpaMetrics m = gpa_metrics("pets.d", + "class Animal\n" + "{\n" + " void breathe() {}\n" + "}\n" + "\n" + "class Dog : Animal\n" + "{\n" + " void bark() {}\n" + "}\n"); + ASSERT_TRUE(m.ok); + /* RED: D class not yet extracted as Class node nor INHERITS edge produced. */ + ASSERT_TRUE(m.types >= 1); /* expected RED — no D class extractor */ + ASSERT_TRUE(m.inherits >= 1); /* expected RED — no D INHERITS resolver */ + PASS(); +} + +/* D: interface definition. + * RED: D interfaces not expected in current grammar histogram. */ +TEST(probe_d_interface_node) { + GpaMetrics m = gpa_metrics("iface.d", + "interface IWriter\n" + "{\n" + " void write(string s);\n" + " void flush();\n" + "}\n" + "\n" + "void noop() {}\n"); + ASSERT_TRUE(m.ok); + /* RED: D interface not yet yielding Interface node. */ + ASSERT_TRUE(m.types >= 1); /* expected RED */ + PASS(); +} + +/* D: import in two-file fixture → IMPORTS edge. + * RED: grammar-only D has no import resolver in the pipeline. */ +TEST(probe_d_imports_edge) { + static const GpaFile files[] = { + {"util.d", + "module util;\n" + "\n" + "int double_val(int x) { return x * 2; }\n"}, + {"main.d", + "module main;\n" + "\n" + "import util;\n" + "\n" + "void run() {\n" + " int y = double_val(21);\n" + "}\n"} + }; + GpaMetrics m = gpa_metrics_files(files, 2); + ASSERT_TRUE(m.ok); + /* RED: D `import` not resolved into IMPORTS edges. */ + ASSERT_TRUE(m.imports >= 1); /* expected RED */ + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════ + * GROUP 8 — Emacs Lisp (.el) + * + * EmacsLisp label histogram: Function:2, Module:1 + * Emacs Lisp uses `defun`; `require`/`provide` for modules; no OOP classes. + * ══════════════════════════════════════════════════════════════════ */ + +/* Emacs Lisp: two `defun` definitions → 2 Function nodes. */ +TEST(probe_emacslisp_functions) { + GpaMetrics m = gpa_metrics("util.el", + "(defun el-square (x)\n" + " \"Return X squared.\"\n" + " (* x x))\n" + "\n" + "(defun el-cube (x)\n" + " \"Return X cubed.\"\n" + " (* x (el-square x)))\n"); + ASSERT_TRUE(m.ok); + /* GREEN: both defun forms must produce Function nodes. */ + ASSERT_TRUE(m.functions >= 2); + PASS(); +} + +/* Emacs Lisp: defvar / defconst — global variable definitions. */ +TEST(probe_emacslisp_defvar) { + GpaMetrics m = gpa_metrics("config.el", + "(defvar my-config-max 100\n" + " \"Maximum value for config.\")\n" + "\n" + "(defconst my-version \"1.0\"\n" + " \"Package version.\")\n" + "\n" + "(defun my-get-max () my-config-max)\n"); + ASSERT_TRUE(m.ok); + /* GREEN: at minimum the defun should produce a Function node. */ + ASSERT_TRUE(m.functions >= 1); + PASS(); +} + +/* Emacs Lisp: `require` in two-file fixture → IMPORTS edge. + * RED: grammar-only EmacsLisp has no require-resolver in the pipeline. */ +TEST(probe_emacslisp_imports_edge) { + static const GpaFile files[] = { + {"math-util.el", + "(provide 'math-util)\n" + "\n" + "(defun math-util-double (x) (* 2 x))\n"}, + {"main.el", + "(require 'math-util)\n" + "\n" + "(defun main-run (n) (math-util-double n))\n"} + }; + GpaMetrics m = gpa_metrics_files(files, 2); + ASSERT_TRUE(m.ok); + /* RED: EmacsLisp `require` not resolved into IMPORTS edges. */ + ASSERT_TRUE(m.imports >= 1); /* expected RED */ + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════ + * GROUP 9 — Fennel (.fnl) + * + * Fennel label histogram: Function:2, Module:1 + * Fennel is a Lisp that compiles to Lua; `require` for imports; no OOP classes. + * ══════════════════════════════════════════════════════════════════ */ + +/* Fennel: two `fn` definitions → 2 Function nodes. */ +TEST(probe_fennel_functions) { + GpaMetrics m = gpa_metrics("ops.fnl", + "(fn add [a b]\n" + " (+ a b))\n" + "\n" + "(fn mul [a b]\n" + " (* a b))\n"); + ASSERT_TRUE(m.ok); + /* GREEN: both fn forms must be Function nodes. */ + ASSERT_TRUE(m.functions >= 2); + PASS(); +} + +/* Fennel: `lambda` (shorthand fn) definition. */ +TEST(probe_fennel_lambda) { + GpaMetrics m = gpa_metrics("lambdas.fnl", + "(fn double [x] (* 2 x))\n" + "(fn square [x] (* x x))\n" + "(fn cube [x] (* x x x))\n"); + ASSERT_TRUE(m.ok); + /* GREEN: all three fn forms must be Function nodes. */ + ASSERT_TRUE(m.functions >= 3); + PASS(); +} + +/* Fennel: `require` in two-file fixture → IMPORTS edge. + * RED: grammar-only Fennel has no require-resolver in the pipeline. */ +TEST(probe_fennel_imports_edge) { + static const GpaFile files[] = { + {"util.fnl", + "(fn double [x] (* 2 x))\n" + "{:double double}\n"}, + {"main.fnl", + "(local util (require :util))\n" + "\n" + "(fn run [n]\n" + " (util.double n))\n"} + }; + GpaMetrics m = gpa_metrics_files(files, 2); + ASSERT_TRUE(m.ok); + /* RED: Fennel `require` not resolved into IMPORTS edges. */ + ASSERT_TRUE(m.imports >= 1); /* expected RED */ + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════ + * GROUP 10 — Fish (.fish) + * + * Fish label histogram: Function:1, Module:1 + * Fish shell scripting: `function` keyword; no imports, no OOP. + * The histogram shows only 1 function extracted (CALL_CASES fixture has 2 fns + * but only 1 reaches the graph — possible extraction gap for the second fn). + * ══════════════════════════════════════════════════════════════════ */ + +/* Fish: single function definition → at least 1 Function node. */ +TEST(probe_fish_function_node) { + GpaMetrics m = gpa_metrics("greet.fish", + "function greet\n" + " set name $argv[1]\n" + " echo \"Hello, $name!\"\n" + "end\n"); + ASSERT_TRUE(m.ok); + /* GREEN: the function must appear as a node. */ + ASSERT_TRUE(m.functions >= 1); + PASS(); +} + +/* Fish: two function definitions — exercises the known histogram gap. + * The P5 CALL_CASES fixture has 2 fns but histogram shows Function:1. + * RED: second fish function not extracted (possible grammar gap in extractor). */ +TEST(probe_fish_two_functions) { + GpaMetrics m = gpa_metrics("funcs.fish", + "function say_hello\n" + " echo \"Hello!\"\n" + "end\n" + "\n" + "function say_bye\n" + " echo \"Bye!\"\n" + "end\n"); + ASSERT_TRUE(m.ok); + /* RED: only 1 of the 2 fish functions appears to be extracted (histogram gap). */ + ASSERT_TRUE(m.functions >= 2); /* expected RED — second fn not extracted */ + PASS(); +} + +/* Fish has no OOP and no import mechanism; verify no spurious type nodes. */ +TEST(probe_fish_no_type_nodes) { + GpaMetrics m = gpa_metrics("noop.fish", + "function noop\n" + " true\n" + "end\n"); + ASSERT_TRUE(m.ok); + /* GREEN: fish produces 0 type-like nodes. */ + ASSERT_TRUE(m.types == 0); + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════ + * GROUP 11 — F# (.fs) + * + * F# label histogram: Function:2, Module:1 + * F# is a functional-first .NET language: `let` functions, `type` records/unions, + * `open` for imports, `type Child() = inherit Parent()` for OOP. + * ══════════════════════════════════════════════════════════════════ */ + +/* F#: two `let` function bindings → 2 Function nodes. */ +TEST(probe_fsharp_functions) { + GpaMetrics m = gpa_metrics("calc.fs", + "let square x = x * x\n" + "\n" + "let cube x = x * square x\n"); + ASSERT_TRUE(m.ok); + /* GREEN: both let-bindings must produce Function nodes. */ + ASSERT_TRUE(m.functions >= 2); + PASS(); +} + +/* F#: record type definition → type-like node. + * RED: the grammar histogram shows only Function/Module; record types not yet + * extracted as Type/Struct nodes. */ +TEST(probe_fsharp_record_type) { + GpaMetrics m = gpa_metrics("model.fs", + "type Point = { X: float; Y: float }\n" + "\n" + "let origin = { X = 0.0; Y = 0.0 }\n" + "\n" + "let dist (a: Point) (b: Point) =\n" + " let dx = a.X - b.X\n" + " let dy = a.Y - b.Y\n" + " sqrt (dx*dx + dy*dy)\n"); + ASSERT_TRUE(m.ok); + /* RED: F# record type not yet extracted as a Type/Struct node. */ + ASSERT_TRUE(m.types >= 1); /* expected RED */ + PASS(); +} + +/* F#: discriminated union → type-like node. + * RED: same root cause as record types. */ +TEST(probe_fsharp_discriminated_union) { + GpaMetrics m = gpa_metrics("shape.fs", + "type Shape =\n" + " | Circle of float\n" + " | Rectangle of float * float\n" + "\n" + "let area shape =\n" + " match shape with\n" + " | Circle r -> System.Math.PI * r * r\n" + " | Rectangle (w, h) -> w * h\n"); + ASSERT_TRUE(m.ok); + /* RED: F# discriminated union not yet extracted as a Type node. */ + ASSERT_TRUE(m.types >= 1); /* expected RED */ + PASS(); +} + +/* F#: class with `inherit` → Class node + INHERITS edge. + * RED: class nodes not in histogram; no F# INHERITS resolver. */ +TEST(probe_fsharp_class_inherits) { + GpaMetrics m = gpa_metrics("oop.fs", + "type Animal(name: string) =\n" + " member _.Name = name\n" + " abstract member Speak: unit -> string\n" + " default _.Speak() = \"...\"\n" + "\n" + "type Dog(name: string) =\n" + " inherit Animal(name)\n" + " override _.Speak() = \"Woof!\"\n"); + ASSERT_TRUE(m.ok); + /* RED: F# OOP class/inherit not yet modeled by the extractor. */ + ASSERT_TRUE(m.types >= 1); /* expected RED — no F# class extraction */ + ASSERT_TRUE(m.inherits >= 1); /* expected RED — no F# INHERITS resolver */ + PASS(); +} + +/* F#: `open` in two-file fixture → IMPORTS edge. + * RED: grammar-only F# has no `open`-resolver in the pipeline. */ +TEST(probe_fsharp_imports_edge) { + static const GpaFile files[] = { + {"MathUtils.fs", + "module MathUtils\n" + "\n" + "let double x = x * 2\n" + "let triple x = x * 3\n"}, + {"Main.fs", + "module Main\n" + "\n" + "open MathUtils\n" + "\n" + "let run n = double n + triple n\n"} + }; + GpaMetrics m = gpa_metrics_files(files, 2); + ASSERT_TRUE(m.ok); + /* RED: F# `open` not resolved into IMPORTS edges. */ + ASSERT_TRUE(m.imports >= 1); /* expected RED */ + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════ + * GROUP 12 — Gleam (.gleam) + * + * Gleam label histogram: Function:2, Module:1 + * Gleam is a statically-typed functional language on the BEAM (Erlang VM). + * `pub fn` for public functions, `import` for modules; no OOP classes. + * ══════════════════════════════════════════════════════════════════ */ + +/* Gleam: two `pub fn` definitions → 2 Function nodes. */ +TEST(probe_gleam_functions) { + GpaMetrics m = gpa_metrics("math.gleam", + "pub fn square(x: Int) -> Int {\n" + " x * x\n" + "}\n" + "\n" + "pub fn cube(x: Int) -> Int {\n" + " x * square(x)\n" + "}\n"); + ASSERT_TRUE(m.ok); + /* GREEN: both pub fn definitions must produce Function nodes. */ + ASSERT_TRUE(m.functions >= 2); + PASS(); +} + +/* Gleam: private (non-pub) function — must also produce a Function node. */ +TEST(probe_gleam_private_function) { + GpaMetrics m = gpa_metrics("util.gleam", + "fn internal_double(x: Int) -> Int {\n" + " x * 2\n" + "}\n" + "\n" + "pub fn double(x: Int) -> Int {\n" + " internal_double(x)\n" + "}\n"); + ASSERT_TRUE(m.ok); + /* GREEN: both the private and public fn must become Function nodes. */ + ASSERT_TRUE(m.functions >= 2); + PASS(); +} + +/* Gleam: custom type definition → type-like node. + * RED: histogram shows Function/Module only; custom types not yet extracted. */ +TEST(probe_gleam_custom_type) { + GpaMetrics m = gpa_metrics("types.gleam", + "pub type Shape {\n" + " Circle(Float)\n" + " Rectangle(Float, Float)\n" + "}\n" + "\n" + "pub fn area(s: Shape) -> Float {\n" + " case s {\n" + " Circle(r) -> 3.14159 *. r *. r\n" + " Rectangle(w, h) -> w *. h\n" + " }\n" + "}\n"); + ASSERT_TRUE(m.ok); + /* RED: Gleam custom type not yet extracted as a Type node. */ + ASSERT_TRUE(m.types >= 1); /* expected RED */ + PASS(); +} + +/* Gleam: `import` in two-file fixture → IMPORTS edge. + * RED: grammar-only Gleam has no import-resolver in the pipeline. */ +TEST(probe_gleam_imports_edge) { + static const GpaFile files[] = { + {"math_utils.gleam", + "pub fn double(x: Int) -> Int {\n" + " x * 2\n" + "}\n"}, + {"main.gleam", + "import math_utils\n" + "\n" + "pub fn run(n: Int) -> Int {\n" + " math_utils.double(n)\n" + "}\n"} + }; + GpaMetrics m = gpa_metrics_files(files, 2); + ASSERT_TRUE(m.ok); + /* RED: Gleam `import` not resolved into IMPORTS edges. */ + ASSERT_TRUE(m.imports >= 1); /* expected RED */ + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════ + * SUITE wiring + * ══════════════════════════════════════════════════════════════════ */ + +SUITE(grammar_probe_a) { + /* Ada */ + RUN_TEST(probe_ada_functions); + RUN_TEST(probe_ada_package_spec_nodes); + RUN_TEST(probe_ada_no_crash); + RUN_TEST(probe_ada_imports_edge); + + /* Awk */ + RUN_TEST(probe_awk_functions); + RUN_TEST(probe_awk_three_functions); + RUN_TEST(probe_awk_no_type_nodes); + + /* Cairo */ + RUN_TEST(probe_cairo_functions); + RUN_TEST(probe_cairo_trait_or_type); + RUN_TEST(probe_cairo_three_functions); + + /* Clojure */ + RUN_TEST(probe_clojure_functions); + RUN_TEST(probe_clojure_defmulti); + RUN_TEST(probe_clojure_defrecord); + RUN_TEST(probe_clojure_imports_edge); + + /* Common Lisp */ + RUN_TEST(probe_commonlisp_functions); + RUN_TEST(probe_commonlisp_defmacro); + RUN_TEST(probe_commonlisp_defstruct); + RUN_TEST(probe_commonlisp_imports_edge); + + /* Crystal */ + RUN_TEST(probe_crystal_class_node); + RUN_TEST(probe_crystal_subclass_node); + RUN_TEST(probe_crystal_inherits_edge); + RUN_TEST(probe_crystal_module_node); + RUN_TEST(probe_crystal_imports_edge); + + /* D */ + RUN_TEST(probe_d_functions); + RUN_TEST(probe_d_struct_node); + RUN_TEST(probe_d_class_inherits); + RUN_TEST(probe_d_interface_node); + RUN_TEST(probe_d_imports_edge); + + /* Emacs Lisp */ + RUN_TEST(probe_emacslisp_functions); + RUN_TEST(probe_emacslisp_defvar); + RUN_TEST(probe_emacslisp_imports_edge); + + /* Fennel */ + RUN_TEST(probe_fennel_functions); + RUN_TEST(probe_fennel_lambda); + RUN_TEST(probe_fennel_imports_edge); + + /* Fish */ + RUN_TEST(probe_fish_function_node); + RUN_TEST(probe_fish_two_functions); + RUN_TEST(probe_fish_no_type_nodes); + + /* F# */ + RUN_TEST(probe_fsharp_functions); + RUN_TEST(probe_fsharp_record_type); + RUN_TEST(probe_fsharp_discriminated_union); + RUN_TEST(probe_fsharp_class_inherits); + RUN_TEST(probe_fsharp_imports_edge); + + /* Gleam */ + RUN_TEST(probe_gleam_functions); + RUN_TEST(probe_gleam_private_function); + RUN_TEST(probe_gleam_custom_type); + RUN_TEST(probe_gleam_imports_edge); +} diff --git a/tests/test_grammar_probe_b.c b/tests/test_grammar_probe_b.c new file mode 100644 index 000000000..2e2577127 --- /dev/null +++ b/tests/test_grammar_probe_b.c @@ -0,0 +1,1204 @@ +/* + * test_grammar_probe_b.c — Grammar-only node/import/inheritance probe for 12 languages. + * + * Scope: glsl, hare, hlsl, ispc, julia, luau, matlab, odin, pascal, powershell, + * racket, rescript. + * + * CALLS coverage for these languages is already handled by P5 (contract_calls_breadth). + * This suite probes: + * - NODE creation: Function/Procedure/Struct/Type nodes reach the graph after + * a full index_repository pipeline run (shader entrypoints, shader struct types, + * OOP types, module-like constructs). + * - IMPORTS edges: where the language has a cross-file import mechanism that the + * extractor knows about (julia `using`/`import`, racket `require`, + * powershell `using module`, rescript `open`, pascal `uses`, odin `import`, + * hare `use`). MATLAB has no standard inter-file import syntax → skip. + * glsl/hlsl/ispc use `#include` / preprocessor — not modeled as IMPORTS → skip. + * - INHERITANCE: powershell classes (`: Base`), pascal OOP (`: Base`), julia + * abstract-type subtyping (`<:`) where same-file base is resolvable. + * Odin, GLSL, HLSL, ISPC, Hare, MATLAB, ReScript, Racket, Luau have no + * class inheritance → skip. + * + * Convention: + * GREEN = contract that SHOULD hold; a failure here is a real regression. + * RED = known bug reproduced as a failing assertion; kept as a regression + * guard until fixed (brief // BUG comment explains root cause). + * + * Do NOT register this SUITE in test_main.c — the build system discovers it + * separately alongside _a and _c. + */ + +#include "../src/foundation/compat.h" +#include "test_framework.h" +#include "test_helpers.h" +#include "cbm.h" +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#if !defined(_WIN32) +#include +#endif + +/* ── Harness (mirrors test_lang_contract.c) ────────────────────────────────── */ + +typedef struct { + char tmpdir[256]; + char dbpath[512]; + char *project; + cbm_mcp_server_t *srv; +} ProbeLangProj; + +typedef struct { + const char *name; + const char *content; +} ProbeLangFile; + +static void pb_fwd_slashes(char *p) { + for (; *p; p++) { + if (*p == '\\') *p = '/'; + } +} + +static cbm_store_t *pb_open_indexed(ProbeLangProj *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); + unlink(lp->dbpath); + 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); +} + +static cbm_store_t *pb_index_files(ProbeLangProj *lp, const ProbeLangFile *files, int nfiles) { + memset(lp, 0, sizeof(*lp)); + snprintf(lp->tmpdir, sizeof(lp->tmpdir), "/tmp/cbm_pb_XXXXXX"); + if (!cbm_mkdtemp(lp->tmpdir)) return NULL; + pb_fwd_slashes(lp->tmpdir); + for (int i = 0; i < nfiles; i++) { + char path[700]; + snprintf(path, sizeof(path), "%s/%s", lp->tmpdir, files[i].name); + char *slash = strrchr(path, '/'); + if (slash && slash > path + strlen(lp->tmpdir)) { + *slash = '\0'; + cbm_mkdir_p(path, 0755); + *slash = '/'; + } + FILE *f = fopen(path, "wb"); + if (!f) return NULL; + fputs(files[i].content, f); + fclose(f); + } + return pb_open_indexed(lp); +} + +static cbm_store_t *pb_index(ProbeLangProj *lp, const char *filename, const char *content) { + ProbeLangFile f = {filename, content}; + return pb_index_files(lp, &f, 1); +} + +static void pb_cleanup(ProbeLangProj *lp, cbm_store_t *store) { + if (store) cbm_store_close(store); + if (lp->srv) { cbm_mcp_server_free(lp->srv); lp->srv = NULL; } + 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); + snprintf(shm, sizeof(shm), "%s-shm", lp->dbpath); + unlink(wal); unlink(shm); +} + +/* Count nodes by label. Returns -1 on store error. */ +static int pb_count_label(cbm_store_t *store, const char *project, const char *label) { + cbm_node_t *nodes = NULL; + int count = 0; + if (cbm_store_find_nodes_by_label(store, project, label, &nodes, &count) != CBM_STORE_OK) + return -1; + cbm_store_free_nodes(nodes, count); + return count; +} + +/* Sum across the labels different languages use for callables. */ +static int pb_callable_nodes(cbm_store_t *store, const char *project) { + int fn = pb_count_label(store, project, "Function"); + int mt = pb_count_label(store, project, "Method"); + return (fn < 0 ? 0 : fn) + (mt < 0 ? 0 : mt); +} + +/* Sum across type-like labels. */ +static int pb_type_nodes(cbm_store_t *store, const char *project) { + static const char *labels[] = {"Class", "Struct", "Interface", "Enum", "Trait", "Type", NULL}; + int total = 0; + for (int i = 0; labels[i]; i++) { + int n = pb_count_label(store, project, labels[i]); + if (n > 0) total += n; + } + return total; +} + +/* ══════════════════════════════════════════════════════════════════════════════ + * GLSL — ext: .glsl + * Shader language: functions + void main() entrypoint + struct types. + * No imports (uses #include which is a preprocessor directive, not modeled). + * No inheritance. + * ══════════════════════════════════════════════════════════════════════════════ */ + +/* GLSL: helper + main entrypoint must both reach graph as Function nodes. */ +TEST(glsl_function_nodes) { + ProbeLangProj lp; + cbm_store_t *store = pb_index(&lp, "shader.glsl", + "float luminance(vec3 color) {\n" + " return dot(color, vec3(0.299, 0.587, 0.114));\n" + "}\n\n" + "void main() {\n" + " float lum = luminance(vec3(1.0, 0.5, 0.2));\n" + " gl_FragColor = vec4(lum, lum, lum, 1.0);\n" + "}\n"); + int fns = store ? pb_callable_nodes(store, lp.project) : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(fns >= 1); /* luminance + main */ + PASS(); +} + +/* GLSL: struct type (e.g. Light) must appear as a type node. */ +TEST(glsl_struct_node) { + ProbeLangProj lp; + cbm_store_t *store = pb_index(&lp, "light.glsl", + "struct Light {\n" + " vec3 position;\n" + " vec3 color;\n" + " float intensity;\n" + "};\n\n" + "float attenuation(Light l, vec3 pos) {\n" + " float d = length(l.position - pos);\n" + " return l.intensity / (d * d);\n" + "}\n\n" + "void main() {\n" + " Light sun;\n" + " sun.intensity = 1.0;\n" + " gl_FragColor = vec4(attenuation(sun, vec3(0.0)), 1.0);\n" + "}\n"); + int types = store ? pb_type_nodes(store, lp.project) : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(types >= 1); /* struct Light */ + PASS(); +} + +/* GLSL: vertex shader with multiple stage-specific functions. */ +TEST(glsl_vertex_shader_functions) { + ProbeLangProj lp; + cbm_store_t *store = pb_index(&lp, "vert.glsl", + "vec4 transform(vec4 pos, mat4 mvp) {\n" + " return mvp * pos;\n" + "}\n\n" + "vec3 compute_normal(vec3 n, mat3 normal_mat) {\n" + " return normalize(normal_mat * n);\n" + "}\n\n" + "void main() {\n" + " gl_Position = transform(vec4(0.0), mat4(1.0));\n" + "}\n"); + int fns = store ? pb_callable_nodes(store, lp.project) : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(fns >= 1); /* transform, compute_normal, main */ + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════════════════ + * HARE — ext: .ha + * Systems language: fn declarations, type definitions, `use` imports. + * No class inheritance. + * ══════════════════════════════════════════════════════════════════════════════ */ + +/* Hare: function nodes from top-level fn declarations. */ +TEST(hare_function_nodes) { + ProbeLangProj lp; + cbm_store_t *store = pb_index(&lp, "math.ha", + "fn square(x: i64) i64 = x * x;\n\n" + "fn cube(x: i64) i64 = x * x * x;\n\n" + "fn sum_of_squares(a: i64, b: i64) i64 = {\n" + " return square(a) + square(b);\n" + "};\n"); + int fns = store ? pb_callable_nodes(store, lp.project) : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(fns >= 1); /* square, cube, sum_of_squares */ + PASS(); +} + +/* Hare: type definition (struct) reaches graph as a type node. */ +TEST(hare_struct_node) { + ProbeLangProj lp; + cbm_store_t *store = pb_index(&lp, "point.ha", + "type point = struct {\n" + " x: i64,\n" + " y: i64,\n" + "};\n\n" + "fn distance(a: point, b: point) f64 = {\n" + " let dx = (b.x - a.x): f64;\n" + " let dy = (b.y - a.y): f64;\n" + " return 0.0; // simplified\n" + "};\n"); + int types = store ? pb_type_nodes(store, lp.project) : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(types >= 1); /* type point */ + PASS(); +} + +/* Hare: `use` import statement → IMPORTS edge (extraction-level check). + * The graph-level IMPORTS edge requires cross-file resolution which a single + * fixture cannot trigger — so we probe at extraction level via cbm_extract_file. */ +TEST(hare_use_import_extracted) { + static const char *src = + "use fmt;\n" + "use strings;\n\n" + "fn greet(name: str) void = {\n" + " fmt::println(name);\n" + "};\n"; + CBMFileResult *r = cbm_extract_file(src, (int)strlen(src), + CBM_LANG_HARE, "lc", "greet.ha", 0, NULL, NULL); + ASSERT_NOT_NULL(r); + int n = r->imports.count; + cbm_free_result(r); + ASSERT_TRUE(n >= 1); /* use fmt; use strings; → BUG if 0: hare `use` not extracted */ + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════════════════ + * HLSL — ext: .hlsl + * DirectX shader language: functions + cbuffer struct-like types. + * No imports. No inheritance. + * ══════════════════════════════════════════════════════════════════════════════ */ + +/* HLSL: pixel shader functions must reach graph. */ +TEST(hlsl_function_nodes) { + ProbeLangProj lp; + cbm_store_t *store = pb_index(&lp, "pixel.hlsl", + "float4 tint(float4 color, float factor) {\n" + " return color * factor;\n" + "}\n\n" + "float luminance(float4 c) {\n" + " return dot(c.rgb, float3(0.299, 0.587, 0.114));\n" + "}\n\n" + "float4 PSMain(float4 pos : SV_Position) : SV_Target {\n" + " float4 c = float4(1.0, 0.5, 0.2, 1.0);\n" + " return tint(c, luminance(c));\n" + "}\n"); + int fns = store ? pb_callable_nodes(store, lp.project) : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(fns >= 1); /* tint, luminance, PSMain */ + PASS(); +} + +/* HLSL: struct definition reaches graph as type node. */ +TEST(hlsl_struct_node) { + ProbeLangProj lp; + cbm_store_t *store = pb_index(&lp, "types.hlsl", + "struct VSInput {\n" + " float4 position : POSITION;\n" + " float2 texcoord : TEXCOORD0;\n" + "};\n\n" + "struct VSOutput {\n" + " float4 position : SV_Position;\n" + " float2 texcoord : TEXCOORD0;\n" + "};\n\n" + "VSOutput VSMain(VSInput input) {\n" + " VSOutput output;\n" + " output.position = input.position;\n" + " output.texcoord = input.texcoord;\n" + " return output;\n" + "}\n"); + int types = store ? pb_type_nodes(store, lp.project) : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(types >= 1); /* VSInput, VSOutput */ + PASS(); +} + +/* HLSL: compute shader kernel (another entrypoint style). */ +TEST(hlsl_compute_shader_function) { + ProbeLangProj lp; + cbm_store_t *store = pb_index(&lp, "compute.hlsl", + "RWBuffer gOutput : register(u0);\n\n" + "float transform(float x) {\n" + " return x * x;\n" + "}\n\n" + "[numthreads(64, 1, 1)]\n" + "void CSMain(uint3 id : SV_DispatchThreadID) {\n" + " gOutput[id.x] = transform((float)id.x);\n" + "}\n"); + int fns = store ? pb_callable_nodes(store, lp.project) : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(fns >= 1); /* transform, CSMain */ + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════════════════ + * ISPC — ext: .ispc + * Intel SPMD language: task/export/uniform functions, struct types. + * No imports. No inheritance. + * ══════════════════════════════════════════════════════════════════════════════ */ + +/* ISPC: uniform and varying function nodes. */ +TEST(ispc_function_nodes) { + ProbeLangProj lp; + cbm_store_t *store = pb_index(&lp, "kernel.ispc", + "float square(float x) {\n" + " return x * x;\n" + "}\n\n" + "export void add_arrays(uniform float output[],\n" + " uniform float a[],\n" + " uniform float b[],\n" + " uniform int count) {\n" + " foreach (i = 0 ... count) {\n" + " output[i] = a[i] + square(b[i]);\n" + " }\n" + "}\n"); + int fns = store ? pb_callable_nodes(store, lp.project) : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(fns >= 1); /* square, add_arrays */ + PASS(); +} + +/* ISPC: struct definition reaches graph as type node. */ +TEST(ispc_struct_node) { + ProbeLangProj lp; + cbm_store_t *store = pb_index(&lp, "types.ispc", + "struct Vec3 {\n" + " float x, y, z;\n" + "};\n\n" + "float dot3(Vec3 a, Vec3 b) {\n" + " return a.x*b.x + a.y*b.y + a.z*b.z;\n" + "}\n\n" + "export void normalize_batch(uniform Vec3 vecs[], uniform int n) {\n" + " foreach (i = 0 ... n) {\n" + " float len = dot3(vecs[i], vecs[i]);\n" + " (void)len;\n" + " }\n" + "}\n"); + int types = store ? pb_type_nodes(store, lp.project) : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(types >= 1); /* struct Vec3 */ + PASS(); +} + +/* ISPC: task function (another callable kind). */ +TEST(ispc_task_function) { + ProbeLangProj lp; + cbm_store_t *store = pb_index(&lp, "tasks.ispc", + "uniform float compute(uniform float x) {\n" + " return x * 2.0f;\n" + "}\n\n" + "task void render_tile(uniform float buf[], uniform int w, uniform int h) {\n" + " foreach (j = 0 ... h, i = 0 ... w) {\n" + " buf[j*w + i] = compute((float)i);\n" + " }\n" + "}\n"); + int fns = store ? pb_callable_nodes(store, lp.project) : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(fns >= 1); /* compute, render_tile */ + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════════════════ + * JULIA — ext: .jl + * Dynamic language: function defs, struct types, `using`/`import` imports, + * abstract type subtyping with `<:`. + * ══════════════════════════════════════════════════════════════════════════════ */ + +/* Julia: function and struct nodes. */ +TEST(julia_function_and_struct_nodes) { + ProbeLangProj lp; + cbm_store_t *store = pb_index(&lp, "geom.jl", + "struct Point\n" + " x::Float64\n" + " y::Float64\n" + "end\n\n" + "function distance(a::Point, b::Point)::Float64\n" + " return sqrt((b.x - a.x)^2 + (b.y - a.y)^2)\n" + "end\n\n" + "function midpoint(a::Point, b::Point)::Point\n" + " return Point((a.x + b.x) / 2, (a.y + b.y) / 2)\n" + "end\n"); + int fns = store ? pb_callable_nodes(store, lp.project) : -1; + int types = store ? pb_type_nodes(store, lp.project) : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(fns >= 1); /* distance, midpoint */ + ASSERT_TRUE(types >= 1); /* struct Point */ + PASS(); +} + +/* Julia: `using` import statement captured at extraction level. */ +TEST(julia_using_import_extracted) { + static const char *src = + "using LinearAlgebra\n" + "using Statistics: mean, std\n\n" + "function norm_vec(v::Vector{Float64})::Float64\n" + " return norm(v)\n" + "end\n"; + CBMFileResult *r = cbm_extract_file(src, (int)strlen(src), + CBM_LANG_JULIA, "lc", "vec.jl", 0, NULL, NULL); + ASSERT_NOT_NULL(r); + int n = r->imports.count; + cbm_free_result(r); + ASSERT_TRUE(n >= 1); /* using LinearAlgebra; using Statistics → BUG if 0 */ + PASS(); +} + +/* Julia: `import` statement captured at extraction level. */ +TEST(julia_import_extracted) { + static const char *src = + "import Base: show, length\n" + "import Random\n\n" + "struct MyVec\n" + " data::Vector{Float64}\n" + "end\n\n" + "Base.length(v::MyVec) = length(v.data)\n"; + CBMFileResult *r = cbm_extract_file(src, (int)strlen(src), + CBM_LANG_JULIA, "lc", "myvec.jl", 0, NULL, NULL); + ASSERT_NOT_NULL(r); + int n = r->imports.count; + cbm_free_result(r); + ASSERT_TRUE(n >= 1); /* import Base; import Random → BUG if 0 */ + PASS(); +} + +/* Julia: abstract type subtyping (`<:`) — extraction captures base_classes. */ +TEST(julia_abstract_subtype_extracted) { + static const char *src = + "abstract type Shape end\n\n" + "abstract type Polygon <: Shape end\n\n" + "struct Triangle <: Polygon\n" + " a::Float64\n" + " b::Float64\n" + " c::Float64\n" + "end\n\n" + "function area(t::Triangle)::Float64\n" + " s = (t.a + t.b + t.c) / 2\n" + " return sqrt(s * (s-t.a) * (s-t.b) * (s-t.c))\n" + "end\n"; + CBMFileResult *r = cbm_extract_file(src, (int)strlen(src), + CBM_LANG_JULIA, "lc", "shapes.jl", 0, NULL, NULL); + ASSERT_NOT_NULL(r); + /* We expect at least 1 def whose base_classes is non-empty (Polygon<:Shape or + * Triangle<:Polygon). Iterate defs to check. */ + int found_base = 0; + for (int i = 0; i < r->defs.count; i++) { + if (r->defs.items[i].base_classes && r->defs.items[i].base_classes[0] != NULL) { + found_base = 1; + break; + } + } + cbm_free_result(r); + ASSERT_TRUE(found_base); /* BUG if 0: julia `<:` base not extracted */ + PASS(); +} + +/* Julia: full-pipeline INHERITS edge for same-file abstract subtype chain. */ +TEST(julia_inherits_edge) { + ProbeLangProj lp; + cbm_store_t *store = pb_index(&lp, "animals.jl", + "abstract type Animal end\n\n" + "abstract type Pet <: Animal end\n\n" + "struct Dog <: Pet\n" + " name::String\n" + "end\n\n" + "function speak(d::Dog)::String\n" + " return \"Woof: \" * d.name\n" + "end\n"); + int inherits = store ? cbm_store_count_edges_by_type(store, lp.project, "INHERITS") : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(inherits >= 1); /* Pet<:Animal, Dog<:Pet → BUG if 0 */ + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════════════════ + * LUAU — ext: .luau + * Roblox Lua dialect: function nodes. No class inheritance, no module imports + * that map to IMPORTS edges (require() is dynamic, not a static import). + * ══════════════════════════════════════════════════════════════════════════════ */ + +/* Luau: function nodes reach the graph. */ +TEST(luau_function_nodes) { + ProbeLangProj lp; + cbm_store_t *store = pb_index(&lp, "utils.luau", + "local function clamp(value: number, min: number, max: number): number\n" + " if value < min then return min end\n" + " if value > max then return max end\n" + " return value\n" + "end\n\n" + "local function lerp(a: number, b: number, t: number): number\n" + " return a + (b - a) * clamp(t, 0, 1)\n" + "end\n\n" + "return { clamp = clamp, lerp = lerp }\n"); + int fns = store ? pb_callable_nodes(store, lp.project) : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(fns >= 1); /* clamp, lerp */ + PASS(); +} + +/* Luau: type alias and interface-like construct (type keyword). */ +TEST(luau_type_nodes) { + ProbeLangProj lp; + cbm_store_t *store = pb_index(&lp, "types.luau", + "type Vector2 = {\n" + " x: number,\n" + " y: number,\n" + "}\n\n" + "type Entity = {\n" + " id: number,\n" + " name: string,\n" + " position: Vector2,\n" + "}\n\n" + "local function make_entity(id: number, name: string): Entity\n" + " return { id = id, name = name, position = { x = 0, y = 0 } }\n" + "end\n\n" + "return { make_entity = make_entity }\n"); + /* Either type nodes or function nodes must be present */ + int nodes = store ? (pb_callable_nodes(store, lp.project) + pb_type_nodes(store, lp.project)) : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(nodes >= 1); /* make_entity function at minimum */ + PASS(); +} + +/* Luau: class-style OOP via metatables (functions only — no native class keyword). */ +TEST(luau_class_style_functions) { + ProbeLangProj lp; + cbm_store_t *store = pb_index(&lp, "player.luau", + "local Player = {}\n" + "Player.__index = Player\n\n" + "function Player.new(name: string): Player\n" + " return setmetatable({ name = name, health = 100 }, Player)\n" + "end\n\n" + "function Player:takeDamage(amount: number): ()\n" + " self.health = self.health - amount\n" + "end\n\n" + "function Player:isAlive(): boolean\n" + " return self.health > 0\n" + "end\n\n" + "return Player\n"); + int fns = store ? pb_callable_nodes(store, lp.project) : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(fns >= 1); /* Player.new, Player:takeDamage, Player:isAlive */ + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════════════════ + * MATLAB — ext: .m + * Scientific language: function definitions in .m files. + * No standard cross-file import syntax modeled as IMPORTS. + * No inheritance (classdef exists but rarely used; probe basic functions only). + * ══════════════════════════════════════════════════════════════════════════════ */ + +/* MATLAB: top-level function and local subfunctions reach graph. */ +TEST(matlab_function_nodes) { + ProbeLangProj lp; + cbm_store_t *store = pb_index(&lp, "stats.m", + "function result = compute_mean(data)\n" + " result = sum_values(data) / length(data);\n" + "end\n\n" + "function s = sum_values(v)\n" + " s = 0;\n" + " for i = 1:length(v)\n" + " s = s + v(i);\n" + " end\n" + "end\n"); + int fns = store ? pb_callable_nodes(store, lp.project) : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(fns >= 1); /* compute_mean, sum_values */ + PASS(); +} + +/* MATLAB: multiple function file — all functions are nodes. */ +TEST(matlab_multiple_functions) { + ProbeLangProj lp; + cbm_store_t *store = pb_index(&lp, "linalg.m", + "function c = dot_product(a, b)\n" + " c = sum(a .* b);\n" + "end\n\n" + "function n = vec_norm(v)\n" + " n = sqrt(dot_product(v, v));\n" + "end\n\n" + "function u = normalize(v)\n" + " u = v / vec_norm(v);\n" + "end\n"); + int fns = store ? pb_callable_nodes(store, lp.project) : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(fns >= 1); /* dot_product, vec_norm, normalize */ + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════════════════ + * ODIN — ext: .odin + * Systems language: proc declarations, struct types, `import` statements. + * No class inheritance (Odin is not OOP). + * ══════════════════════════════════════════════════════════════════════════════ */ + +/* Odin: proc nodes reach graph. */ +TEST(odin_proc_nodes) { + ProbeLangProj lp; + cbm_store_t *store = pb_index(&lp, "math.odin", + "package math\n\n" + "square :: proc(x: f64) -> f64 {\n" + " return x * x\n" + "}\n\n" + "cube :: proc(x: f64) -> f64 {\n" + " return x * square(x)\n" + "}\n\n" + "clamp :: proc(v, lo, hi: f64) -> f64 {\n" + " if v < lo { return lo }\n" + " if v > hi { return hi }\n" + " return v\n" + "}\n"); + int fns = store ? pb_callable_nodes(store, lp.project) : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(fns >= 1); /* square, cube, clamp */ + PASS(); +} + +/* Odin: struct type node. */ +TEST(odin_struct_node) { + ProbeLangProj lp; + cbm_store_t *store = pb_index(&lp, "point.odin", + "package point\n\n" + "Vec2 :: struct {\n" + " x, y: f64,\n" + "}\n\n" + "Vec3 :: struct {\n" + " x, y, z: f64,\n" + "}\n\n" + "length2 :: proc(v: Vec2) -> f64 {\n" + " return v.x*v.x + v.y*v.y\n" + "}\n"); + int types = store ? pb_type_nodes(store, lp.project) : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(types >= 1); /* Vec2, Vec3 */ + PASS(); +} + +/* Odin: `import` statement captured at extraction level. */ +TEST(odin_import_extracted) { + static const char *src = + "package main\n\n" + "import \"core:fmt\"\n" + "import \"core:os\"\n" + "import math \"core:math\"\n\n" + "main :: proc() {\n" + " fmt.println(\"hello\")\n" + "}\n"; + CBMFileResult *r = cbm_extract_file(src, (int)strlen(src), + CBM_LANG_ODIN, "lc", "main.odin", 0, NULL, NULL); + ASSERT_NOT_NULL(r); + int n = r->imports.count; + cbm_free_result(r); + ASSERT_TRUE(n >= 1); /* import "core:fmt"; import "core:os" → BUG if 0 */ + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════════════════ + * PASCAL — ext: .pas + * Classic structured language: procedures/functions, record/class types, + * `uses` import clause, OOP inheritance via `: TBase`. + * ══════════════════════════════════════════════════════════════════════════════ */ + +/* Pascal: procedure and function nodes. */ +TEST(pascal_function_nodes) { + ProbeLangProj lp; + cbm_store_t *store = pb_index(&lp, "arith.pas", + "unit Arith;\n\n" + "interface\n\n" + "function Add(a, b: Integer): Integer;\n" + "function Multiply(a, b: Integer): Integer;\n" + "procedure PrintResult(val: Integer);\n\n" + "implementation\n\n" + "function Add(a, b: Integer): Integer;\n" + "begin\n" + " Result := a + b;\n" + "end;\n\n" + "function Multiply(a, b: Integer): Integer;\n" + "begin\n" + " Result := a * b;\n" + "end;\n\n" + "procedure PrintResult(val: Integer);\n" + "begin\n" + " Writeln(val);\n" + "end;\n\n" + "end.\n"); + int fns = store ? pb_callable_nodes(store, lp.project) : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(fns >= 1); /* Add, Multiply, PrintResult */ + PASS(); +} + +/* Pascal: record type node. */ +TEST(pascal_record_node) { + ProbeLangProj lp; + cbm_store_t *store = pb_index(&lp, "geom.pas", + "unit Geom;\n\n" + "interface\n\n" + "type\n" + " TPoint = record\n" + " X, Y: Double;\n" + " end;\n\n" + " TRect = record\n" + " TopLeft: TPoint;\n" + " BottomRight: TPoint;\n" + " end;\n\n" + "function Distance(P1, P2: TPoint): Double;\n\n" + "implementation\n\n" + "function Distance(P1, P2: TPoint): Double;\n" + "begin\n" + " Result := 0.0;\n" + "end;\n\n" + "end.\n"); + int types = store ? pb_type_nodes(store, lp.project) : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(types >= 1); /* TPoint, TRect */ + PASS(); +} + +/* Pascal: `uses` clause captured at extraction level. */ +TEST(pascal_uses_import_extracted) { + static const char *src = + "unit MyUnit;\n\n" + "interface\n\n" + "uses\n" + " SysUtils, Classes, Math;\n\n" + "procedure DoWork;\n\n" + "implementation\n\n" + "procedure DoWork;\n" + "begin\n" + " Writeln(IntToStr(42));\n" + "end;\n\n" + "end.\n"; + CBMFileResult *r = cbm_extract_file(src, (int)strlen(src), + CBM_LANG_PASCAL, "lc", "myunit.pas", 0, NULL, NULL); + ASSERT_NOT_NULL(r); + int n = r->imports.count; + cbm_free_result(r); + ASSERT_TRUE(n >= 1); /* uses SysUtils, Classes, Math → BUG if 0 */ + PASS(); +} + +/* Pascal: OOP class inheritance (`: TAnimal`) — extraction captures base_classes. */ +TEST(pascal_class_inheritance_extracted) { + static const char *src = + "unit Zoo;\n\n" + "interface\n\n" + "type\n" + " TAnimal = class\n" + " procedure Speak; virtual;\n" + " end;\n\n" + " TDog = class(TAnimal)\n" + " procedure Speak; override;\n" + " end;\n\n" + " TCat = class(TAnimal)\n" + " procedure Speak; override;\n" + " end;\n\n" + "implementation\n\n" + "procedure TAnimal.Speak; begin end;\n" + "procedure TDog.Speak; begin Writeln('Woof'); end;\n" + "procedure TCat.Speak; begin Writeln('Meow'); end;\n\n" + "end.\n"; + CBMFileResult *r = cbm_extract_file(src, (int)strlen(src), + CBM_LANG_PASCAL, "lc", "zoo.pas", 0, NULL, NULL); + ASSERT_NOT_NULL(r); + int found_base = 0; + for (int i = 0; i < r->defs.count; i++) { + if (r->defs.items[i].base_classes && r->defs.items[i].base_classes[0] != NULL) { + found_base = 1; + break; + } + } + cbm_free_result(r); + ASSERT_TRUE(found_base); /* TDog(TAnimal), TCat(TAnimal) → BUG if 0 */ + PASS(); +} + +/* Pascal: full-pipeline INHERITS edge for OOP class hierarchy. */ +TEST(pascal_inherits_edge) { + ProbeLangProj lp; + cbm_store_t *store = pb_index(&lp, "vehicles.pas", + "unit Vehicles;\n\n" + "interface\n\n" + "type\n" + " TVehicle = class\n" + " function GetSpeed: Integer; virtual;\n" + " end;\n\n" + " TCar = class(TVehicle)\n" + " function GetSpeed: Integer; override;\n" + " end;\n\n" + " TTruck = class(TVehicle)\n" + " function GetSpeed: Integer; override;\n" + " end;\n\n" + "implementation\n\n" + "function TVehicle.GetSpeed: Integer; begin Result := 0; end;\n" + "function TCar.GetSpeed: Integer; begin Result := 100; end;\n" + "function TTruck.GetSpeed: Integer; begin Result := 80; end;\n\n" + "end.\n"); + int inherits = store ? cbm_store_count_edges_by_type(store, lp.project, "INHERITS") : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(inherits >= 1); /* TCar->TVehicle, TTruck->TVehicle → BUG if 0 */ + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════════════════ + * POWERSHELL — ext: .ps1 + * Scripting language: function + class definitions, `using module` imports. + * OOP inheritance via `: Base`. + * ══════════════════════════════════════════════════════════════════════════════ */ + +/* PowerShell: function nodes reach graph. */ +TEST(powershell_function_nodes) { + ProbeLangProj lp; + cbm_store_t *store = pb_index(&lp, "utils.ps1", + "function Get-Square {\n" + " param([double]$x)\n" + " return $x * $x\n" + "}\n\n" + "function Get-Cube {\n" + " param([double]$x)\n" + " return $x * (Get-Square $x)\n" + "}\n\n" + "function Test-Positive {\n" + " param([double]$x)\n" + " return $x -gt 0\n" + "}\n"); + int fns = store ? pb_callable_nodes(store, lp.project) : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(fns >= 1); /* Get-Square, Get-Cube, Test-Positive */ + PASS(); +} + +/* PowerShell: class definition reaches graph as type node. */ +TEST(powershell_class_node) { + ProbeLangProj lp; + cbm_store_t *store = pb_index(&lp, "shapes.ps1", + "class Shape {\n" + " [string]$Name\n" + " Shape([string]$name) { $this.Name = $name }\n" + " [double] Area() { return 0.0 }\n" + "}\n\n" + "class Circle : Shape {\n" + " [double]$Radius\n" + " Circle([double]$r) : base('Circle') { $this.Radius = $r }\n" + " [double] Area() { return [Math]::PI * $this.Radius * $this.Radius }\n" + "}\n"); + int types = store ? pb_type_nodes(store, lp.project) : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(types >= 1); /* Shape, Circle */ + PASS(); +} + +/* PowerShell: `using module` / `using namespace` captured at extraction level. */ +TEST(powershell_using_import_extracted) { + static const char *src = + "using module ./MyModule\n" + "using namespace System.IO\n" + "using assembly System.Drawing\n\n" + "function Invoke-Task {\n" + " param([string]$Path)\n" + " [System.IO.File]::Exists($Path)\n" + "}\n"; + CBMFileResult *r = cbm_extract_file(src, (int)strlen(src), + CBM_LANG_POWERSHELL, "lc", "task.ps1", 0, NULL, NULL); + ASSERT_NOT_NULL(r); + int n = r->imports.count; + cbm_free_result(r); + ASSERT_TRUE(n >= 1); /* using module / using namespace → BUG if 0 */ + PASS(); +} + +/* PowerShell: class inheritance (`: Base`) — extraction captures base_classes. */ +TEST(powershell_class_inheritance_extracted) { + static const char *src = + "class Animal {\n" + " [string]$Name\n" + " Animal([string]$name) { $this.Name = $name }\n" + " [string] Speak() { return 'some sound' }\n" + "}\n\n" + "class Dog : Animal {\n" + " Dog([string]$name) : base($name) {}\n" + " [string] Speak() { return 'Woof' }\n" + "}\n\n" + "class Cat : Animal {\n" + " Cat([string]$name) : base($name) {}\n" + " [string] Speak() { return 'Meow' }\n" + "}\n"; + CBMFileResult *r = cbm_extract_file(src, (int)strlen(src), + CBM_LANG_POWERSHELL, "lc", "animals.ps1", 0, NULL, NULL); + ASSERT_NOT_NULL(r); + int found_base = 0; + for (int i = 0; i < r->defs.count; i++) { + if (r->defs.items[i].base_classes && r->defs.items[i].base_classes[0] != NULL) { + found_base = 1; + break; + } + } + cbm_free_result(r); + ASSERT_TRUE(found_base); /* Dog : Animal, Cat : Animal → BUG if 0 */ + PASS(); +} + +/* PowerShell: full-pipeline INHERITS edge from same-file class hierarchy. */ +TEST(powershell_inherits_edge) { + ProbeLangProj lp; + cbm_store_t *store = pb_index(&lp, "exceptions.ps1", + "class AppException {\n" + " [string]$Message\n" + " AppException([string]$msg) { $this.Message = $msg }\n" + "}\n\n" + "class DatabaseException : AppException {\n" + " DatabaseException([string]$msg) : base($msg) {}\n" + "}\n\n" + "class NetworkException : AppException {\n" + " NetworkException([string]$msg) : base($msg) {}\n" + "}\n"); + int inherits = store ? cbm_store_count_edges_by_type(store, lp.project, "INHERITS") : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(inherits >= 1); /* Database->App, Network->App → BUG if 0 */ + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════════════════ + * RACKET — ext: .rkt + * Lisp dialect: define functions/procedures, `require` imports. + * No class-style inheritance (uses `struct` for types, not OOP classes). + * ══════════════════════════════════════════════════════════════════════════════ */ + +/* Racket: function (define) nodes reach graph. */ +TEST(racket_function_nodes) { + ProbeLangProj lp; + cbm_store_t *store = pb_index(&lp, "math.rkt", + "#lang racket\n\n" + "(define (square x) (* x x))\n\n" + "(define (cube x) (* x (square x)))\n\n" + "(define (sum-of-squares a b)\n" + " (+ (square a) (square b)))\n\n" + "(define (hypotenuse a b)\n" + " (sqrt (sum-of-squares a b)))\n"); + int fns = store ? pb_callable_nodes(store, lp.project) : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(fns >= 1); /* square, cube, sum-of-squares, hypotenuse */ + PASS(); +} + +/* Racket: struct definition reaches graph as type node. */ +TEST(racket_struct_node) { + ProbeLangProj lp; + cbm_store_t *store = pb_index(&lp, "point.rkt", + "#lang racket\n\n" + "(struct point (x y) #:transparent)\n\n" + "(struct circle (center radius) #:transparent)\n\n" + "(define (distance p1 p2)\n" + " (let ([dx (- (point-x p2) (point-x p1))]\n" + " [dy (- (point-y p2) (point-y p1))])\n" + " (sqrt (+ (* dx dx) (* dy dy)))))\n"); + int types = store ? pb_type_nodes(store, lp.project) : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(types >= 1); /* struct point, struct circle → BUG if 0 */ + PASS(); +} + +/* Racket: `require` import captured at extraction level. */ +TEST(racket_require_import_extracted) { + static const char *src = + "#lang racket\n\n" + "(require racket/list)\n" + "(require racket/string)\n" + "(require (only-in racket/math pi))\n\n" + "(define (circle-area r)\n" + " (* pi r r))\n\n" + "(define (join-words words)\n" + " (string-join words \" \"))\n"; + CBMFileResult *r = cbm_extract_file(src, (int)strlen(src), + CBM_LANG_RACKET, "lc", "utils.rkt", 0, NULL, NULL); + ASSERT_NOT_NULL(r); + int n = r->imports.count; + cbm_free_result(r); + ASSERT_TRUE(n >= 1); /* require racket/list etc. → BUG if 0 */ + PASS(); +} + +/* Racket: graph-level IMPORTS edge from two-file project with require. */ +TEST(racket_imports_edge) { + static const ProbeLangFile files[] = { + {"math.rkt", + "#lang racket\n" + "(provide square cube)\n" + "(define (square x) (* x x))\n" + "(define (cube x) (* x (square x)))\n"}, + {"main.rkt", + "#lang racket\n" + "(require \"math.rkt\")\n" + "(define (run n) (+ (square n) (cube n)))\n"}}; + ProbeLangProj lp; + cbm_store_t *store = pb_index_files(&lp, files, 2); + int imports = store ? cbm_store_count_edges_by_type(store, lp.project, "IMPORTS") : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(imports >= 1); /* main.rkt requires math.rkt → BUG if 0 */ + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════════════════ + * RESCRIPT — ext: .res + * ReasonML-like JS compiler: let-bound functions, type definitions, + * `open` module imports. No class inheritance. + * ══════════════════════════════════════════════════════════════════════════════ */ + +/* ReScript: let-bound function nodes reach graph. */ +TEST(rescript_function_nodes) { + ProbeLangProj lp; + cbm_store_t *store = pb_index(&lp, "math.res", + "let square = (x: float) => x *. x\n\n" + "let cube = (x: float) => x *. square(x)\n\n" + "let clamp = (v: float, lo: float, hi: float) =>\n" + " if v < lo { lo } else if v > hi { hi } else { v }\n"); + int fns = store ? pb_callable_nodes(store, lp.project) : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(fns >= 1); /* square, cube, clamp */ + PASS(); +} + +/* ReScript: type definition reaches graph as type node. */ +TEST(rescript_type_node) { + ProbeLangProj lp; + cbm_store_t *store = pb_index(&lp, "types.res", + "type color = Red | Green | Blue\n\n" + "type point = {\n" + " x: float,\n" + " y: float,\n" + "}\n\n" + "let origin: point = {x: 0.0, y: 0.0}\n\n" + "let to_hex = (c: color) =>\n" + " switch c {\n" + " | Red => \"#ff0000\"\n" + " | Green => \"#00ff00\"\n" + " | Blue => \"#0000ff\"\n" + " }\n"); + int types = store ? pb_type_nodes(store, lp.project) : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(types >= 1); /* type color, type point → BUG if 0 */ + PASS(); +} + +/* ReScript: `open` import captured at extraction level. */ +TEST(rescript_open_import_extracted) { + static const char *src = + "open Belt\n" + "open Belt.Array\n\n" + "let sum = (arr: array) =>\n" + " Array.reduce(arr, 0, (acc, x) => acc + x)\n\n" + "let max_val = (arr: array) =>\n" + " Array.reduce(arr, Int.min_int, (acc, x) => if x > acc { x } else { acc })\n"; + CBMFileResult *r = cbm_extract_file(src, (int)strlen(src), + CBM_LANG_RESCRIPT, "lc", "utils.res", 0, NULL, NULL); + ASSERT_NOT_NULL(r); + int n = r->imports.count; + cbm_free_result(r); + ASSERT_TRUE(n >= 1); /* open Belt; open Belt.Array → BUG if 0 */ + PASS(); +} + +/* ReScript: graph-level IMPORTS edge from two-file project. */ +TEST(rescript_imports_edge) { + static const ProbeLangFile files[] = { + {"Utils.res", + "let square = (x: float) => x *. x\n" + "let cube = (x: float) => x *. square(x)\n"}, + {"Main.res", + "open Utils\n\n" + "let run = (n: float) => square(n) +. cube(n)\n"}}; + ProbeLangProj lp; + cbm_store_t *store = pb_index_files(&lp, files, 2); + int imports = store ? cbm_store_count_edges_by_type(store, lp.project, "IMPORTS") : -1; + pb_cleanup(&lp, store); + ASSERT_TRUE(imports >= 1); /* Main opens Utils → BUG if 0 */ + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════════════════ + * SUITE REGISTRATION + * ══════════════════════════════════════════════════════════════════════════════ */ + +SUITE(grammar_probe_b) { + /* ── GLSL (3 cases) ── */ + RUN_TEST(glsl_function_nodes); + RUN_TEST(glsl_struct_node); + RUN_TEST(glsl_vertex_shader_functions); + + /* ── Hare (3 cases) ── */ + RUN_TEST(hare_function_nodes); + RUN_TEST(hare_struct_node); + RUN_TEST(hare_use_import_extracted); + + /* ── HLSL (3 cases) ── */ + RUN_TEST(hlsl_function_nodes); + RUN_TEST(hlsl_struct_node); + RUN_TEST(hlsl_compute_shader_function); + + /* ── ISPC (3 cases) ── */ + RUN_TEST(ispc_function_nodes); + RUN_TEST(ispc_struct_node); + RUN_TEST(ispc_task_function); + + /* ── Julia (5 cases) ── */ + RUN_TEST(julia_function_and_struct_nodes); + RUN_TEST(julia_using_import_extracted); + RUN_TEST(julia_import_extracted); + RUN_TEST(julia_abstract_subtype_extracted); + RUN_TEST(julia_inherits_edge); + + /* ── Luau (3 cases) ── */ + RUN_TEST(luau_function_nodes); + RUN_TEST(luau_type_nodes); + RUN_TEST(luau_class_style_functions); + + /* ── MATLAB (2 cases) ── */ + RUN_TEST(matlab_function_nodes); + RUN_TEST(matlab_multiple_functions); + + /* ── Odin (3 cases) ── */ + RUN_TEST(odin_proc_nodes); + RUN_TEST(odin_struct_node); + RUN_TEST(odin_import_extracted); + + /* ── Pascal (5 cases) ── */ + RUN_TEST(pascal_function_nodes); + RUN_TEST(pascal_record_node); + RUN_TEST(pascal_uses_import_extracted); + RUN_TEST(pascal_class_inheritance_extracted); + RUN_TEST(pascal_inherits_edge); + + /* ── PowerShell (5 cases) ── */ + RUN_TEST(powershell_function_nodes); + RUN_TEST(powershell_class_node); + RUN_TEST(powershell_using_import_extracted); + RUN_TEST(powershell_class_inheritance_extracted); + RUN_TEST(powershell_inherits_edge); + + /* ── Racket (4 cases) ── */ + RUN_TEST(racket_function_nodes); + RUN_TEST(racket_struct_node); + RUN_TEST(racket_require_import_extracted); + RUN_TEST(racket_imports_edge); + + /* ── ReScript (4 cases) ── */ + RUN_TEST(rescript_function_nodes); + RUN_TEST(rescript_type_node); + RUN_TEST(rescript_open_import_extracted); + RUN_TEST(rescript_imports_edge); +} diff --git a/tests/test_grammar_probe_c.c b/tests/test_grammar_probe_c.c new file mode 100644 index 000000000..ba8b2b14e --- /dev/null +++ b/tests/test_grammar_probe_c.c @@ -0,0 +1,1027 @@ +/* + * test_grammar_probe_c.c — Grammar-only node / import / inheritance probe. + * + * Covers 11 grammar-only code languages: + * scheme, slang, solidity, squirrel, starlark, sway, tcl, teal, + * vimscript, wgsl, zsh + * + * Scope: NODE creation + IMPORTS + INHERITANCE (where OOP applies). + * CALLS edges are already covered by the P5 contract_calls_breadth suite and + * are NOT re-probed here. + * + * Convention: + * green = guard (must stay green; a regression turns it red) + * red = known bug (stays red until fixed; brief comment names the root cause) + * + * This file is NOT registered in test_main.c — it is a standalone probe that + * can be run via SUITE(grammar_probe_c) once linked. + */ +#include "../src/foundation/compat.h" +#include "test_framework.h" +#include "test_helpers.h" +#include "cbm.h" +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#if !defined(_WIN32) +#include +#endif + +/* ── Harness (mirrors test_lang_contract.c) ─────────────────────────────── */ + +typedef struct { + char tmpdir[256]; + char dbpath[512]; + char *project; + cbm_mcp_server_t *srv; +} GP_Proj; + +typedef struct { + const char *name; + const char *content; +} GP_File; + +static void gp_to_fwd_slashes(char *p) { + for (; *p; p++) { + if (*p == '\\') *p = '/'; + } +} + +static cbm_store_t *gp_open_indexed(GP_Proj *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); + unlink(lp->dbpath); + 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); +} + +static cbm_store_t *gp_index_files(GP_Proj *lp, const GP_File *files, int nfiles) { + memset(lp, 0, sizeof(*lp)); + snprintf(lp->tmpdir, sizeof(lp->tmpdir), "/tmp/cbm_gpc_XXXXXX"); + if (!cbm_mkdtemp(lp->tmpdir)) return NULL; + gp_to_fwd_slashes(lp->tmpdir); + for (int i = 0; i < nfiles; i++) { + char path[700]; + snprintf(path, sizeof(path), "%s/%s", lp->tmpdir, files[i].name); + char *slash = strrchr(path, '/'); + if (slash && slash > path + strlen(lp->tmpdir)) { + *slash = '\0'; + cbm_mkdir_p(path, 0755); + *slash = '/'; + } + FILE *f = fopen(path, "wb"); + if (!f) return NULL; + fputs(files[i].content, f); + fclose(f); + } + return gp_open_indexed(lp); +} + +static cbm_store_t *gp_index(GP_Proj *lp, const char *filename, const char *content) { + GP_File f = {filename, content}; + return gp_index_files(lp, &f, 1); +} + +static void gp_cleanup(GP_Proj *lp, cbm_store_t *store) { + if (store) cbm_store_close(store); + if (lp->srv) { cbm_mcp_server_free(lp->srv); lp->srv = NULL; } + 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); + snprintf(shm, sizeof(shm), "%s-shm", lp->dbpath); + unlink(wal); unlink(shm); +} + +/* Count graph nodes by label. Returns -1 on query error. */ +static int gp_count_label(cbm_store_t *store, const char *project, const char *label) { + cbm_node_t *nodes = NULL; + int count = 0; + if (cbm_store_find_nodes_by_label(store, project, label, &nodes, &count) != CBM_STORE_OK) + return -1; + cbm_store_free_nodes(nodes, count); + return count; +} + +/* Count type-like nodes (Class/Struct/Interface/Enum/Trait/Type). */ +static int gp_type_like(cbm_store_t *store, const char *project) { + static const char *labels[] = {"Class", "Struct", "Interface", "Enum", "Trait", "Type", NULL}; + int total = 0; + for (int i = 0; labels[i]; i++) { + int n = gp_count_label(store, project, labels[i]); + if (n > 0) total += n; + } + return total; +} + +/* Extraction-level: count imports via cbm_extract_file (bypasses graph). */ +static int gp_extract_imports(const char *src, CBMLanguage lang, const char *path) { + CBMFileResult *r = + cbm_extract_file(src, (int)strlen(src), lang, "gpc", path, 0, NULL, NULL); + if (!r) return -1; + int n = r->imports.count; + cbm_free_result(r); + return n; +} + +/* Extraction-level: count defs with given label. */ +static int gp_extract_label(const char *src, CBMLanguage lang, const char *path, + const char *label) { + CBMFileResult *r = + cbm_extract_file(src, (int)strlen(src), lang, "gpc", path, 0, NULL, NULL); + if (!r) return -1; + int n = 0; + for (int i = 0; i < r->defs.count; i++) { + if (strcmp(r->defs.items[i].label, label) == 0) n++; + } + cbm_free_result(r); + return n; +} + +/* Extraction-level: check any def has at least one non-NULL base_class. */ +static int gp_extract_has_base_class(const char *src, CBMLanguage lang, const char *path) { + CBMFileResult *r = + cbm_extract_file(src, (int)strlen(src), lang, "gpc", path, 0, NULL, NULL); + if (!r) return 0; + int found = 0; + for (int i = 0; i < r->defs.count && !found; i++) { + const char **bc = r->defs.items[i].base_classes; + if (bc && bc[0]) found = 1; + } + cbm_free_result(r); + return found; +} + +/* ══════════════════════════════════════════════════════════════════ + * SCHEME (.scm) — functional, no OOP, no import extraction support + * ══════════════════════════════════════════════════════════════════ */ + +/* Scheme: two (define ...) forms → two Function nodes in the graph. */ +TEST(scheme_function_nodes) { + GP_Proj lp; + cbm_store_t *store = gp_index(&lp, "a.scm", + "(define (compute x)\n" + " (* x x))\n" + "\n" + "(define (display-result n)\n" + " (display n)\n" + " (newline))\n"); + int fns = store ? gp_count_label(store, lp.project, "Function") : -1; + gp_cleanup(&lp, store); + ASSERT_TRUE(fns >= 2); /* green: both defines become Function nodes */ + PASS(); +} + +/* Scheme: (import ...) / (require ...) — no import_types configured in lang_specs; + * extraction yields 0 imports. This is a known gap (missing scheme branch). */ +TEST(scheme_import_not_extracted) { + /* red: scheme_import_types = empty_types in lang_specs; import forms silently dropped */ + int n = gp_extract_imports( + "(import (rnrs io ports))\n" + "(import (srfi srfi-1))\n" + "\n" + "(define (run) (display \"hi\"))\n", + CBM_LANG_SCHEME, "a.scm"); + ASSERT_TRUE(n >= 1); /* CURRENTLY FAILS: scheme has no import_types; n == 0 */ + PASS(); +} + +/* Scheme: (require ...) form (Racket-style; also common in R7RS implementations). */ +TEST(scheme_require_not_extracted) { + /* red: same root cause — scheme import_types = empty_types */ + int n = gp_extract_imports( + "(require 'srfi/1)\n" + "(require 'json)\n" + "\n" + "(define (run) (display 1))\n", + CBM_LANG_SCHEME, "a.scm"); + ASSERT_TRUE(n >= 1); /* CURRENTLY FAILS: n == 0 */ + PASS(); +} + +/* Scheme: Module node always emitted as the file wrapper. */ +TEST(scheme_module_node) { + GP_Proj lp; + cbm_store_t *store = gp_index(&lp, "calc.scm", + "(define (add a b) (+ a b))\n"); + int mods = store ? gp_count_label(store, lp.project, "Module") : -1; + gp_cleanup(&lp, store); + ASSERT_TRUE(mods >= 1); /* green: Module wrapper always created */ + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════ + * SLANG (.slang) — GPU shader language (HLSL-derivative) + * ══════════════════════════════════════════════════════════════════ */ + +/* Slang: plain functions become Function nodes. */ +TEST(slang_function_nodes) { + GP_Proj lp; + cbm_store_t *store = gp_index(&lp, "a.slang", + "float helper(float x)\n" + "{\n" + " return x * 2.0;\n" + "}\n" + "\n" + "float compute(float v)\n" + "{\n" + " return helper(v) + 1.0;\n" + "}\n"); + int fns = store ? gp_count_label(store, lp.project, "Function") : -1; + gp_cleanup(&lp, store); + ASSERT_TRUE(fns >= 2); /* green: both function_definitions become Function nodes */ + PASS(); +} + +/* Slang: struct → Class node (slang_class_types includes class_specifier). */ +TEST(slang_struct_node) { + int cls = gp_extract_label( + "struct Vertex {\n" + " float3 position;\n" + " float2 uv;\n" + "};\n" + "\n" + "struct Material {\n" + " float4 color;\n" + "};\n", + CBM_LANG_SLANG, "types.slang", "Class"); + /* green if class_specifier is covered — red if struct maps to an uncovered node kind */ + ASSERT_TRUE(cls >= 1); /* EXPECTED GREEN: slang_class_types includes class_specifier */ + PASS(); +} + +/* Slang: shader entry point annotation ([shader("vertex")]) — the annotated + * function is still a function_definition; should produce a Function node. */ +TEST(slang_shader_entry_point) { + int fns = gp_extract_label( + "[shader(\"vertex\")]\n" + "VertexOutput vertMain(VertexInput input)\n" + "{\n" + " VertexOutput o;\n" + " o.position = float4(input.position, 1.0);\n" + " return o;\n" + "}\n" + "\n" + "[shader(\"fragment\")]\n" + "float4 fragMain(VertexOutput input) : SV_Target\n" + "{\n" + " return float4(1.0, 0.0, 0.0, 1.0);\n" + "}\n", + CBM_LANG_SLANG, "shader.slang", "Function"); + ASSERT_TRUE(fns >= 2); /* green: annotated functions are still function_definitions */ + PASS(); +} + +/* Slang: #include → extracted import (slang_import_types includes preproc_include). */ +TEST(slang_include_import) { + int n = gp_extract_imports( + "#include \"math_utils.slang\"\n" + "#include \"common.slang\"\n" + "\n" + "float run(float x) { return x; }\n", + CBM_LANG_SLANG, "main.slang"); + /* REAL BUG (class 2): SLANG absent from cbm_extract_imports() dispatch switch + * in extract_imports.c — slang_import_types is configured but never consumed; + * imports always 0. Fix: add CBM_LANG_SLANG case to the dispatch. */ + ASSERT_TRUE(n >= 2); + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════ + * SOLIDITY (.sol) — EVM smart contract language + * ══════════════════════════════════════════════════════════════════ */ + +/* Solidity: contract → Class node (solidity_class_types = contract_declaration). */ +TEST(solidity_contract_node) { + GP_Proj lp; + cbm_store_t *store = gp_index(&lp, "token.sol", + "// SPDX-License-Identifier: MIT\n" + "pragma solidity ^0.8.0;\n" + "\n" + "contract Token {\n" + " mapping(address => uint256) public balances;\n" + "\n" + " function mint(address to, uint256 amount) public {\n" + " balances[to] += amount;\n" + " }\n" + "}\n"); + int cls = store ? gp_type_like(store, lp.project) : -1; + gp_cleanup(&lp, store); + ASSERT_TRUE(cls >= 1); /* green: contract_declaration → Class node */ + PASS(); +} + +/* Solidity: interface → Interface node. */ +TEST(solidity_interface_node) { + int cls = gp_extract_label( + "// SPDX-License-Identifier: MIT\n" + "pragma solidity ^0.8.0;\n" + "\n" + "interface IERC20 {\n" + " function totalSupply() external view returns (uint256);\n" + " function transfer(address to, uint256 amount) external returns (bool);\n" + "}\n", + CBM_LANG_SOLIDITY, "IERC20.sol", "Interface"); + /* green: interface_declaration in solidity_class_types maps to Interface label */ + ASSERT_TRUE(cls >= 1); + PASS(); +} + +/* Solidity: contract methods → Method nodes inside the contract Class. */ +TEST(solidity_method_nodes) { + GP_Proj lp; + cbm_store_t *store = gp_index(&lp, "vault.sol", + "// SPDX-License-Identifier: MIT\n" + "pragma solidity ^0.8.0;\n" + "\n" + "contract Vault {\n" + " uint256 private balance;\n" + "\n" + " function deposit(uint256 amount) public {\n" + " balance += amount;\n" + " }\n" + "\n" + " function withdraw(uint256 amount) public {\n" + " require(balance >= amount);\n" + " balance -= amount;\n" + " }\n" + "}\n"); + int methods = store ? gp_count_label(store, lp.project, "Method") : -1; + gp_cleanup(&lp, store); + ASSERT_TRUE(methods >= 2); /* green: contract functions with parent class → Method */ + PASS(); +} + +/* Solidity: import directive extracted at extraction level. */ +TEST(solidity_import_extracted) { + int n = gp_extract_imports( + "// SPDX-License-Identifier: MIT\n" + "pragma solidity ^0.8.0;\n" + "\n" + "import \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\n" + "import {Ownable} from \"@openzeppelin/contracts/access/Ownable.sol\";\n" + "\n" + "contract MyToken {\n" + "}\n", + CBM_LANG_SOLIDITY, "a.sol"); + /* REAL BUG (class 2): SOLIDITY absent from cbm_extract_imports() dispatch in + * extract_imports.c — solidity_import_types configured but never consumed. */ + ASSERT_TRUE(n >= 2); + PASS(); +} + +/* Solidity: contract inheritance (`is` clause) → base_classes populated. + * The Solidity grammar uses "inheritance_specifier" child nodes under + * contract_declaration, which the fallback in extract_base_classes covers. */ +TEST(solidity_contract_inheritance) { + int has_base = gp_extract_has_base_class( + "// SPDX-License-Identifier: MIT\n" + "pragma solidity ^0.8.0;\n" + "\n" + "contract Base {\n" + " function baseFunc() public virtual {}\n" + "}\n" + "\n" + "contract Child is Base {\n" + " function childFunc() public {}\n" + "}\n", + CBM_LANG_SOLIDITY, "inherit.sol"); + /* UNCERTAIN/RED: extract_base_classes looks for "inheritance_specifier" in + * the fallback list; if tree-sitter-solidity uses a different field name + * (e.g. "heritage") the base_classes array will be empty → INHERITS edge lost. */ + ASSERT_TRUE(has_base >= 1); + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════ + * SQUIRREL (.nut) — scripting language (game engines, Squirrel VM) + * ══════════════════════════════════════════════════════════════════ */ + +/* Squirrel: top-level functions become Function nodes. */ +TEST(squirrel_function_nodes) { + GP_Proj lp; + cbm_store_t *store = gp_index(&lp, "util.nut", + "function greet(name) {\n" + " return \"Hello, \" + name\n" + "}\n" + "\n" + "function farewell(name) {\n" + " return \"Goodbye, \" + name\n" + "}\n"); + int fns = store ? gp_count_label(store, lp.project, "Function") : -1; + gp_cleanup(&lp, store); + ASSERT_TRUE(fns >= 2); /* green: function_declaration → Function nodes */ + PASS(); +} + +/* Squirrel: class declaration → Class node. + * squirrel_class_types includes "class_declaration". */ +TEST(squirrel_class_node) { + int cls = gp_extract_label( + "class Animal {\n" + " name = null\n" + " constructor(n) { name = n }\n" + " function speak() { return name + \" speaks\" }\n" + "}\n", + CBM_LANG_SQUIRREL, "animal.nut", "Class"); + /* REAL BUG (NEW-16, node-extraction incompleteness): squirrel_class_types lists + * "class_declaration" and the vendored squirrel grammar emits that exact node, + * yet extract_defs produces 0 Class nodes. Squirrel class defs are not reaching + * extract_class_def (lang_specs/extract_defs gap for Squirrel). */ + ASSERT_TRUE(cls >= 1); + PASS(); +} + +/* Squirrel: class methods inside a class body → Method nodes. */ +TEST(squirrel_class_methods) { + int methods = gp_extract_label( + "class Calculator {\n" + " function add(a, b) { return a + b }\n" + " function sub(a, b) { return a - b }\n" + "}\n", + CBM_LANG_SQUIRREL, "calc.nut", "Method"); + /* REAL BUG (NEW-16): consequence of squirrel class defs not extracted — with no + * Class node, in-body functions never get parent_class set, so 0 Method nodes. */ + ASSERT_TRUE(methods >= 2); + PASS(); +} + +/* Squirrel: class inheritance via `extends`. + * The Squirrel grammar may use "base_class" or an inline expression; + * extract_base_classes' field scan covers "superclass" — may not match. */ +TEST(squirrel_class_inheritance) { + int has_base = gp_extract_has_base_class( + "class Animal {\n" + " function speak() { return \"...\" }\n" + "}\n" + "\n" + "class Dog extends Animal {\n" + " function speak() { return \"Woof\" }\n" + "}\n", + CBM_LANG_SQUIRREL, "dog.nut"); + /* UNCERTAIN/RED: Squirrel `extends` field name in tree-sitter-squirrel may + * differ from the field names searched by extract_base_classes; no explicit + * Squirrel branch exists → base_classes likely empty → INHERITS edge lost. */ + ASSERT_TRUE(has_base >= 1); + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════ + * STARLARK (.bzl / .star) — Bazel/Buck build language + * ══════════════════════════════════════════════════════════════════ */ + +/* Starlark: def statements → Function nodes. */ +TEST(starlark_function_nodes) { + GP_Proj lp; + cbm_store_t *store = gp_index(&lp, "rules.bzl", + "def cc_library_rule(name, srcs, deps = []):\n" + " native.cc_library(\n" + " name = name,\n" + " srcs = srcs,\n" + " deps = deps,\n" + " )\n" + "\n" + "def cc_binary_rule(name, srcs):\n" + " native.cc_binary(name = name, srcs = srcs)\n"); + int fns = store ? gp_count_label(store, lp.project, "Function") : -1; + gp_cleanup(&lp, store); + ASSERT_TRUE(fns >= 2); /* green: def_statement → Function nodes */ + PASS(); +} + +/* Starlark: load() call — starlark_import_types uses "with_clause" which is + * the Python-style 'with' node, NOT "load_statement". The Starlark grammar + * uses "load_statement" for load(); "with_clause" is a grammar mismatch. */ +TEST(starlark_load_not_extracted) { + /* red: starlark_import_types = "with_clause" but load() is a "load_statement"; + * grammar type mismatch → imports always 0 for Starlark load() calls. */ + int n = gp_extract_imports( + "load(\"@rules_cc//cc:defs.bzl\", \"cc_library\")\n" + "load(\"//tools:build_rules.bzl\", \"my_rule\")\n" + "\n" + "def my_target(name):\n" + " cc_library(name = name)\n", + CBM_LANG_STARLARK, "BUILD.bzl"); + ASSERT_TRUE(n >= 2); /* CURRENTLY FAILS: with_clause ≠ load_statement → n == 0 */ + PASS(); +} + +/* Starlark: .star extension also routes to Starlark grammar. */ +TEST(starlark_star_extension_function_nodes) { + GP_Proj lp; + cbm_store_t *store = gp_index(&lp, "macros.star", + "def format_label(name, pkg = None):\n" + " if pkg:\n" + " return \"//{}/:{}\".format(pkg, name)\n" + " return \":{}\".format(name)\n"); + int fns = store ? gp_count_label(store, lp.project, "Function") : -1; + gp_cleanup(&lp, store); + ASSERT_TRUE(fns >= 1); /* green: .star → CBM_LANG_STARLARK → Function node */ + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════ + * SWAY (.sw) — Fuel VM smart contract language (Rust-inspired) + * ══════════════════════════════════════════════════════════════════ */ + +/* Sway: fn items → Function nodes. */ +TEST(sway_function_nodes) { + GP_Proj lp; + cbm_store_t *store = gp_index(&lp, "contract.sw", + "fn compute(x: u64) -> u64 {\n" + " x * x\n" + "}\n" + "\n" + "fn validate(v: u64) -> bool {\n" + " v > 0\n" + "}\n"); + int fns = store ? gp_count_label(store, lp.project, "Function") : -1; + gp_cleanup(&lp, store); + ASSERT_TRUE(fns >= 2); /* green: function_item → Function nodes */ + PASS(); +} + +/* Sway: struct → type-like node (Struct label). */ +TEST(sway_struct_node) { + int structs = gp_extract_label( + "struct Point {\n" + " x: u64,\n" + " y: u64,\n" + "}\n" + "\n" + "struct Rectangle {\n" + " origin: Point,\n" + " width: u64,\n" + " height: u64,\n" + "}\n", + CBM_LANG_SWAY, "geom.sw", "Struct"); + /* REAL BUG (NEW-16): sway_class_types lists "struct_item" and the vendored sway + * grammar emits that node, yet 0 Struct nodes are produced — Sway type defs are + * not reaching extract_class_def. */ + ASSERT_TRUE(structs >= 2); + PASS(); +} + +/* Sway: `use` declaration → extracted import (sway_import_types = use_declaration). */ +TEST(sway_use_import_extracted) { + int n = gp_extract_imports( + "use fuel_std::address::Address;\n" + "use fuel_std::token::transfer;\n" + "\n" + "fn run() {}\n", + CBM_LANG_SWAY, "a.sw"); + /* REAL BUG (class 2): SWAY absent from cbm_extract_imports() dispatch in + * extract_imports.c — sway_import_types configured but never consumed. */ + ASSERT_TRUE(n >= 2); + PASS(); +} + +/* Sway: `abi` declaration → type-like node (abi_item in sway_class_types). */ +TEST(sway_abi_node) { + int types = gp_extract_label( + "abi MyContract {\n" + " fn total_supply() -> u64;\n" + " fn balance_of(addr: b256) -> u64;\n" + "}\n", + CBM_LANG_SWAY, "abi.sw", "Interface"); + /* REAL BUG (NEW-16): abi_item is in sway_class_types and the grammar emits it, + * but 0 Interface nodes result (Sway type defs not extracted). Note also + * class_label_for_kind has no "abi_item" case, so even if extracted it would + * fall to the default "Class" label, not "Interface". */ + ASSERT_TRUE(types >= 1); + PASS(); +} + +/* Sway: trait implementation (impl_item in sway_class_types). */ +TEST(sway_impl_node) { + int types = gp_extract_label( + "struct Counter { value: u64 }\n" + "\n" + "impl Counter {\n" + " fn new() -> Self { Counter { value: 0 } }\n" + " fn increment(ref mut self) { self.value += 1; }\n" + "}\n", + CBM_LANG_SWAY, "counter.sw", "Class"); + /* green: impl_item in sway_class_types → Class label */ + ASSERT_TRUE(types >= 1); + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════ + * TCL (.tcl) — Tool Command Language + * ══════════════════════════════════════════════════════════════════ */ + +/* Tcl: proc declarations → Function nodes. */ +TEST(tcl_proc_nodes) { + GP_Proj lp; + cbm_store_t *store = gp_index(&lp, "utils.tcl", + "proc greet {name} {\n" + " return \"Hello, $name\"\n" + "}\n" + "\n" + "proc farewell {name} {\n" + " return \"Goodbye, $name\"\n" + "}\n" + "\n" + "proc run {} {\n" + " puts [greet \"world\"]\n" + "}\n"); + int fns = store ? gp_count_label(store, lp.project, "Function") : -1; + gp_cleanup(&lp, store); + ASSERT_TRUE(fns >= 3); /* green: proc → Function nodes */ + PASS(); +} + +/* Tcl: namespace → type-like (tcl_class_types = namespace → Class label). */ +TEST(tcl_namespace_node) { + int cls = gp_extract_label( + "namespace eval MathUtils {\n" + " proc add {a b} { expr {$a + $b} }\n" + " proc mul {a b} { expr {$a * $b} }\n" + "}\n", + CBM_LANG_TCL, "math.tcl", "Class"); + /* REAL BUG (NEW-16): tcl_class_types lists "namespace" and the grammar emits a + * "namespace" node, yet 0 Class nodes are produced — Tcl namespace defs are not + * reaching extract_class_def. */ + ASSERT_TRUE(cls >= 1); + PASS(); +} + +/* Tcl: `source` command — tcl import_types = empty_types; no extraction support. + * A Tcl script can source other files but the extractor ignores these commands. */ +TEST(tcl_source_not_extracted) { + /* red: tcl_import_types = empty_types; "source" and "package require" are + * plain commands, not tagged with a distinct AST node type. */ + int n = gp_extract_imports( + "source lib/utils.tcl\n" + "source lib/config.tcl\n" + "package require http\n" + "\n" + "proc run {} { puts \"running\" }\n", + CBM_LANG_TCL, "main.tcl"); + ASSERT_TRUE(n >= 2); /* CURRENTLY FAILS: n == 0 */ + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════ + * TEAL (.tl) — Typed Lua (Teal language) + * ══════════════════════════════════════════════════════════════════ */ + +/* Teal: local function declarations → Function nodes. */ +TEST(teal_function_nodes) { + GP_Proj lp; + cbm_store_t *store = gp_index(&lp, "math.tl", + "local function add(a: number, b: number): number\n" + " return a + b\n" + "end\n" + "\n" + "local function multiply(a: number, b: number): number\n" + " return a * b\n" + "end\n" + "\n" + "local function square(x: number): number\n" + " return multiply(x, x)\n" + "end\n"); + int fns = store ? gp_count_label(store, lp.project, "Function") : -1; + gp_cleanup(&lp, store); + ASSERT_TRUE(fns >= 3); /* green: function_statement → Function nodes */ + PASS(); +} + +/* Teal: record declaration → type-like node (teal_class_types includes record_declaration). */ +TEST(teal_record_node) { + int types = gp_extract_label( + "local record Point\n" + " x: number\n" + " y: number\n" + "end\n" + "\n" + "local record Color\n" + " r: number\n" + " g: number\n" + " b: number\n" + "end\n", + CBM_LANG_TEAL, "types.tl", "Class"); + /* green: record_declaration in teal_class_types → Class label */ + ASSERT_TRUE(types >= 2); + PASS(); +} + +/* Teal: `require` — teal_import_types = empty_types; no extraction support. + * Teal uses Lua-style require() but the extractor has no import_types configured. */ +TEST(teal_require_not_extracted) { + /* red: teal_import_types = empty_types; require() is a plain call_expression + * and is not recognized as an import statement. */ + int n = gp_extract_imports( + "local json = require(\"dkjson\")\n" + "local socket = require(\"socket\")\n" + "\n" + "local function run()\n" + " return json.encode({x = 1})\n" + "end\n", + CBM_LANG_TEAL, "main.tl"); + ASSERT_TRUE(n >= 2); /* CURRENTLY FAILS: n == 0 */ + PASS(); +} + +/* Teal: interface declaration → Interface node. */ +TEST(teal_interface_node) { + int ifaces = gp_extract_label( + "local interface Drawable\n" + " draw: function(self: Drawable)\n" + "end\n", + CBM_LANG_TEAL, "drawable.tl", "Interface"); + /* green: interface_declaration in teal_class_types → Interface label */ + ASSERT_TRUE(ifaces >= 1); + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════ + * VIMSCRIPT (.vim) — Vim configuration/plugin language + * ══════════════════════════════════════════════════════════════════ */ + +/* VimScript: function! declarations → Function nodes. */ +TEST(vimscript_function_bang_nodes) { + GP_Proj lp; + cbm_store_t *store = gp_index(&lp, "plugin.vim", + "function! FormatBuffer() abort\n" + " %s/\\s\\+$//e\n" + "endfunction\n" + "\n" + "function! RunTests() abort\n" + " call FormatBuffer()\n" + " echom 'tests done'\n" + "endfunction\n" + "\n" + "function! SetupKeyMaps() abort\n" + " nnoremap :call RunTests()\n" + "endfunction\n"); + int fns = store ? gp_count_label(store, lp.project, "Function") : -1; + gp_cleanup(&lp, store); + /* green: function! declarations are extracted as Function nodes */ + ASSERT_TRUE(fns >= 2); /* grammar_labels shows Function:1 for single function fixture; + * multiple functions → multiple nodes */ + PASS(); +} + +/* VimScript: function without bang. */ +TEST(vimscript_function_no_bang) { + int fns = gp_extract_label( + "function MyPlugin#Init()\n" + " let g:myplugin_loaded = 1\n" + "endfunction\n" + "\n" + "function MyPlugin#Enable()\n" + " call MyPlugin#Init()\n" + "endfunction\n", + CBM_LANG_VIMSCRIPT, "myplugin.vim", "Function"); + /* green: non-bang function declarations also extracted */ + ASSERT_TRUE(fns >= 2); + PASS(); +} + +/* VimScript: autoload namespace (Plugin#Function convention) — still Function nodes. */ +TEST(vimscript_autoload_function) { + int fns = gp_extract_label( + "function! airline#statusline#update()\n" + " let s:parts = []\n" + "endfunction\n", + CBM_LANG_VIMSCRIPT, "statusline.vim", "Function"); + ASSERT_TRUE(fns >= 1); /* green: autoload-namespaced function → Function node */ + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════ + * WGSL (.wgsl) — WebGPU Shading Language + * ══════════════════════════════════════════════════════════════════ */ + +/* WGSL: plain fn items → Function nodes. */ +TEST(wgsl_function_nodes) { + GP_Proj lp; + cbm_store_t *store = gp_index(&lp, "compute.wgsl", + "fn dot2(v: vec2) -> f32 {\n" + " return v.x * v.x + v.y * v.y;\n" + "}\n" + "\n" + "fn normalize2(v: vec2) -> vec2 {\n" + " let len = sqrt(dot2(v));\n" + " return vec2(v.x / len, v.y / len);\n" + "}\n"); + int fns = store ? gp_count_label(store, lp.project, "Function") : -1; + gp_cleanup(&lp, store); + ASSERT_TRUE(fns >= 2); /* green: function_declaration → Function nodes */ + PASS(); +} + +/* WGSL: @vertex / @fragment / @compute entry point attributes — the decorated + * function is still a function_declaration; should produce Function nodes. */ +TEST(wgsl_entry_point_functions) { + int fns = gp_extract_label( + "struct VertexOutput {\n" + " @builtin(position) position: vec4,\n" + " @location(0) color: vec4,\n" + "}\n" + "\n" + "@vertex\n" + "fn vs_main(@location(0) pos: vec2) -> VertexOutput {\n" + " var out: VertexOutput;\n" + " out.position = vec4(pos, 0.0, 1.0);\n" + " out.color = vec4(1.0, 0.0, 0.0, 1.0);\n" + " return out;\n" + "}\n" + "\n" + "@fragment\n" + "fn fs_main(in: VertexOutput) -> @location(0) vec4 {\n" + " return in.color;\n" + "}\n" + "\n" + "@compute @workgroup_size(64)\n" + "fn cs_main(@builtin(global_invocation_id) id: vec3) {}\n", + CBM_LANG_WGSL, "pipeline.wgsl", "Function"); + ASSERT_TRUE(fns >= 3); /* green: all three entry points are function_declarations */ + PASS(); +} + +/* WGSL: struct → type-like node (wgsl_class_types). */ +TEST(wgsl_struct_node) { + int types = gp_extract_label( + "struct Uniforms {\n" + " mvp: mat4x4,\n" + " time: f32,\n" + "}\n" + "\n" + "struct LightData {\n" + " position: vec3,\n" + " intensity: f32,\n" + "}\n", + CBM_LANG_WGSL, "uniforms.wgsl", "Struct"); + /* REAL BUG (NEW-16): wgsl_class_types lists "struct_declaration" and the vendored + * wgsl grammar emits that node, yet 0 Struct nodes result — WGSL struct defs are + * not reaching extract_class_def. */ + ASSERT_TRUE(types >= 1); + PASS(); +} + +/* WGSL: `enable` directive — wgsl_import_types includes "enable_directive". */ +TEST(wgsl_enable_directive_extracted) { + int n = gp_extract_imports( + "enable f16;\n" + "enable dual_source_blending;\n" + "\n" + "fn run() -> f16 { return f16(1.0); }\n", + CBM_LANG_WGSL, "ext.wgsl"); + /* REAL BUG (class 2): WGSL absent from cbm_extract_imports() dispatch in + * extract_imports.c — wgsl_import_types ("enable_directive") never consumed. */ + ASSERT_TRUE(n >= 2); + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════ + * ZSH (.zsh) — Z shell scripting language + * ══════════════════════════════════════════════════════════════════ */ + +/* Zsh: function declarations → Function nodes. */ +TEST(zsh_function_nodes) { + GP_Proj lp; + cbm_store_t *store = gp_index(&lp, "deploy.zsh", + "function check_deps {\n" + " command -v docker || return 1\n" + " command -v kubectl || return 1\n" + "}\n" + "\n" + "function build_image {\n" + " docker build -t myapp:latest .\n" + "}\n" + "\n" + "function deploy {\n" + " check_deps && build_image\n" + " kubectl apply -f k8s/\n" + "}\n"); + int fns = store ? gp_count_label(store, lp.project, "Function") : -1; + gp_cleanup(&lp, store); + ASSERT_TRUE(fns >= 3); /* green: function {} → Function nodes */ + PASS(); +} + +/* Zsh: function keyword shorthand. */ +TEST(zsh_function_shorthand) { + int fns = gp_extract_label( + "setup() {\n" + " echo \"setting up\"\n" + "}\n" + "\n" + "teardown() {\n" + " echo \"tearing down\"\n" + "}\n", + CBM_LANG_ZSH, "test_hooks.zsh", "Function"); + /* green: both POSIX-style `name()` and `function name` forms should be extracted */ + ASSERT_TRUE(fns >= 2); + PASS(); +} + +/* Zsh: `source` command — zsh_import_types = empty_types; no extraction. + * Shell `source` / `. file` is a command invocation, not a tagged import node. */ +TEST(zsh_source_not_extracted) { + /* red: zsh_import_types = empty_types; source/. are plain command nodes, + * not import_statement or similar tagged nodes. */ + int n = gp_extract_imports( + "source ~/.zshrc\n" + "source lib/utils.zsh\n" + ". lib/helpers.zsh\n" + "\n" + "function run {\n" + " echo \"running\"\n" + "}\n", + CBM_LANG_ZSH, "main.zsh"); + ASSERT_TRUE(n >= 2); /* CURRENTLY FAILS: n == 0 */ + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════ + * SUITE DECLARATION + * ══════════════════════════════════════════════════════════════════ */ + +void suite_grammar_probe_c(void) { + /* scheme */ + RUN_TEST(scheme_function_nodes); + RUN_TEST(scheme_import_not_extracted); + RUN_TEST(scheme_require_not_extracted); + RUN_TEST(scheme_module_node); + + /* slang */ + RUN_TEST(slang_function_nodes); + RUN_TEST(slang_struct_node); + RUN_TEST(slang_shader_entry_point); + RUN_TEST(slang_include_import); + + /* solidity */ + RUN_TEST(solidity_contract_node); + RUN_TEST(solidity_interface_node); + RUN_TEST(solidity_method_nodes); + RUN_TEST(solidity_import_extracted); + RUN_TEST(solidity_contract_inheritance); + + /* squirrel */ + RUN_TEST(squirrel_function_nodes); + RUN_TEST(squirrel_class_node); + RUN_TEST(squirrel_class_methods); + RUN_TEST(squirrel_class_inheritance); + + /* starlark */ + RUN_TEST(starlark_function_nodes); + RUN_TEST(starlark_load_not_extracted); + RUN_TEST(starlark_star_extension_function_nodes); + + /* sway */ + RUN_TEST(sway_function_nodes); + RUN_TEST(sway_struct_node); + RUN_TEST(sway_use_import_extracted); + RUN_TEST(sway_abi_node); + RUN_TEST(sway_impl_node); + + /* tcl */ + RUN_TEST(tcl_proc_nodes); + RUN_TEST(tcl_namespace_node); + RUN_TEST(tcl_source_not_extracted); + + /* teal */ + RUN_TEST(teal_function_nodes); + RUN_TEST(teal_record_node); + RUN_TEST(teal_require_not_extracted); + RUN_TEST(teal_interface_node); + + /* vimscript */ + RUN_TEST(vimscript_function_bang_nodes); + RUN_TEST(vimscript_function_no_bang); + RUN_TEST(vimscript_autoload_function); + + /* wgsl */ + RUN_TEST(wgsl_function_nodes); + RUN_TEST(wgsl_entry_point_functions); + RUN_TEST(wgsl_struct_node); + RUN_TEST(wgsl_enable_directive_extracted); + + /* zsh */ + RUN_TEST(zsh_function_nodes); + RUN_TEST(zsh_function_shorthand); + RUN_TEST(zsh_source_not_extracted); +} diff --git a/tests/test_main.c b/tests/test_main.c index 01ecfb500..0a953f332 100644 --- a/tests/test_main.c +++ b/tests/test_main.c @@ -86,6 +86,9 @@ extern void suite_edge_types_probe(void); extern void suite_convergence_probe(void); extern void suite_matrix_known_classes(void); extern void suite_matrix_new_constructs(void); +extern void suite_grammar_probe_a(void); +extern void suite_grammar_probe_b(void); +extern void suite_grammar_probe_c(void); extern void suite_incremental(void); extern void suite_simhash(void); extern void suite_stack_overflow(void); @@ -229,6 +232,9 @@ int main(void) { RUN_SUITE(convergence_probe); RUN_SUITE(matrix_known_classes); RUN_SUITE(matrix_new_constructs); + RUN_SUITE(grammar_probe_a); + RUN_SUITE(grammar_probe_b); + RUN_SUITE(grammar_probe_c); RUN_SUITE(incremental);