Skip to content

Definition-anchored line-table dependencies for offset-based span hashing - #99

Draft
xmakro wants to merge 3 commits into
mainfrom
perf/def-anchored-lines
Draft

Definition-anchored line-table dependencies for offset-based span hashing#99
xmakro wants to merge 3 commits into
mainfrom
perf/def-anchored-lines

Conversation

@xmakro

@xmakro xmakro commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Replaces the witness-and-lint enforcement that was previously in this PR: same span-hashing change, same fused def_anchor query, but soundness no longer depends on enumerating rendering sites. Line observations are tracked automatically at the lookup layer, and an observation that no anchor covers makes the observing task unconditionally red instead of silently going stale.

Span fingerprints and the TAG_FULL_SPAN cache encoding are as before: (file, offset in file, length), no line-table work in hashing, CachingSourceMapView deleted, hardened offset-range assert on decode. What changes is how line-observing consumers are invalidated.

The query

def_anchor(DefId) is an eval_always query hashing everything that determines rendered line/column values for positions within a definition's extent: the file id, the line index of the definition's start, the definition's column on that line, the lengths of the extent's lines, and the multibyte-character and position-normalization entries inside the extent relative to its start (the line begin of the definition's first line, so character columns of a definition starting mid-line after a sibling are covered). Lengths are the shift-invariant representation of line structure, and aligned runs of 64 of them come from a per-file cache built lazily in one pass, so a large extent costs its edges plus one cached fingerprint per covered block. See SourceFile::line_extent_hash. Anchors coalesce typeck children (closures, coroutines, inline consts) into their root definition's node: the root's extent contains theirs and they codegen into the root's CGU anyway, so per-child nodes would add count without precision.

line(pos) for a position in the extent is the anchor line plus the count of line starts between the definition's start and pos, and columns count from the last line start at or before pos, so the hash covers exactly the rendered values. Consequences:

  • An edit after the extent changes nothing.
  • An edit before the extent that only shifts byte offsets changes nothing: the relative entries, the anchor line and the anchor column are unchanged.
  • An edit before the extent that adds or removes a line break changes the anchor line: renderers re-run, which is the correct outcome (their rendered lines changed).
  • A net-zero line move inside the extent (the Incremental build panic running cargo test rust-lang/rust#74890 class) changes the relative entries: covered.
  • A definition moving within its line changes the anchor column: covered. This entry is what makes the query a complete description of the rendered values, which the next PR in the stack relies on when it stops fingerprinting definitions' absolute positions.

Soundness: automatic tracking, fail red

The previous revision enforced anchoring by threading a zero-sized witness through the codegen backends and denying raw lookups with an internal lint. That put the soundness argument in the review process: every line-rendering site had to be found, converted, and kept converted, and out-of-tree backends could not use the lint at all. This revision inverts the posture.

SourceFile::lookup_line and the accessors built on it notify rustc_span::LINE_TABLE_TRACK, a callback installed by rustc_interface in the same way as the existing SPAN_TRACK. Inside a dep task the callback resolves every observation in one of three ways:

  • Covered. TyCtxt::track_def_anchor records the def_anchor dependency and registers the anchored extent in the task's TaskDeps. An observation at a position inside a registered extent is already invalidated by the recorded anchor and needs nothing further.
  • Anchored on the fly. An observation on a parented span whose position no registered extent covers records the parent definition's anchor and retries the containment check. This makes parented renderings sound with no per-site wiring at all.
  • Fail red. Anything else reads FOREVER_RED_NODE: the observing task re-executes every session. A missed anchor, a mismatched anchor whose extent does not contain the rendered position, or a rendering class nobody thought about costs reuse, never staleness.

The anchors themselves are tiered by how much of the recorded span data can be trusted, computed by the session-memoized def_anchor_extent query (eval_always no_hash: consumers read it without a dependency, and its result carries session-local positions):

  • A definition not created by any expansion (expn_that_defined is the root) anchors its full source_span extent: the precise per-definition channel. A file module anchors its inner span, because its source_span is the mod foo; declaration in the parent file.
  • A definition created by a bang macro anchors its own recorded extent (passthrough $item tokens keep their real spans, so a function inside a cfg-style wrapper keeps its true extent) plus the invocation's extent; def-site renderings from the macro's own file are covered by additionally anchoring the macro's definition.
  • A derive-generated definition anchors the annotated type's extent widened over the derive's call site: the body carries the type's field spans, and the generated items' collapsed decl positions render at the derive path inside the attribute, which item spans do not include.
  • A definition re-emitted by an attribute macro (the tracing::instrument class, whose recorded spans cover a few signature tokens) anchors the hull of its HIR spans within its own file: the one tier that walks HIR, on the rarest class.
  • A foreign definition anchors crate_source_anchor(cnum): a digest over (file id, content hash) of every source file of the crate, precomputed at metadata encode time (CrateRoot::source_files_digest), so the node costs a single metadata read to re-execute during try-mark-green. Foreign metadata only carries shrunk signature spans, and content hashes cover line structure.
  • Registered coverage widens to line boundaries (rendering a column reads the line's start, which for a mid-line extent lies just before it), and observations at a file's first byte are skipped: dummy spans render there, and the values (line 1, column 1) are constants.

The expansion anchors are tiered the same way: a bang invocation's expn_anchor hashes the call-site extent (the whole foo!(...) span), while derive and attribute invocations, whose recorded call sites are just the path, stay file-granular.

Outside dep tasks the callback is a no-op, and diagnostic emission already renders under Ignore (see track_diagnostic), so error rendering costs nothing. eval_always tasks discard reads, which also makes the anchor providers themselves (which walk the line table) exempt by construction.

The explicit anchor calls remain, but as precision opt-ins rather than soundness obligations: codegen_mir anchors the instance root once so the per-instruction debug-location lookups resolve as covered, the cg_ssa funnels anchor spans parented outside the instance (inlined callee bodies), coverage anchors the body definition, and the type pretty-printer anchors the printed definition for {closure@file:line:col} strings that reach cached, replayed diagnostics. Deleting any of these calls would turn the affected consumers red, not stale.

This also removes the residual known gap of the previous revision, and the fallback earned its keep immediately: converting to fail-red surfaced two real staleness bugs in the previous revision of this stack. The anchors hashed def_span extents, which are shrunk to the signature, so a net-zero line move inside a function body invalidated nothing while its rendered #[track_caller] and debuginfo lines changed (a third build pass in incr-track-caller-line-shift now pins this), and expansion-created definitions were covered by extents that never contained their rendered positions. Both classes previously passed the test suite by silently staying green; under fail-red they showed up as reuse failures and are now anchored correctly. The expansion-anchor PR later in the stack turns the remaining parentless call-site class back into a precise dependency, as an optimization on a sound base rather than a soundness patch.

Compared to the previous revision this deletes the DefAnchored witness type and its threading through the cg_ssa debuginfo traits, cg_llvm, cg_gcc and cg_clif (the backend trait signatures revert to upstream), the rustc::untracked_line_lookup internal lint with its attribute plumbing, and the known-gaps section. The line lookups that legitimately bypass the hook (the anchor providers, metadata encoding of the line table, and post-lookup indexing at sites that already notified) use explicit *_untracked accessors.

What this deletes compared to #91 is unchanged: KeyFingerprintStyle::SelfHash and the new DepNodeKey impl, the truncated file-id index on the SourceMap and its collision abort, the import-all-crates fallback in the provider, the Option/sentinel for vanished files, and the bucket-boundary arithmetic.

Wiring

  • codegen_mir anchors each codegened function once when debuginfo is enabled: that covers its DW_AT_decl_line and every body position within its extent.
  • The cg_ssa debuginfo funnels record the anchor of spans parented outside the current instance, which covers inlined callee bodies from other files; dbg_scope_fn records the callee's anchor for the inlined function's own declaration position. cg_clif does the equivalent in its own codegen loop, including for statics' DW_AT_decl_line; cg_gcc is covered by the cg_ssa paths.
  • Coverage anchors the body definition (TyCtxt::source_file_tracked(pos, def)), which is finer than a whole-file dependency. A mapping span outside the anchored extent trips the fallback and costs that codegen task's reuse rather than emitting stale coordinates.
  • span_as_caller_location and the const-eval location renderer take the calling definition as the fallback anchor for parentless cause spans; parented cause spans use their parent.
  • The type pretty-printer's {closure@file:line:col} and <impl at ...> forms anchor to the printed definition.
  • -Zdebug-info-type-line-numbers type DIEs and coroutine variant DIEs anchor to the rendered definition or the span's parent, falling back to the coroutine definition for synthesized parentless variant spans.

Validation

x check green including cg_clif and cg_gcc, tidy and fmt clean, 50/50 rustc_span unit tests, 7/7 run-make (incr-track-caller-line-shift including the new intra-body third pass, incr-line-table-bucket-boundary, incr-foreign-line-table-dep, incr-coverage-line-move, incr-multibyte-caller-column, incr-anchor-move, incr-macro-static-decl-line), 178/178 tests/incremental, 634 closure/track_caller ui tests. The reuse assertions in the run-make tests double as enforcement tests: a hole in the extent-coverage bookkeeping fails rustc_partition_reused instead of going stale. Fallback-frequency probes on real crates (regex-automata, cargo at Debug and Opt with incremental) show zero uncovered codegen renderings on regex-automata and a residual tail of ~20 on cargo, plus a class inside early_lint_checks, which reads the untracked AST and re-executes every session regardless.

Perf

Standalone, this PR measures full -0.22%, incr-full +0.03%, incr-unchanged +0.38%, incr-patched -0.42%, all cells -0.10% against the merge-base: broad small wins (check incr-unchanged around -1.7% on most crates) against cargo's macro-heavy cells going red (opt incr-unchanged +21.0%), because the renderers' expansion-side coverage only lands with the expansion-anchor PR, and the position-carrying source_span fingerprint is only removed by the next PR. The stack is where the design pays; the standalone shape is the expected one.

Same-day A/B against the merge-base (fresh base run) on the usual setup (instructions:u, jemalloc, 6 crates x Check/Debug/Opt x scenarios = 81 cells). Cumulative for the full reworked stack (#99 + #97 + #98): full -0.15%, incr-full +0.07%, incr-unchanged -0.47%, incr-patched -4.03%, all cells -1.48%, with 47 cells improved by at least 0.25% and 8 regressed. Every check-profile cell improves (serde incr-patched -19.0% to -20.7%, the other crates' check patched around -6%). The largest residual is cargo debug incr-unchanged +9.4%; the rest are at or below +1%.

The precision tiers above were driven to a near-zero uncovered-rendering rate by fallback-frequency probes on regex-automata, syn, cargo, hyper (with --features=full) and ripgrep across Debug and Opt: each crate exposed a distinct span-trust failure (derive attr offsets, cfg-wrapper passthrough, nested helper-macro call sites, inert tool attributes on generated files, mid-line extent starts), each now pinned by the tier that covers it. A CGU-reuse investigation on ripgrep's patched rebuild additionally motivated recording ExpnHash-keyed anchors only where no definition-keyed anchor covers the position (see the expansion-anchor PR): with that, the patched rebuild re-codegens one module where the baseline compiler re-codegens five.

@xmakro
xmakro force-pushed the perf/def-anchored-lines branch from 78f3825 to 3178a45 Compare August 7, 2026 10:18
@xmakro xmakro changed the title Definition-anchored line-table dependencies for offset-based span hashing (alternative to #91) Definition-anchored line-table dependencies for offset-based span hashing Aug 7, 2026
@xmakro
xmakro force-pushed the perf/def-anchored-lines branch 2 times, most recently from 6bf4d05 to cb68f0c Compare August 7, 2026 21:13
@xmakro
xmakro force-pushed the perf/def-anchored-lines branch from cb68f0c to 9d318eb Compare August 8, 2026 00:43
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