perf(extract): O(1) walk-state maintenance; gate parent probes by language

The deep-nesting torture tests (stack_overflow_a/b) went from 0-1s per test on
main to 39-119s on this branch -- the 900s suite budget killed them on every
venue except the M4 (three GitHub CI platforms and the local Linux leg, each
dying mid-suite at a DIFFERENT test, which is what pointed at shared machinery
rather than any one language).

Sampling the child process found two quadratic layers, both branch-added:

1. recompute_state iterated the WHOLE scope stack on every code-bearing node
   to rebuild the walk-state flags -- O(depth) per node, and a deep descent
   pushes a frame per level, so deep trees paid O(n x depth). Each frame now
   saves the complete walk-state tuple it displaces and pop restores it
   verbatim: push and pop are O(1) and kind-agnostic, and the per-node
   recompute is gone entirely. The CALL frame's effect is applied by
   push_call_scope after the caller fills the invocation triple, preserving
   the old ordering exactly.

2. is_reference_node fetched ts_node_parent for EVERY identifier in EVERY
   language to serve a Puppet/Vimscript sigil-wrapper check -- the language
   gate sat inside the condition, after the fetch. ts_node_parent descends
   from the root (O(depth)), so all languages paid O(depth) per identifier.
   The gate now precedes the fetch; only Puppet/Vimscript files pay it.

macOS, both suites together: 543s -> 54s. Per test: ts_cyclic 119s -> 6s,
python_deep 105s -> 6s, go_deep 39s -> 8s, php_deep 45s -> 8s. The residual
6-8s vs main's 0-1s is the branch's larger legitimate per-node work; the
remaining Python attribute-site parent walk is a bounded follow-up, recorded.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-08-03 00:16:12 +02:00
1 parent 43e861e36a
commit 004a9a499f
3 files changed
+98 -56

No files matched your search

+75 -54
View File
@@ -123,13 +123,49 @@ static bool push_scope(WalkState *state, uint8_t kind, uint32_t depth, const cha
if (!ensure_scope_capacity(state)) {
return false;
}
state->scopes[state->scope_top].kind = kind;
state->scopes[state->scope_top].depth = depth;
state->scopes[state->scope_top].qn = qn;
state->scopes[state->scope_top].lexical_scope_id = 0;
state->scopes[state->scope_top].invocation_kind = CBM_INVOCATION_NONE;
state->scopes[state->scope_top].callee_expr = (TSNode){0};
state->scopes[state->scope_top].callee_leaf = (TSNode){0};
CBMWalkScope *f = &state->scopes[state->scope_top];
f->kind = kind;
f->depth = depth;
f->qn = qn;
f->lexical_scope_id = 0;
f->invocation_kind = CBM_INVOCATION_NONE;
f->callee_expr = (TSNode){0};
f->callee_leaf = (TSNode){0};
/* Save the complete displaced tuple, then apply this frame's effect --
* pop_expired_scopes restores verbatim. O(1) either way; see the
* CBMWalkScope comment for the quadratic recompute this replaces. */
f->prev_enclosing_func_qn = state->enclosing_func_qn;
f->prev_enclosing_class_qn = state->enclosing_class_qn;
f->prev_invocation_kind = state->invocation_kind;
f->prev_callee_expr = state->callee_expr;
f->prev_callee_leaf = state->callee_leaf;
f->prev_inside_import = state->inside_import;
f->prev_loop_depth = state->loop_depth;
f->prev_branch_depth = state->branch_depth;
switch (kind) {
case SCOPE_FUNC:
state->enclosing_func_qn = qn;
break;
case SCOPE_CLASS:
case SCOPE_NAMESPACE:
state->enclosing_class_qn = qn;
break;
case SCOPE_CALL:
/* The invocation triple is applied by push_call_scope once the caller
* has filled the frame fields. */
break;
case SCOPE_IMPORT:
state->inside_import = true;
break;
case SCOPE_LOOP:
state->loop_depth++;
break;
case SCOPE_BRANCH:
state->branch_depth++;
break;
default:
break;
}
state->scope_top++;
return true;
}
@@ -203,52 +239,27 @@ static void push_call_scope(WalkState *state, uint32_t depth,
state->scopes[state->scope_top - SKIP_ONE].invocation_kind = invocation->kind;
state->scopes[state->scope_top - SKIP_ONE].callee_expr = invocation->callee_expr;
state->scopes[state->scope_top - SKIP_ONE].callee_leaf = invocation->callee_leaf;
/* Apply the CALL effect (push_scope saved the displaced tuple already;
* the frame fields had to be filled first). */
state->invocation_kind = invocation->kind;
state->callee_expr = invocation->callee_expr;
state->callee_leaf = invocation->callee_leaf;
}
// Pop scopes that we've ascended out of (depth >= current cursor depth).
// Pop scopes that we've ascended out of (depth >= current cursor depth),
// restoring the walk-state tuple each frame displaced. LIFO order keeps the
// restores exact; O(1) per frame.
static void pop_expired_scopes(WalkState *state, uint32_t cur_depth) {
while (state->scope_top > 0 && state->scopes[state->scope_top - SKIP_ONE].depth >= cur_depth) {
state->scope_top--;
}
}
// Recompute state flags from the current scope stack.
static void recompute_state(WalkState *state, const char *module_qn) {
state->enclosing_func_qn = module_qn;
state->enclosing_class_qn = NULL;
state->invocation_kind = CBM_INVOCATION_NONE;
state->callee_expr = (TSNode){0};
state->callee_leaf = (TSNode){0};
state->inside_import = false;
state->loop_depth = 0;
state->branch_depth = 0;
for (int i = 0; i < state->scope_top; i++) {
switch (state->scopes[i].kind) {
case SCOPE_FUNC:
state->enclosing_func_qn = state->scopes[i].qn;
break;
case SCOPE_CLASS:
case SCOPE_NAMESPACE:
state->enclosing_class_qn = state->scopes[i].qn;
break;
case SCOPE_CALL:
state->invocation_kind = state->scopes[i].invocation_kind;
state->callee_expr = state->scopes[i].callee_expr;
state->callee_leaf = state->scopes[i].callee_leaf;
break;
case SCOPE_IMPORT:
state->inside_import = true;
break;
case SCOPE_LOOP:
state->loop_depth++;
break;
case SCOPE_BRANCH:
state->branch_depth++;
break;
default:
break;
}
const CBMWalkScope *f = &state->scopes[--state->scope_top];
state->enclosing_func_qn = f->prev_enclosing_func_qn;
state->enclosing_class_qn = f->prev_enclosing_class_qn;
state->invocation_kind = f->prev_invocation_kind;
state->callee_expr = f->prev_callee_expr;
state->callee_leaf = f->prev_callee_leaf;
state->inside_import = f->prev_inside_import;
state->loop_depth = f->prev_loop_depth;
state->branch_depth = f->prev_branch_depth;
}
}
@@ -2089,6 +2100,16 @@ void cbm_extract_unified(CBMExtractCtx *ctx) {
state.lexical_scope_capacity = INLINE_LEXICAL_SCOPES;
state.usage_start_index = ctx->result->usages.count;
state.root_lexical_scope_id = add_lexical_scope(&state, ctx->root, CBM_LEXICAL_SCOPE_MODULE);
/* Base walk-state tuple (previously established by the first
* recompute_state call): module scope, nothing else active. */
state.enclosing_func_qn = ctx->module_qn;
state.enclosing_class_qn = NULL;
state.invocation_kind = CBM_INVOCATION_NONE;
state.callee_expr = (TSNode){0};
state.callee_leaf = (TSNode){0};
state.inside_import = false;
state.loop_depth = 0;
state.branch_depth = 0;
uint32_t depth = 0;
@@ -2097,12 +2118,12 @@ void cbm_extract_unified(CBMExtractCtx *ctx) {
bool trivia = is_unified_trivia_node(node);
if (!trivia) {
/* Trivia consumes no semantic state. Scope expiry may be deferred
* until the next code-bearing node, which recomputes before use. */
* until the next code-bearing node; pop restores the displaced
* tuple and push applies the new frame's effect, both O(1) --
* the old whole-stack recompute here was O(depth) per node and
* quadratic on the deep-nesting torture tests. */
pop_expired_scopes(&state, depth);
recompute_state(&state, ctx->module_qn);
if (push_pre_node_scope(ctx, node, spec, &state, depth)) {
recompute_state(&state, ctx->module_qn);
}
(void)push_pre_node_scope(ctx, node, spec, &state, depth);
handle_string_constants(ctx, node, &state);
handle_objectscript_type_map(ctx, node, &state);
+14
View File
@@ -59,6 +59,20 @@ typedef struct {
CBMInvocationKind invocation_kind;
TSNode callee_expr;
TSNode callee_leaf;
/* The complete walk-state tuple this frame displaced, restored verbatim on
* pop. Saving the full tuple makes push and pop O(1) and kind-agnostic;
* the previous design recomputed the state by iterating the WHOLE scope
* stack on every code-bearing node, which is O(depth) per node and turned
* the deep-nesting torture tests quadratic (0-1s on main, 39-119s here,
* suite-budget kills on every non-M4 venue). */
const char *prev_enclosing_func_qn;
const char *prev_enclosing_class_qn;
CBMInvocationKind prev_invocation_kind;
TSNode prev_callee_expr;
TSNode prev_callee_leaf;
bool prev_inside_import;
int prev_loop_depth;
int prev_branch_depth;
} CBMWalkScope;
typedef enum {
+9 -2
View File
@@ -106,8 +106,15 @@ static bool is_reference_node(TSNode node, CBMLanguage lang) {
/* 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`). */
if (strcmp(kind, "identifier") == 0) {
* 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) ||