Skip to content

WIP groundwork: flat token cursor for the parser [+0.3% as-is; needs lexer fusion]#57

Draft
xmakro wants to merge 5 commits into
perf/base-0713from
perf/flat-token-cursor
Draft

WIP groundwork: flat token cursor for the parser [+0.3% as-is; needs lexer fusion]#57
xmakro wants to merge 5 commits into
perf/base-0713from
perf/flat-token-cursor

Conversation

@xmakro

@xmakro xmakro commented Jul 14, 2026

Copy link
Copy Markdown
Owner

Strategic rearchitecture groundwork, benchmarked and banked rather than merged.

What it does: replaces the parser's tree-walking TokenCursor with a flat pre-materialized token buffer + integer cursor. bump = index increment; look_ahead = direct indexing (no cursor clone even for deep lookahead); Parser::clone (mbe black-box, ~30 diagnostic snapshots) = two refcount bumps instead of a stack Vec clone; lazy token-stream capture replay reads a shared slice; big-tt skips are O(1) via an open/close match table; Parser shrinks by 16 bytes. Skipped invisible delimiters stay in the buffer as entries (filtered on consumption) so tree-level lookahead and depth queries keep byte-exact semantics.

Correctness: bootstraps the compiler and passes the full ui suite with zero new diffs — the risky part of the rearchitecture is retired.

Performance as placed today: +0.29% geomean (syn check +0.96%). Root cause is structural: the tree is still built by the lexer and then flattened at Parser construction (an extra eager copy pass the lazy cursor never paid), and every expansion-output re-parse pays it too, which is most of the parsing in derive-heavy crates.

The profitable path, in order:

  1. Fuse flat emission into the lexer (it already produces tokens linearly and then builds the tree this change un-builds) — removes the per-delimited-group tree allocations and the flatten pass for primary parses.
  2. Make expansion-output re-parses flat-first, which is where syn-class crates actually spend parsing time.
  3. Only then consider trimming the TokenStream tree to lazily-built views.

Measurement: ThinLTO stage2, glibc, instructions:u, full scenario, measured on top of the whole perf stack.

xmakro and others added 5 commits July 14, 2026 21:06
…pre-flattened buffer

Replaces the parser's tree-walking TokenCursor with a single flat,
pre-materialized token buffer and an integer cursor: bump is an index
increment, lookahead is direct indexing, parser snapshots and lazy
token-stream replay share the buffer via two refcount bumps, and whole
delimited sequences skip in one step through an open/close match table.
Skipped invisible delimiters are retained as entries (filtered during
consumption) so tree-level lookahead and nesting-depth queries keep
their exact semantics; the buffer records per-entry depth for that.

Correctness is fully validated: the compiler bootstraps itself and the
complete ui suite passes with zero new diffs.

Current placement regresses instruction counts (+0.29 percent geomean,
syn check +0.96 percent): flattening at Parser construction adds an
eager copy pass per parsed stream, where the old cursor was lazy, and
every macro expansion output re-parse pays it. The profitable endgame,
in order: (1) fuse flat emission into the lexer, which today produces
tokens linearly and then builds the tree this change un-builds — that
also deletes the per-delimited-group tree allocations; (2) make
expansion-output re-parses flat-first as well, which is where
derive-heavy crates spend most of their parsing. Groundwork PR; not
for merging as is.
The lexer now lexes straight into the parser's flat token buffer
(FlatEntry entries + open/close match table), never materializing the
token tree it used to build: delimited groups become an open entry, the
contents, and a close entry patched into the match table. This deletes
the per-delimited-group tree allocations and the eager flatten pass at
Parser construction for every primary parse (crate root, mod files,
include!), which was the structural regression left by the previous
commit. Depth and spacing bookkeeping reproduce FlatTokenCursor::new
byte-for-byte; all delimiter-recovery diagnostics keep their exact
behavior (every recovery path still ends in Err, so only the diagnostic
side effects matter there).

The few consumers that need a real TokenStream (proc-macro from_str,
cmdline attrs, fake token streams for diagnostics) rebuild the tree
from the flat buffer via flat_range_to_stream, which also replaces the
parser's private rebuild_flat_subtree. Expansion-output re-parses still
go through Parser::new -> FlatTokenCursor::new; making those flat-first
is the next step.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… tt fragments splice as slices

transcribe() now builds the expansion output directly in the parser's
flat form through a FlatSink (open/close entries with match-table
patching), so the result-stack machinery disappears and
ParserAnyMacro re-parses expansion output with no flatten pass.
Captured $x:tt fragments become FlatTt: a slice of the source buffer
for delimited groups (two refcount bumps, spliced into the output by
memcpy with depth/match rebasing) or a by-value token for single
tokens, which may be unglued halves no slice can represent. The
maybe_use_metavar_location share/respan heuristic carries over: splice
verbatim on no-collision, otherwise rewrite the boundary delimiter
spans after splicing. AST-fragment metavars emit their invisible
delimiters straight into the sink and splice their token streams via
the shared tree-flatten path (FlatTokenCursor::new is now a trivial
wrapper over FlatSink::splice_stream). The rare mbe attr/derive rule
paths materialize a tree at the boundary for now.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…flat mbe matching

TokenStream gains a second backing: a lazily materialized view of one
nesting level of a flat token buffer (TokenStreamInner::Flat with a
OnceLock for the trees). Macro-invocation DelimArgs are parsed as such
views (two refcount bumps instead of rebuilding the tree), while
attributes and macro-definition bodies stay eager so long-lived nodes
do not pin token buffers. All tree-level accessors materialize
transparently on first use, and equality/hash/encode/stable-hash
operate on the materialized trees, so behavior and hashes are
unchanged for every consumer that actually needs a tree.

FlatTokenCursor gains an exclusive end bound (index/end shrunk to u32
to keep the cursor at 24 bytes) so a parser can run over a sub-range
of a shared buffer and see Eof at the range end. The mbe matcher and
Parser::new both recognize view-backed streams and parse straight from
the buffer — the per-invocation flatten pass and the args tree rebuild
are gone; the doc-comment desugaring pre-pass falls back to the eager
path in the rare case a doc comment appears in macro arguments.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dents scan

Two findings from adversarial review of the lazy-view commits:

The derived Debug on the view-backed TokenStream (and FlatTokenSlice)
printed the entire underlying buffer — a whole crate's tokens — for
every unexpanded macro call's arguments, making -Zunpretty=ast-tree
quadratic in crate size (2.4MB -> 1.49GB on 400 invocations). Manual
impls now print only the viewed range, in the pre-existing format.

The pre-expansion KeywordIdents lint iterated every macro invocation's
argument stream, silently materializing the tree the lazy views exist
to avoid — the optimization only survived for macro-generated
invocations. check_tokens now scans view entries in place; delimiter
entries reset the dollar state exactly like the tree walk.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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