Add -Zrmeta-content-svh, -Zrmeta-strip-spans, -Zrmeta-normalize-src-hash: byte-stable .rmeta under non-interface edits - #2
Merged
Conversation
Content-addressed build systems (nix-cargo-unit) get early cutoff for free: if a rebuilt artifact is byte-identical, dependents are not re-derived. Today that cutoff almost never fires for Rust crates because crate metadata is oversensitive: a comment edit, a whitespace shift, or a private function body edit all change the .rmeta even though nothing a dependent crate consumes has changed. This adds three opt-in unstable flags, each addressing one churn source located by byte-diffing rmeta files across edit classes: * -Zrmeta-content-svh derives the SVH embedded in the crate root (and in the metadata stub) from the encoded metadata bytes themselves, plus the session dep-tracking hash and the stable crate id, instead of from the HIR. The SVH becomes stable exactly when the rest of the metadata is. The crate loader's link-time consistency check is unaffected: it compares encoded SVHs for equality, and the .rmeta and .rlib from one invocation embed the same bytes. The local crate_hash query is unchanged; the flag is rejected together with -Cincremental, whose green-reuse path never re-encodes metadata. * -Zrmeta-strip-spans=non-exported|all replaces spans with DUMMY_SP at the single span encoding choke point. Spans in metadata are byte offsets into local source files, so any edit that shifts text perturbs them. "non-exported" preserves spans in the regions whose contents dependents compile into their own output (MIR bodies, hygiene expansion data); "all" strips those too and substitutes expansion-index-derived ExpnHashes for the span-dependent real ones. Costs are documented per mode in the unstable book and at each site where fidelity is given up. Proc-macro crates are exempt. * -Zrmeta-normalize-src-hash zeroes the per-file source content hashes (and optional cargo checksums) recorded in the source file table. These cover a file's whole raw text, so any edit to a referenced file perturbs them. Dependent crates lose cross-crate diagnostic source snippets and debuginfo file checksums for this crate; the zero hash never matches real source, so consumers see "source unavailable" rather than stale text. With all three flags, recompiling a library crate after an identical rebuild, a comment edit, a line-shifting whitespace edit, or a private non-generic non-inlinable function body edit produces a byte-identical .rmeta, while generic/inlinable body edits and signature changes still change it (their MIR and types are legitimately part of what dependents consume). -Zrmeta-normalize-src-hash alone stabilizes length-and-line-preserving comment edits. The new run-make test rmeta-stability pins all of these properties, including the controls. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DWARF inspection of a dependent crate falsified the mode's original debuginfo claim: a dependent deriving debuginfo for a cross-crate inlined function takes its declaration file/line from the item's definition span, and binds the inlined instructions' line rows to that file. With definition spans stripped but body spans kept, the dependent emitted line rows carrying real line numbers bound to its own first source file, i.e. confidently wrong debuginfo, arguably worse than the honest no-line-info of =all. Preserve def_span and def_ident_span for exactly the defs whose MIR is exported (the should_encode_mir set). Re-verification shows the consumer's DW_AT_decl_file/decl_line and .debug_line rows for the inlined function now match a no-flags baseline exactly, with =all unchanged. Also document the measured stability boundary of this mode honestly: since preserved spans are byte offsets resolved through the encoded source file length and line tables, only byte-position-preserving edits keep the rmeta identical under =non-exported; any length change, even a trailing comment, perturbs the source file record (source_len and line table, verified by byte diff). Full stability under general non-interface edits remains the province of =all, which the extended run-make test now also pins for trailing comments, and the non-exported boundary (same-length edits stable, length-changing edits churn) is pinned as well. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds three opt-in
-Zflags that make a library crate's.rmetabyte-identical across non-interface edits, so that content-addressed build systems (nix-cargo-unit derivations) get early cutoff: when a rebuilt.rmetais byte-identical, dependents are not re-derived.Flags
-Zrmeta-content-svh-Cincremental.crate_hashquery is untouched; the loader's link-time check keeps comparing encoded SVHs for equality.-Zrmeta-strip-spans=non-exported|allDUMMY_SPat the single metadata span-encoding choke point.non-exportedpreserves spans in exported MIR bodies, hygiene data, and the definition spans of MIR-exported items;allstrips everything and substitutes expansion-index-derivedExpnHashes. Proc-macro crates are exempt.all; for items without exported MIR undernon-exported). Underall, debuginfo a dependent emits for functions inlined from this crate resolves to the dependent's own first file at line 1 (verified in DWARF; call-site lines stay correct), and const-eval backtraces plus macro-backtrace notes into this crate degrade. Undernon-exported, dependent DWARF was re-verified byte-equivalent to a no-flags baseline.-Zrmeta-normalize-src-hash-Zchecksum-hash-algorithmchecksums) in the encoded source file table.file:line:colinstead of risking quoting stale source), and debuginfo file checksums for this crate become zero.All three are
[TRACKED], default off. Flag-off encoding is unchanged: every new code path is gated on the flag values, and the flag-off byte-diff signature of a probe crate (rmeta size and per-edit-class differing byte counts) is identical to the pre-patch compiler's.Per-edit-class byte-stability (stage1, small lib crate,
--emit metadata,link -Copt-level=3 --remap-path-prefix, no incremental)content-svh+strip-spans=all+normalize-src-hashstrip-spans=non-exportednormalize-src-hashonly#[inline(never)]body edit (same length)The controls keep churning by design: exported MIR and interface data must stay visible to dependents. Isolation runs confirm each flag is necessary: with spans and src-hash handled but the SVH flag off, exactly the 16 SVH bytes still differ on every class.
non-exported's boundary is deliberate and now documented: its preserved spans are byte offsets resolved through the encoded source file length/line tables, so only byte-position-preserving edits stay identical (byte diff of case 2t shows the residual churn is exactlysource_lenand the line table of theSourceFilerecord). It is the zero-debuginfo-cost mode;allis the cutoff mode.Verification history worth keeping (negative result)
The first version of
non-exportedstripped item definition spans while keeping MIR body spans. Independent DWARF inspection falsified its debuginfo claim: the consumer'sDW_AT_decl_file/decl_linefor a cross-crate-inlined function came out as the consumer's own first file, line 1, and the.debug_linerows bound this crate's real line numbers to the consumer's file, i.e. confidently wrong debuginfo, worse thanall's honest no-line-info. Cause: the declaration file/line and the line-row file binding derive from the item's definition span, which is item metadata, not body metadata. Fixed by preservingdef_span/def_ident_spanfor exactly theshould_encode_mirset; re-verification shows consumer DWARF (DIEs and decoded line table) semantically identical to a no-flags baseline, withallunchanged.End-to-end relink check (independently verified)
dep v1 vs v2 differ only in a private
#[inline(never)]body (x + 1vsx + 100). With the flags: the two.rmetas are byte-identical; the rlibs differ only in the codegen object member (the embedded.rmetasections, extracted withobjcopy --dump-section, are byte-identical to the standalone files). Anapprlib compiled only against v1's.rmeta, linked into a binary against v2's rlib without recompilingapp, links cleanly (no SVH mismatch) and prints v2's result (240); linked against v1's rlib as control it prints 42. That is precisely the cutoff-then-relink flow. Adding a newpub fnto dep changes the rmeta, i.e. the flags are not blanking metadata.Tests
tests/run-make/rmeta-stabilitypins classes 1 to 4 plus 2c/2t, both controls, and thenon-exportedboundary (same-length edits identical, length-changing comment must differ): 1 passed../x test compiler/rustc_interface --stage 1(includes the option dep-tracking tests with the three newtracked!entries): 17 passed, 0 failed../x test tests/incremental --stage 1(flags off): 178 passed, 0 failed../x test tests/ui --stage 1(flags off): 21689 passed, 0 failed, 238 ignored../x test tidy: clean.#[macro_export]macro, generics,#[inline]fns; app using all of it, compiled without flags against the flagged dep): builds, runs,--testbinary 2 passed, 0 failed. A cross-crateE0308against a dep type renders sanely; the visible degradation underallis exactly the "defined here" note losing its location (= note: function defined hereinstead of a snippet pointing into dep source). A proc-macro crate compiles under the flags (exempt, no ICE) and its consumer works.pub const/pub staticwith a line shift) compile without ICE; the const/static crate's rmeta is byte-identical under the line shift withall.Known limitations (deliberate or out of scope)
#[inline], and small functions auto-selected for cross-crate inlining at-O(use#[inline(never)]or-Zcross-crate-inline-threshold=neverto opt bodies out). This is required for correctness.DefIndex/ExpnIdnumbering and still churn the rmeta even for private bodies.all, dependent-side debuginfo misattributes inlined-from-this-crate declaration DIEs to the consumer's first file, line 1 (documented in the unstable book).-Zrmeta-content-svhis rejected with-Cincremental(the green-reuse path never re-encodes metadata, so there are no fresh bytes to hash).Upstream context
The unconditional
SourceFile::src_hashencoding (16 bytes of MD5 churn on any file edit, even at-Cdebuginfo=0) appears to be an unreported gap upstream: the relink-dont-rebuild tracker (rust-lang#158844) does not list it, andchecksum_hashright next to it is already optional behind-Zchecksum-hash-algorithm. Prior art: rust-lang#154724 (crate hash from the metadata encoding, the same direction as-Zrmeta-content-svh), rust-lang#151265 (-Zstable-crate-hashwith a separate.spansfile), rust-lang#143249 (wholesale span deletion; broke thiserror-dependent crates, which is why proc-macro crates are exempt here and span stripping is opt-in and graduated).Note for index: rmeta byte-stability alone does not make the cutoff fire end to end. index still needs to split metadata and link outputs into separate derivations (today one unit derivation carries both rmeta and rlib, and the rlib legitimately changes on body edits), and the dep-info
.doutput with an unremapped store path must be fixed separately.🤖 Generated with Claude Code
Note
Add
-Zrmeta-content-svh,-Zrmeta-strip-spans,-Zrmeta-normalize-src-hashfor byte-stable.rmetaunder non-interface edits.rmetafiles byte-stable when only non-interface changes (e.g. function bodies, comments) are made, without affecting the crate's public interface hash.-Zrmeta-strip-spans(None/NonExported/All) replaces encoded spans withDUMMY_SPin metadata, withNonExportedpreserving spans for items whose MIR is exported.-Zrmeta-normalize-src-hashzeroes out per-file source content hashes in crate metadata so source file edits don't propagate into.rmetabytes.-Zrmeta-content-svhderives the crate header SVH from encoded metadata bytes rather than the HIR hash; incompatible with-C incremental(rejected at session setup)..rmetabyte stability across various edit/flag combinations.DUMMY_SPfor stripped spans, and expansion hashes in hygiene data become index-derived rather than content-derived whenAllmode is active.Macroscope summarized 19bc529.