diff --git a/Cargo.lock b/Cargo.lock index 1b57dbf60195..8ebae98d6278 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4380,6 +4380,7 @@ dependencies = [ "rustc_expand", "rustc_feature", "rustc_fs_util", + "rustc_hashes", "rustc_hir", "rustc_hir_pretty", "rustc_index", diff --git a/compiler/rustc_interface/src/tests.rs b/compiler/rustc_interface/src/tests.rs index bb22bca27bf3..265098a4476d 100644 --- a/compiler/rustc_interface/src/tests.rs +++ b/compiler/rustc_interface/src/tests.rs @@ -16,8 +16,9 @@ use rustc_session::config::{ InstrumentCoverage, InstrumentMcount, InstrumentMcountOpts, InstrumentXRay, LinkSelfContained, LinkerPluginLto, LocationDetail, LtoCli, MirIncludeSpans, NextSolverConfig, Offload, Options, OutFileName, OutputType, OutputTypes, PAuthKey, PacRet, Passes, PatchableFunctionEntry, - Polonius, ProcMacroExecutionStrategy, Strip, SwitchWithOptPath, SymbolManglingVersion, - WasiExecModel, build_configuration, build_session_options, rustc_optgroups, + Polonius, ProcMacroExecutionStrategy, RmetaStripSpans, Strip, SwitchWithOptPath, + SymbolManglingVersion, WasiExecModel, build_configuration, build_session_options, + rustc_optgroups, }; use rustc_session::lint::Level; use rustc_session::search_paths::SearchPath; @@ -877,6 +878,9 @@ fn test_unstable_options_tracking_hash() { tracked!(regparm, Some(3)); tracked!(relax_elf_relocations, Some(true)); tracked!(remap_cwd_prefix, Some(PathBuf::from("abc"))); + tracked!(rmeta_content_svh, true); + tracked!(rmeta_normalize_src_hash, true); + tracked!(rmeta_strip_spans, RmetaStripSpans::All); tracked!(sanitizer, SanitizerSet::ADDRESS); tracked!(sanitizer_cfi_canonical_jump_tables, None); tracked!(sanitizer_cfi_generalize_pointers, Some(true)); diff --git a/compiler/rustc_metadata/Cargo.toml b/compiler/rustc_metadata/Cargo.toml index 2fce131e08b6..a5ae2de8832e 100644 --- a/compiler/rustc_metadata/Cargo.toml +++ b/compiler/rustc_metadata/Cargo.toml @@ -16,6 +16,7 @@ rustc_errors = { path = "../rustc_errors" } rustc_expand = { path = "../rustc_expand" } rustc_feature = { path = "../rustc_feature" } rustc_fs_util = { path = "../rustc_fs_util" } +rustc_hashes = { path = "../rustc_hashes" } rustc_hir = { path = "../rustc_hir" } rustc_hir_pretty = { path = "../rustc_hir_pretty" } rustc_index = { path = "../rustc_index" } diff --git a/compiler/rustc_metadata/src/rmeta/encoder.rs b/compiler/rustc_metadata/src/rmeta/encoder.rs index b32a23f53f8c..7b45eabbece9 100644 --- a/compiler/rustc_metadata/src/rmeta/encoder.rs +++ b/compiler/rustc_metadata/src/rmeta/encoder.rs @@ -7,9 +7,12 @@ use std::sync::Arc; use rustc_data_structures::fx::{FxIndexMap, FxIndexSet}; use rustc_data_structures::memmap::{Mmap, MmapMut}; +use rustc_data_structures::stable_hash::StableHasher; +use rustc_data_structures::svh::Svh; use rustc_data_structures::sync::{par_for_each_in, par_join}; use rustc_data_structures::temp_dir::MaybeTempDir; use rustc_data_structures::thousands::usize_with_underscores; +use rustc_hashes::Hash64; use rustc_hir as hir; use rustc_hir::attrs::{AttributeKind, EncodeCrossCrate}; use rustc_hir::def_id::{CRATE_DEF_ID, LOCAL_CRATE, LocalDefId, LocalDefIdSet}; @@ -27,12 +30,12 @@ use rustc_middle::ty::fast_reject::{self, TreatParams}; use rustc_middle::{bug, span_bug}; use rustc_serialize::{Decodable, Decoder, Encodable, Encoder, opaque}; use rustc_session::config::mitigation_coverage::DeniedPartialMitigation; -use rustc_session::config::{CrateType, OptLevel, TargetModifier}; +use rustc_session::config::{CrateType, OptLevel, RmetaStripSpans, TargetModifier}; use rustc_span::def_id::CRATE_MOD_ID; -use rustc_span::hygiene::HygieneEncodeContext; +use rustc_span::hygiene::{ExpnHash, HygieneEncodeContext}; use rustc_span::{ - ByteSymbol, ExternalSource, FileName, SourceFile, SpanData, SpanEncoder, StableSourceFileId, - Symbol, SyntaxContext, sym, + ByteSymbol, DUMMY_SP, ExternalSource, FileName, SourceFile, SpanData, SpanEncoder, + StableSourceFileId, Symbol, SyntaxContext, sym, }; use tracing::{debug, instrument, trace}; @@ -68,6 +71,29 @@ pub(super) struct EncodeContext<'a, 'tcx> { hygiene_ctxt: &'a HygieneEncodeContext, // Used for both `Symbol`s and `ByteSymbol`s. symbol_index_table: FxHashMap, + + // The effective `-Zrmeta-strip-spans` mode. Forced to + // `RmetaStripSpans::None` for proc-macro crates: their metadata consists + // almost entirely of span, hygiene and quoted-span data that dependent + // crates rely on for expansion, so stripping it would gut the crate + // rather than stabilize it. + strip_spans: RmetaStripSpans, + // While `true`, `encode_span` does not strip spans in + // `RmetaStripSpans::NonExported` mode. Set around the metadata regions + // whose contents are compiled into dependent crates (MIR bodies and + // hygiene expansion data), where spans feed debuginfo and macro expansion + // rather than just diagnostics. + span_stripping_paused: bool, + // The content-derived SVH computed by `encode_crate_root` under + // `-Zrmeta-content-svh`, so `encode_metadata` can reuse it for the + // metadata stub. `None` when the flag is off or the root has not been + // encoded yet. + content_svh: Option, + // In `RmetaStripSpans::NonExported` mode, the local defs whose MIR is + // exported (the `should_encode_mir` set); their item-level spans are + // preserved together with their body spans. Empty in other modes. + // Populated by `encode_def_ids`. + mir_exported_defs: LocalDefIdSet, } /// If the current crate is a proc-macro, returns early with `LazyArray::default()`. @@ -171,6 +197,16 @@ impl<'a, 'tcx> SpanEncoder for EncodeContext<'a, 'tcx> { } fn encode_span(&mut self, span: Span) { + // `-Zrmeta-strip-spans`: encode a dummy span instead of the real one, + // so that the encoded bytes do not depend on source positions. What + // this gives up: any consumer of this span in a dependent crate (e.g. + // a "function defined here" diagnostic note, or, in `all` mode, + // debuginfo line info for inlined MIR) sees `DUMMY_SP` instead of a + // location in this crate's source. Spans of source files that are + // never referenced are not encoded at all, which also keeps the + // source file table (and its per-file content hashes) from being + // encoded for stripped files. + let span = if self.should_strip_span() { DUMMY_SP } else { span }; match self.span_shorthands.entry(span) { Entry::Occupied(o) => { // If an offset is smaller than the absolute position, we encode with the offset. @@ -426,6 +462,35 @@ macro_rules! record_defaulted_array { } impl<'a, 'tcx> EncodeContext<'a, 'tcx> { + /// Whether `encode_span` should replace the span it is about to encode + /// with `DUMMY_SP`. See `RmetaStripSpans` for the semantics of each mode. + fn should_strip_span(&self) -> bool { + match self.strip_spans { + RmetaStripSpans::None => false, + RmetaStripSpans::NonExported => !self.span_stripping_paused, + RmetaStripSpans::All => true, + } + } + + /// Runs `f` with span stripping paused, for metadata regions whose spans + /// are exported for downstream compilation (MIR bodies, hygiene data) and + /// must stay faithful in `RmetaStripSpans::NonExported` mode. + fn with_span_stripping_paused(&mut self, f: impl FnOnce(&mut Self) -> R) -> R { + let old = std::mem::replace(&mut self.span_stripping_paused, true); + let res = f(self); + self.span_stripping_paused = old; + res + } + + /// Whether `def_id`'s item-level spans (`def_span`, `def_ident_span`) + /// must be preserved in `RmetaStripSpans::NonExported` mode because the + /// item's MIR is exported. See the comment at the use site in + /// `encode_def_ids`. + fn preserve_item_spans(&self, def_id: LocalDefId) -> bool { + self.strip_spans == RmetaStripSpans::NonExported + && self.mir_exported_defs.contains(&def_id) + } + fn emit_lazy_distance(&mut self, position: NonZero) { let pos = position.get(); let distance = match self.lazy_state { @@ -596,6 +661,31 @@ impl<'a, 'tcx> EncodeContext<'a, 'tcx> { local_crate_stable_id, ); + // `-Zrmeta-normalize-src-hash`: the recorded hash covers the whole + // file's raw text, so *any* edit to the file (even a comment that + // moves no token) perturbs it and thereby the metadata bytes. + // Replace it with an all-zero hash of the same kind. What is given + // up, per consumer of this field: + // - Cross-crate diagnostic snippet rendering: a dependent crate + // verifies the on-disk source against this hash before quoting + // it in diagnostics. The zero hash never matches, so such + // diagnostics degrade to file:line:col without a quoted snippet + // (instead of risking quoting stale source). + // - Debuginfo: file checksums recorded for this crate's files in + // dependent crates' debug info become zero, so debuggers cannot + // detect source-file staleness for them. + // Proc-macro crates are exempt (see the `strip_spans` field docs). + if !self.is_proc_macro && self.tcx.sess.opts.unstable_opts.rmeta_normalize_src_hash { + adapted_source_file.src_hash = + rustc_span::SourceFileHash::zeroed(adapted_source_file.src_hash.kind); + // Same reasoning for the optional cargo-freshness checksum + // (`-Zchecksum-hash-algorithm`), which also covers the whole + // file's text. + adapted_source_file.checksum_hash = adapted_source_file + .checksum_hash + .map(|hash| rustc_span::SourceFileHash::zeroed(hash.kind)); + } + let on_disk_index: u32 = on_disk_index.try_into().expect("cannot export more than U32_MAX files"); adapted.set_some(on_disk_index, self.lazy(adapted_source_file)); @@ -655,7 +745,11 @@ impl<'a, 'tcx> EncodeContext<'a, 'tcx> { let incoherent_impls = stat!("incoherent-impls", || self.encode_incoherent_impls()); - _ = stat!("mir", || self.encode_mir()); + // MIR bodies are compiled into dependent crates (cross-crate inlining, + // generic instantiation, const eval), so their spans reach dependent + // debuginfo and const-eval backtraces. In `-Zrmeta-strip-spans=non-exported` + // mode they are preserved; only `all` mode strips them. + _ = stat!("mir", || self.with_span_stripping_paused(|this| this.encode_mir())); _ = stat!("def-ids", || self.encode_def_ids()); @@ -712,7 +806,12 @@ impl<'a, 'tcx> EncodeContext<'a, 'tcx> { // the incremental cache. If this causes us to deserialize a `Span`, then we may load // additional `SyntaxContext`s into the global `HygieneData`. Therefore, we need to encode // the hygiene data last to ensure that we encode any `SyntaxContext`s that might be used. - let (syntax_contexts, expn_data, expn_hashes) = stat!("hygiene", || self.encode_hygiene()); + // Hygiene expansion data feeds macro expansion and hygiene resolution + // in dependent crates, so like MIR it is preserved in + // `-Zrmeta-strip-spans=non-exported` mode and only stripped (with + // span-independent expansion hashes substituted) in `all` mode. + let (syntax_contexts, expn_data, expn_hashes) = + stat!("hygiene", || self.with_span_stripping_paused(|this| this.encode_hygiene())); let def_path_hash_map = stat!("def-path-hash-map", || self.encode_def_path_hash_map()); @@ -725,11 +824,12 @@ impl<'a, 'tcx> EncodeContext<'a, 'tcx> { let root = stat!("final", || { let attrs = tcx.hir_krate_attrs(); + let hash = self.metadata_svh(); self.lazy(CrateRoot { header: CrateHeader { name: tcx.crate_name(LOCAL_CRATE), triple: tcx.sess.opts.target_triple.clone(), - hash: tcx.crate_hash(LOCAL_CRATE), + hash, is_proc_macro_crate: proc_macro_data.is_some(), is_stub: false, }, @@ -849,6 +949,78 @@ impl<'a, 'tcx> EncodeContext<'a, 'tcx> { root } + + /// Returns the SVH to embed in the crate root header (and, mirrored, in + /// the metadata stub header). + /// + /// By default this is the HIR-based `crate_hash` query, which covers + /// essentially the whole crate (all bodies, source file names, spans when + /// incremental is enabled, ...), so *any* edit to the crate changes it, + /// and with it the metadata bytes, even when nothing a dependent crate + /// consumes has changed. + /// + /// Under `-Zrmeta-content-svh` the SVH is instead derived from the + /// encoded metadata bytes themselves: everything written before the + /// `CrateRoot` (this method must be called exactly when the root is about + /// to be encoded), which is all tables, MIR, hygiene and source map data. + /// That makes the SVH stable exactly when the rest of the metadata is. + /// Two extra inputs are mixed in for fields that appear only in the root + /// and thus after the hashed region: the session's dep-tracking hash + /// (covering every `[TRACKED]` option, e.g. `-Cextra-filename`, panic + /// strategy, edition, symbol mangling version) and the `StableCrateId`. + /// + /// Soundness of the link-time consistency check: a dependent compiled + /// against this crate's `.rmeta` records the SVH it read from that + /// metadata (`CrateDep.hash`), and the crate loader accepts an `.rlib` at + /// link time only if the metadata embedded in it carries an equal SVH. + /// The `.rmeta` and `.rlib` produced by one compiler invocation embed the + /// same encoded metadata, so they always agree. If the crate is + /// recompiled after a non-interface edit and the metadata bytes come out + /// identical, the new `.rlib` carries the same SVH as the cached + /// `.rmeta`, so a dependent compiled against the cached `.rmeta` links + /// the fresh `.rlib` -- which is precisely the early-cutoff behavior this + /// flag exists for. If any encoded byte differs, the SVH differs and the + /// loader rejects the mismatch, exactly as with the HIR-based SVH. + /// + /// Mixed toolchains: the SVH is an opaque 128-bit value compared only for + /// equality, and it never leaves the encoded metadata (the local + /// `crate_hash` query is unaffected by this flag). A dependent compiled + /// by an older compiler against old-scheme metadata simply recorded that + /// metadata's (old-scheme) SVH; artifacts from different compilers or + /// different flag settings have different metadata bytes anyway and are + /// never interchangeable in the first place. + fn metadata_svh(&mut self) -> Svh { + let tcx = self.tcx; + if !tcx.sess.opts.unstable_opts.rmeta_content_svh { + return tcx.crate_hash(LOCAL_CRATE); + } + + // Hash the bytes encoded so far, i.e. the whole metadata file except + // the `CrateRoot` that is about to be written. This mirrors the + // re-read that `-Zmeta-stats` performs. + self.opaque.flush(); + let mut file = self.opaque.file(); + let pos_before_rewind = file.stream_position().unwrap(); + file.rewind().unwrap(); + + let mut hasher = StableHasher::new(); + let mut buf = [0u8; 16 * 1024]; + loop { + let n = file.read(&mut buf).unwrap(); + if n == 0 { + break; + } + std::hash::Hasher::write(&mut hasher, &buf[..n]); + } + assert_eq!(file.stream_position().unwrap(), pos_before_rewind); + + std::hash::Hash::hash(&tcx.sess.opts.dep_tracking_hash(true), &mut hasher); + std::hash::Hash::hash(&tcx.stable_crate_id(LOCAL_CRATE), &mut hasher); + + let svh = Svh::new(hasher.finish()); + self.content_svh = Some(svh); + svh + } } struct AnalyzeAttrState { @@ -1417,6 +1589,23 @@ impl<'a, 'tcx> EncodeContext<'a, 'tcx> { let tcx = self.tcx; + if self.strip_spans == RmetaStripSpans::NonExported { + // Compute the set of defs whose MIR will be exported (the same + // filter `encode_mir` uses), so their item-level spans can be + // preserved below. + let reachable_set = tcx.reachable_set(()); + self.mir_exported_defs = tcx + .mir_keys(()) + .iter() + .copied() + .filter(|&def_id| { + let (encode_const, encode_opt) = + should_encode_mir(tcx, reachable_set, def_id); + encode_const || encode_opt + }) + .collect(); + } + for local_id in tcx.iter_local_def_id() { let def_id = local_id.to_def_id(); let def_kind = tcx.def_kind(local_id); @@ -1440,9 +1629,25 @@ impl<'a, 'tcx> EncodeContext<'a, 'tcx> { record!(self.tables.default_fields[def_id] <- anon.def_id.to_def_id()); } + // In `RmetaStripSpans::NonExported` mode, the definition spans of + // items whose MIR is exported must survive alongside the MIR body + // spans: a dependent crate that inlines such an item derives the + // function's debuginfo declaration file/line from its `def_span`, + // and binds the inlined instructions' line rows to that file. With + // the definition span stripped but body spans kept, the dependent + // would emit line rows carrying this crate's real line numbers + // bound to one of its own files, i.e. confidently wrong debuginfo + // (empirically verified via DWARF inspection). + let preserve_item_spans = self.preserve_item_spans(local_id); if should_encode_span(def_kind) { let def_span = tcx.def_span(local_id); - record!(self.tables.def_span[def_id] <- def_span); + if preserve_item_spans { + self.with_span_stripping_paused( + |this| record!(this.tables.def_span[def_id] <- def_span), + ); + } else { + record!(self.tables.def_span[def_id] <- def_span); + } } if should_encode_attrs(def_kind) { self.encode_attrs(local_id); @@ -1453,7 +1658,13 @@ impl<'a, 'tcx> EncodeContext<'a, 'tcx> { if should_encode_span(def_kind) && let Some(ident_span) = tcx.def_ident_span(def_id) { - record!(self.tables.def_ident_span[def_id] <- ident_span); + if preserve_item_spans { + self.with_span_stripping_paused( + |this| record!(this.tables.def_ident_span[def_id] <- ident_span), + ); + } else { + record!(self.tables.def_ident_span[def_id] <- ident_span); + } } if def_kind.has_codegen_attrs() { record!(self.tables.codegen_fn_attrs[def_id] <- self.tcx.codegen_fn_attrs(def_id)); @@ -1961,6 +2172,35 @@ impl<'a, 'tcx> EncodeContext<'a, 'tcx> { }, |(this, _, expn_data_table, expn_hash_table), index, expn_data, hash| { if let Some(index) = index.as_local() { + // `-Zrmeta-strip-spans=all`: the real `ExpnHash` is + // computed over the `ExpnData`, whose inputs include + // source positions (call site and definition site spans), + // so it churns whenever source positions shift. Since the + // `ExpnData` we encode alongside it has had its spans + // stripped anyway, substitute a hash derived from the + // crate-local expansion index. Dependent crates use this + // hash only as an opaque, globally unique identity for + // deduplicating decoded expansions (its upper half must + // remain this crate's `StableCrateId`, which + // `ExpnHash::from_parts` preserves); the index is unique + // within the crate, and `index + 1` keeps it non-zero so + // it cannot collide with the root expansion hash. What is + // given up: the hash no longer reflects the expansion's + // contents, so a dependent's incremental cache keyed on it + // sees changes only when this crate's metadata changes at + // all (which is exactly the point). The root expansion + // (index 0) keeps its real hash, which is already + // span-independent (`Fingerprint::ZERO`). + let hash = if this.strip_spans == RmetaStripSpans::All + && index.as_raw().as_u32() != 0 + { + ExpnHash::from_parts( + this.tcx.stable_crate_id(LOCAL_CRATE), + Hash64::new(u64::from(index.as_raw().as_u32()) + 1), + ) + } else { + hash + }; expn_data_table.set_some(index.as_raw(), this.lazy(expn_data)); expn_hash_table.set_some(index.as_raw(), this.lazy(hash)); } @@ -2454,19 +2694,13 @@ pub fn encode_metadata(tcx: TyCtxt<'_>, path: &Path, ref_path: Option<&Path>) { tcx.dep_graph.assert_ignored(); // Generate the metadata stub manually, as that is a small file compared to full metadata. - if let Some(ref_path) = ref_path { - let _prof_timer = tcx.prof.verbose_generic_activity("generate_crate_metadata_stub"); - - with_encode_metadata_header(tcx, ref_path, |ecx| { - let header: LazyValue = ecx.lazy(CrateHeader { - name: tcx.crate_name(LOCAL_CRATE), - triple: tcx.sess.opts.target_triple.clone(), - hash: tcx.crate_hash(LOCAL_CRATE), - is_proc_macro_crate: false, - is_stub: true, - }); - header.position.get() - }) + // + // With `-Zrmeta-content-svh` the SVH is only known once the full metadata + // has been encoded, so the stub (which must carry the same SVH) is + // generated after the full metadata instead, at the end of this function. + let rmeta_content_svh = tcx.sess.opts.unstable_opts.rmeta_content_svh; + if !rmeta_content_svh && let Some(ref_path) = ref_path { + encode_stub(tcx, ref_path, tcx.crate_hash(LOCAL_CRATE)); } let _prof_timer = tcx.prof.verbose_generic_activity("generate_crate_metadata"); @@ -2505,7 +2739,7 @@ pub fn encode_metadata(tcx: TyCtxt<'_>, path: &Path, ref_path: Option<&Path>) { // Perform metadata encoding inside a task, so the dep-graph can check if any encoded // information changes, and maybe reuse the work product. - tcx.dep_graph.with_task( + let (content_svh, _) = tcx.dep_graph.with_task( dep_node, tcx, || { @@ -2523,18 +2757,44 @@ pub fn encode_metadata(tcx: TyCtxt<'_>, path: &Path, ref_path: Option<&Path>) { ecx.opaque.file().metadata().unwrap().len(), ); - root.position.get() + (root.position.get(), ecx.content_svh) }) }, None, ); + + // See above: with `-Zrmeta-content-svh` the stub is generated after the + // full metadata so it can embed the content-derived SVH. The incremental + // green-reuse path above cannot be taken here because the flag is + // rejected in combination with `-Cincremental` at session setup. + if rmeta_content_svh && let Some(ref_path) = ref_path { + let content_svh = + content_svh.expect("-Zrmeta-content-svh: no SVH computed during metadata encoding"); + encode_stub(tcx, ref_path, content_svh); + } } -fn with_encode_metadata_header( +/// Writes a metadata stub (a file containing only the crate header) to `path`. +fn encode_stub(tcx: TyCtxt<'_>, path: &Path, hash: Svh) { + let _prof_timer = tcx.prof.verbose_generic_activity("generate_crate_metadata_stub"); + + with_encode_metadata_header(tcx, path, |ecx| { + let header: LazyValue = ecx.lazy(CrateHeader { + name: tcx.crate_name(LOCAL_CRATE), + triple: tcx.sess.opts.target_triple.clone(), + hash, + is_proc_macro_crate: false, + is_stub: true, + }); + (header.position.get(), ()) + }) +} + +fn with_encode_metadata_header( tcx: TyCtxt<'_>, path: &Path, - f: impl FnOnce(&mut EncodeContext<'_, '_>) -> usize, -) { + f: impl FnOnce(&mut EncodeContext<'_, '_>) -> (usize, R), +) -> R { let mut encoder = opaque::FileEncoder::new(path) .unwrap_or_else(|err| tcx.dcx().emit_fatal(FailCreateFileEncoder { err })); encoder.emit_raw_bytes(METADATA_HEADER); @@ -2564,12 +2824,21 @@ fn with_encode_metadata_header( is_proc_macro: tcx.crate_types().contains(&CrateType::ProcMacro), hygiene_ctxt: &hygiene_ctxt, symbol_index_table: Default::default(), + // See the field docs for why proc-macro crates are exempt. + strip_spans: if tcx.crate_types().contains(&CrateType::ProcMacro) { + RmetaStripSpans::None + } else { + tcx.sess.opts.unstable_opts.rmeta_strip_spans + }, + span_stripping_paused: false, + content_svh: None, + mir_exported_defs: LocalDefIdSet::default(), }; // Encode the rustc version string in a predictable location. rustc_version(tcx.sess.cfg_version).encode(&mut ecx); - let root_position = f(&mut ecx); + let (root_position, res) = f(&mut ecx); // Make sure we report any errors from writing to the file. // If we forget this, compilation can succeed with an incomplete rmeta file, @@ -2582,6 +2851,8 @@ fn with_encode_metadata_header( if let Err(err) = encode_root_position(file, root_position) { tcx.dcx().emit_fatal(FailWriteFile { path: ecx.opaque.path(), err }); } + + res } fn encode_root_position(mut file: &File, pos: usize) -> Result<(), std::io::Error> { diff --git a/compiler/rustc_session/src/config.rs b/compiler/rustc_session/src/config.rs index d73601aea318..bccb43d98be8 100644 --- a/compiler/rustc_session/src/config.rs +++ b/compiler/rustc_session/src/config.rs @@ -637,6 +637,69 @@ impl FromStr for SplitDwarfKind { } } +/// The value of `-Zrmeta-strip-spans`, controlling which spans are replaced by +/// `DUMMY_SP` when encoding crate metadata. +/// +/// Spans in crate metadata are encoded as byte offsets into this crate's +/// source files, so any edit that shifts source positions (adding a comment, a +/// blank line, ...) changes the encoded bytes of the `.rmeta` even when the +/// crate's interface is unchanged. Build systems that content-address the +/// `.rmeta` (to skip rebuilding dependents when it is unchanged) can opt into +/// stripping those spans, trading diagnostic and debuginfo quality in +/// *dependent* crates for byte-stability of the metadata. See the variants for +/// exactly what each level gives up. Has no effect on proc-macro crates, whose +/// metadata is dominated by span and hygiene data that consumers rely on. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum RmetaStripSpans { + /// Encode all spans faithfully (default). + None, + /// Strip spans everywhere except in the parts of metadata that dependent + /// crates compile into their own output: encoded MIR bodies (used for + /// cross-crate inlining, generic instantiation, and const evaluation), + /// hygiene expansion data, and the definition spans of items whose MIR is + /// exported (a dependent deriving debuginfo for an inlined function takes + /// its declaration file/line from the definition span; stripping it while + /// keeping body spans was verified to produce line rows bound to the + /// wrong file). What is given up: diagnostics reported in dependent + /// crates lose the ability to point into this crate's source for + /// item-level spans of items without exported MIR (e.g. "function defined + /// here" notes fall back to dummy spans). + /// + /// Stability boundary (measured, deliberate): because the preserved spans + /// are byte offsets and the referenced source files' length and line + /// tables must stay real for those spans to resolve correctly in + /// dependents, only edits that preserve byte positions (same-length + /// comment rewrites, same-length body edits) leave the metadata + /// byte-identical in this mode. Any length-changing edit, even a comment + /// appended at end of file, perturbs the source file record and thus the + /// metadata. Use `All` when byte-stability under general non-interface + /// edits is the goal. + NonExported, + /// Additionally strip spans inside encoded MIR bodies and hygiene + /// expansion data, and replace expansion hashes with span-independent + /// ones. What is given up on top of `NonExported`: debuginfo line + /// information in dependent crates for code inlined or instantiated from + /// this crate, const-eval error backtraces pointing into this crate, and + /// macro-backtrace notes for this crate's macros. In exchange, the + /// metadata bytes no longer depend on source positions at all, so + /// non-interface edits (comments, whitespace, private non-inlined + /// function bodies) leave the `.rmeta` byte-identical. + All, +} + +impl FromStr for RmetaStripSpans { + type Err = (); + + fn from_str(s: &str) -> Result { + Ok(match s { + "none" => RmetaStripSpans::None, + "non-exported" => RmetaStripSpans::NonExported, + "all" => RmetaStripSpans::All, + _ => return Err(()), + }) + } +} + macro_rules! define_output_types { ( $( @@ -2736,6 +2799,16 @@ pub fn build_session_options(early_dcx: &mut EarlyDiagCtxt, matches: &getopts::M let incremental = cg.incremental.as_ref().map(PathBuf::from); + // `-Zrmeta-content-svh` derives the metadata SVH from the freshly encoded + // metadata bytes. With incremental compilation the metadata work product + // can be reused from a previous session without re-encoding, in which case + // there are no fresh bytes to hash (and the incremental system separately + // uses the HIR-based SVH to name session directories). Rather than + // silently mixing the two schemes, reject the combination. + if unstable_opts.rmeta_content_svh && incremental.is_some() { + early_dcx.early_fatal("option `-Z rmeta-content-svh` cannot be used with `-C incremental`"); + } + if cg.profile_generate.enabled() && cg.profile_use.is_some() { early_dcx.early_fatal("options `-C profile-generate` and `-C profile-use` are exclusive"); } @@ -3316,8 +3389,8 @@ pub(crate) mod dep_tracking { InstrumentMcountOpts, InstrumentXRay, LinkerPluginLto, LocationDetail, LtoCli, MirStripDebugInfo, NextSolverConfig, Offload, OptLevel, OutFileName, OutputType, OutputTypes, PatchableFunctionEntry, PointerAuthOption, Polonius, ResolveDocLinks, - SourceFileHashAlgorithm, SplitDwarfKind, SwitchWithOptPath, SymbolManglingVersion, - WasiExecModel, + RmetaStripSpans, SourceFileHashAlgorithm, SplitDwarfKind, SwitchWithOptPath, + SymbolManglingVersion, WasiExecModel, }; use crate::lint; use crate::utils::NativeLib; @@ -3402,6 +3475,7 @@ pub(crate) mod dep_tracking { Edition, LinkerPluginLto, ResolveDocLinks, + RmetaStripSpans, SplitDebuginfo, SplitDwarfKind, StackProtector, diff --git a/compiler/rustc_session/src/options.rs b/compiler/rustc_session/src/options.rs index 54288346f61b..1053beb0fb48 100644 --- a/compiler/rustc_session/src/options.rs +++ b/compiler/rustc_session/src/options.rs @@ -887,6 +887,7 @@ mod desc { "one of supported split-debuginfo modes (`off`, `packed`, or `unpacked`)"; pub(crate) const parse_split_dwarf_kind: &str = "one of supported split dwarf modes (`split` or `single`)"; + pub(crate) const parse_rmeta_strip_spans: &str = "one of `none`, `non-exported` or `all`"; pub(crate) const parse_link_self_contained: &str = "one of: `y`, `yes`, `on`, `n`, `no`, `off`, or a list of enabled (`+` prefix) and disabled (`-` prefix) \ components: `crto`, `libc`, `unwind`, `linker`, `sanitizers`, `mingw`"; pub(crate) const parse_linker_features: &str = @@ -1997,6 +1998,14 @@ pub mod parse { true } + pub(crate) fn parse_rmeta_strip_spans(slot: &mut RmetaStripSpans, v: Option<&str>) -> bool { + match v.and_then(|s| RmetaStripSpans::from_str(s).ok()) { + Some(e) => *slot = e, + _ => return false, + } + true + } + pub(crate) fn parse_stack_protector(slot: &mut StackProtector, v: Option<&str>) -> bool { match v.and_then(|s| StackProtector::from_str(s).ok()) { Some(ssp) => *slot = ssp, @@ -2792,6 +2801,23 @@ written to standard error output)"), "do not skip rigid aliases in normalization for internal debugging"), retpoline: bool = (false, parse_bool, [TRACKED] { TARGET_MODIFIER: Retpoline }, "enables retpoline-indirect-branches and retpoline-indirect-calls target features (default: no)"), + rmeta_content_svh: bool = (false, parse_bool, [TRACKED], + "derive the SVH stored in crate metadata from the encoded metadata bytes themselves \ + instead of from the HIR, so that it is stable exactly when the rest of the metadata is; \ + incompatible with incremental compilation (default: no)"), + rmeta_normalize_src_hash: bool = (false, parse_bool, [TRACKED], + "replace per-file source content hashes in crate metadata with zeroes, so that the \ + metadata does not vary with source file contents that no encoded item depends on; \ + dependent crates lose cross-crate diagnostic source snippets and debuginfo file \ + checksums for this crate (default: no)"), + rmeta_strip_spans: RmetaStripSpans = (RmetaStripSpans::None, parse_rmeta_strip_spans, [TRACKED], + "replace spans with dummy spans when encoding crate metadata, so that the encoded bytes \ + do not depend on source positions; degrades diagnostics and (with `all`) debuginfo \ + reported in dependent crates against this crate + + `none`: encode all spans faithfully (default) + `non-exported`: strip spans except in encoded MIR bodies and hygiene expansion data + `all`: strip every span, including exported MIR and hygiene expansion data"), retpoline_external_thunk: bool = (false, parse_bool, [TRACKED] { TARGET_MODIFIER: RetpolineExternalThunk }, "enables retpoline-external-thunk, retpoline-indirect-branches and retpoline-indirect-calls \ target features (default: no)"), diff --git a/compiler/rustc_span/src/hygiene.rs b/compiler/rustc_span/src/hygiene.rs index d57f21fc4222..823750e52c0a 100644 --- a/compiler/rustc_span/src/hygiene.rs +++ b/compiler/rustc_span/src/hygiene.rs @@ -158,6 +158,20 @@ impl ExpnHash { fn new(stable_crate_id: StableCrateId, local_hash: Hash64) -> ExpnHash { ExpnHash(Fingerprint::new(stable_crate_id.0, local_hash)) } + + /// Builds an [ExpnHash] from its parts, bypassing the usual computation + /// over the expansion's [ExpnData]. + /// + /// This is only meant for `-Zrmeta-strip-spans=all` metadata encoding, + /// which replaces the real expansion hashes (whose inputs include source + /// positions) with hashes derived from the crate-local expansion index, so + /// that the encoded metadata does not vary with source positions. + /// `local_hash` must be unique within the crate identified by + /// `stable_crate_id`, and must be non-zero for non-root expansions so the + /// result cannot be confused with the root [ExpnHash]. + pub fn from_parts(stable_crate_id: StableCrateId, local_hash: Hash64) -> ExpnHash { + ExpnHash::new(stable_crate_id, local_hash) + } } /// A property of a macro expansion that determines how identifiers diff --git a/compiler/rustc_span/src/lib.rs b/compiler/rustc_span/src/lib.rs index b8cf472309ba..e8eb91181ae6 100644 --- a/compiler/rustc_span/src/lib.rs +++ b/compiler/rustc_span/src/lib.rs @@ -1761,6 +1761,18 @@ impl Display for SourceFileHash { } impl SourceFileHash { + /// Returns a `SourceFileHash` of the given `kind` with an all-zero value. + /// + /// This is only meant for `-Zrmeta-normalize-src-hash`, which replaces + /// the per-file content hashes recorded in crate metadata so that they do + /// not vary with file contents. An all-zero value does not match the hash + /// of any actual source file (up to hash collisions), so consumers that + /// verify on-disk source against the recorded hash treat the source as + /// unavailable rather than silently using stale source. + pub fn zeroed(kind: SourceFileHashAlgorithm) -> SourceFileHash { + SourceFileHash { kind, value: [0; 32] } + } + pub fn new_in_memory(kind: SourceFileHashAlgorithm, src: impl AsRef<[u8]>) -> SourceFileHash { let mut hash = SourceFileHash { kind, value: Default::default() }; let len = hash.hash_len(); diff --git a/src/doc/unstable-book/src/compiler-flags/rmeta-content-svh.md b/src/doc/unstable-book/src/compiler-flags/rmeta-content-svh.md new file mode 100644 index 000000000000..6e917b86d89e --- /dev/null +++ b/src/doc/unstable-book/src/compiler-flags/rmeta-content-svh.md @@ -0,0 +1,23 @@ +# `rmeta-content-svh` + +--- + +This flag derives the strict version hash (SVH) stored in crate metadata from +the encoded metadata bytes themselves, instead of from the crate's HIR. + +The default HIR-based SVH covers essentially the whole crate, including every +function body and (indirectly) source positions, so any edit to the crate +changes the SVH and with it the metadata, even when nothing a dependent crate +consumes has changed. With this flag, the SVH is a hash of everything else in +the metadata file (plus the session's dependency-tracking hash and the stable +crate id), so it is stable exactly when the rest of the metadata is. Combined +with `-Zrmeta-strip-spans` and `-Zrmeta-normalize-src-hash`, this makes the +`.rmeta` byte-identical across rebuilds after non-interface edits, which lets +content-addressed build systems skip rebuilding dependents. + +The link-time consistency check is unaffected: the `.rmeta` and `.rlib` +produced by one compiler invocation embed the same metadata and therefore the +same SVH, and the crate loader continues to compare SVHs for equality. + +This flag is rejected in combination with `-Cincremental`, and it does not +change the `crate_hash` query used within the compiling session. diff --git a/src/doc/unstable-book/src/compiler-flags/rmeta-normalize-src-hash.md b/src/doc/unstable-book/src/compiler-flags/rmeta-normalize-src-hash.md new file mode 100644 index 000000000000..ae0594825b58 --- /dev/null +++ b/src/doc/unstable-book/src/compiler-flags/rmeta-normalize-src-hash.md @@ -0,0 +1,26 @@ +# `rmeta-normalize-src-hash` + +--- + +This flag replaces the per-file source content hashes recorded in crate +metadata with zeroes. The recorded hash covers a source file's entire raw +text, so by default any edit to a file referenced from metadata, even a +comment edit that moves no token, changes the crate's metadata bytes. + +Costs, per consumer of the recorded hash: + +* Cross-crate diagnostic snippets: a dependent crate verifies this crate's + on-disk source against the recorded hash before quoting it in diagnostics. + The zero hash never matches, so such diagnostics degrade to plain + `file:line:col` references without a quoted snippet (rather than risking + quoting stale source). +* Debuginfo: file checksums recorded for this crate's files in dependent + crates' debug info become zero, so debuggers cannot detect source + staleness for them. + +On its own, this flag stabilizes the metadata against comment edits that +preserve byte length and line positions. For general comment, whitespace, +and private function body edits, combine it with `-Zrmeta-strip-spans=all` +and `-Zrmeta-content-svh`. + +The flag has no effect on proc-macro crates. diff --git a/src/doc/unstable-book/src/compiler-flags/rmeta-strip-spans.md b/src/doc/unstable-book/src/compiler-flags/rmeta-strip-spans.md new file mode 100644 index 000000000000..9e1c0f273544 --- /dev/null +++ b/src/doc/unstable-book/src/compiler-flags/rmeta-strip-spans.md @@ -0,0 +1,52 @@ +# `rmeta-strip-spans` + +--- + +This flag replaces spans with dummy spans when encoding crate metadata, so +that the encoded bytes do not depend on source positions. Spans in metadata +are byte offsets into this crate's source files, so by default any edit that +shifts source text (adding a comment, a blank line, editing a function body) +changes the metadata of the crate even when its interface is unchanged. + +* `-Zrmeta-strip-spans=none` (default): encode all spans faithfully. +* `-Zrmeta-strip-spans=non-exported`: strip spans except in the metadata that + dependent crates compile into their own output: encoded MIR bodies (used + for cross-crate inlining, generic instantiation, and const evaluation), + hygiene expansion data, and the definition spans of items whose MIR is + exported. The definition spans must accompany the body spans: a dependent + crate derives an inlined function's debuginfo declaration file/line from + the definition span and binds the inlined line rows to that file, so + stripping it while keeping body spans yields line rows bound to the wrong + file. Cost: diagnostics reported in dependent crates can no longer point + into this crate's source for item-level spans of items without exported + MIR ("function defined here" style notes degrade to dummy spans). + Stability boundary: because the preserved spans are byte offsets and the + referenced files' length and line tables must stay real for those spans + to resolve correctly in dependents, only byte-position-preserving edits + (same-length comment rewrites, same-length body edits of non-exported + functions) leave the metadata byte-identical in this mode; any + length-changing edit, even a comment appended at end of file, perturbs + the encoded source file record. This mode is for toolchains that want + the SVH and source-hash normalization plus reduced item-span surface + with zero debuginfo cost; use `all` when byte-stability under general + non-interface edits is the goal. +* `-Zrmeta-strip-spans=all`: additionally strip spans inside exported MIR and + hygiene data, and replace expansion hashes with span-independent ones. + Additional cost: debuginfo line information in dependent crates for code + inlined or instantiated from this crate, const-eval error backtraces + pointing into this crate, and macro-backtrace notes for this crate's + macros. Concretely, the declaration debuginfo a dependent crate emits for + a function inlined from this crate carries the dummy span, which resolves + to the dependent's own first source file at line 1 instead of this + crate's source; call-site line information in the dependent is + unaffected. In exchange the metadata does not depend on source positions at + all: with `-Zrmeta-content-svh` and `-Zrmeta-normalize-src-hash` the + `.rmeta` is byte-identical after comment, whitespace, and private + non-inlinable function body edits. + +Interface-relevant changes (signatures, visibility, adding or removing items, +and bodies of generic or inlinable functions, whose MIR is exported) still +change the metadata under every mode. + +The flag has no effect on proc-macro crates: their metadata consists largely +of span and hygiene data that dependent crates rely on for expansion. diff --git a/tests/run-make/rmeta-stability/rmake.rs b/tests/run-make/rmeta-stability/rmake.rs new file mode 100644 index 000000000000..bf50ba0f635f --- /dev/null +++ b/tests/run-make/rmeta-stability/rmake.rs @@ -0,0 +1,164 @@ +// Test that `-Zrmeta-content-svh`, `-Zrmeta-strip-spans=all` and +// `-Zrmeta-normalize-src-hash` together make a library crate's `.rmeta` +// byte-identical across non-interface edits: +// +// 1. an identical rebuild (this must also hold with the flags off), +// 2. a comment-only edit, +// 3. a whitespace edit that shifts every subsequent line, +// 4. a private non-generic non-inlinable function body edit, +// +// while interface-relevant edits must still change the metadata: +// +// 5. a generic function body edit (its MIR is encoded for downstream +// instantiation), +// 6. a signature change. +// +// `-Zrmeta-normalize-src-hash` alone is also checked to stabilize the +// weakest edit class: a comment edit that preserves byte length and line +// positions, which perturbs nothing but the recorded source file hash. + +//@ ignore-cross-compile + +use run_make_support::{rfs, rust_lib_name, rustc}; + +// The private function carries `#[inline(never)]` so that it is not +// auto-selected for cross-crate inlining at -Copt-level=3; a small private +// function without it would have its (legitimately body-dependent) MIR +// encoded into the metadata. +const BASE: &str = r#"//! Demo crate for rmeta stability. + +// helper does the arithmetic heavy lifting + +pub struct Config { + pub retries: u32, +} + +pub fn make_config() -> Config { + Config { retries: 3 } +} + +pub fn double(x: u64) -> u64 { + helper(x) * 2 +} + +#[inline(never)] +fn helper(x: u64) -> u64 { + x + 1 +} + +pub fn generic_max(a: T, b: T) -> T { + if a > b { a } else { b } +} + +#[inline] +pub fn inlined_add(a: u32, b: u32) -> u32 { + a + b +} +"#; + +const STABILITY_FLAGS: &[&str] = + &["-Zrmeta-content-svh", "-Zrmeta-strip-spans=all", "-Zrmeta-normalize-src-hash"]; + +fn compile(source: &str, flags: &[&str]) -> Vec { + rfs::write("lib.rs", source); + rustc() + .input("lib.rs") + .crate_name("demo") + .crate_type("lib") + .edition("2024") + .opt_level("3") + .emit("metadata,link") + .args(flags) + .run(); + rfs::read(rust_lib_name("demo").replace(".rlib", ".rmeta")) +} + +fn main() { + // Edit classes 2 to 4: must be byte-identical with the stability flags. + let comment_edit = BASE.replace("x + 1", "x + 1 // tweaked comment"); + let whitespace_edit = BASE.replacen( + "//! Demo crate for rmeta stability.\n", + "//! Demo crate for rmeta stability.\n\n", + 1, + ); + let private_body_edit = BASE.replace("x + 1", "x + 2"); + // Controls 5 and 6: must still differ with the stability flags. + let generic_body_edit = BASE.replace("if a > b", "if a >= b"); + let signature_edit = + BASE.replace("pub fn double(x: u64)", "pub fn double(x: u64, _unused: u8)"); + // Class 2c: same byte length, same line structure, different comment text. + let comment_edit_same_len = + BASE.replace("arithmetic heavy lifting", "arithmetic heavy WORKING"); + assert_eq!(BASE.len(), comment_edit_same_len.len()); + + // Class 1: identical rebuilds must be byte-identical even without flags. + let base_no_flags = compile(BASE, &[]); + let rebuild_no_flags = compile(BASE, &[]); + assert!(base_no_flags == rebuild_no_flags, "identical rebuild changed the rmeta (flags off)"); + + let base = compile(BASE, STABILITY_FLAGS); + let rebuild = compile(BASE, STABILITY_FLAGS); + assert!(base == rebuild, "identical rebuild changed the rmeta (flags on)"); + + let comment = compile(&comment_edit, STABILITY_FLAGS); + assert!(base == comment, "comment-only edit changed the rmeta despite stability flags"); + + let whitespace = compile(&whitespace_edit, STABILITY_FLAGS); + assert!(base == whitespace, "whitespace-only edit changed the rmeta despite stability flags"); + + let private_body = compile(&private_body_edit, STABILITY_FLAGS); + assert!( + base == private_body, + "private non-inlinable body edit changed the rmeta despite stability flags" + ); + + // The controls must keep churning: a stability scheme that hides interface + // changes would let dependents link against incompatible artifacts. + let generic_body = compile(&generic_body_edit, STABILITY_FLAGS); + assert!( + base != generic_body, + "generic function body edit did NOT change the rmeta; exported MIR must stay visible" + ); + + let signature = compile(&signature_edit, STABILITY_FLAGS); + assert!( + base != signature, + "signature change did NOT change the rmeta; interface changes must stay visible" + ); + + // A trailing comment (nothing after it changes) must also be stable. + let trailing_edit = format!("{BASE}// trailing note\n"); + let trailing = compile(&trailing_edit, STABILITY_FLAGS); + assert!(base == trailing, "trailing comment changed the rmeta despite stability flags"); + + // `-Zrmeta-normalize-src-hash` alone must stabilize a length-and-line + // preserving comment edit (the only churn there is the source file hash). + let base_src_hash_only = compile(BASE, &["-Zrmeta-normalize-src-hash"]); + let comment_2c = compile(&comment_edit_same_len, &["-Zrmeta-normalize-src-hash"]); + assert!( + base_src_hash_only == comment_2c, + "length-preserving comment edit changed the rmeta despite -Zrmeta-normalize-src-hash" + ); + + // `-Zrmeta-strip-spans=non-exported` preserves exported-MIR spans and the + // source file shape data they resolve through, so its documented + // stability boundary is byte-position-preserving edits: the same-length + // comment rewrite and the same-length private body edit stay identical, + // while the length-changing comment edit legitimately differs. + const NON_EXPORTED_FLAGS: &[&str] = + &["-Zrmeta-content-svh", "-Zrmeta-strip-spans=non-exported", "-Zrmeta-normalize-src-hash"]; + let ne_base = compile(BASE, NON_EXPORTED_FLAGS); + let ne_2c = compile(&comment_edit_same_len, NON_EXPORTED_FLAGS); + assert!(ne_base == ne_2c, "same-length comment edit changed the rmeta under non-exported"); + let ne_priv = compile(&private_body_edit, NON_EXPORTED_FLAGS); + assert!( + ne_base == ne_priv, + "same-length private body edit changed the rmeta under non-exported" + ); + let ne_comment = compile(&comment_edit, NON_EXPORTED_FLAGS); + assert!( + ne_base != ne_comment, + "length-changing comment edit did NOT change the rmeta under non-exported; \ + preserved MIR spans should have shifted (documented stability boundary)" + ); +}