Skip to content

fix: grow the ts_lsp AST walk stacks so wide files keep every declaration - #2091

Open
CaptainMittens wants to merge 2 commits into
DeusData:mainfrom
CaptainMittens:fix/ts-lsp-walk-stack-drops-nodes
Open

fix: grow the ts_lsp AST walk stacks so wide files keep every declaration#2091
CaptainMittens wants to merge 2 commits into
DeusData:mainfrom
CaptainMittens:fix/ts-lsp-walk-stack-drops-nodes

Conversation

@CaptainMittens

@CaptainMittens CaptainMittens commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

The problem

Three AST walks in internal/cbm/lsp/ts_lsp.c held at most 256 nodes in a fixed TSNode stack[256] and stopped pushing when it filled. Nothing reported the stop, so each pass answered as if it had seen the whole file.

In rebuild_signatures_from_ast and convert_signature_type_params the seeding loop alone can fill all 256 slots, because it pushes every top-level child of the file. A file with more than 256 top-level declarations therefore skipped every declaration past that point. Each one kept whatever signature the first extraction pass had guessed, and a generic function past the cap kept NAMED where it needed TYPE_PARAM, so calls through it stopped resolving.

In infer_return_type_from_body the same cap made a wide function body answer "unknown return type" for a return the walk had never reached.

The fix

The repo already carries the module that solves this: internal/cbm/extract_node_stack.h, written for #199 to replace fixed TSNode stack[] arrays that silently drop AST subtrees. Nine extract files use it. ts_lsp.c was never converted. This converts it.

The header gains ts_nstack_init_arena, which takes an arena directly, because the LSP passes hold a TSLSPContext rather than a CBMExtractCtx. ts_nstack_init keeps its context argument — that argument is what makes handing over the wrong arena a type error — and now delegates to the new function, so its behaviour is unchanged.

Two things follow from using the shared module:

  • Traversal order flips from last-child-first to first-child-first, because ts_nstack_push_children reverses on push. For infer_return_type_from_body the new order matches the comment the function already carried: it finds the first return, where the old code found the last.
  • One O(N^2) loop goes away. ts_nstack_push_children does a single cursor pass instead of ts_node_child(n, i) per child.

Which arena

The stacks are cut from ctx->arena. collect_children at ts_lsp.c:201 already allocates the same kind of per-file scratch from that arena, and the per-file scratch arena added in #1997 never reaches cbm_run_ts_lspcbm.c:1690 passes a. Threading a second arena into this file would touch every allocation in it, which is a larger change than this fix needs.

Test

Two tests, one per observable site. Each repeats an existing passing test's claim, moved past the old cap:

  • tslsp_stress_declarations_past_stack_cap — the claim of tslsp_generic_identity_inference, 300 top-level declarations deeper. Covers convert_signature_type_params.
  • tslsp_stress_return_past_stack_cap — the claim of tslsp_return_inferred_local, with the return behind 300 statements. Covers infer_return_type_from_body.

Without this change the suite is 303 passed, 2 failed, both failures being these two. With it, 305 passed, 0 failed.

rebuild_signatures_from_ast has no test of its own: it shares the seeding shape with convert_signature_type_params, and I could not find an observable difference it produces alone that the first test does not already cover.

The full local suite is 7949 passed, 2 failed, 7 skipped. Both failures are subprocess_run_clean and subprocess_run_exit_nonzero, which return CBM_PROC_SPAWN_FAILED because my local sandbox blocks fork/exec of /bin/sh. Nothing in this diff reaches fork or exec, so they are unrelated to it; I did not re-run the full suite on the reverted tree, only the ts_lsp suite.

…tion

Three walks in internal/cbm/lsp/ts_lsp.c held at most 256 AST nodes in a
fixed array and stopped pushing when it filled. Nothing reported the stop,
so the passes answered as if they had seen the whole file.

In rebuild_signatures_from_ast and convert_signature_type_params the
seeding loop alone can fill all 256 slots, because it pushes every
top-level child of the file. A file with more than 256 top-level
declarations therefore skipped every declaration past that point: each one
kept whatever signature the first extraction pass had guessed, and a
generic function past the cap kept NAMED instead of TYPE_PARAM, so calls
through it stopped resolving. In infer_return_type_from_body the same cap
made a wide function body answer "unknown return type" for a return the
walk had never reached.

The repo already carries the module that solves this:
internal/cbm/extract_node_stack.h, written for GitHub issue DeusData#199 to
replace fixed TSNode stack[] arrays that silently drop subtrees. Nine
extract files use it; ts_lsp.c was never converted. This converts it.

The header gains ts_nstack_init_arena, which takes an arena directly,
because the LSP passes hold a TSLSPContext rather than a CBMExtractCtx.
ts_nstack_init keeps its context argument, which is what makes handing
over the wrong arena a type error, and now delegates to the new function.

Traversal order changes from last-child-first to first-child-first,
because ts_nstack_push_children reverses on push. That also drops an
O(N^2) ts_node_child(n, i) loop for one cursor pass. For
infer_return_type_from_body the new order matches the comment the
function already carried: it finds the first return, where the old code
found the last.

New test tslsp_stress_declarations_past_stack_cap makes the same claim as
tslsp_generic_identity_inference, 300 declarations deeper in the file.
Without this change it fails with "MISSING resolved call: caller~.go ->
callee~use"; with it the ts_lsp suite is 304 passed, 0 failed.

Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown

Thanks for opening this — it has been seen, and it is queued.

This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence.

Current review status: working through a backlog. 0.9.1-rc.1 is out, so the release freeze that held reviews is over — but it left a large queue of open pull requests behind it, and we are reading through them oldest-first. The background is in discussion #1144.

What that means for this PR, concretely:

  • It will not be closed for inactivity. No stale bot touches pull requests here.
  • It may still sit a while before a human reads it. That is on us, not on you.
  • Older PRs are read first, so a recent one is not being skipped — it is behind a queue.

Things that will genuinely speed it up whenever review does happen:

  • Keep it rebased on main — the tree is moving quickly right now, and a conflicting branch cannot be reviewed as the diff you intended.
  • Get CI green, or say which failures you believe are pre-existing.
  • Keep the change to one claim. Bundled features and refactors get split before they get merged, which costs you a round trip.
  • Every commit needs a sign-off (git commit -s) — CI enforces DCO.

If this fixes a bug, a reproduction we can run is worth more than a description of the symptom.

Thanks for contributing, and sorry in advance for the wait.

The first commit converted three walks in ts_lsp.c but tested only two of
them. infer_return_type_from_body was changed without a test of its own.

tslsp_stress_return_past_stack_cap makes the same claim as the existing
tslsp_return_inferred_local, with the return placed after 300 statements
— more than the 256 slots the old fixed array held, so the walk used to
stop before reaching it and answer "unknown return type".

Without the fix the suite is 303 passed, 2 failed, the second failure
being this test. With it, 305 passed.

Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
@CaptainMittens

CaptainMittens commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

test / test-windows-guards is red on ec56ea8f. It is not this branch — it is the section_cold_storm race in tests/windows/test_daemon_stability.py, which is already open as #2057:

RED: cold-storm client 0 failed (racing daemon spawn):
codebase-memory-mcp: secure CLI coordination could not be created (endpoint)

Three things say it is unrelated:

  1. The same job passed on this branch's previous commit b974adf6, which already carried the whole ts_lsp.c and header change. The only difference in ec56ea8f is 30 lines inside tests/test_ts_lsp.c.
  2. The other six sections of that guard passed in the same run, including the permanent-daemon one.
  3. The identical failure appears on two other recent PR runs on unrelated branches — 34118689934 and 34098390879 — each with RED: cold-storm client 0 failed (racing daemon spawn) as the only red. That is the pattern test-windows-guards: test_daemon_stability turns setup failures and timeouts into REGRESSION verdicts on unrelated PRs #2057 describes.

I cannot re-run the job myself (gh run rerun needs admin rights here). Could you re-run that one job when convenient?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant