Repository navigation
Use a flat Python AST in Ruff and ty - #29817
MichaReiser wants to merge 56 commits into
Conversation
Typing conformance resultsNo changes detected ✅Current numbersThe percentage of diagnostics emitted that were expected errors held steady at 98.60%. The percentage of expected errors that received a diagnostic held steady at 98.86%. The number of fully passing files held steady at 137/146. Results for 7610fac · Workflow run |
Memory usage reportSummary
Significant changesClick to expand detailed breakdownflake8
trio
sphinx
prefect
Results for 7610fac · Workflow run |
|
|
| let rewritten = owned_pattern_to_expr(pattern.view(storage).to_owned_node()); | ||
| let id = ast::flat::Lower::lower(&rewritten, storage); |
There was a problem hiding this comment.
[P3] Avoid retaining entire subtrees during pattern recovery
A malformed class pattern whose head is a sequence reaches this conversion at each nesting level. to_owned_node() copies the entire previously recovered subtree, and Lower::lower appends its literal bytes to the same Builder without reclaiming the originals. Nesting around one large string therefore retains memory proportional to literal size × nesting depth, allowing an attacker-controlled file in an untrusted language-server workspace to exhaust host memory. The previous implementation moved owned children. Construct the recovery nodes using existing child handles to avoid retaining these repeated copies.
Merging this PR will regress 1 benchmark
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance Changes
Tip Investigate this regression by commenting Comparing Footnotes
|
bd231d4 to
59e0aa9
Compare
|
YES |
46a2f59 to
8b89865
Compare
8b89865 to
1d9bbf2
Compare
8eff396 to
f64d1e4
Compare
Summary
Ruff and ty parse Python directly into a table of 24-byte AST rows. Generated typed accessors resolve four-byte child handles and borrow fields from the parsed module. Expression and statement consumers pass
View<Expr>andView<Stmt>by value and callclassify()to match individual variants. Ruff lint rules can materialize a subtree when constructing replacement syntax. ty compacts the backing storage before caching a module and resolves its saved node references directly by arena row. Python AST storage and traversalReviewing the change
Start with the architecture guide in commit 2, then review the schema in commit 1 and parser construction in commit 3.
trybody. Ruff lint rulesAsFormatimplementation covers every formattable AST view. Use the flat AST in Ruff’s formatterThe parser and AST crates build at commit 3, Ruff’s linter at commit 5, and its formatter at commit 6. The full workspace builds from commit 8.
Performance
CodSpeed compared
310009awith3b5d4a6and reported 169 improved, 1 regressed, 114 unchanged, and 14 skipped. Pre-parsed linting with all preview rules onnumpy/globals.pyhad 4.17% lower simulated efficiency. Parser peak heap onlarge/dataset.pyfell from 645.3 KB to 513.3 KB (20.5%). Benchmark reportIn the earlier comparison below, the local CodSpeed runner executed 1,050,949 instructions on
86b7fd1and 1,013,610 ond22dfa8(3.5% fewer). Simulated L1 instruction-cache misses rose from 24,982 to 28,561 (14.3%), while L1 data-read misses fell from 2,631 to 2,236. The two functions that dispatch expression and statement rules grew from 45.5 to 65.8 KiB and 40.9 to 51.8 KiB in these instrumented binaries. Most of the instruction-cache difference was present before the ty node-ID change. Its parent recorded 28,264 misses.Native Criterion timings for pre-parsed preview lint on one core were inconclusive against the earlier base. Across 24 alternating pairs,
d22dfa8took a median 0.45% less time than86b7fd1(95% bootstrap interval, 0.70% less time to 0.13% more). Against the boxed-AST revision52cca7e, it took 0.68% less time across 18 pairs (95% interval, 0.37% to 1.85% less).Same-machine CLI measurements
These runs compare
d22dfa8and52cca7eon the Python 3.12 standard library with four workers pinned to the same four cores. Each command produced identical output before timing. The time change is the median of 33 alternating pairs. The interval is a 95% bootstrap interval over those pairs.Stripped binary sizes for these builds were Ruff +1.91% (27.50 to 28.02 MiB) and ty -0.49% (35.16 to 34.99 MiB).
Linting an already-parsed module
The repository’s linter benchmark parses the module before the timed batch. Each cell shows the median elapsed-time change over eight alternating pairs of
52cca7eanda4ee55don one CPU core. Negative values mean less time. Linter benchmarklarge/dataset.pynumpy/ctypeslib.pynumpy/globals.pypydantic/types.pyunicode/pypinyin.pyA separate 24-block run repeated
numpy/globals.pyon the same core with balanced execution order. The paired time changes and 95% bootstrap intervals were -3.22% (-4.12% to -1.99%) for default rules, +1.53% (+0.66% to +2.13%) for all stable rules, -0.33% (-0.74% to +0.44%) for all rules with preview.Callgrind counted 0.66% to 2.60% fewer instructions in the pre-parsed, all-stable lint kernel across the five fixtures. On
numpy/globals.py, it counted 1.16% fewer instructions and 9.48% more simulated L1 instruction-cache misses. The cache counts come from simulation, while the elapsed times above come from native execution.ty module cache
Caching an ordinary module trims the parser's buffers and uses the existing arena rows as node IDs. The two source-order lookup tables took 77,580 bytes on the 9,698-row
large/dataset.pyfixture. The complete cached parse now takes 432,147 bytes, down from 509,719 bytes. Over 60 samples, median cache preparation time on that fixture fell from 117.33 µs to 1.25 µs. Callgrind counted 1,015,995 instructions per call to prepare the arena ona4ee55dand 7,668 ond22dfa8. Node identityIsolating the node-ID change (
a4ee55dtod22dfa8) changed end-to-endty checktime on the standard library by -6.64% (95% interval -10.40% to -3.30%) and peak RSS by -3.36 MiB over 33 pairs. In 32 alternating pairs per project, Pygments 2.17.2 took 7.46% less time (95% interval, 2.49% to 15.96% less). Jinja2 3.1.2 took 1.96% more time (95% interval, 2.09% less to 8.96% more). Median peak RSS fell by 2.55 MiB and 1.79 MiB, respectively. Diagnostics matched in every pair.Native lint, traversal, CLI, and binary size measurements
The native measurements below compare
243a767with boxed baselinedc988f6, before the rebase and recovery change.The native lint benchmarks start with a parsed module. Each cell shows the median elapsed-time change in eight alternating pairs on one CPU core. Negative values mean less time.
large/dataset.pynumpy/ctypeslib.pynumpy/globals.pypydantic/types.pyunicode/pypinyin.pyAcross these 15 lint-only cases, the simulator counted 1.00% to 13.78% fewer instructions than on the boxed baseline.
On
numpy/globals.py, a separate 21-pair measurement gave −0.66% with all stable rules (95% interval −1.37% to +0.36%) and −1.15% with preview rules enabled (95% interval −1.70% to −0.38%).The source-order visitor used 9.20% to 18.32% fewer instructions than the boxed visitor across these five files. The descendant iterator used 4.32% to 17.22% fewer. Node counts and checksums of node kinds and source ranges matched in every case. Peak allocated bytes during parsing fell by 18.67% to 32.24%.
Each CLI comparison ran both profiling builds on the CPython 3.12 standard library in 33 pairs using four workers. The commands produced identical output. Elapsed time is the median paired difference. Intervals are distribution-free 95% confidence intervals for that median. RSS is the difference in median peak resident memory.
In the stripped Linux profiling binaries, Ruff grew by 587 KiB (+2.08%) and ty grew by 70 KiB (+0.20%).
Test plan
All 9,464 selected workspace tests passed with the file-watching test binary excluded. Strict workspace Clippy passed across all targets and features. The tests cover resolving handles after cache eviction, quoted annotation key limits, IDE annotation lookup, and dynamic class identity across edits. The recovery change produced identical ASTs and parser errors on 562 inputs, including all 561 Python parser fixtures.