Skip to content

perf(python): climb the walk cursor for scope-directive and default checks - #2419

Open
Fieldnote-Echo wants to merge 5 commits into
DeusData:mainfrom
Fieldnote-Echo:perf/py-lexical-ancestor-kind
Open

Fieldnote-Echo wants to merge 5 commits into
DeusData:mainfrom
Fieldnote-Echo:perf/py-lexical-ancestor-kind

Conversation

@Fieldnote-Echo

@Fieldnote-Echo Fieldnote-Echo commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #2417

What does this PR do?

For nearly every Python identifier, usage extraction climbs toward the root with ts_node_parent, looking for a global/nonlocal ancestor (lexical_ancestor_kind, internal/cbm/extract_usages.c:1883) and for a parameter default (python_default_value_reference, :1782). Each ts_node_parent call descends from the root again, so an expression N terms deep costs O(N³). On error-free trees the climbs now follow the walk's occurrence cursor (O(1) per hop, as #2352 did for ReScript). For global/nonlocal one parent step is enough, because the grammar makes those names direct children of the statement. Trees with errors keep today's climb. The graph is byte-identical on 8 corpora, error-recovery trees included.

Linux x86_64, 32 logical CPUs, GCC 16 main this PR
Definitions pass, generated r = c0*x + …, N = 1000 / 2000 16.8 s / unfinished at 90 s 40 / 150 ms
Definitions pass, sympy 1.14 resolvent_lookup.py alone 8.7-8.8 s 53-54 ms
parallel_extract median, 823 / 2063 ordinary .py files 1.54 / 4.75 s 1.15 / 3.58 s

extract_python_deep_default_parent_climbs_are_linear counts root-descending parent steps at 200 and 400 terms: 0 here, 62,544 and 245,044 on main with the same counter wired in. Unmodified main passes it, because its climbs do not report to the counter. repro_python_scope_directives_bind_the_named_scope covers the global/nonlocal binding.

Checklist

  • Every commit is signed off (git commit -s) — required, CI rejects
    unsigned commits (DCO, see CONTRIBUTING.md)
  • Tests pass locally (make -f Makefile.cbm test)
    (run as make -f Makefile.cbm test-par)
  • Lint passes (make -f Makefile.cbm lint-ci)
    (for this PR's changes: CI's lint job passes on this head, and local cppcheck 2.22 / clang-format 23 add no findings over main)
  • New behavior is covered by a test (reproduce-first for bug fixes)

Prepared with AI assistance (exploration, reproduction, verification, implementation); the spec and design decisions are mine, and I reviewed and ran everything above.

…hecks

For nearly every Python identifier, lexical_ancestor_kind and
python_default_value_reference climbed toward the root with ts_node_parent,
which re-descends from the root on every step, so a deep expression was
cubic. In error-free trees, climb the walk's occurrence cursor instead and
take a single parent step for global/nonlocal names, which the grammar makes
direct children; trees with errors keep the original climb.

sympy's polys/numberfields/resolvent_lookup.py drops from about 8.7 s to
54 ms in the definitions pass, ordinary Python extraction is about 25%
faster, and the graph is byte-identical.

Signed-off-by: Nelson Spence <nelson@projectnavi.ai>
@github-actions

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.

Signed-off-by: Nelson Spence <nelson@projectnavi.ai>
@DeusData DeusData added stability/performance Server crashes, OOM, hangs, high CPU/memory priority/normal Standard review queue; useful PR with ordinary maintainer urgency. labels Oct 19, 2026
@DeusData

Copy link
Copy Markdown
Owner

Thanks for the cursor-based Python walk improvement and the explicit note about the instrumented baseline. I have read the corpus-equivalence and error-tree limits; this remains pending review, with no new change request from this sweep.

Brings DeusData#2419 onto main a222715. The merge is clean: since the
branch's base 0f52d30, main has not changed
python_default_value_reference, lexical_ancestor_kind,
usage_lexical_scope_id_for_node, record_lexical_binding or
handle_usages, nor the occurrence-cursor helpers the change builds
on (reset_occurrence_cursor, occurrence_parent, the slow-parent test
counter). The merged tree is main plus exactly this PR's three files
(internal/cbm/extract_usages.c +57 -16,
tests/repro/repro_lexical_binding_precision.c +54,
tests/test_extraction.c +44).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData

Copy link
Copy Markdown
Owner

Thank you, @Fieldnote-Echo -- this is a careful, well-measured fix. Climbing the walk's occurrence cursor instead of ts_node_parent (the same approach #2352 took for ReScript), the single parent step for global/nonlocal names that the grammar guarantees, keeping error-recovery trees on the old climb, and your candid note about the instrumented baseline all made it straightforward to verify.

We pushed one commit to your branch:

  1. Merged current main. The merge is clean and your change is untouched: since your base, main has not modified python_default_value_reference, lexical_ancestor_kind, their call sites, or the occurrence-cursor helpers they use, so the merged tree is main plus exactly your three files.

What we checked on the merged tree:

  • extract_python_deep_default_parent_climbs_are_linear reports 0 root-descending steps at 200 and 400 terms. With both climbs forced back onto ts_node_parent it fails with 62,544 and 245,044 steps, matching your numbers, so the counter binds the change.
  • With the global/nonlocal detection disabled, repro_python_scope_directives_bind_the_named_scope fails on 7 of its 9 sites, in both the error-free and the error-recovery source.
  • sympy 1.14's polys/numberfields/resolvent_lookup.py indexes in 0.9 s instead of 8.6-8.9 s end to end, and a generated 1,000-term expression in 0.9 s instead of 15.5 s.
  • The graph (every node and edge, with properties) is identical to main's on Django (322,460 edges), the Python 3.14 standard library (574,632 edges), sympy (247,460 edges) and the generated file.
  • The extraction, parallel, and usage/lexical-binding suites pass (738 tests), and cppcheck and clang-format are clean on extract_usages.c.

Thanks again for the thorough measurements and the clear write-up.

@DeusData

Copy link
Copy Markdown
Owner

Thank you again for the Python cursor improvement. The October updates record the branch refresh and targeted checks as completed on our side. No further contributor work is requested; sorry for the delay in the remaining follow-up, and I’ll follow up soon.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData

Copy link
Copy Markdown
Owner

Thanks again, @Fieldnote-Echo. We merged current main into your branch once more: one merge commit on top, your three files untouched, and the merge was clean. The earlier red checks came from a wording change on main's website that tripped a docs consistency check on every test leg. Main has fixed that since, and this refresh brings the fix in. While the checks run, we are also comparing graph quality between main and this branch on Django and the Python standard library, which we now do for every performance change before merging. If you push again, please pull first so the merge commit is kept.

This branch has not been deployed

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

Labels

priority/normal Standard review queue; useful PR with ordinary maintainer urgency. stability/performance Server crashes, OOM, hangs, high CPU/memory

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf(python): usage extraction climbs every ancestor per identifier, cubic in expression depth

2 participants