Files
codebase-memory-mcp/internal/cbm/helpers.h
T
Martin Vogelanddarkverbito 706bb2ce81 feat(lang): add Chialisp support with a first-party tree-sitter grammar
Chialisp (.clsp/.clib/.clinc) is the Chia smart-coin language. The only
public grammar, Quexington/tree-sitter-chialisp, cannot parse it: on the five
reference files from chia-blockchain@main it produced 3-34 ERROR nodes each.
Three defects account for all of them, and each desynchronised the rest of the
file rather than losing one node:

  * comments were `/;.*\r\n/` -- CRLF-only, while real files are LF;
  * `(include foo.clib)` was rejected because of the dot in the filename;
  * `(defconstant NAME <expr>)` accepted only a primitive after the name.

So the grammar is written here instead, in tools/tree-sitter-chialisp/, and
files as first-party: its vendored LICENSE is the project's own, byte-identical
to the repository root, carrying no third-party copyright.

It is deliberately GENERIC -- source_file / list / symbol / string / number /
hex / dot / comment, modelling the clvm_tools reader rather than the Chialisp
form vocabulary. `mod`, `defun`, `defconstant` and `include` are ordinary head
symbols; which lists are definitions is decided in extract_defs.c. A dialect
that adds a form (cl-21 -> cl-23 -> cl-26 each added several) therefore needs
no regenerated parser. Acceptance: all five reference files parse with zero
ERROR and zero MISSING nodes; the three defects above are pinned as tests.

Extraction, and the four decisions in it:

  * `Constant` is admitted to the cross-file name registry
    (cbm_label_is_registry_symbol). Chialisp's condition codes live as
    `(defconstant ...)` in a shared .clib and are referenced by name from every
    puzzle; labelled but unregistered they are seeded nowhere, and roughly half
    of a Chialisp graph would be present as nodes yet never the target of an
    edge. All three registry-seeding sites route through that one predicate, so
    they cannot diverge. No other language emits `Constant`, so the blast radius
    is exactly Chialisp. Pinned by a two-file graph test that goes red when the
    label is removed from the predicate.
  * `export` / `namespace` are NOT definition heads. `(export foo)` names a
    function already defined in the same file, so treating it as a def head
    mints a second node for one symbol. They stay in the not-a-call filter.
  * A parameter list is a `list` too, but it binds rather than invokes. Without
    that gate `(defun check_conditions (HEIGHTLOCK conditions) ...)` mints a
    phantom call to HEIGHTLOCK, and real Chialisp is mostly such definitions.
  * `(include *standard-cl-26*)` is a dialect directive, not a file dependency;
    recording it would invent a dependency on a file that does not exist.

Three helpers that the defs, calls and unified walks all need -- the quote-data
gate, comment-skipping named-child access, and the Chialisp def-head set -- live
in helpers.c rather than being copied per translation unit. They previously
existed as byte-identical duplicates that had already drifted: extract_defs.c
skipped comments when reading a def head while its mirror in extract_unified.c
did not, so a comment between a head and its name desynchronised defs from call
scope. Both now use the same accessor.

Ledger: 161 -> 162 grammars across the seven published surfaces, ABI-14 86 -> 87,
first-party 13 -> 14, CBM_LANG_COUNT 164 -> 165.

Distilled from PR #919 by darkverbito, whose extraction logic -- the CLVM
not-a-call vocabulary, the quoted-data gate, the filename-derived `mod` name and
the include/embed import handling -- is kept and is the substance of this change.
The grammar that PR vendored is not used.

Co-authored-by: darkverbito <8049985+darkverbito@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-28 21:30:06 +02:00

233 lines
13 KiB
C

#ifndef CBM_HELPERS_H
#define CBM_HELPERS_H
#include "cbm.h"
// Portable memmem: find first occurrence of `needle` (needle_len bytes) within
// `haystack` (haystack_len bytes). Returns a pointer into haystack, or NULL.
// Hand-rolled so it compiles identically on all platforms (GNU/BSD-only
// memmem is unavailable under msys2-clang on Windows).
void *cbm_memmem(const void *haystack, size_t haystack_len, const void *needle, size_t needle_len);
// Extract text of a node from source. Returns arena-allocated string.
char *cbm_node_text(CBMArena *a, TSNode node, const char *source);
// Check if a string is a language keyword (should be skipped as callee/usage).
bool cbm_is_keyword(const char *name, CBMLanguage lang);
// Check if a name is a builtin we mint a real graph node for, so a CALL to it
// must NOT be keyword-filtered out of call extraction (the LSP resolves it to
// the injected builtin node and forms a CALLS edge). Narrower than
// cbm_is_keyword: it only covers builtins with a target node, so un-filtering
// them cannot produce a node-less / Module-sourced edge. The Python set MUST
// stay in sync with kPyBuiltinNodes in internal/cbm/lsp/py_builtins.c.
bool cbm_is_resolvable_builtin(const char *name, CBMLanguage lang);
// Classify a string literal as URL, config, or neither.
// Returns CBM_STRREF_URL (0), CBM_STRREF_CONFIG (1), or -1 for neither.
int cbm_classify_string(const char *str, int len);
// Check if a name is exported per language convention.
bool cbm_is_exported(const char *name, CBMLanguage lang);
// Check if a file is a test file based on path and language.
bool cbm_is_test_file(const char *rel_path, CBMLanguage lang);
// Find the innermost enclosing function node by walking parent chain.
// Returns a null node if none found.
TSNode cbm_find_enclosing_func(TSNode node, CBMLanguage lang);
// Get the QN of an enclosing function, or module_qn if none.
const char *cbm_enclosing_func_qn(CBMArena *a, TSNode node, CBMLanguage lang, const char *source,
const char *project, const char *rel_path, const char *module_qn);
// Cached version: uses ctx->ef_cache to avoid repeated parent-chain walks.
const char *cbm_enclosing_func_qn_cached(CBMExtractCtx *ctx, TSNode node);
// Max declarator-chain descent depth for C/C++/CUDA/GLSL function-name
// resolution. Single source of truth — extract_defs.c's DECLARATOR_DEPTH_LIMIT
// is derived from this so the three extractors cannot drift.
#define CBM_DECLARATOR_DEPTH_LIMIT 8
// Resolve the function-name node for a C/C++/CUDA/GLSL `function_definition`.
// Such nodes have no `name` field — the name is nested in the declarator chain
// (pointer/function/parenthesized/array declarators wrap it; out-of-line method
// definitions name it with a qualified_identifier). Descends the `declarator`
// field to the innermost name node and returns it, or a null node if none is
// found. Shared by the defs, calls, and unified extractors so all three agree on
// enclosing-function attribution — drift between private copies caused #438.
TSNode cbm_resolve_c_declarator_name_node(TSNode func_node);
// Convert a resolved function/method name node to its name string, normalizing a
// C++ conversion-operator's `operator_cast` node (which spans the full
// "operator bool() const") down to "operator bool". Shared by the defs and
// unified extractors so the def name and call-scope QN agree.
// Also strips the surrounding quotes from a Nix quoted attrpath segment, so
// `"kebab-case" = a: a;` is named kebab-case rather than "kebab-case". Takes the
// language for that reason; every caller must pass ctx->language.
char *cbm_func_name_node_text(CBMArena *a, TSNode name_node, const char *source, CBMLanguage lang);
// ── Nix attrpath helpers ──
// A Nix binding's name is a PATH (`a.b.c = …`) whose segments may be quoted or
// interpolated. Shared by the defs and unified (call-scope) extractors so both
// derive the same name and the same scope prefix — divergence makes a CALLS edge
// name a source node that does not exist, and it is dropped at write.
// Strip one matching pair of surrounding double quotes, in place.
void cbm_nix_strip_attr_quotes(char *text);
// True when an attrpath segment contains a `${...}` interpolation and therefore
// has no statically knowable name.
bool cbm_nix_attr_is_interpolated(TSNode attr);
// The leaf segment of an attrpath — the name. Null node for an empty attrpath.
TSNode cbm_nix_attrpath_last_attr(TSNode attrpath);
// The scope prefix of an attrpath: all segments but the leaf, quote-stripped and
// dot-joined, so `a.b.fn = …` qualifies identically to `a = { b = { fn = …; }; }`.
// NULL for a single-segment path, or when a leading segment is interpolated.
const char *cbm_nix_attrpath_scope(CBMArena *a, TSNode attrpath, const char *source);
// True when a Nix `binding`'s value is an attribute set — the binding names a
// scope rather than defining a value. Excludes let-bindings and lambda values.
bool cbm_nix_binding_is_attrset_scope(TSNode node);
// The scope QN contributed by a Nix `binding` whose value is an attribute set.
// Called by BOTH extract_defs.c and extract_unified.c, which carry separate
// compute_class_qn implementations — sharing this makes a def/call-scope QN
// mismatch (which silently drops the CALLS edge) structurally impossible.
const char *cbm_nix_binding_scope_qn(CBMExtractCtx *ctx, TSNode node, const char *saved_enclosing);
// The QN-relative name of a Nix binding — its attrpath scope joined to `name`.
// Callers prepend the enclosing attrset scope (or the module QN), so a dotted
// attrpath and an enclosing attrset compose into one qualified name.
const char *cbm_nix_qn_name(CBMArena *a, TSNode func_node, const char *source, const char *name);
// Resolve a function/method definition node's NAME node across all ~130 grammars
// (generic `name` field, arrow→declarator, C/C++ declarator chain, plus the many
// per-language quirks: Fortran subroutine, SCSS mixin, SQL create_function, R,
// PowerShell, Ada, the Lisp/FP family, etc.). Defined in extract_defs.c. Shared by
// the defs, calls, and unified extractors so all three agree on enclosing-function
// naming — drift between private copies caused the Module-mis-attribution of
// gap #3 (and #438 for the C-declarator case).
TSNode cbm_resolve_func_name(TSNode node, CBMLanguage lang);
// C++/CUDA out-of-line method definition (`void Foo::bar() {...}`): return the
// immediate enclosing class name ("Foo") from the qualified declarator, or NULL
// for a plain free function. Defined in extract_defs.c. Shared so the unified
// (call-scope) extractor computes the SAME class-qualified enclosing QN as the
// def extractor — drift dropped the class qualifier from in-body calls (#554/#621).
char *cbm_cpp_out_of_line_parent_class(CBMArena *a, TSNode node, const char *source);
// Find a child node by kind string.
TSNode cbm_find_child_by_kind(TSNode parent, const char *kind);
/* --- Lisp-family shared gates ---------------------------------------------
* The defs, calls and unified extractors each walk the same generic `list`
* node and must agree on what it means. The predicates below therefore live
* here rather than being copied per translation unit: a private copy in each
* drifts, and the drift is silent — defs and call-scope simply stop describing
* the same tree.
*/
/* True when any ancestor list of `node` is headed by a quote symbol
* (`q` / `quote` / `qq`) — its contents are DATA, not code, so no def and no
* call may be minted from them. Bounded ancestor walk; the arena is used only
* for the head-text reads. */
bool cbm_lisp_node_in_quote(CBMArena *a, TSNode node, const char *source);
/* The `want`-th named child of `node`, skipping `comment` nodes. Comments are
* named in the s-expression grammars and so occupy named-child indices: a
* comment between a def head and its name shifts every later index by one.
* Definition extraction and call-scope attribution MUST use this same skipping
* rule or they desynchronise on exactly the files that carry doc comments. */
TSNode cbm_lisp_named_child_skip_comments(TSNode node, uint32_t want);
/* True for a Chialisp definition-form head. Deliberately separate from the
* Clojure/Scheme def-head set: Chialisp shares the generic `list` kind, and
* treating `mod`/`defconstant` as defs must not leak into the other lisps (in
* Scheme `(mod x y)` is a call). Excludes the expression-local binding forms
* (`let`, `assign`, `lambda`), which would fragment call attribution, and
* excludes `export`/`namespace`, which NAME an already-defined function rather
* than defining one. */
bool cbm_chialisp_is_def_head(const char *t);
// Check if node kind matches a set of types (NULL-terminated array of strings).
bool cbm_kind_in_set(TSNode node, const char **types);
/* Namespace/module declarations that extend a qualified-name scope without
* turning their children into class methods. Shared by definition and unified
* walks so TS/TSX scope attribution cannot drift. */
bool cbm_is_namespace_scope_kind(CBMLanguage lang, const char *kind);
// Free the calling thread's cbm_kind_in_set bitset cache (call at thread/process
// teardown so the thread-local cache is not reported as a leak).
void cbm_kind_in_set_free_cache(void);
// Check if node has an ancestor of the given kind, within max_depth levels.
bool cbm_has_ancestor_kind(TSNode node, const char *kind, int max_depth);
// Count nodes of given kinds in subtree (for complexity metric).
int cbm_count_branching(TSNode node, const char **branching_types);
// Per-function structural complexity, computed in a single AST walk.
typedef struct {
int cyclomatic; // branching-node count (matches def.complexity)
int cognitive; // nesting-weighted flow-break count (Campbell-style approximation)
int loop_count; // total loop constructs in the body
int loop_depth; // maximum nested-loop depth — structural bottleneck proxy
int max_access_depth; // deepest chained member/subscript access (a.b.c.d → 4) — structure smell
} cbm_complexity_t;
// Compute the metrics above in one traversal of `node`'s subtree.
// `branching_types` is the language's branching node-type set.
void cbm_compute_complexity(TSNode node, const char **branching_types, cbm_complexity_t *out);
// Is `kind` a loop construct node type? Language-agnostic curated set (for/while/
// do/foreach/repeat/loop variants). Exposed so the unified walk can track loop
// nesting at call sites without re-deriving the set.
bool cbm_is_loop_node_type(const char *kind);
// Is this a module-level node? (not nested inside function/class body)
bool cbm_is_module_level(TSNode node, CBMLanguage lang);
// Same check, but the node's PARENT is supplied directly — avoids the
// O(n) ts_node_parent rescan. Use at call sites iterating a known
// parent's children (the common case). `parent` is the parent of the
// node being classified.
bool cbm_is_module_level_p(TSNode parent, CBMLanguage lang);
// --- FQN computation ---
// Compute qualified name: project.rel_path_parts.name
char *cbm_fqn_compute(CBMArena *a, const char *project, const char *rel_path, const char *name);
// Module QN (file without name): project.rel_path_parts
char *cbm_fqn_module(CBMArena *a, const char *project, const char *rel_path);
// Language-aware module QN. For directory-module languages (Java package, Go
// package) the module is derived from the CONTAINING DIRECTORY (the filename
// stem is NOT baked in): `Outer.java` at root -> "proj", `myapp/db/conn.go` ->
// "proj.myapp.db". For every OTHER language this returns exactly what
// cbm_fqn_module returns (no behavior change).
char *cbm_fqn_module_source_lang(CBMArena *a, const char *project, const char *rel_path,
CBMLanguage lang);
// Language-aware symbol QN. For directory-module languages this is the
// directory-based module + "." + name (so a top-level class `Outer` in
// `Outer.java` is "proj.Outer", not "proj.Outer.Outer"). For every other
// language this is exactly cbm_fqn_compute (no behavior change).
char *cbm_fqn_compute_source_lang(CBMArena *a, const char *project, const char *rel_path,
const char *name, CBMLanguage lang);
// Folder QN: project.dir_parts
char *cbm_fqn_folder(CBMArena *a, const char *project, const char *rel_dir);
/* Flatten a JS/TS `template_string` node into plain text: string fragments are
* kept verbatim and each ${...} substitution becomes the "{}" placeholder, so
* client-side URLs built from template literals share the canonical parameter
* shape that server-side route paths already use. NULL when empty/oversized. */
const char *cbm_template_string_text(CBMArena *a, TSNode node, const char *source);
#endif // CBM_HELPERS_H