Files
Martin Vogel a4c0ffbce8 perf(cs): eliminate the C# corpus-proportional scans — 1211s -> 450s, +37% edges
Four measured changes on the dotnet/runtime corpus (58,656 files; every
step's numbers below are from full-corpus runs on the same host, baselines
captured this session; v0.9.0 = 444.3 s):

1. Two-phase cs registry build (types -> finalize -> funcs), the same
   pre-finalize linear-lookup disease fixed for Java: prepare 280 -> 140 s.

2. C# import-context carve-out: cs_import_types lists namespace_declaration
   (for namespace-name mapping) and using_statement (C#'s RAII block — a
   grammar-name collision), so EVERY namespaced C# file's whole body ran
   with inside_import=true. That both suppressed ordinary usage extraction
   under namespaces and sent every identifier through the ancestor-walking
   import-binding check (tree-sitter's ts_node_parent re-descends from the
   root, so wide files went quadratic: one 147 KB JIT torture file cost
   490 s; 6.8 s after). Only using_directive / namespace_use_declaration
   open an import scope now. Restores the suppressed usages:
   edges 4,291,387 -> 5,869,093 (+37%), nodes identical.

3. The usages walker maintains call/import ancestry as enter/exit counters
   on its explicit stack instead of per-node ancestor re-walks
   (extract_usages.c had grown from 6 to 100 ts_node_parent calls since
   v0.9.0; the two per-node gates are now O(1) with semantics preserved —
   strict ancestors only, emit before self-count).

4. Registry short-name indexes replace the two remaining full scans:
   cs_lookup_extension walked all 963k funcs per unresolved invocation
   (now the existing free-func short-name iterator, first-match order
   preserved via min-index selection), and cs_resolve_type_name's step-9
   fallback scanned every type per unresolved name IN BOTH the builder and
   per-file resolution (new type_short index in finalize, same
   reverse-insertion ascending-order pattern, best-score ties keep the
   first-in-registration-order winner). Builder 140,095 -> 500 ms; resolve
   cross-LSP CPU 5.5M -> 317k ms (us_per_file_per_kdef 191 -> 11).

End state: 449.9 s wall (1.01x of v0.9.0) with +48.6% edges vs v0.9.0 —
per-edge cost 32% BETTER than v0.9.0. Extract's remaining 359 s is the
24 MB hugeexpr1.cs parse floor both versions pay.

Guarded by the complexity suite; cs_lsp/extraction/edge/lang-contract
suites green including the 53-language calls-breadth contract.

Refs #1669.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-16 21:50:34 +02:00

2621 lines
111 KiB
C

#include "cbm.h"
#include "helpers.h"
#include "lang_specs.h"
#include "extract_unified.h"
#include "tree_sitter/api.h" // TSNode, ts_node_*
#include "foundation/constants.h"
#include "extract_node_stack.h"
enum { LAST_IDX = 1 };
#include <stdint.h> // uint32_t
#include <stdlib.h> // qsort
#include <string.h>
#include <strings.h>
#include <ctype.h>
#if defined(CBM_CALL_REFERENCE_LOOKUP_TEST_API) && CBM_CALL_REFERENCE_LOOKUP_TEST_API
#include <stdatomic.h>
static _Atomic uint64_t g_usage_field_lookup_work = 0;
static _Atomic uint64_t g_usage_slow_parent_fallbacks = 0;
void cbm_usage_field_lookup_test_reset(void) {
atomic_store_explicit(&g_usage_field_lookup_work, 0, memory_order_relaxed);
atomic_store_explicit(&g_usage_slow_parent_fallbacks, 0, memory_order_relaxed);
}
uint64_t cbm_usage_field_lookup_test_work(void) {
return atomic_load_explicit(&g_usage_field_lookup_work, memory_order_relaxed);
}
uint64_t cbm_usage_slow_parent_fallback_test_count(void) {
return atomic_load_explicit(&g_usage_slow_parent_fallbacks, memory_order_relaxed);
}
static void usage_field_lookup_test_note_work(void) {
atomic_fetch_add_explicit(&g_usage_field_lookup_work, 1, memory_order_relaxed);
}
static void usage_slow_parent_fallback_test_note(void) {
atomic_fetch_add_explicit(&g_usage_slow_parent_fallbacks, 1, memory_order_relaxed);
}
#else
static void usage_field_lookup_test_note_work(void) {}
static void usage_slow_parent_fallback_test_note(void) {}
#endif
// Forward declaration
static void walk_usages(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec);
static bool is_direct_argument_value(TSNode node);
static TSNode python_direct_callable_attribute_site(TSNode node);
// Is this an identifier-like node that represents a reference?
static bool is_reference_node(TSNode node, CBMLanguage lang) {
const char *kind = ts_node_type(node);
/* Python's attribute node and its terminal identifier describe the same
* source occurrence. For a direct callable-value argument, keep the leaf
* as the raw carrier (so unresolved cases retain a useful short-name
* USAGE) and stamp it with the full attribute span below. Receivers remain
* independent value references. */
if (lang == CBM_LANG_PYTHON && strcmp(kind, "attribute") == 0 &&
!ts_node_is_null(python_direct_callable_attribute_site(node))) {
return false;
}
/* A Rust scoped_identifier owns the complete path occurrence. Its nested
* path/identifier children are grammar structure, not additional value
* references; keeping them would let a short-name fallback bind a decoy. */
if (lang == CBM_LANG_RUST &&
(strcmp(kind, "identifier") == 0 || strcmp(kind, "scoped_identifier") == 0)) {
TSNode parent = ts_node_parent(node);
if (!ts_node_is_null(parent) && strcmp(ts_node_type(parent), "scoped_identifier") == 0) {
return false;
}
}
/* Some grammars expose the sigil/scope as a named wrapper around a generic
* identifier. Keep exactly one occurrence: the wrapper carries the source
* spelling that resolution needs (`$watched`, `a:watched`).
*
* The language gate MUST precede the parent fetch: ts_node_parent descends
* from the root (O(depth)), and fetching it for every identifier in every
* language made deep-nesting extraction quadratic across the board (the
* stack_overflow deep tests went 0-1s -> 39-119s and blew the suite
* budget on every non-M4 venue). */
if ((lang == CBM_LANG_PUPPET || lang == CBM_LANG_VIMSCRIPT) &&
strcmp(kind, "identifier") == 0) {
TSNode parent = ts_node_parent(node);
if (!ts_node_is_null(parent) &&
((lang == CBM_LANG_PUPPET && strcmp(ts_node_type(parent), "variable") == 0) ||
(lang == CBM_LANG_VIMSCRIPT && strcmp(ts_node_type(parent), "argument") == 0))) {
return false;
}
}
// Common identifier types across languages
if (strcmp(kind, "identifier") == 0 || strcmp(kind, "simple_identifier") == 0 ||
strcmp(kind, "type_identifier") == 0) {
return true;
}
// Language-specific reference types
switch (lang) {
case CBM_LANG_JAVASCRIPT:
case CBM_LANG_TYPESCRIPT:
case CBM_LANG_TSX:
case CBM_LANG_QML:
case CBM_LANG_CFSCRIPT:
return strcmp(kind, "property_identifier") == 0 ||
strcmp(kind, "private_property_identifier") == 0;
case CBM_LANG_GO:
return strcmp(kind, "field_identifier") == 0 || strcmp(kind, "package_identifier") == 0;
case CBM_LANG_PYTHON:
return strcmp(kind, "attribute") == 0;
case CBM_LANG_RUST:
return strcmp(kind, "field_identifier") == 0 || strcmp(kind, "scoped_identifier") == 0;
case CBM_LANG_C:
case CBM_LANG_CPP:
case CBM_LANG_CUDA:
return strcmp(kind, "field_identifier") == 0;
case CBM_LANG_PHP:
return strcmp(kind, "name") == 0 || strcmp(kind, "variable_name") == 0;
case CBM_LANG_HASKELL:
return strcmp(kind, "variable") == 0 || strcmp(kind, "constructor") == 0;
case CBM_LANG_OCAML:
return strcmp(kind, "value_path") == 0 || strcmp(kind, "constructor_path") == 0;
case CBM_LANG_ERLANG:
return strcmp(kind, "atom") == 0 || strcmp(kind, "var") == 0;
case CBM_LANG_CSS:
/* Custom-property reads inside `var(...)` are exposed as plain_value;
* declarations use the separate property_name alias. */
return strcmp(kind, "plain_value") == 0;
case CBM_LANG_SCSS:
/* SCSS value occurrences retain their `$` spelling in variable_value;
* declaration parameters use the distinct variable_name node. */
return strcmp(kind, "variable_value") == 0;
case CBM_LANG_LLVM_IR:
return strcmp(kind, "local_var") == 0 || strcmp(kind, "global_var") == 0;
case CBM_LANG_PUPPET:
case CBM_LANG_POWERSHELL:
/* Puppet keeps the sigil in a dedicated `variable` node (`$watched`). */
return strcmp(kind, "variable") == 0;
case CBM_LANG_BASH:
case CBM_LANG_FISH:
case CBM_LANG_ZSH:
/* Expansion wrappers retain `$`; their named leaf is the bare variable name. */
return strcmp(kind, "variable_name") == 0;
case CBM_LANG_PERL:
/* Perl's named scalar node retains its sigil (`$watched`). */
return strcmp(kind, "scalar") == 0;
case CBM_LANG_CLOJURE:
case CBM_LANG_COMMONLISP:
return strcmp(kind, "sym_lit") == 0;
case CBM_LANG_VIMSCRIPT:
return strcmp(kind, "scoped_identifier") == 0 || strcmp(kind, "argument") == 0;
case CBM_LANG_ELM:
return strcmp(kind, "lower_case_identifier") == 0;
case CBM_LANG_COBOL:
return strcmp(kind, "qualified_word") == 0;
case CBM_LANG_EMACSLISP:
case CBM_LANG_SCHEME:
case CBM_LANG_FENNEL:
case CBM_LANG_RACKET:
case CBM_LANG_LINKERSCRIPT:
return strcmp(kind, "symbol") == 0;
case CBM_LANG_MAKEFILE:
return strcmp(kind, "variable_reference") == 0;
case CBM_LANG_CMAKE:
/* `${watched}` contains a named `variable` leaf spanning only `watched`. */
return strcmp(kind, "variable") == 0;
case CBM_LANG_WOLFRAM:
return strcmp(kind, "user_symbol") == 0;
case CBM_LANG_TYPST:
return strcmp(kind, "ident") == 0;
case CBM_LANG_TCL:
/* Tcl's value spelling includes the sigil, so retain the substitution wrapper. */
return strcmp(kind, "variable_substitution") == 0;
case CBM_LANG_TLAPLUS:
/* Definition-side names/parameters are `identifier`; expression-side
* values and operator arguments use the distinct `identifier_ref`. */
return strcmp(kind, "identifier_ref") == 0;
case CBM_LANG_AGDA:
/* `_qid` is hidden and aliases its public expression leaf to `qid`. */
return strcmp(kind, "qid") == 0;
case CBM_LANG_RESCRIPT:
return strcmp(kind, "value_identifier") == 0;
case CBM_LANG_PURESCRIPT:
return strcmp(kind, "variable") == 0;
case CBM_LANG_NICKEL:
return strcmp(kind, "ident") == 0;
case CBM_LANG_JSONNET:
return strcmp(kind, "id") == 0;
case CBM_LANG_CFML:
return strcmp(kind, "property_identifier") == 0;
case CBM_LANG_OBJECTSCRIPT_UDL:
case CBM_LANG_OBJECTSCRIPT_ROUTINE:
return strcmp(kind, "objectscript_identifier") == 0 ||
strcmp(kind, "objectscript_identifier_special") == 0;
default:
return false;
}
}
static bool node_contains(TSNode outer, TSNode inner) {
return !ts_node_is_null(outer) && !ts_node_is_null(inner) &&
ts_node_start_byte(outer) <= ts_node_start_byte(inner) &&
ts_node_end_byte(outer) >= ts_node_end_byte(inner);
}
static bool vhdl_forward_callee_wrapper(TSNode node) {
const char *kind = ts_node_type(node);
return strcmp(kind, "identifier") == 0 || strcmp(kind, "simple_identifier") == 0 ||
strcmp(kind, "library_function") == 0 || strcmp(kind, "name") == 0 ||
strcmp(kind, "simple_name") == 0;
}
static TSNode terminal_vhdl_identifier(TSNode node, int remaining_depth) {
if (ts_node_is_null(node) || remaining_depth < 0) {
return (TSNode){0};
}
uint32_t count = ts_node_named_child_count(node);
for (uint32_t i = count; i > 0; i--) {
TSNode found =
terminal_vhdl_identifier(ts_node_named_child(node, i - 1), remaining_depth - 1);
if (!ts_node_is_null(found)) {
return found;
}
}
const char *kind = ts_node_type(node);
return strcmp(kind, "identifier") == 0 || strcmp(kind, "simple_identifier") == 0 ? node
: (TSNode){0};
}
// Dart and VHDL represent some calls as a callee node immediately followed by
// the invocation node. The unified pre-order walk has already visited that
// callee when the call descriptor is created, so suppress only the exact
// language-specific preceding occurrence. Do not generalize this to arbitrary
// identifiers: a sibling selector/parenthesis group is the grammar contract.
static bool is_forward_sibling_callee(CBMLanguage language, TSNode node) {
if (language == CBM_LANG_DART && strcmp(ts_node_type(node), "identifier") == 0) {
TSNode next = ts_node_next_named_sibling(node);
return !ts_node_is_null(next) && strcmp(ts_node_type(next), "selector") == 0;
}
if (language != CBM_LANG_VHDL) {
return false;
}
TSNode owner = node;
for (int depth = 0; depth < 4 && vhdl_forward_callee_wrapper(owner); depth++) {
TSNode next = ts_node_next_named_sibling(owner);
if (!ts_node_is_null(next) && strcmp(ts_node_type(next), "parenthesis_group") == 0) {
TSNode leaf = terminal_vhdl_identifier(owner, 8);
return !ts_node_is_null(leaf) && ts_node_eq(node, leaf);
}
TSNode parent = ts_node_parent(owner);
if (ts_node_is_null(parent) || !vhdl_forward_callee_wrapper(parent)) {
break;
}
owner = parent;
}
return false;
}
static const char *field_name_for_node(TSNode parent, TSNode child) {
const char *field = NULL;
TSTreeCursor cursor = ts_tree_cursor_new(parent);
if (ts_tree_cursor_goto_first_child(&cursor)) {
do {
usage_field_lookup_test_note_work();
if (ts_node_eq(ts_tree_cursor_current_node(&cursor), child)) {
field = ts_tree_cursor_current_field_name(&cursor);
break;
}
} while (ts_tree_cursor_goto_next_sibling(&cursor));
}
ts_tree_cursor_delete(&cursor);
return field;
}
/* The unified walk already owns the exact cursor path to the current node.
* Reset one reusable scratch cursor to that path before each occurrence
* classifier, then walk its parents without restarting a sibling scan at
* every level. Legacy extraction callers retain the node-based fallback. */
static TSTreeCursor *reset_occurrence_cursor(WalkState *state, TSNode node) {
if (!state || !state->current_cursor || !state->occurrence_cursor ||
!ts_node_eq(ts_tree_cursor_current_node(state->current_cursor), node)) {
return NULL;
}
ts_tree_cursor_reset_to(state->occurrence_cursor, state->current_cursor);
return state->occurrence_cursor;
}
static bool occurrence_parent(TSTreeCursor *cursor, TSNode current, TSNode *parent,
const char **field) {
if (cursor) {
*field = ts_tree_cursor_current_field_name(cursor);
usage_field_lookup_test_note_work();
if (!ts_tree_cursor_goto_parent(cursor)) {
*parent = (TSNode){0};
return false;
}
*parent = ts_tree_cursor_current_node(cursor);
return true;
}
*parent = ts_node_parent(current);
if (ts_node_is_null(*parent)) {
*field = NULL;
return false;
}
*field = field_name_for_node(*parent, current);
return true;
}
static const char *field_name_for_walk_node(WalkState *state, TSNode parent, TSNode node) {
if (state && state->current_cursor &&
ts_node_eq(ts_tree_cursor_current_node(state->current_cursor), node)) {
usage_field_lookup_test_note_work();
return ts_tree_cursor_current_field_name(state->current_cursor);
}
return field_name_for_node(parent, node);
}
static bool is_value_field(const char *field) {
return field && (strcmp(field, "value") == 0 || strcmp(field, "right") == 0 ||
strcmp(field, "initializer") == 0 || strcmp(field, "default") == 0 ||
strcmp(field, "default_value") == 0 || strcmp(field, "body") == 0 ||
strcmp(field, "arguments") == 0 || strcmp(field, "condition") == 0 ||
strcmp(field, "consequence") == 0 || strcmp(field, "alternative") == 0 ||
strcmp(field, "expression") == 0 || strcmp(field, "result") == 0);
}
static bool kind_in_exact_set(const char *kind, const char *const *set) {
if (!kind || !set) {
return false;
}
for (const char *const *candidate = set; *candidate; candidate++) {
if (strcmp(kind, *candidate) == 0) {
return true;
}
}
return false;
}
static bool field_contains_node(TSNode parent, const char *field, TSNode node) {
TSNode value = ts_node_child_by_field_name(parent, field, (uint32_t)strlen(field));
return node_contains(value, node);
}
typedef enum {
CBM_OCCURRENCE_STANDARD = 0,
CBM_OCCURRENCE_LISP_DEF,
CBM_OCCURRENCE_COMMONLISP_DEFUN,
CBM_OCCURRENCE_FENNEL_FN,
CBM_OCCURRENCE_ELIXIR_DEF,
CBM_OCCURRENCE_JULIA_FUNCTION,
CBM_OCCURRENCE_WOLFRAM_SET,
CBM_OCCURRENCE_TYPST_LET,
CBM_OCCURRENCE_AGDA_FUNCTION,
CBM_OCCURRENCE_TLAPLUS_OPERATOR,
CBM_OCCURRENCE_COBOL_MOVE,
CBM_OCCURRENCE_HCL_ATTRIBUTE,
CBM_OCCURRENCE_ELM_VALUE,
CBM_OCCURRENCE_RESCRIPT_LET,
CBM_OCCURRENCE_PURESCRIPT_LHS,
CBM_OCCURRENCE_NICKEL_LET,
CBM_OCCURRENCE_ERLANG_CLAUSE,
CBM_OCCURRENCE_NIX_FUNCTION,
CBM_OCCURRENCE_MATLAB_ARGUMENTS,
CBM_OCCURRENCE_LEAN_BINDER,
CBM_OCCURRENCE_PASCAL_PROC,
CBM_OCCURRENCE_TEAL_FUNCTION,
CBM_OCCURRENCE_VHDL_INTERFACE,
CBM_OCCURRENCE_PINE_FUNCTION,
CBM_OCCURRENCE_LLVM_FUNCTION,
} CBMOccurrencePolicy;
typedef struct {
const char *const *whole_binding_nodes;
const char *const *write_nodes;
CBMOccurrencePolicy policy;
bool first_named_child_is_write;
} CBMOccurrenceSpec;
/* These are exact grammar roles, not name fragments. Default/value/body
* fields are barriers, so an initializer nested below one of these containers
* remains a read. */
static const char *const common_whole_binding_nodes[] = {"formal_parameter",
"formal_parameters",
"parameter",
"parameters",
"parameter_list",
"parameter_declaration",
"parameter_specification",
"required_parameter",
"optional_parameter",
"default_parameter",
"typed_parameter",
"function_value_parameter",
"function_value_parameters",
"lambda_parameter",
"lambda_parameters",
"function_parameter_declaration",
"closure_parameters",
"block_parameters",
"receiver",
NULL};
static const char *const field_binding_nodes[] = {"variable_declarator",
"init_declarator",
"variable_declaration",
"const_declaration",
"lexical_declaration",
"short_var_declaration",
"local_variable_declaration",
"property_declaration",
"field_declaration",
"value_declaration",
"val_definition",
"var_definition",
"let_declaration",
"local_bind",
"let_binding",
"data_declaration",
"net_declaration",
"object_declaration",
"number_declaration",
"typed_binding",
"variable_assignment",
NULL};
static const char *const binding_fields[] = {"name", "pattern", "declarator", "parameter",
"parameters", "left", "variable", "variables",
"key", NULL};
static const char *const sql_binding_nodes[] = {"function_argument", NULL};
static const char *const haskell_binding_nodes[] = {"patterns", NULL};
static const char *const fsharp_binding_nodes[] = {
"function_declaration_left", "value_declaration_left", "argument_patterns", NULL};
static const char *const crystal_binding_nodes[] = {"param_list", NULL};
static const char *const awk_binding_nodes[] = {"param_list", NULL};
static const char *const teal_binding_nodes[] = {"function_signature", NULL};
static const char *const systemverilog_binding_nodes[] = {"tf_port_item", "tf_port_item1", NULL};
static const char *const rescript_binding_nodes[] = {"formal_parameters", "labeled_parameter",
"parameter", NULL};
static const char *const purescript_binding_nodes[] = {"bind_pattern", "pattern", "patterns", NULL};
static const char *const nickel_binding_nodes[] = {"pattern_fun", NULL};
static const char *const jsonnet_binding_nodes[] = {"param", NULL};
static const char *const llvm_binding_nodes[] = {"function_header", NULL};
static const char *const linkerscript_write_nodes[] = {"assignment", NULL};
static const char *const meson_write_nodes[] = {"operatorunit", NULL};
static const char *const gn_write_nodes[] = {"assignment_statement", NULL};
static const char *const objectscript_binding_nodes[] = {"argument", "tag_parameter", NULL};
static const char *const objectscript_write_nodes[] = {"set_argument", NULL};
/* Parallel to lang_specs: occurrence semantics evolve without adding fields to
* the positional CBMLangSpec initializer used by every language. */
static const CBMOccurrenceSpec occurrence_specs[CBM_LANG_COUNT] = {
[CBM_LANG_SQL] = {sql_binding_nodes, NULL, CBM_OCCURRENCE_STANDARD, false},
[CBM_LANG_CLOJURE] = {NULL, NULL, CBM_OCCURRENCE_LISP_DEF, false},
[CBM_LANG_SCHEME] = {NULL, NULL, CBM_OCCURRENCE_LISP_DEF, false},
[CBM_LANG_RACKET] = {NULL, NULL, CBM_OCCURRENCE_LISP_DEF, false},
[CBM_LANG_COMMONLISP] = {NULL, NULL, CBM_OCCURRENCE_COMMONLISP_DEFUN, false},
[CBM_LANG_FENNEL] = {NULL, NULL, CBM_OCCURRENCE_FENNEL_FN, false},
[CBM_LANG_ELIXIR] = {NULL, NULL, CBM_OCCURRENCE_ELIXIR_DEF, false},
[CBM_LANG_JULIA] = {NULL, NULL, CBM_OCCURRENCE_JULIA_FUNCTION, false},
[CBM_LANG_WOLFRAM] = {NULL, NULL, CBM_OCCURRENCE_WOLFRAM_SET, false},
[CBM_LANG_TYPST] = {NULL, NULL, CBM_OCCURRENCE_TYPST_LET, false},
[CBM_LANG_AGDA] = {NULL, NULL, CBM_OCCURRENCE_AGDA_FUNCTION, false},
[CBM_LANG_TLAPLUS] = {NULL, NULL, CBM_OCCURRENCE_TLAPLUS_OPERATOR, false},
[CBM_LANG_COBOL] = {NULL, NULL, CBM_OCCURRENCE_COBOL_MOVE, false},
[CBM_LANG_HCL] = {NULL, NULL, CBM_OCCURRENCE_HCL_ATTRIBUTE, false},
[CBM_LANG_ELM] = {NULL, NULL, CBM_OCCURRENCE_ELM_VALUE, false},
[CBM_LANG_RESCRIPT] = {rescript_binding_nodes, NULL, CBM_OCCURRENCE_RESCRIPT_LET, false},
[CBM_LANG_PURESCRIPT] = {purescript_binding_nodes, NULL, CBM_OCCURRENCE_PURESCRIPT_LHS, false},
[CBM_LANG_NICKEL] = {nickel_binding_nodes, NULL, CBM_OCCURRENCE_NICKEL_LET, false},
[CBM_LANG_JSONNET] = {jsonnet_binding_nodes, NULL, CBM_OCCURRENCE_STANDARD, false},
[CBM_LANG_HASKELL] = {haskell_binding_nodes, NULL, CBM_OCCURRENCE_STANDARD, false},
[CBM_LANG_ERLANG] = {NULL, NULL, CBM_OCCURRENCE_ERLANG_CLAUSE, false},
[CBM_LANG_FSHARP] = {fsharp_binding_nodes, NULL, CBM_OCCURRENCE_STANDARD, false},
[CBM_LANG_NIX] = {NULL, NULL, CBM_OCCURRENCE_NIX_FUNCTION, false},
[CBM_LANG_MATLAB] = {NULL, NULL, CBM_OCCURRENCE_MATLAB_ARGUMENTS, false},
[CBM_LANG_LEAN] = {NULL, NULL, CBM_OCCURRENCE_LEAN_BINDER, false},
[CBM_LANG_PASCAL] = {NULL, NULL, CBM_OCCURRENCE_PASCAL_PROC, false},
[CBM_LANG_VERILOG] = {systemverilog_binding_nodes, NULL, CBM_OCCURRENCE_STANDARD, false},
[CBM_LANG_AWK] = {awk_binding_nodes, NULL, CBM_OCCURRENCE_STANDARD, false},
[CBM_LANG_CRYSTAL] = {crystal_binding_nodes, NULL, CBM_OCCURRENCE_STANDARD, false},
[CBM_LANG_TEAL] = {teal_binding_nodes, NULL, CBM_OCCURRENCE_TEAL_FUNCTION, false},
[CBM_LANG_VHDL] = {NULL, NULL, CBM_OCCURRENCE_VHDL_INTERFACE, false},
[CBM_LANG_SYSTEMVERILOG] = {systemverilog_binding_nodes, NULL, CBM_OCCURRENCE_STANDARD, false},
[CBM_LANG_PINE] = {NULL, NULL, CBM_OCCURRENCE_PINE_FUNCTION, false},
[CBM_LANG_PUPPET] = {NULL, NULL, CBM_OCCURRENCE_STANDARD, true},
[CBM_LANG_LLVM_IR] = {llvm_binding_nodes, NULL, CBM_OCCURRENCE_LLVM_FUNCTION, false},
[CBM_LANG_MESON] = {NULL, meson_write_nodes, CBM_OCCURRENCE_STANDARD, true},
[CBM_LANG_GN] = {NULL, gn_write_nodes, CBM_OCCURRENCE_STANDARD, true},
[CBM_LANG_LINKERSCRIPT] = {NULL, linkerscript_write_nodes, CBM_OCCURRENCE_STANDARD, true},
[CBM_LANG_OBJECTSCRIPT_UDL] = {objectscript_binding_nodes, objectscript_write_nodes,
CBM_OCCURRENCE_STANDARD, true},
[CBM_LANG_OBJECTSCRIPT_ROUTINE] = {objectscript_binding_nodes, objectscript_write_nodes,
CBM_OCCURRENCE_STANDARD, true},
};
static bool text_equals(CBMExtractCtx *ctx, TSNode node, const char *expected) {
char *text = cbm_node_text(ctx->arena, node, ctx->source);
return text && strcmp(text, expected) == 0;
}
static bool named_child_contains(TSNode parent, uint32_t index, TSNode node) {
return ts_node_named_child_count(parent) > index &&
node_contains(ts_node_named_child(parent, index), node);
}
static bool lisp_def_head(const char *text) {
static const char *const heads[] = {"defn",
"defn-",
"def",
"defmacro",
"defmulti",
"defmethod",
"defprotocol",
"defrecord",
"deftype",
"definterface",
"defonce",
"define",
"define-syntax",
"define-values",
"define-struct",
"define-record-type",
"define/contract",
"struct",
NULL};
return kind_in_exact_set(text, heads);
}
static bool is_lisp_def_binding(CBMExtractCtx *ctx, TSNode node) {
for (TSNode form = ts_node_parent(node); !ts_node_is_null(form); form = ts_node_parent(form)) {
const char *kind = ts_node_type(form);
if ((strcmp(kind, "list") != 0 && strcmp(kind, "list_lit") != 0) ||
ts_node_named_child_count(form) < 2) {
continue;
}
TSNode head_node = ts_node_named_child(form, 0);
char *head = cbm_node_text(ctx->arena, head_node, ctx->source);
if (!lisp_def_head(head)) {
continue;
}
if (node_contains(head_node, node) || named_child_contains(form, 1, node)) {
return true;
}
if (ctx->language == CBM_LANG_CLOJURE && ts_node_named_child_count(form) > 2 &&
named_child_contains(form, 2, node)) {
return true;
}
return false;
}
return false;
}
static bool is_fennel_fn_binding(CBMExtractCtx *ctx, TSNode node) {
for (TSNode form = ts_node_parent(node); !ts_node_is_null(form); form = ts_node_parent(form)) {
if (strcmp(ts_node_type(form), "list") != 0 || ts_node_named_child_count(form) < 2 ||
!text_equals(ctx, ts_node_named_child(form, 0), "fn")) {
continue;
}
TSNode first = ts_node_named_child(form, 1);
const char *first_kind = ts_node_type(first);
bool anonymous = strcmp(first_kind, "sequence") == 0 || strcmp(first_kind, "table") == 0 ||
strcmp(first_kind, "vector") == 0;
if (anonymous) {
return named_child_contains(form, 0, node) || node_contains(first, node);
}
return named_child_contains(form, 0, node) || node_contains(first, node) ||
(ts_node_named_child_count(form) > 2 && named_child_contains(form, 2, node));
}
return false;
}
static bool is_elixir_def_binding(CBMExtractCtx *ctx, TSNode node) {
for (TSNode form = ts_node_parent(node); !ts_node_is_null(form); form = ts_node_parent(form)) {
if (strcmp(ts_node_type(form), "call") != 0 || ts_node_named_child_count(form) < 2) {
continue;
}
TSNode head = ts_node_named_child(form, 0);
if (!text_equals(ctx, head, "def") && !text_equals(ctx, head, "defp") &&
!text_equals(ctx, head, "defmacro")) {
continue;
}
TSNode arguments = ts_node_child_by_field_name(form, TS_FIELD("arguments"));
if (ts_node_is_null(arguments) || ts_node_named_child_count(arguments) == 0) {
arguments = ts_node_named_child(form, 1);
}
TSNode signature = ts_node_named_child_count(arguments) > 0
? ts_node_named_child(arguments, 0)
: arguments;
return node_contains(head, node) || node_contains(signature, node);
}
return false;
}
static bool is_first_named_part_of(TSNode node, const char *container_kind) {
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
parent = ts_node_parent(parent)) {
if (strcmp(ts_node_type(parent), container_kind) == 0) {
return named_child_contains(parent, 0, node);
}
}
return false;
}
static bool is_wolfram_lhs(TSNode node) {
static const char *const set_nodes[] = {
"set", "set_top", "set_delayed", "set_delayed_top",
"tag_set", "tag_set_top", "tag_set_delayed", "tag_set_delayed_top",
"up_set", "up_set_top", "up_set_delayed", "up_set_delayed_top",
NULL};
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
parent = ts_node_parent(parent)) {
if (kind_in_exact_set(ts_node_type(parent), set_nodes)) {
return named_child_contains(parent, 0, node);
}
}
return false;
}
static bool any_field_contains_node(TSNode parent, const char *field, TSNode node);
static bool is_tlaplus_binding(TSNode node) {
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
parent = ts_node_parent(parent)) {
const char *kind = ts_node_type(parent);
if (strcmp(kind, "operator_definition") == 0) {
/* `name:` is the callable declaration, while every repeated
* `parameter:` field is a function-wide lexical binder. */
return any_field_contains_node(parent, "parameter", node);
}
if (strcmp(kind, "function_definition") == 0) {
/* `F[x \in S] == ...`: only quantifier_bound.intro binds. The set
* expression S remains an ordinary identifier_ref usage. */
for (TSNode bound = ts_node_parent(node);
!ts_node_is_null(bound) && !ts_node_eq(bound, parent);
bound = ts_node_parent(bound)) {
if (strcmp(ts_node_type(bound), "quantifier_bound") == 0) {
return any_field_contains_node(bound, "intro", node);
}
}
return false;
}
if (strcmp(kind, "bounded_quantification") == 0) {
for (TSNode bound = ts_node_parent(node);
!ts_node_is_null(bound) && !ts_node_eq(bound, parent);
bound = ts_node_parent(bound)) {
if (strcmp(ts_node_type(bound), "quantifier_bound") == 0) {
return any_field_contains_node(bound, "intro", node);
}
}
return false;
}
if (strcmp(kind, "unbounded_quantification") == 0) {
return any_field_contains_node(parent, "intro", node);
}
}
return false;
}
static bool is_cobol_move_destination(TSNode node) {
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
parent = ts_node_parent(parent)) {
if (strcmp(ts_node_type(parent), "move_statement") != 0) {
continue;
}
if (field_contains_node(parent, "destination", node) ||
field_contains_node(parent, "target", node)) {
return true;
}
uint32_t count = ts_node_named_child_count(parent);
return count > 1 && named_child_contains(parent, count - 1, node);
}
return false;
}
/* Some grammars expose declaration roles as repeated/inherited fields. The
* public child-by-field helper returns only one match, so inspect every direct
* child and preserve the exact field role while allowing nested wrappers. */
static bool any_field_contains_node(TSNode parent, const char *field, TSNode node) {
uint32_t count = ts_node_child_count(parent);
for (uint32_t i = 0; i < count; i++) {
const char *child_field = ts_node_field_name_for_child(parent, i);
if (child_field && strcmp(child_field, field) == 0 &&
node_contains(ts_node_child(parent, i), node)) {
return true;
}
}
return false;
}
static bool is_perl_lexical_declaration_binding(TSNode node) {
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
parent = ts_node_parent(parent)) {
if (strcmp(ts_node_type(parent), "variable_declaration") == 0) {
/* Perl permits repeated `variables:` fields for `my ($a, $b)`;
* inspect all direct field instances, not only the first. */
return any_field_contains_node(parent, "variables", node);
}
if (strcmp(ts_node_type(parent), "assignment_expression") == 0) {
return false;
}
}
return false;
}
static bool is_cmake_function_parameter(TSNode node) {
if (strcmp(ts_node_type(node), "unquoted_argument") != 0) {
return false;
}
TSNode argument = ts_node_parent(node);
TSNode arguments = ts_node_parent(argument);
TSNode command = ts_node_parent(arguments);
if (ts_node_is_null(argument) || strcmp(ts_node_type(argument), "argument") != 0 ||
ts_node_is_null(arguments) || strcmp(ts_node_type(arguments), "argument_list") != 0 ||
ts_node_is_null(command) ||
(strcmp(ts_node_type(command), "function_command") != 0 &&
strcmp(ts_node_type(command), "macro_command") != 0)) {
return false;
}
uint32_t count = ts_node_named_child_count(arguments);
for (uint32_t i = 1; i < count; i++) {
if (node_contains(ts_node_named_child(arguments, i), node)) {
return true;
}
}
return false;
}
static bool fish_argument_marker(CBMExtractCtx *ctx, TSNode node) {
char *text = cbm_node_text(ctx->arena, node, ctx->source);
return text && (strcmp(text, "-a") == 0 || strcmp(text, "--argument") == 0 ||
strcmp(text, "--argument-names") == 0);
}
static bool is_fish_function_parameter(CBMExtractCtx *ctx, TSNode node, WalkState *state) {
if (strcmp(ts_node_type(node), "word") != 0) {
return false;
}
TSNode definition = ts_node_parent(node);
const char *field =
ts_node_is_null(definition) ? NULL : field_name_for_walk_node(state, definition, node);
if (!field || strcmp(field, "option") != 0 ||
strcmp(ts_node_type(definition), "function_definition") != 0) {
return false;
}
for (TSNode previous = ts_node_prev_named_sibling(node); !ts_node_is_null(previous);
previous = ts_node_prev_named_sibling(previous)) {
char *text = cbm_node_text(ctx->arena, previous, ctx->source);
if (!text || text[0] != '-') {
continue;
}
return fish_argument_marker(ctx, previous);
}
return false;
}
static bool is_tcl_procedure_parameter(TSNode node) {
TSNode argument = ts_node_parent(node);
if (ts_node_is_null(argument) || strcmp(ts_node_type(argument), "argument") != 0 ||
!any_field_contains_node(argument, "name", node)) {
return false;
}
TSNode arguments = ts_node_parent(argument);
TSNode procedure = ts_node_parent(arguments);
return !ts_node_is_null(arguments) && strcmp(ts_node_type(arguments), "arguments") == 0 &&
!ts_node_is_null(procedure) && strcmp(ts_node_type(procedure), "procedure") == 0 &&
any_field_contains_node(procedure, "arguments", node);
}
static bool cfml_argument_tag_name_binding(CBMExtractCtx *ctx, TSNode node) {
if (strcmp(ts_node_type(node), "attribute_value") != 0) {
return false;
}
TSNode quoted = ts_node_parent(node);
TSNode attribute = ts_node_parent(quoted);
if (ts_node_is_null(attribute) || strcmp(ts_node_type(attribute), "cf_attribute") != 0) {
return false;
}
TSNode attribute_name = cbm_find_child_by_kind(attribute, "cf_attribute_name");
char *name_text = cbm_node_text(ctx->arena, attribute_name, ctx->source);
if (!name_text || strcasecmp(name_text, "name") != 0) {
return false;
}
TSNode tag = ts_node_parent(attribute);
if (ts_node_is_null(tag) || strcmp(ts_node_type(tag), "cf_selfclose_tag") != 0) {
return false;
}
char *tag_text = cbm_node_text(ctx->arena, tag, ctx->source);
static const char argument_tag[] = "<cfargument";
size_t prefix_length = sizeof(argument_tag) - 1U;
if (!tag_text || strncasecmp(tag_text, argument_tag, prefix_length) != 0 ||
(tag_text[prefix_length] != '\0' && !isspace((unsigned char)tag_text[prefix_length]) &&
tag_text[prefix_length] != '>' && tag_text[prefix_length] != '/')) {
return false;
}
for (TSNode parent = ts_node_parent(tag); !ts_node_is_null(parent);
parent = ts_node_parent(parent)) {
if (strcmp(ts_node_type(parent), "cf_function_tag") == 0) {
return true;
}
}
return false;
}
static bool is_exact_language_binding(CBMExtractCtx *ctx, TSNode node, WalkState *state) {
const char *kind = ts_node_type(node);
switch (ctx->language) {
case CBM_LANG_OCAML: {
if (strcmp(kind, "value_pattern") != 0) {
return false;
}
TSNode parameter = ts_node_parent(node);
return !ts_node_is_null(parameter) && strcmp(ts_node_type(parameter), "parameter") == 0 &&
field_contains_node(parameter, "pattern", node);
}
case CBM_LANG_SCSS: {
if (strcmp(kind, "variable_name") != 0) {
return false;
}
TSNode parameter = ts_node_parent(node);
return !ts_node_is_null(parameter) && strcmp(ts_node_type(parameter), "parameter") == 0;
}
case CBM_LANG_FORM: {
if (strcmp(kind, "parameter") != 0) {
return false;
}
TSNode parameters = ts_node_parent(node);
return !ts_node_is_null(parameters) &&
strcmp(ts_node_type(parameters), "parameter_list") == 0;
}
case CBM_LANG_FUNC: {
if (strcmp(kind, "parameter") != 0) {
return false;
}
TSNode declaration = ts_node_parent(node);
return !ts_node_is_null(declaration) &&
strcmp(ts_node_type(declaration), "parameter_declaration") == 0 &&
field_contains_node(declaration, "name", node);
}
case CBM_LANG_PERL:
return is_perl_lexical_declaration_binding(node);
case CBM_LANG_CMAKE:
return is_cmake_function_parameter(node);
case CBM_LANG_FISH:
return is_fish_function_parameter(ctx, node, state);
case CBM_LANG_TCL:
return is_tcl_procedure_parameter(node);
case CBM_LANG_CFML:
return cfml_argument_tag_name_binding(ctx, node);
default:
return false;
}
}
static bool ancestor_field_binds(TSNode node, const char *container_kind,
const char *const *fields) {
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
parent = ts_node_parent(parent)) {
if (strcmp(ts_node_type(parent), container_kind) != 0) {
continue;
}
for (const char *const *field = fields; *field; field++) {
if (any_field_contains_node(parent, *field, node)) {
return true;
}
}
return false;
}
return false;
}
static bool is_erlang_clause_binding(TSNode node) {
static const char *const fields[] = {"args", NULL};
return ancestor_field_binds(node, "function_clause", fields);
}
static bool is_nix_function_binding(TSNode node) {
/* tree-sitter-nix names a simple `x: body` binder `universal`; set
* destructuring uses the distinct `formals` field. */
static const char *const fields[] = {"universal", "formals", NULL};
return ancestor_field_binds(node, "function_expression", fields);
}
static bool is_lean_binder_name(TSNode node) {
static const char *const binder_kinds[] = {"explicit_binder", "implicit_binder",
"instance_binder", NULL};
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
parent = ts_node_parent(parent)) {
if (kind_in_exact_set(ts_node_type(parent), binder_kinds)) {
return any_field_contains_node(parent, "name", node);
}
}
return false;
}
static bool is_pascal_proc_binding(TSNode node) {
static const char *const fields[] = {"args", NULL};
return ancestor_field_binds(node, "declProc", fields) ||
ancestor_field_binds(node, "defProc", fields);
}
static bool is_teal_function_binding(TSNode node) {
TSNode arguments = {0};
for (TSNode current = ts_node_parent(node); !ts_node_is_null(current);
current = ts_node_parent(current)) {
const char *kind = ts_node_type(current);
if (ts_node_is_null(arguments) && strcmp(kind, "arguments") == 0) {
arguments = current;
continue;
}
if (strcmp(kind, "function_signature") == 0) {
return !ts_node_is_null(arguments) &&
any_field_contains_node(current, "arguments", node);
}
if (strcmp(kind, "function_statement") == 0) {
if (ts_node_is_null(arguments)) {
return false;
}
TSNode signature = ts_node_child_by_field_name(current, TS_FIELD("signature"));
return node_contains(signature, arguments);
}
}
return false;
}
static bool is_commonlisp_defun_binding(TSNode node) {
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
parent = ts_node_parent(parent)) {
if (strcmp(ts_node_type(parent), "defun_header") != 0) {
continue;
}
return field_contains_node(parent, "function_name", node) ||
field_contains_node(parent, "lambda_list", node);
}
return false;
}
static bool is_hcl_attribute_binding(TSNode node) {
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
parent = ts_node_parent(parent)) {
if (strcmp(ts_node_type(parent), "attribute") == 0) {
/* HCL's attribute production exposes no fields. The first named
* child is its key; the expression is the second named child. */
return named_child_contains(parent, 0, node);
}
}
return false;
}
static bool is_matlab_argument_binding(TSNode node) {
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
parent = ts_node_parent(parent)) {
const char *kind = ts_node_type(parent);
if (strcmp(kind, "function_arguments") == 0 || strcmp(kind, "lambda_arguments") == 0) {
return true;
}
}
return false;
}
static bool is_vhdl_interface_binding(TSNode node) {
static const char *const interface_kinds[] = {
"interface_constant_declaration", "interface_signal_declaration",
"interface_variable_declaration", "interface_declaration", NULL};
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
parent = ts_node_parent(parent)) {
if (kind_in_exact_set(ts_node_type(parent), interface_kinds)) {
/* FIELD_COUNT does not name this role; every interface production
* starts with its identifier_list before mode/type/default. */
return named_child_contains(parent, 0, node);
}
}
return false;
}
static bool is_pine_function_binding(TSNode node) {
static const char *const fields[] = {"argument", NULL};
return ancestor_field_binds(node, "function_declaration_statement", fields);
}
static bool is_policy_binding(CBMExtractCtx *ctx, TSNode node,
const CBMOccurrenceSpec *occurrence) {
switch (occurrence->policy) {
case CBM_OCCURRENCE_LISP_DEF:
return is_lisp_def_binding(ctx, node);
case CBM_OCCURRENCE_COMMONLISP_DEFUN:
return is_commonlisp_defun_binding(node);
case CBM_OCCURRENCE_FENNEL_FN:
return is_fennel_fn_binding(ctx, node);
case CBM_OCCURRENCE_ELIXIR_DEF:
return is_elixir_def_binding(ctx, node);
case CBM_OCCURRENCE_JULIA_FUNCTION:
return is_first_named_part_of(node, "function_definition");
case CBM_OCCURRENCE_WOLFRAM_SET:
return is_wolfram_lhs(node);
case CBM_OCCURRENCE_TYPST_LET:
return is_first_named_part_of(node, "let");
case CBM_OCCURRENCE_AGDA_FUNCTION:
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
parent = ts_node_parent(parent)) {
if (strcmp(ts_node_type(parent), "lhs") == 0) {
return true;
}
}
return false;
case CBM_OCCURRENCE_TLAPLUS_OPERATOR:
return is_tlaplus_binding(node);
case CBM_OCCURRENCE_COBOL_MOVE:
return is_cobol_move_destination(node);
case CBM_OCCURRENCE_HCL_ATTRIBUTE:
return is_hcl_attribute_binding(node);
case CBM_OCCURRENCE_ELM_VALUE:
return is_first_named_part_of(node, "value_declaration");
case CBM_OCCURRENCE_RESCRIPT_LET:
return is_first_named_part_of(node, "let_binding");
case CBM_OCCURRENCE_PURESCRIPT_LHS:
return is_first_named_part_of(node, "function");
case CBM_OCCURRENCE_NICKEL_LET:
return is_first_named_part_of(node, "let_binding") ||
is_first_named_part_of(node, "pattern_fun");
case CBM_OCCURRENCE_ERLANG_CLAUSE:
return is_erlang_clause_binding(node);
case CBM_OCCURRENCE_NIX_FUNCTION:
return is_nix_function_binding(node);
case CBM_OCCURRENCE_MATLAB_ARGUMENTS:
return is_matlab_argument_binding(node);
case CBM_OCCURRENCE_LEAN_BINDER:
return is_lean_binder_name(node);
case CBM_OCCURRENCE_PASCAL_PROC:
return is_pascal_proc_binding(node);
case CBM_OCCURRENCE_TEAL_FUNCTION:
return is_teal_function_binding(node);
case CBM_OCCURRENCE_VHDL_INTERFACE:
return is_vhdl_interface_binding(node);
case CBM_OCCURRENCE_PINE_FUNCTION:
return is_pine_function_binding(node);
case CBM_OCCURRENCE_LLVM_FUNCTION:
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
parent = ts_node_parent(parent)) {
if (strcmp(ts_node_type(parent), "function_header") == 0) {
return field_contains_node(parent, "arguments", node);
}
}
return false;
case CBM_OCCURRENCE_STANDARD:
default:
return false;
}
}
static bool elixir_binary_operator_binds(CBMExtractCtx *ctx, TSNode node) {
if (ctx->language != CBM_LANG_ELIXIR || strcmp(ts_node_type(node), "binary_operator") != 0) {
return false;
}
TSNode operator_node = ts_node_child_by_field_name(node, TS_FIELD("operator"));
return text_equals(ctx, operator_node, "=") || text_equals(ctx, operator_node, "<-") ||
text_equals(ctx, operator_node, "->") || text_equals(ctx, operator_node, "\\\\");
}
static bool is_binding_occurrence(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec,
WalkState *state) {
const CBMOccurrenceSpec *occurrence = &occurrence_specs[ctx->language];
if (is_exact_language_binding(ctx, node, state)) {
return true;
}
if (is_policy_binding(ctx, node, occurrence)) {
return true;
}
TSNode current = node;
TSTreeCursor *cursor = reset_occurrence_cursor(state, node);
while (!ts_node_is_null(current)) {
TSNode parent;
const char *field;
if (!occurrence_parent(cursor, current, &parent, &field)) {
break;
}
if (is_value_field(field)) {
return false;
}
/* A declaration container binds its name/pattern, not the symbols used
* by its type annotation. Both TypeScript (`cfg: Config`) and Go
* (`cfg Config`) expose that annotation through the exact `type` field;
* stop before the whole-parameter binding rule can swallow `Config`.
* The emitted occurrence remains an ordinary USAGE, never a callable
* reference merely because the target happens to be a type. */
if (field && strcmp(field, "type") == 0) {
return false;
}
const char *kind = ts_node_type(parent);
if (kind_in_exact_set(kind, common_whole_binding_nodes) ||
kind_in_exact_set(kind, occurrence->whole_binding_nodes)) {
return true;
}
bool variable_container =
spec->variable_node_types && cbm_kind_in_set(parent, spec->variable_node_types);
if (ctx->language == CBM_LANG_ELIXIR && strcmp(kind, "binary_operator") == 0) {
variable_container = elixir_binary_operator_binds(ctx, parent);
}
bool declared_container =
kind_in_exact_set(kind, field_binding_nodes) ||
(spec->function_node_types && cbm_kind_in_set(parent, spec->function_node_types)) ||
(spec->class_node_types && cbm_kind_in_set(parent, spec->class_node_types)) ||
(spec->field_node_types && cbm_kind_in_set(parent, spec->field_node_types)) ||
variable_container;
if (declared_container) {
for (const char *const *binding_field = binding_fields; *binding_field;
binding_field++) {
if (field_contains_node(parent, *binding_field, node)) {
return true;
}
}
}
current = parent;
}
return false;
}
static bool assignment_reads_target(TSNode assignment) {
static const char *const read_write_nodes[] = {"augmented_assignment",
"augmented_assignment_expression",
"compound_assignment_expr",
"compound_assignment_expression",
"operator_assignment",
"operator_assign",
"update_exp",
"postfix_unary_expression",
"prefix_unary_expression",
NULL};
if (kind_in_exact_set(ts_node_type(assignment), read_write_nodes)) {
return true;
}
static const char *const read_write_operators[] = {"+=",
"-=",
"*=",
"/=",
"%=",
"&=",
"|=",
"^=",
"<<=",
">>=",
"?"
"?=",
"++",
"--",
NULL};
uint32_t count = ts_node_child_count(assignment);
for (uint32_t i = 0; i < count; i++) {
if (kind_in_exact_set(ts_node_type(ts_node_child(assignment, i)), read_write_operators)) {
return true;
}
}
return false;
}
static bool is_write_occurrence(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec,
WalkState *state) {
const CBMOccurrenceSpec *occurrence = &occurrence_specs[ctx->language];
TSNode current = node;
TSTreeCursor *cursor = reset_occurrence_cursor(state, node);
while (!ts_node_is_null(current)) {
TSNode parent;
const char *field;
if (!occurrence_parent(cursor, current, &parent, &field)) {
break;
}
if (is_value_field(field)) {
return false;
}
bool assignment =
(spec->assignment_node_types && cbm_kind_in_set(parent, spec->assignment_node_types)) ||
kind_in_exact_set(ts_node_type(parent), occurrence->write_nodes);
if (ctx->language == CBM_LANG_ELIXIR &&
strcmp(ts_node_type(parent), "binary_operator") == 0) {
assignment = elixir_binary_operator_binds(ctx, parent);
}
if (assignment) {
if (assignment_reads_target(parent)) {
return false;
}
if (ctx->language == CBM_LANG_LINKERSCRIPT &&
strcmp(ts_node_type(parent), "assignment") == 0) {
/* This grammar's assignment production has no field map. Its
* first direct child is the lhs, even when that child is the
* anonymous location-counter token `.`. */
return ts_node_child_count(parent) > 0 &&
node_contains(ts_node_child(parent, 0), node);
}
TSNode left = ts_node_child_by_field_name(parent, TS_FIELD("left"));
if (node_contains(left, node) || field_contains_node(parent, "target", node) ||
field_contains_node(parent, "destination", node)) {
return true;
}
return occurrence->first_named_child_is_write && named_child_contains(parent, 0, node);
}
current = parent;
}
return false;
}
static bool is_argument_container_kind(const char *kind) {
return kind && (strcmp(kind, "arguments") == 0 || strcmp(kind, "argument_list") == 0 ||
strcmp(kind, "value_arguments") == 0);
}
static bool is_labeled_argument_kind(const char *kind) {
return kind && (strcmp(kind, "keyword_argument") == 0 || strcmp(kind, "named_argument") == 0 ||
strcmp(kind, "labeled_argument") == 0);
}
/* A named/labeled argument's key describes the callee parameter; it is not a
* value read in the caller. Keep this separate from binding detection: the
* label must neither emit USAGE nor create a caller-local lexical binding. */
static bool is_call_argument_label(TSNode node) {
usage_slow_parent_fallback_test_note();
for (TSNode current = node; !ts_node_is_null(current);) {
TSNode parent = ts_node_parent(current);
if (ts_node_is_null(parent)) {
return false;
}
const char *parent_kind = ts_node_type(parent);
if (is_labeled_argument_kind(parent_kind)) {
return field_contains_node(parent, "name", node) ||
field_contains_node(parent, "label", node) ||
field_contains_node(parent, "key", node);
}
if (is_argument_container_kind(parent_kind)) {
return false;
}
current = parent;
}
return false;
}
static bool is_call_argument_label_walk(TSNode node, WalkState *state) {
TSTreeCursor *cursor = reset_occurrence_cursor(state, node);
if (!cursor)
return is_call_argument_label(node);
TSNode current = node;
while (!ts_node_is_null(current)) {
TSNode parent;
const char *field;
if (!occurrence_parent(cursor, current, &parent, &field))
return false;
const char *parent_kind = ts_node_type(parent);
if (is_labeled_argument_kind(parent_kind)) {
return field && (strcmp(field, "name") == 0 || strcmp(field, "label") == 0 ||
strcmp(field, "key") == 0);
}
if (is_argument_container_kind(parent_kind))
return false;
current = parent;
}
return false;
}
static bool is_direct_argument_value(TSNode node) {
usage_slow_parent_fallback_test_note();
TSNode parent = ts_node_parent(node);
if (ts_node_is_null(parent)) {
return false;
}
if (is_labeled_argument_kind(ts_node_type(parent))) {
TSNode value = ts_node_child_by_field_name(parent, TS_FIELD("value"));
return !ts_node_is_null(value) && ts_node_eq(value, node) &&
is_direct_argument_value(parent);
}
TSNode direct_arguments = ts_node_child_by_field_name(parent, TS_FIELD("arguments"));
if (!ts_node_is_null(direct_arguments) && ts_node_eq(direct_arguments, node)) {
return true;
}
if (is_argument_container_kind(ts_node_type(parent))) {
return true;
}
const char *parent_kind = ts_node_type(parent);
if (strcmp(parent_kind, "list_expression") == 0) {
TSNode call = ts_node_parent(parent);
if (!ts_node_is_null(call)) {
TSNode arguments = ts_node_child_by_field_name(call, TS_FIELD("arguments"));
return !ts_node_is_null(arguments) && ts_node_eq(arguments, parent);
}
return false;
}
if (strcmp(parent_kind, "argument") != 0 && strcmp(parent_kind, "value_argument") != 0) {
return false;
}
TSNode grandparent = ts_node_parent(parent);
return !ts_node_is_null(grandparent) && is_argument_container_kind(ts_node_type(grandparent));
}
/* Cursor-backed counterpart for the unified walker. `cursor` must currently
* point at `node`; it is consumed while walking toward the argument owner. */
static bool is_direct_argument_value_cursor(TSNode node, TSTreeCursor *cursor) {
TSNode current = node;
while (!ts_node_is_null(current)) {
TSNode parent;
const char *field;
if (!occurrence_parent(cursor, current, &parent, &field))
return false;
const char *parent_kind = ts_node_type(parent);
if (is_labeled_argument_kind(parent_kind)) {
if (!field || strcmp(field, "value") != 0)
return false;
current = parent;
continue;
}
if (field && strcmp(field, "arguments") == 0)
return true;
if (is_argument_container_kind(parent_kind))
return true;
if (strcmp(parent_kind, "list_expression") == 0) {
TSNode call;
const char *list_field;
return occurrence_parent(cursor, parent, &call, &list_field) && list_field &&
strcmp(list_field, "arguments") == 0;
}
if (strcmp(parent_kind, "argument") != 0 && strcmp(parent_kind, "value_argument") != 0) {
return false;
}
TSNode grandparent;
const char *argument_field;
return occurrence_parent(cursor, parent, &grandparent, &argument_field) &&
is_argument_container_kind(ts_node_type(grandparent));
}
return false;
}
static bool is_direct_argument_value_walk(TSNode node, WalkState *state) {
TSTreeCursor *cursor = reset_occurrence_cursor(state, node);
return cursor ? is_direct_argument_value_cursor(node, cursor) : is_direct_argument_value(node);
}
static TSNode python_direct_callable_attribute_site(TSNode node) {
usage_slow_parent_fallback_test_note();
if (ts_node_is_null(node) || strcmp(ts_node_type(node), "attribute") != 0) {
return (TSNode){0};
}
TSNode site = node;
TSNode parent = ts_node_parent(site);
while (!ts_node_is_null(parent) &&
strcmp(ts_node_type(parent), "parenthesized_expression") == 0) {
TSNode inner = ts_node_child_by_field_name(parent, TS_FIELD("expression"));
if (ts_node_is_null(inner) && ts_node_named_child_count(parent) == 1) {
inner = ts_node_named_child(parent, 0);
}
if (ts_node_is_null(inner) || !ts_node_eq(inner, site)) {
return (TSNode){0};
}
site = parent;
parent = ts_node_parent(site);
}
return is_direct_argument_value(site) ? site : (TSNode){0};
}
static bool language_may_stamp_exact_callable_value_candidate(CBMLanguage language) {
switch (language) {
case CBM_LANG_JAVASCRIPT:
case CBM_LANG_TYPESCRIPT:
case CBM_LANG_TSX:
case CBM_LANG_GO:
case CBM_LANG_PYTHON:
case CBM_LANG_C:
case CBM_LANG_CPP:
case CBM_LANG_CUDA:
case CBM_LANG_RUST:
case CBM_LANG_CSHARP:
case CBM_LANG_KOTLIN:
return true;
default:
return false;
}
}
/* Direct identifiers and direct, statically-named TS member values passed as
* arguments are narrow syntactic candidates only. A language LSP must still
* prove one target at this exact occurrence before the graph upgrades USAGE to
* CALL_REFERENCE; unresolved, reassigned, and composite expressions stay USAGE. */
static TSNode csharp_callable_value_site(TSNode node) {
usage_slow_parent_fallback_test_note();
const char *kind = ts_node_type(node);
if (strcmp(kind, "identifier") != 0 && strcmp(kind, "simple_identifier") != 0) {
return (TSNode){0};
}
TSNode site = node;
TSNode parent = ts_node_parent(site);
if (!ts_node_is_null(parent) && strcmp(ts_node_type(parent), "generic_name") == 0) {
TSNode name = ts_node_child_by_field_name(parent, TS_FIELD("name"));
if (ts_node_is_null(name) && ts_node_named_child_count(parent) > 0) {
name = ts_node_named_child(parent, 0);
}
if (ts_node_is_null(name) || !ts_node_eq(name, node)) {
return (TSNode){0};
}
site = parent;
parent = ts_node_parent(site);
}
if (!ts_node_is_null(parent) && strcmp(ts_node_type(parent), "member_access_expression") == 0) {
TSNode member = ts_node_child_by_field_name(parent, TS_FIELD("name"));
if (ts_node_is_null(member) || !ts_node_eq(member, site)) {
return (TSNode){0};
}
site = parent;
parent = ts_node_parent(site);
}
while (!ts_node_is_null(parent) &&
strcmp(ts_node_type(parent), "parenthesized_expression") == 0) {
TSNode inner = ts_node_child_by_field_name(parent, TS_FIELD("expression"));
if (ts_node_is_null(inner) && ts_node_named_child_count(parent) == 1) {
inner = ts_node_named_child(parent, 0);
}
if (ts_node_is_null(inner) || !ts_node_eq(inner, site)) {
return (TSNode){0};
}
site = parent;
parent = ts_node_parent(site);
}
/* The wrapper chain proves admission, but the semantic row is keyed to the
* terminal method-name identifier. Keep the raw carrier on that leaf. */
return is_direct_argument_value(site) ? node : (TSNode){0};
}
/* Climb out of the parentheses wrapping a direct argument and return the
* outermost wrapper, or null when the node is not a parenthesised direct
* argument. Mirrors python_direct_callable_attribute_site so the bare
* identifier and bound method forms agree on one occurrence. */
static TSNode paren_wrapped_direct_argument_site(TSNode node, TSNode parent, TSTreeCursor *cursor,
WalkState *state) {
/* The caller already resolved `parent`; checking its kind first keeps the
* common case (an identifier that is not parenthesised at all) free of any
* extra parent resolution. Walking up with ts_node_parent here instead
* costs one slow fallback per identifier in the file, which the linearity
* guard in test_extraction.c rightly rejects. */
if (ts_node_is_null(parent) || strcmp(ts_node_type(parent), "parenthesized_expression") != 0) {
return (TSNode){0};
}
TSNode site = node;
TSNode wrapper = parent;
for (int depth = 0; depth < 64; depth++) {
TSNode inner = ts_node_child_by_field_name(wrapper, TS_FIELD("expression"));
if (ts_node_is_null(inner) && ts_node_named_child_count(wrapper) == 1) {
inner = ts_node_named_child(wrapper, 0);
}
if (ts_node_is_null(inner) || !ts_node_eq(inner, site)) {
return (TSNode){0};
}
site = wrapper;
TSNode next = {0};
const char *field = NULL;
if (cursor) {
if (!occurrence_parent(cursor, site, &next, &field)) {
next = (TSNode){0};
}
} else {
usage_slow_parent_fallback_test_note();
next = ts_node_parent(site);
}
if (ts_node_is_null(next) || strcmp(ts_node_type(next), "parenthesized_expression") != 0) {
break;
}
wrapper = next;
}
return is_direct_argument_value_walk(site, state) ? site : (TSNode){0};
}
static TSNode call_reference_candidate_site(CBMExtractCtx *ctx, TSNode node, const char *name,
WalkState *state) {
if (!ctx || !name || !language_may_stamp_exact_callable_value_candidate(ctx->language)) {
return (TSNode){0};
}
const char *kind = ts_node_type(node);
TSTreeCursor *cursor = reset_occurrence_cursor(state, node);
TSNode parent = {0};
const char *parent_field = NULL;
if (!cursor) {
usage_slow_parent_fallback_test_note();
parent = ts_node_parent(node);
} else {
(void)occurrence_parent(cursor, node, &parent, &parent_field);
}
bool ts_family = ctx->language == CBM_LANG_JAVASCRIPT || ctx->language == CBM_LANG_TYPESCRIPT ||
ctx->language == CBM_LANG_TSX;
if (ts_family && strcmp(kind, "property_identifier") == 0 && !ts_node_is_null(parent) &&
strcmp(ts_node_type(parent), "member_expression") == 0) {
TSNode property = ts_node_child_by_field_name(parent, TS_FIELD("property"));
TSNode arguments = {0};
const char *member_field = NULL;
if (cursor) {
(void)occurrence_parent(cursor, parent, &arguments, &member_field);
} else {
usage_slow_parent_fallback_test_note();
arguments = ts_node_parent(parent);
}
return !ts_node_is_null(property) && ts_node_eq(property, node) &&
!ts_node_is_null(arguments) &&
strcmp(ts_node_type(arguments), "arguments") == 0
? node
: (TSNode){0};
}
if (ctx->language == CBM_LANG_PYTHON && strcmp(kind, "identifier") == 0 &&
!ts_node_is_null(parent) && strcmp(ts_node_type(parent), "attribute") == 0) {
TSNode attribute = ts_node_child_by_field_name(parent, TS_FIELD("attribute"));
return !ts_node_is_null(attribute) && ts_node_eq(attribute, node)
? python_direct_callable_attribute_site(parent)
: (TSNode){0};
}
if (ctx->language == CBM_LANG_GO && strcmp(kind, "field_identifier") == 0 &&
!ts_node_is_null(parent) && strcmp(ts_node_type(parent), "selector_expression") == 0) {
TSNode field = ts_node_child_by_field_name(parent, TS_FIELD("field"));
return !ts_node_is_null(field) && ts_node_eq(field, node) &&
(cursor ? is_direct_argument_value_cursor(parent, cursor)
: is_direct_argument_value(parent))
? parent
: (TSNode){0};
}
if (ctx->language == CBM_LANG_RUST && strcmp(kind, "scoped_identifier") == 0) {
return is_direct_argument_value_walk(node, state) ? node : (TSNode){0};
}
if (ctx->language == CBM_LANG_CSHARP) {
return csharp_callable_value_site(node);
}
if (strcmp(kind, "identifier") != 0 && strcmp(kind, "simple_identifier") != 0) {
return (TSNode){0};
}
/* Kotlin: a navigation member read (`obj.member`, and `value::member`,
* which the vendored grammar also parses as navigation) is a candidate at
* the member occurrence, so a receiver-typed LSP row can claim it instead
* of the name-only registry fallback (maintainer decision, option B). With
* no row at the occurrence the join finds nothing and behavior is exactly
* as before. */
if (ctx->language == CBM_LANG_KOTLIN && !ts_node_is_null(parent) &&
strcmp(ts_node_type(parent), "navigation_suffix") == 0) {
return node;
}
/* Python: the right-hand side of `callback = handler` is a genuine value
* occurrence of handler, and when the LSP proves its exact callable
* identity it becomes a CALL_REFERENCE like any proven argument value
* (maintainer decision on the local-alias fixture). Bare identifier RHS
* only, in lockstep with the row py_lsp.c emits -- a candidate the LSP
* never matches would change nothing, but the asymmetry would be a trap. */
if (ctx->language == CBM_LANG_PYTHON && !ts_node_is_null(parent) &&
strcmp(ts_node_type(parent), "assignment") == 0) {
TSNode right = ts_node_child_by_field_name(parent, TS_FIELD("right"));
return !ts_node_is_null(right) && ts_node_eq(right, node) ? node : (TSNode){0};
}
if (is_direct_argument_value_walk(node, state)) {
return node;
}
/* `f((handler))` passes handler just as directly as `f(handler)`. The
* semantic row is recorded on the outermost parenthesised wrapper -- the
* site python_direct_callable_attribute_site already returns for the bound
* method form -- so climb to that same wrapper here. A bare identifier
* previously stopped at the parentheses and became no candidate at all, so
* the occurrence-exact join had nothing to join and a parenthesised
* callable argument produced no reference. */
return paren_wrapped_direct_argument_site(node, parent, cursor, state);
}
static char *reference_name(CBMExtractCtx *ctx, TSNode node) {
char *name = cbm_node_text(ctx->arena, node, ctx->source);
if (!name || ctx->language != CBM_LANG_MAKEFILE ||
strcmp(ts_node_type(node), "variable_reference") != 0) {
return name;
}
/* Make exposes `$(watched)` as one named reference node.
* Normalize only that grammar wrapper; the recorded site still spans the
* exact source occurrence for occurrence-sensitive resolution. */
size_t length = strlen(name);
bool parenthesized = length > 3 && name[0] == '$' && name[1] == '(' && name[length - 1] == ')';
return parenthesized ? cbm_arena_strndup(ctx->arena, name + 2, length - 3) : name;
}
static bool lexical_identifier_case_insensitive(CBMLanguage language) {
switch (language) {
case CBM_LANG_POWERSHELL:
case CBM_LANG_FORTRAN:
case CBM_LANG_ADA:
case CBM_LANG_PASCAL:
case CBM_LANG_COBOL:
case CBM_LANG_VHDL:
return true;
default:
return false;
}
}
static const char *lexical_binding_key(CBMExtractCtx *ctx, WalkState *state, const char *name) {
if (!ctx || !name) {
return name;
}
/* Preserve language namespaces: Perl sigils and Vim prefixes distinguish
* different variables. Tcl's grammar is asymmetric (`name` versus
* `$name`), so unwrap only that language-defined reference sigil. */
if (ctx->language == CBM_LANG_TCL && name[0] == '$') {
name++;
}
if (!lexical_identifier_case_insensitive(ctx->language)) {
return name;
}
size_t n = strlen(name);
char *folded = (char *)cbm_arena_alloc(ctx->arena, n + 1U);
if (!folded) {
if (state) {
state->lexical_binding_tracking_failed = true;
}
return NULL;
}
for (size_t i = 0; i < n; i++) {
folded[i] = (char)tolower((unsigned char)name[i]);
}
folded[n] = '\0';
return folded;
}
static void stamp_usage_site(CBMExtractCtx *ctx, CBMUsage *usage, TSNode node, const char *name,
WalkState *state) {
TSNode candidate_site = call_reference_candidate_site(ctx, node, name, state);
TSNode site = ts_node_is_null(candidate_site) ? node : candidate_site;
usage->site_start_byte = ts_node_start_byte(site);
usage->site_end_byte = ts_node_end_byte(site);
usage->may_be_call_reference = !ts_node_is_null(candidate_site);
}
static const CBMLexicalScope *usage_lexical_scope(const WalkState *state, uint32_t id) {
return state && id > 0 && id <= (uint32_t)state->lexical_scope_count
? &state->lexical_scopes[id - 1U]
: NULL;
}
static uint32_t active_lexical_scope_id(const WalkState *state) {
if (!state) {
return 0;
}
for (int i = state->scope_top - 1; i >= 0; i--) {
if (state->scopes[i].lexical_scope_id != 0) {
return state->scopes[i].lexical_scope_id;
}
}
return state->root_lexical_scope_id;
}
static uint32_t lexical_ancestor_of_kind(const WalkState *state, uint32_t start_id,
bool want_function, bool want_block);
static bool python_default_value_reference(TSNode node) {
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
parent = ts_node_parent(parent)) {
const char *kind = ts_node_type(parent);
/* A nested executable scope inside the default owns its own lookups;
* only a direct default expression is evaluated in the declaring
* function's structural parent namespace. */
if (strcmp(kind, "lambda") == 0 || strcmp(kind, "list_comprehension") == 0 ||
strcmp(kind, "set_comprehension") == 0 ||
strcmp(kind, "dictionary_comprehension") == 0 ||
strcmp(kind, "generator_expression") == 0) {
return false;
}
if ((strcmp(kind, "default_parameter") == 0 ||
strcmp(kind, "typed_default_parameter") == 0) &&
(field_contains_node(parent, "value", node) ||
field_contains_node(parent, "default", node))) {
return true;
}
}
return false;
}
static uint32_t usage_lexical_scope_id_for_node(CBMExtractCtx *ctx, const WalkState *state,
TSNode node) {
uint32_t scope_id = active_lexical_scope_id(state);
if (!ctx || ctx->language != CBM_LANG_PYTHON || !python_default_value_reference(node)) {
return scope_id;
}
uint32_t function_id = lexical_ancestor_of_kind(state, scope_id, true, false);
const CBMLexicalScope *function_scope = usage_lexical_scope(state, function_id);
/* Method bodies intentionally skip their class lookup parent, but Python
* evaluates the method's default expressions while the class namespace is
* executing. Use structural ownership here, not body lookup ownership. */
return function_scope ? function_scope->parent_id : scope_id;
}
static uint32_t lexical_ancestor_of_kind(const WalkState *state, uint32_t start_id,
bool want_function, bool want_block) {
uint32_t id = start_id;
int remaining = state ? state->lexical_scope_count : 0;
while (id != 0 && remaining-- > 0) {
const CBMLexicalScope *scope = usage_lexical_scope(state, id);
if (!scope) {
return 0;
}
bool function_scope = scope->kind == CBM_LEXICAL_SCOPE_FUNCTION ||
scope->kind == CBM_LEXICAL_SCOPE_COMPREHENSION;
bool block_scope = scope->kind == CBM_LEXICAL_SCOPE_BLOCK;
if ((want_function && function_scope) || (want_block && block_scope)) {
return id;
}
id = scope->parent_id;
}
return 0;
}
static uint32_t python_nearest_namespace(const WalkState *state, uint32_t start_id) {
uint32_t id = start_id;
int remaining = state ? state->lexical_scope_count : 0;
while (id != 0 && remaining-- > 0) {
const CBMLexicalScope *scope = usage_lexical_scope(state, id);
if (!scope) {
return 0;
}
if (scope->kind == CBM_LEXICAL_SCOPE_FUNCTION ||
scope->kind == CBM_LEXICAL_SCOPE_COMPREHENSION ||
scope->kind == CBM_LEXICAL_SCOPE_CLASS || scope->kind == CBM_LEXICAL_SCOPE_MODULE) {
return id;
}
id = scope->parent_id;
}
return 0;
}
static bool ensure_lexical_binding_capacity(WalkState *state) {
if (!state->lexical_bindings) {
state->lexical_bindings = state->inline_lexical_bindings;
state->lexical_binding_capacity = INLINE_LEXICAL_BINDINGS;
memset(state->inline_lexical_bindings, 0, sizeof(state->inline_lexical_bindings));
}
if (state->lexical_binding_count < state->lexical_binding_capacity) {
return true;
}
if (state->lexical_binding_capacity > INT32_MAX / PAIR_LEN) {
state->lexical_binding_tracking_failed = true;
return false;
}
int new_capacity = state->lexical_binding_capacity * PAIR_LEN;
CBMLexicalBinding *grown =
(CBMLexicalBinding *)cbm_arena_alloc(state->arena, (size_t)new_capacity * sizeof(*grown));
if (!grown) {
state->lexical_binding_tracking_failed = true;
return false;
}
memcpy(grown, state->lexical_bindings, (size_t)state->lexical_binding_count * sizeof(*grown));
state->lexical_bindings = grown;
state->lexical_binding_capacity = new_capacity;
return true;
}
static bool lexical_ancestor_kind(TSNode node, const char *kind) {
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
parent = ts_node_parent(parent)) {
if (strcmp(ts_node_type(parent), kind) == 0) {
return true;
}
}
return false;
}
static TSNode declared_function_name_owner(TSNode node, const CBMLangSpec *spec) {
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
parent = ts_node_parent(parent)) {
if (spec->function_node_types && cbm_kind_in_set(parent, spec->function_node_types)) {
return field_contains_node(parent, "name", node) ? parent : (TSNode){0};
}
}
return (TSNode){0};
}
static TSNode declared_class_name_owner(TSNode node, const CBMLangSpec *spec) {
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
parent = ts_node_parent(parent)) {
if (spec->class_node_types && cbm_kind_in_set(parent, spec->class_node_types)) {
return field_contains_node(parent, "name", node) ? parent : (TSNode){0};
}
}
return (TSNode){0};
}
static bool ensure_python_directive_capacity(WalkState *state) {
if (!state->python_directives) {
state->python_directives = state->inline_python_directives;
state->python_directive_capacity = INLINE_PYTHON_DIRECTIVES;
memset(state->inline_python_directives, 0, sizeof(state->inline_python_directives));
}
if (state->python_directive_count < state->python_directive_capacity) {
return true;
}
if (state->python_directive_capacity > INT32_MAX / PAIR_LEN) {
state->lexical_binding_tracking_failed = true;
return false;
}
int new_capacity = state->python_directive_capacity * PAIR_LEN;
CBMPythonDirective *grown =
(CBMPythonDirective *)cbm_arena_alloc(state->arena, (size_t)new_capacity * sizeof(*grown));
if (!grown) {
state->lexical_binding_tracking_failed = true;
return false;
}
memcpy(grown, state->python_directives, (size_t)state->python_directive_count * sizeof(*grown));
state->python_directives = grown;
state->python_directive_capacity = new_capacity;
return true;
}
static void record_python_directive(WalkState *state, uint32_t function_scope_id, const char *name,
CBMPythonDirectiveKind kind) {
if (!function_scope_id || !name || !ensure_python_directive_capacity(state)) {
return;
}
for (int i = 0; i < state->python_directive_count; i++) {
CBMPythonDirective *directive = &state->python_directives[i];
if (directive->function_scope_id == function_scope_id &&
strcmp(directive->name, name) == 0) {
directive->kind = (uint8_t)kind;
return;
}
}
CBMPythonDirective *directive = &state->python_directives[state->python_directive_count++];
directive->function_scope_id = function_scope_id;
directive->name = name;
directive->kind = (uint8_t)kind;
}
static CBMPythonDirectiveKind python_directive_for(const WalkState *state,
uint32_t function_scope_id, const char *name) {
for (int i = state ? state->python_directive_count - 1 : -1; i >= 0; i--) {
const CBMPythonDirective *directive = &state->python_directives[i];
if (directive->function_scope_id == function_scope_id && directive->name &&
strcmp(directive->name, name) == 0) {
return (CBMPythonDirectiveKind)directive->kind;
}
}
return 0;
}
static uint32_t python_enclosing_function_namespace(const WalkState *state,
uint32_t inner_function_id) {
const CBMLexicalScope *inner = usage_lexical_scope(state, inner_function_id);
uint32_t id = inner ? inner->parent_id : 0;
int remaining = state ? state->lexical_scope_count : 0;
while (id != 0 && remaining-- > 0) {
const CBMLexicalScope *scope = usage_lexical_scope(state, id);
if (!scope) {
return 0;
}
if (scope->kind == CBM_LEXICAL_SCOPE_FUNCTION ||
scope->kind == CBM_LEXICAL_SCOPE_COMPREHENSION) {
return id;
}
id = scope->parent_id;
}
return 0;
}
static bool binding_is_parameter(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) {
const CBMOccurrenceSpec *occurrence = &occurrence_specs[ctx->language];
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
parent = ts_node_parent(parent)) {
const char *kind = ts_node_type(parent);
if (kind_in_exact_set(kind, common_whole_binding_nodes) ||
kind_in_exact_set(kind, occurrence->whole_binding_nodes)) {
return true;
}
if (spec->function_node_types && cbm_kind_in_set(parent, spec->function_node_types)) {
return field_contains_node(parent, "parameter", node) ||
field_contains_node(parent, "parameters", node);
}
}
return false;
}
static bool js_var_binding(TSNode node) {
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
parent = ts_node_parent(parent)) {
const char *kind = ts_node_type(parent);
if (strcmp(kind, "variable_declaration") == 0) {
return true;
}
if (strcmp(kind, "lexical_declaration") == 0) {
return false;
}
}
return false;
}
static bool import_kind_matches(TSNode node, const CBMLangSpec *spec) {
return (spec->import_node_types && cbm_kind_in_set(node, spec->import_node_types)) ||
(spec->import_from_types && cbm_kind_in_set(node, spec->import_from_types));
}
static TSNode nearest_import_ancestor(TSNode node, const CBMLangSpec *spec) {
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
parent = ts_node_parent(parent)) {
if (import_kind_matches(parent, spec)) {
return parent;
}
}
return (TSNode){0};
}
static TSNode first_named_leaf(TSNode node) {
while (!ts_node_is_null(node) && ts_node_named_child_count(node) > 0) {
node = ts_node_named_child(node, 0);
}
return node;
}
static bool import_alias_contains(TSNode node, TSNode boundary) {
for (TSNode parent = ts_node_parent(node);
!ts_node_is_null(parent) && !ts_node_eq(parent, boundary);
parent = ts_node_parent(parent)) {
if (field_contains_node(parent, "alias", node)) {
return true;
}
}
return false;
}
static bool rust_use_list_has_direct_self(TSNode list) {
uint32_t count = ts_node_named_child_count(list);
for (uint32_t i = 0; i < count; i++) {
if (strcmp(ts_node_type(ts_node_named_child(list, i)), "self") == 0) {
return true;
}
}
return false;
}
/* Import subtrees are excluded from ordinary usage emission, but their local
* names still participate in lexical lookup. Keep this classifier narrow: it
* identifies only the binding side, never a module path or imported source
* name. */
static bool is_import_binding_occurrence(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) {
TSNode boundary = nearest_import_ancestor(node, spec);
if (ts_node_is_null(boundary)) {
return false;
}
/* Rust's extern-crate alias is a direct field of the import boundary,
* unlike the nested alias containers used by Python and ES imports. */
if (field_contains_node(boundary, "alias", node)) {
return true;
}
if (import_alias_contains(node, boundary)) {
return true;
}
switch (ctx->language) {
case CBM_LANG_PYTHON: {
const char *boundary_kind = ts_node_type(boundary);
if (strcmp(boundary_kind, "future_import_statement") == 0) {
return false;
}
for (TSNode parent = ts_node_parent(node);
!ts_node_is_null(parent) && !ts_node_eq(parent, boundary);
parent = ts_node_parent(parent)) {
/* In an aliased import, only the alias binds. */
if (strcmp(ts_node_type(parent), "aliased_import") == 0) {
return false;
}
}
bool from_import = strcmp(boundary_kind, "import_from_statement") == 0;
if (from_import) {
if (field_contains_node(boundary, "module_name", node) ||
field_contains_node(boundary, "module", node)) {
return false;
}
/* Older Python grammar variants omit the module field. Their first
* named child is still the source module, never a local binding. */
if (ts_node_named_child_count(boundary) > 1 &&
node_contains(ts_node_named_child(boundary, 0), node)) {
return false;
}
return strcmp(ts_node_type(node), "identifier") == 0;
}
TSNode entry = node;
TSNode parent = ts_node_parent(entry);
while (!ts_node_is_null(parent) && !ts_node_eq(parent, boundary)) {
entry = parent;
parent = ts_node_parent(parent);
}
TSNode first = first_named_leaf(entry);
return !ts_node_is_null(first) && ts_node_eq(first, node);
}
case CBM_LANG_JAVASCRIPT:
case CBM_LANG_TYPESCRIPT:
case CBM_LANG_TSX: {
if (strcmp(ts_node_type(boundary), "import_statement") != 0) {
return false;
}
for (TSNode parent = ts_node_parent(node);
!ts_node_is_null(parent) && !ts_node_eq(parent, boundary);
parent = ts_node_parent(parent)) {
const char *kind = ts_node_type(parent);
if (strcmp(kind, "import_specifier") == 0) {
TSNode alias = ts_node_child_by_field_name(parent, TS_FIELD("alias"));
if (!ts_node_is_null(alias)) {
return false;
}
return field_contains_node(parent, "name", node) ||
ts_node_eq(first_named_leaf(parent), node);
}
if (strcmp(kind, "namespace_import") == 0) {
return field_contains_node(parent, "name", node) ||
ts_node_eq(first_named_leaf(parent), node);
}
if (strcmp(kind, "import_clause") == 0) {
/* A direct identifier is the default import. Named and
* namespace imports were handled by their inner containers. */
return ts_node_eq(first_named_leaf(parent), node);
}
if (strcmp(kind, "import_require_clause") == 0) {
/* TypeScript `import local = require("module")` and
* `import local = Namespace.member` put the binding first.
* Current grammar variants expose only the source field, so
* retain the first-leaf fallback as the exact role check. */
return field_contains_node(parent, "name", node) ||
ts_node_eq(first_named_leaf(parent), node);
}
}
return false;
}
case CBM_LANG_RUST: {
TSNode boundary_alias = ts_node_child_by_field_name(boundary, TS_FIELD("alias"));
if (!ts_node_is_null(boundary_alias) && field_contains_node(boundary, "name", node)) {
/* `extern crate source as local` binds only local. */
return false;
}
bool terminal = false;
for (TSNode parent = ts_node_parent(node);
!ts_node_is_null(parent) && !ts_node_eq(parent, boundary);
parent = ts_node_parent(parent)) {
if (field_contains_node(parent, "path", node)) {
/* `use foo::{self}` imports the module itself under the final
* segment of the prefix (`foo`). An aliased `self as x` is a
* use_as_clause child rather than a direct self entry and is
* already handled by the alias rule above. */
if (strcmp(ts_node_type(parent), "scoped_use_list") == 0 &&
strcmp(ts_node_type(node), "identifier") == 0) {
TSNode path = ts_node_child_by_field_name(parent, TS_FIELD("path"));
TSNode list = ts_node_child_by_field_name(parent, TS_FIELD("list"));
bool terminal_path = terminal || ts_node_eq(path, node);
if (terminal_path && !ts_node_is_null(list) &&
rust_use_list_has_direct_self(list)) {
return true;
}
}
return false;
}
if (field_contains_node(parent, "name", node)) {
terminal = true;
}
}
return terminal || strcmp(ts_node_type(node), "identifier") == 0;
}
default:
return false;
}
}
static uint32_t lexical_import_scope(const WalkState *state, uint32_t start_id) {
uint32_t id = start_id;
int remaining = state ? state->lexical_scope_count : 0;
while (id != 0 && remaining-- > 0) {
const CBMLexicalScope *scope = usage_lexical_scope(state, id);
if (!scope) {
return 0;
}
if (scope->kind == CBM_LEXICAL_SCOPE_MODULE || scope->kind == CBM_LEXICAL_SCOPE_FUNCTION ||
scope->kind == CBM_LEXICAL_SCOPE_COMPREHENSION ||
scope->kind == CBM_LEXICAL_SCOPE_BLOCK) {
return id;
}
id = scope->parent_id;
}
return 0;
}
static void record_lexical_binding(CBMExtractCtx *ctx, WalkState *state, TSNode node,
const CBMLangSpec *spec, const char *raw_name,
bool import_binding) {
if (!ctx || !state || !raw_name || !raw_name[0] || state->lexical_binding_tracking_failed) {
return;
}
bool parameter = binding_is_parameter(ctx, node, spec);
if (ctx->language == CBM_LANG_VIMSCRIPT && parameter && !strchr(raw_name, ':')) {
raw_name = cbm_arena_sprintf(ctx->arena, "a:%s", raw_name);
if (!raw_name) {
state->lexical_binding_tracking_failed = true;
return;
}
}
const char *name = lexical_binding_key(ctx, state, raw_name);
if (!name || !name[0]) {
return;
}
uint32_t current_id = active_lexical_scope_id(state);
uint32_t scope_id = 0;
bool whole_scope = false;
TSNode function_declaration = declared_function_name_owner(node, spec);
TSNode class_declaration = declared_class_name_owner(node, spec);
if (ctx->language == CBM_LANG_PYTHON && (lexical_ancestor_kind(node, "global_statement") ||
lexical_ancestor_kind(node, "nonlocal_statement"))) {
uint32_t function_id = lexical_ancestor_of_kind(state, current_id, true, false);
CBMPythonDirectiveKind directive = lexical_ancestor_kind(node, "global_statement")
? CBM_PYTHON_DIRECTIVE_GLOBAL
: CBM_PYTHON_DIRECTIVE_NONLOCAL;
record_python_directive(state, function_id, name, directive);
return;
}
if (!ts_node_is_null(function_declaration)) {
uint32_t function_id = lexical_ancestor_of_kind(state, current_id, true, false);
const CBMLexicalScope *function_scope = usage_lexical_scope(state, function_id);
uint32_t parent_id = function_scope ? function_scope->parent_id : 0;
if (ctx->language == CBM_LANG_PYTHON) {
scope_id = python_nearest_namespace(state, parent_id);
const CBMLexicalScope *owner = usage_lexical_scope(state, scope_id);
if (!owner || (owner->kind != CBM_LEXICAL_SCOPE_FUNCTION &&
owner->kind != CBM_LEXICAL_SCOPE_COMPREHENSION)) {
return;
}
} else {
scope_id = lexical_ancestor_of_kind(state, parent_id, true, true);
}
/* Top-level/class callable declarations are themselves semantic
* targets, not local-value blockers. Nested declarations still bind
* their enclosing executable scope and must block raw-name fallback. */
if (scope_id == 0) {
return;
}
whole_scope = true;
} else if (ctx->language == CBM_LANG_PYTHON && !ts_node_is_null(class_declaration)) {
uint32_t class_id = python_nearest_namespace(state, current_id);
const CBMLexicalScope *class_scope = usage_lexical_scope(state, class_id);
scope_id = python_nearest_namespace(state, class_scope ? class_scope->parent_id : 0);
const CBMLexicalScope *owner = usage_lexical_scope(state, scope_id);
if (!owner || (owner->kind != CBM_LEXICAL_SCOPE_FUNCTION &&
owner->kind != CBM_LEXICAL_SCOPE_COMPREHENSION)) {
return;
}
whole_scope = true;
} else if (parameter) {
scope_id = lexical_ancestor_of_kind(state, current_id, true, false);
whole_scope = true;
} else if (ctx->language == CBM_LANG_PYTHON) {
uint32_t namespace_id = python_nearest_namespace(state, current_id);
const CBMLexicalScope *namespace_scope = usage_lexical_scope(state, namespace_id);
uint32_t function_id =
namespace_scope && (namespace_scope->kind == CBM_LEXICAL_SCOPE_FUNCTION ||
namespace_scope->kind == CBM_LEXICAL_SCOPE_COMPREHENSION)
? namespace_id
: lexical_ancestor_of_kind(state, current_id, true, false);
CBMPythonDirectiveKind directive = python_directive_for(state, function_id, name);
if (directive == CBM_PYTHON_DIRECTIVE_GLOBAL) {
scope_id = state->root_lexical_scope_id;
} else if (directive == CBM_PYTHON_DIRECTIVE_NONLOCAL) {
scope_id = python_enclosing_function_namespace(state, function_id);
} else {
scope_id = namespace_id;
}
const CBMLexicalScope *owner = usage_lexical_scope(state, scope_id);
if (!owner) {
return;
}
whole_scope = directive == 0 && (owner->kind == CBM_LEXICAL_SCOPE_FUNCTION ||
owner->kind == CBM_LEXICAL_SCOPE_COMPREHENSION);
} else if (ctx->language == CBM_LANG_POWERSHELL) {
scope_id = lexical_ancestor_of_kind(state, current_id, true, false);
whole_scope = true;
} else if (ctx->language == CBM_LANG_JAVASCRIPT || ctx->language == CBM_LANG_TYPESCRIPT ||
ctx->language == CBM_LANG_TSX) {
bool is_var = js_var_binding(node);
scope_id = lexical_ancestor_of_kind(state, current_id, is_var, !is_var);
if (scope_id == 0) {
scope_id = lexical_ancestor_of_kind(state, current_id, true, false);
}
if (scope_id == 0) {
scope_id = state->root_lexical_scope_id;
}
whole_scope = true; /* var hoisting and let/const TDZ */
} else if (ctx->language == CBM_LANG_RUST && import_binding) {
scope_id = lexical_import_scope(state, current_id);
if (scope_id == 0) {
scope_id = state->root_lexical_scope_id;
}
/* A Rust use/extern-crate declaration is an item whose name is in
* scope for the complete containing block or module, independent of
* textual declaration order. */
whole_scope = true;
} else {
scope_id = lexical_ancestor_of_kind(state, current_id, true, true);
if (scope_id == 0) {
scope_id = state->root_lexical_scope_id;
}
}
const CBMLexicalScope *scope = usage_lexical_scope(state, scope_id);
if (!scope || !ensure_lexical_binding_capacity(state)) {
return;
}
CBMLexicalBinding *binding = &state->lexical_bindings[state->lexical_binding_count++];
binding->scope_id = scope_id;
binding->active_start = whole_scope ? scope->start_byte : ts_node_end_byte(node);
/* Zero means "the finalized end of this concrete scope". Split
* signature/body languages extend the scope after parameters are seen. */
binding->active_end = 0;
binding->name = name;
}
static int compare_lexical_bindings(const void *left_ptr, const void *right_ptr) {
const CBMLexicalBinding *left = (const CBMLexicalBinding *)left_ptr;
const CBMLexicalBinding *right = (const CBMLexicalBinding *)right_ptr;
if (left->scope_id != right->scope_id) {
return left->scope_id < right->scope_id ? -1 : 1;
}
int name_order = strcmp(left->name, right->name);
if (name_order != 0) {
return name_order;
}
if (left->active_start != right->active_start) {
return left->active_start < right->active_start ? -1 : 1;
}
if (left->active_end != right->active_end) {
return left->active_end < right->active_end ? -1 : 1;
}
return 0;
}
static int compare_binding_key(const CBMLexicalBinding *binding, uint32_t scope_id,
const char *name) {
if (binding->scope_id != scope_id) {
return binding->scope_id < scope_id ? -1 : 1;
}
return strcmp(binding->name, name);
}
static int lexical_binding_lower_bound(const WalkState *state, uint32_t scope_id,
const char *name) {
int low = 0;
int high = state->lexical_binding_count;
while (low < high) {
int middle = low + (high - low) / PAIR_LEN;
if (compare_binding_key(&state->lexical_bindings[middle], scope_id, name) < 0) {
low = middle + 1;
} else {
high = middle;
}
}
return low;
}
static bool lexical_binding_active_at(const WalkState *state, uint32_t scope_id, const char *name,
uint32_t position) {
const CBMLexicalScope *scope = usage_lexical_scope(state, scope_id);
if (!scope) {
return false;
}
int index = lexical_binding_lower_bound(state, scope_id, name);
while (index < state->lexical_binding_count) {
const CBMLexicalBinding *binding = &state->lexical_bindings[index++];
if (binding->scope_id != scope_id || strcmp(binding->name, name) != 0) {
break;
}
uint32_t active_end = binding->active_end ? binding->active_end : scope->end_byte;
if (binding->active_start <= position && position < active_end) {
return true;
}
}
return false;
}
static const CBMLexicalScope *lexical_binding_scope_for_usage(const WalkState *state,
const CBMUsage *usage,
const char *name) {
uint32_t scope_id = usage->lexical_scope_id;
int remaining = state ? state->lexical_scope_count : 0;
while (scope_id != 0 && remaining-- > 0) {
const CBMLexicalScope *scope = usage_lexical_scope(state, scope_id);
if (!scope) {
return NULL;
}
if (lexical_binding_active_at(state, scope_id, name, usage->site_start_byte)) {
return scope;
}
scope_id = scope->lookup_parent_id;
}
return NULL;
}
void cbm_finalize_lexical_usages(CBMExtractCtx *ctx, WalkState *state) {
if (!ctx || !state) {
return;
}
if (state->lexical_binding_count > 1) {
qsort(state->lexical_bindings, (size_t)state->lexical_binding_count,
sizeof(*state->lexical_bindings), compare_lexical_bindings);
}
for (int i = state->usage_start_index; i < ctx->result->usages.count; i++) {
CBMUsage *usage = &ctx->result->usages.items[i];
if (!usage->ref_name || !usage->ref_name[0]) {
continue;
}
const char *name = lexical_binding_key(ctx, state, usage->ref_name);
if (!name) {
continue;
}
const CBMLexicalScope *binding_scope = lexical_binding_scope_for_usage(state, usage, name);
if (!binding_scope) {
continue;
}
usage->semantic_reference_blocked = true;
usage->semantic_reference_local_shadow = binding_scope->kind != CBM_LEXICAL_SCOPE_MODULE;
}
/* Allocation failure must never widen textual fallback. Exact semantic
* occurrence proof still runs first, but every unproven value fails closed
* until the next clean extraction. */
if (state->lexical_binding_tracking_failed) {
for (int i = state->usage_start_index; i < ctx->result->usages.count; i++) {
CBMUsage *usage = &ctx->result->usages.items[i];
usage->semantic_reference_blocked = true;
const CBMLexicalScope *scope = usage_lexical_scope(state, usage->lexical_scope_id);
usage->semantic_reference_local_shadow =
scope && scope->kind != CBM_LEXICAL_SCOPE_MODULE;
}
}
}
static char *perl_direct_coderef_name(CBMExtractCtx *ctx, TSNode node) {
if (!ctx || ctx->language != CBM_LANG_PERL ||
strcmp(ts_node_type(node), "refgen_expression") != 0 || !is_direct_argument_value(node)) {
return NULL;
}
char *text = cbm_node_text(ctx->arena, node, ctx->source);
const char *amp = text ? strchr(text, '&') : NULL;
if (!amp) {
return NULL;
}
const char *start = amp + 1;
while (*start && isspace((unsigned char)*start)) {
start++;
}
const char *end = start;
while (*end && (isalnum((unsigned char)*end) || *end == '_' || *end == ':')) {
end++;
}
return end > start ? cbm_arena_strndup(ctx->arena, start, (size_t)(end - start)) : NULL;
}
static bool inside_direct_perl_coderef(CBMExtractCtx *ctx, TSNode node) {
if (!ctx || ctx->language != CBM_LANG_PERL) {
return false;
}
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
parent = ts_node_parent(parent)) {
if (strcmp(ts_node_type(parent), "refgen_expression") == 0) {
return is_direct_argument_value(parent);
}
if (is_argument_container_kind(ts_node_type(parent))) {
return false;
}
}
return false;
}
static bool emit_direct_perl_coderef_usage(CBMExtractCtx *ctx, TSNode node,
const char *enclosing_func_qn,
uint32_t lexical_scope_id) {
char *name = perl_direct_coderef_name(ctx, node);
if (!name || !name[0]) {
return false;
}
CBMUsage usage = {0};
usage.ref_name = name;
usage.enclosing_func_qn = enclosing_func_qn;
usage.kind = CBM_USAGE_CALL_REFERENCE;
usage.may_be_call_reference = true;
usage.lexical_scope_id = lexical_scope_id;
usage.site_start_byte = ts_node_start_byte(node);
usage.site_end_byte = ts_node_end_byte(node);
cbm_usages_push(&ctx->result->usages, ctx->arena, usage);
return true;
}
// Try to emit a usage for a reference node. Returns early if the node should be skipped.
static void try_emit_usage(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec,
bool inside_call, bool inside_import) {
if (emit_direct_perl_coderef_usage(ctx, node, cbm_enclosing_func_qn_cached(ctx, node), 0)) {
return;
}
if (inside_direct_perl_coderef(ctx, node)) {
return;
}
if (!is_reference_node(node, ctx->language)) {
return;
}
if (is_call_argument_label(node)) {
return;
}
if (inside_call || inside_import) {
return;
}
if (is_binding_occurrence(ctx, node, spec, NULL) ||
is_write_occurrence(ctx, node, spec, NULL)) {
return;
}
char *name = reference_name(ctx, node);
if (name && name[0] && !cbm_is_keyword(name, ctx->language)) {
CBMUsage usage = {0};
usage.ref_name = name;
usage.enclosing_func_qn = cbm_enclosing_func_qn_cached(ctx, node);
stamp_usage_site(ctx, &usage, node, name, NULL);
cbm_usages_push(&ctx->result->usages, ctx->arena, usage);
}
}
// Iterative usage walker — explicit stack.
//
// The call/import ancestry that gates usage emission is maintained as ENTER/
// EXIT counters on the walk instead of per-node ancestor re-walks: the old
// is_inside_call/is_inside_import helpers climbed every ancestor via
// ts_node_parent, and tree-sitter's ts_node_parent RE-DESCENDS from the root
// scanning siblings — O(depth x sibling-position) per node, which went
// quadratic on wide nodes (a 1,536-argument call in dotnet/runtime's JIT
// torture tests put 92% of extract time into these walks; 490 s for one
// 147 KB file). Counter semantics match the helpers exactly: strict ancestors
// only — a node is emitted BEFORE its own kind increments the counters, so a
// call node itself does not count as "inside a call".
static void walk_usages(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec) {
typedef struct {
TSNode node;
uint32_t next_child;
bool counts_call;
bool counts_import;
} UsageFrame;
int cap = 256;
UsageFrame *frames = (UsageFrame *)cbm_arena_alloc(ctx->arena, (size_t)cap * sizeof(*frames));
if (!frames) {
return;
}
bool has_imports = spec->import_node_types && spec->import_node_types[0];
bool has_from_imports = spec->import_from_types && spec->import_from_types[0];
int call_depth = 0;
int import_depth = 0;
int top = 0;
frames[top++] = (UsageFrame){root, 0, false, false};
bool entering = true;
while (top > 0) {
UsageFrame *f = &frames[top - 1];
if (entering) {
try_emit_usage(ctx, f->node, spec, call_depth > 0, import_depth > 0);
f->counts_call = cbm_kind_in_set(f->node, spec->call_node_types);
f->counts_import =
(has_imports && cbm_kind_in_set(f->node, spec->import_node_types)) ||
(has_from_imports && cbm_kind_in_set(f->node, spec->import_from_types));
if (f->counts_call) {
call_depth++;
}
if (f->counts_import) {
import_depth++;
}
}
uint32_t count = ts_node_child_count(f->node);
if (f->next_child < count) {
TSNode child = ts_node_child(f->node, f->next_child);
f->next_child++;
if (top == cap) {
int new_cap = cap * 2;
UsageFrame *grown =
(UsageFrame *)cbm_arena_alloc(ctx->arena, (size_t)new_cap * sizeof(*grown));
if (!grown) {
return;
}
memcpy(grown, frames, (size_t)cap * sizeof(*frames));
frames = grown;
cap = new_cap;
}
frames[top++] = (UsageFrame){child, 0, false, false};
entering = true;
} else {
if (f->counts_call) {
call_depth--;
}
if (f->counts_import) {
import_depth--;
}
top--;
entering = false;
}
}
}
void cbm_extract_usages(CBMExtractCtx *ctx) {
const CBMLangSpec *spec = cbm_lang_spec(ctx->language);
if (!spec) {
return;
}
walk_usages(ctx, ctx->root, spec);
}
// --- Unified handler: called once per node by the cursor walk ---
// Uses WalkState flags instead of parent-chain walks for O(1) context checks.
void handle_usages(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state) {
if (emit_direct_perl_coderef_usage(ctx, node, state->enclosing_func_qn,
active_lexical_scope_id(state))) {
return;
}
if (inside_direct_perl_coderef(ctx, node)) {
return;
}
bool reference_node = is_reference_node(node, ctx->language);
const CBMOccurrenceSpec *occurrence = &occurrence_specs[ctx->language];
bool possible_binding_leaf =
ts_node_is_named(node) && ts_node_named_child_count(node) == 0 &&
(state->inside_import || is_exact_language_binding(ctx, node, state) ||
is_policy_binding(ctx, node, occurrence));
if (!reference_node && !possible_binding_leaf) {
return;
}
// An invocation relationship replaces only the exact AST occurrence it
// consumes. Receivers, qualifiers, computed keys, arguments, and nested
// bodies remain ordinary value references. Explicit callable syntax keeps
// that exact occurrence as a typed reference instead of erasing it.
bool exact_callee =
(!ts_node_is_null(state->callee_expr) && ts_node_eq(node, state->callee_expr)) ||
(!ts_node_is_null(state->callee_leaf) && ts_node_eq(node, state->callee_leaf));
if (exact_callee) {
if (reference_node && state->invocation_kind == CBM_INVOCATION_CALLABLE_REFERENCE) {
char *name = reference_name(ctx, node);
if (name && name[0] && !cbm_is_keyword(name, ctx->language)) {
CBMUsage usage = {0};
usage.ref_name = name;
usage.enclosing_func_qn = state->enclosing_func_qn;
usage.lexical_scope_id = usage_lexical_scope_id_for_node(ctx, state, node);
/* Kotlin callable-reference syntax is only a precision
* candidate. The semantic pass decides whether `::name`
* denotes one exact callable; unresolved properties and
* ambiguous values must remain ordinary USAGE. */
if (ctx->language == CBM_LANG_KOTLIN) {
usage.kind = CBM_USAGE_VALUE;
usage.may_be_call_reference = true;
} else {
usage.kind = CBM_USAGE_CALL_REFERENCE;
}
usage.site_start_byte = ts_node_start_byte(node);
usage.site_end_byte = ts_node_end_byte(node);
cbm_usages_push(&ctx->result->usages, ctx->arena, usage);
}
}
return;
}
if (is_forward_sibling_callee(ctx->language, node)) {
return;
}
if (is_call_argument_label_walk(node, state)) {
return;
}
// Imports do not emit ordinary usages, but their binding occurrence must
// be recorded before the subtree is skipped.
if (state->inside_import) {
if (is_import_binding_occurrence(ctx, node, spec)) {
char *binding_name = reference_name(ctx, node);
if (binding_name && binding_name[0] && !cbm_is_keyword(binding_name, ctx->language)) {
record_lexical_binding(ctx, state, node, spec, binding_name, true);
}
}
return;
}
bool python_scope_directive =
ctx->language == CBM_LANG_PYTHON && (lexical_ancestor_kind(node, "global_statement") ||
lexical_ancestor_kind(node, "nonlocal_statement"));
if (python_scope_directive || is_binding_occurrence(ctx, node, spec, state)) {
char *binding_name = reference_name(ctx, node);
if (binding_name && binding_name[0] && !cbm_is_keyword(binding_name, ctx->language)) {
record_lexical_binding(ctx, state, node, spec, binding_name, false);
}
return;
}
if (!reference_node) {
return;
}
if (is_write_occurrence(ctx, node, spec, state)) {
if (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL ||
ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE) {
char *binding_name = reference_name(ctx, node);
if (binding_name && binding_name[0] && !cbm_is_keyword(binding_name, ctx->language)) {
record_lexical_binding(ctx, state, node, spec, binding_name, false);
}
}
return;
}
char *name = reference_name(ctx, node);
if (name && name[0] && !cbm_is_keyword(name, ctx->language)) {
CBMUsage usage = {0};
usage.ref_name = name;
usage.enclosing_func_qn = state->enclosing_func_qn;
usage.lexical_scope_id = usage_lexical_scope_id_for_node(ctx, state, node);
stamp_usage_site(ctx, &usage, node, name, state);
cbm_usages_push(&ctx->result->usages, ctx->arena, usage);
}
}