/* * test_grammar_labels.c — Per-grammar node-type LABEL golden snapshot. * * For every supported grammar, snapshot the deterministic node-type-label * histogram that cbm_extract_file() produces for the shared fixture (e.g. * "Class:1,Function:2,Module:1"). Asserting the histogram stays fixed catches * ANY future change to how a grammar's constructs are labeled — e.g. a class * silently downgraded to a Function, a lost Method, or a new spurious node. * * Node counts are deterministic (unlike edge counts), so an exact histogram * golden is stable. Labels come from CBMDefinition.label assigned during * extraction (the same labels that become graph-node labels downstream). * * Capture workflow: a grammar with no golden row prints a `[LABEL-CAPTURE]` * line and fails; copy those lines into LABEL_GOLDENS, then the test asserts. */ #include "test_framework.h" #include "cbm.h" #include "grammar_cases.h" #include #include /* Build a sorted "Label:count,Label:count" histogram of a result's def labels * (includes the always-emitted Module node). Deterministic + stable. */ static void label_histogram(CBMFileResult *r, char *out, size_t out_sz) { /* Collect distinct labels + counts (small N — linear scan is fine). */ enum { MAXL = 32 }; const char *labels[MAXL]; int counts[MAXL]; int nl = 0; for (int i = 0; i < r->defs.count; i++) { const char *l = r->defs.items[i].label ? r->defs.items[i].label : "(null)"; int j = 0; for (; j < nl; j++) { if (strcmp(labels[j], l) == 0) { counts[j]++; break; } } if (j == nl && nl < MAXL) { labels[nl] = l; counts[nl] = 1; nl++; } } /* Insertion sort labels alphabetically for a canonical string. */ for (int i = 1; i < nl; i++) { const char *lk = labels[i]; int ck = counts[i]; int j = i - 1; while (j >= 0 && strcmp(labels[j], lk) > 0) { labels[j + 1] = labels[j]; counts[j + 1] = counts[j]; j--; } labels[j + 1] = lk; counts[j + 1] = ck; } out[0] = '\0'; size_t used = 0; for (int i = 0; i < nl; i++) { int w = snprintf(out + used, out_sz - used, "%s%s:%d", i ? "," : "", labels[i], counts[i]); if (w < 0 || (size_t)w >= out_sz - used) { break; } used += (size_t)w; } } typedef struct { const char *name; const char *hist; } LabelGolden; /* Golden node-type-label histograms — captured from the current (correct) * extractor output. A mismatch = a labeling regression for that grammar. * (Populated from [LABEL-CAPTURE] output; see header.) */ static const LabelGolden LABEL_GOLDENS[] = { {"go", "Function:2,Module:1"}, {"c", "Function:2,Module:1"}, {"cpp", "Class:1,Function:1,Module:1"}, {"cuda", "Function:2,Module:1"}, {"python", "Class:6,Function:3,Method:5,Module:1"}, {"javascript", "Class:1,Function:1,Module:1"}, {"typescript", "Class:1,Function:1,Module:1"}, {"tsx", "Function:1,Module:1"}, {"java", "Class:1,Method:1,Module:1"}, /* +Class:1 (kotlin.Any) +Method:3 (toString/equals/hashCode) injected by * kotlin_builtins.c, as python's golden includes its injected builtins. */ {"kotlin", "Class:2,Function:1,Method:3,Module:1"}, {"rust", "Function:1,Module:1,Struct:1"}, {"ruby", "Class:1,Function:1,Module:1"}, {"php", "Class:1,Function:1,Module:1"}, {"c_sharp", "Class:1,Method:1,Module:1"}, {"bash", "Function:2,Module:1"}, {"zsh", "Function:2,Module:1"}, {"lua", "Function:2,Module:1"}, {"luau", "Function:2,Module:1"}, {"perl", "Function:2,Module:1"}, {"dart", "Class:1,Function:1,Module:1"}, {"swift", "Class:1,Function:1,Module:1"}, {"scala", "Class:1,Function:1,Method:1,Module:1"}, {"gdscript", "Function:1,Module:1"}, {"groovy", "Class:1,Method:1,Module:1"}, {"zig", "Function:2,Module:1"}, {"solidity", "Class:1,Function:1,Method:1,Module:1"}, {"tcl", "Function:2,Module:1"}, {"powershell", "Function:2,Module:1"}, {"r", "Function:2,Module:1"}, {"julia", "Class:1,Function:1,Module:1"}, {"matlab", "Function:2,Module:1"}, {"ada", "Function:1,Module:1"}, {"agda", "Function:1,Module:1"}, {"apex", "Class:1,Method:1,Module:1"}, {"awk", "Function:2,Module:1"}, {"cairo", "Function:2,Module:1"}, {"clojure", "Function:2,Module:1"}, {"commonlisp", "Function:2,Module:1"}, {"emacslisp", "Function:2,Module:1"}, {"crystal", "Class:1,Function:1,Module:1"}, {"d", "Function:2,Module:1"}, {"elixir", "Class:1,Function:1,Module:1"}, {"erlang", "Function:2,Module:1"}, {"fennel", "Function:2,Module:1"}, {"fish", "Function:1,Module:1"}, {"fortran", "Function:1,Module:1"}, {"fsharp", "Function:2,Module:1"}, {"gleam", "Function:2,Module:1"}, {"glsl", "Function:2,Module:1"}, {"hare", "Function:2,Module:1"}, {"haskell", "Function:1,Module:1"}, {"hlsl", "Function:2,Module:1"}, {"ispc", "Function:2,Module:1"}, {"objc", "Class:1,Method:1,Module:1"}, {"ocaml", "Function:2,Module:1"}, {"odin", "Function:2,Module:1"}, {"pascal", "Function:1,Module:1"}, {"pony", "Class:1,Method:1,Module:1"}, {"purescript", "Function:1,Module:1"}, {"racket", "Function:2,Module:1"}, {"rescript", "Function:2,Module:1"}, {"scheme", "Function:2,Module:1"}, {"slang", "Function:2,Module:1"}, {"squirrel", "Function:2,Module:1"}, {"starlark", "Function:2,Module:1"}, {"sway", "Function:2,Module:1"}, {"teal", "Function:2,Module:1"}, {"vimscript", "Function:1,Module:1"}, {"elm", "Class:1,Function:1,Module:1"}, {"func", "Function:1,Module:1"}, {"lean", "Function:2,Module:1"}, {"move", "Function:1,Module:1"}, {"mojo", "Class:1,Function:1,Method:1,Module:1"}, {"smali", "Class:1,Function:1,Module:1"}, {"systemverilog", "Class:1,Function:1,Module:1"}, {"verilog", "Class:1,Module:1"}, {"vhdl", "Class:1,Module:1"}, {"wgsl", "Function:2,Module:1"}, {"tlaplus", "Function:1,Module:1"}, {"llvm", "Function:1,Module:1"}, {"tablegen", "Function:1,Module:1"}, {"puppet", "Class:1,Module:1"}, {"assembly", "Function:1,Module:1"}, {"nasm", "Function:1,Module:1"}, {"cfml", "Function:1,Module:1"}, {"cfscript", "Function:1,Module:1"}, {"cobol", "Function:1,Module:1"}, {"janet", "Function:2,Module:1"}, {"magma", "Function:1,Module:1"}, {"qml", "Function:1,Module:1"}, {"wolfram", "Function:1,Module:1"}, {"pine", "Function:1,Module:1"}, {"form", "Module:1,Variable:1"}, {"protobuf", "Class:1,Module:1"}, {"soql", "Module:1"}, {"sosl", "Module:1"}, {"dotenv", "Module:1"}, {"json", "Module:1,Variable:2"}, {"json5", "Module:1"}, {"jsonnet", "Module:1"}, {"jsdoc", "Module:1"}, {"yaml", "Module:1,Variable:2"}, {"k8s", "Module:1"}, {"kustomize", "Module:1"}, {"toml", "Class:1,Module:1,Variable:1"}, {"ini", "Class:1,Module:1,Variable:1"}, {"csv", "Module:1"}, {"sql", "Module:1,Variable:1"}, {"xml", "Class:2,Module:1"}, {"html", "Module:1"}, {"css", "Module:1"}, {"scss", "Module:1,Variable:1"}, {"markdown", "Module:1,Section:1"}, {"rst", "Module:1"}, {"dockerfile", "Module:1"}, {"makefile", "Function:1,Module:1"}, {"cmake", "Function:1,Module:1"}, {"meson", "Module:1"}, {"gn", "Module:1"}, {"just", "Function:1,Module:1"}, {"hcl", "Class:1,Module:1"}, {"nix", "Module:1"}, {"gomod", "Module:1"}, {"gotemplate", "Module:1"}, {"graphql", "Class:1,Field:1,Module:1"}, {"prisma", "Class:1,Field:1,Module:1"}, {"thrift", "Function:1,Module:1"}, {"capnp", "Class:1,Module:1"}, {"smithy", "Class:1,Module:1"}, {"wit", "Class:2,Function:1,Module:1"}, {"kdl", "Module:1"}, {"ron", "Module:1"}, {"nickel", "Module:1"}, {"pkl", "Module:1"}, {"bicep", "Module:1"}, {"bitbake", "Module:1"}, {"beancount", "Module:1"}, {"bibtex", "Module:1"}, {"po", "Module:1"}, {"diff", "Module:1"}, {"regex", "Module:1"}, {"requirements", "Module:1"}, {"properties", "Module:1,Variable:2"}, {"gitignore", "Module:1"}, {"gitattributes", "Module:1"}, {"sshconfig", "Module:1"}, {"hyprlang", "Module:1"}, {"kconfig", "Class:1,Module:1"}, {"linkerscript", "Module:1"}, {"devicetree", "Module:1"}, {"jinja2", "Module:1"}, {"liquid", "Module:1"}, {"blade", "Module:1"}, {"vue", "Module:1"}, {"svelte", "Module:1"}, {"astro", "Module:1"}, {"templ", "Class:1,Module:1"}, {"typst", "Module:1"}, {"mermaid", "Module:1"}, {NULL, NULL}, }; static const char *golden_for(const char *name) { for (int i = 0; LABEL_GOLDENS[i].name; i++) { if (strcmp(LABEL_GOLDENS[i].name, name) == 0) { return LABEL_GOLDENS[i].hist; } } return NULL; } TEST(grammar_label_goldens) { int n = (int)CBM_GRAMMAR_CASES_COUNT; int failures = 0; int missing = 0; for (int i = 0; i < n; i++) { const GrammarCase *c = &CBM_GRAMMAR_CASES[i]; CBMFileResult *r = cbm_extract_file(c->src, (int)strlen(c->src), c->lang, "lbl", c->path, 0, NULL, NULL); if (!r) { fprintf(stderr, " [LABEL] %-14s extract returned NULL\n", c->name); failures++; continue; } char hist[256]; label_histogram(r, hist, sizeof(hist)); cbm_free_result(r); const char *golden = golden_for(c->name); if (!golden) { fprintf(stderr, " [LABEL-CAPTURE] {\"%s\", \"%s\"},\n", c->name, hist); missing++; } else if (strcmp(golden, hist) != 0) { fprintf(stderr, " [LABEL] %-14s MISMATCH golden=[%s] actual=[%s]\n", c->name, golden, hist); failures++; } } if (missing > 0) { fprintf(stderr, " [LABEL] %d grammar(s) missing a golden — copy the [LABEL-CAPTURE] lines " "into LABEL_GOLDENS\n", missing); } ASSERT_EQ(failures, 0); ASSERT_EQ(missing, 0); PASS(); } /* Count def nodes that are NOT the structural file/module wrappers. */ static int non_module_defs(CBMFileResult *r) { int n = 0; for (int i = 0; i < r->defs.count; i++) { const char *l = r->defs.items[i].label; if (l && strcmp(l, "Module") != 0 && strcmp(l, "File") != 0 && strcmp(l, "Folder") != 0 && strcmp(l, "Package") != 0 && strcmp(l, "Directory") != 0) { n++; } } return n; } /* Code / IDL grammars whose fixture contains a real definition construct * (function / type / message / model / module / label) that MUST extract to at * least one non-Module node. These currently extract only a Module node — an * under-extraction gap reproduced here as a hard failure (fix phase). Data / * config / markup grammars (json/yaml/css/…) are intentionally excluded — a * lone Module node is correct for them. */ static const char *MUST_EXTRACT_DEFS[] = { "agda", "pony", "move", "cobol", "janet", "pine", "smali", "verilog", "vhdl", "systemverilog", "protobuf", "graphql", "thrift", "capnp", "smithy", "wit", "prisma", "cmake", "puppet", "tablegen", "assembly", "nasm", NULL}; TEST(grammar_code_extracts_defs) { int failures = 0; for (int k = 0; MUST_EXTRACT_DEFS[k]; k++) { const char *name = MUST_EXTRACT_DEFS[k]; const GrammarCase *c = NULL; for (int i = 0; i < (int)CBM_GRAMMAR_CASES_COUNT; i++) { if (strcmp(CBM_GRAMMAR_CASES[i].name, name) == 0) { c = &CBM_GRAMMAR_CASES[i]; break; } } if (!c) { fprintf(stderr, " [CODE-DEFS] FAIL %-14s — no fixture in CBM_GRAMMAR_CASES\n", name); failures++; continue; } CBMFileResult *r = cbm_extract_file(c->src, (int)strlen(c->src), c->lang, "cd", c->path, 0, NULL, NULL); if (!r) { fprintf(stderr, " [CODE-DEFS] FAIL %-14s extract returned NULL\n", name); failures++; continue; } int nd = non_module_defs(r); cbm_free_result(r); if (nd < 1) { fprintf(stderr, " [CODE-DEFS] FAIL %-14s extracts 0 non-Module defs (under-extraction " "gap — only a Module node)\n", name); failures++; } } fprintf(stderr, " [CODE-DEFS] %d code/IDL grammars: %d under-extraction FAILURES\n", (int)(sizeof(MUST_EXTRACT_DEFS) / sizeof(MUST_EXTRACT_DEFS[0])) - 1, failures); ASSERT_EQ(failures, 0); PASS(); } void suite_grammar_labels(void) { RUN_TEST(grammar_label_goldens); RUN_TEST(grammar_code_extracts_defs); }