diff options
Diffstat (limited to '')
-rw-r--r-- | compiler/rustc_incremental/src/persist/dirty_clean.rs | 480 |
1 files changed, 480 insertions, 0 deletions
diff --git a/compiler/rustc_incremental/src/persist/dirty_clean.rs b/compiler/rustc_incremental/src/persist/dirty_clean.rs new file mode 100644 index 000000000..710c4a01b --- /dev/null +++ b/compiler/rustc_incremental/src/persist/dirty_clean.rs @@ -0,0 +1,480 @@ +//! Debugging code to test fingerprints computed for query results. For each node marked with +//! `#[rustc_clean]` we will compare the fingerprint from the current and from the previous +//! compilation session as appropriate: +//! +//! - `#[rustc_clean(cfg="rev2", except="typeck")]` if we are +//! in `#[cfg(rev2)]`, then the fingerprints associated with +//! `DepNode::typeck(X)` must be DIFFERENT (`X` is the `DefId` of the +//! current node). +//! - `#[rustc_clean(cfg="rev2")]` same as above, except that the +//! fingerprints must be the SAME (along with all other fingerprints). +//! +//! - `#[rustc_clean(cfg="rev2", loaded_from_disk='typeck")]` asserts that +//! the query result for `DepNode::typeck(X)` was actually +//! loaded from disk (not just marked green). This can be useful +//! to ensure that a test is actually exercising the deserialization +//! logic for a particular query result. This can be combined with +//! `except` +//! +//! Errors are reported if we are in the suitable configuration but +//! the required condition is not met. + +use rustc_ast::{self as ast, Attribute, NestedMetaItem}; +use rustc_data_structures::fx::FxHashSet; +use rustc_hir::def_id::LocalDefId; +use rustc_hir::intravisit; +use rustc_hir::Node as HirNode; +use rustc_hir::{ImplItemKind, ItemKind as HirItem, TraitItemKind}; +use rustc_middle::dep_graph::{label_strs, DepNode, DepNodeExt}; +use rustc_middle::hir::nested_filter; +use rustc_middle::ty::TyCtxt; +use rustc_span::symbol::{sym, Symbol}; +use rustc_span::Span; +use std::iter::FromIterator; +use std::vec::Vec; + +const LOADED_FROM_DISK: Symbol = sym::loaded_from_disk; +const EXCEPT: Symbol = sym::except; +const CFG: Symbol = sym::cfg; + +// Base and Extra labels to build up the labels + +/// For typedef, constants, and statics +const BASE_CONST: &[&str] = &[label_strs::type_of]; + +/// DepNodes for functions + methods +const BASE_FN: &[&str] = &[ + // Callers will depend on the signature of these items, so we better test + label_strs::fn_sig, + label_strs::generics_of, + label_strs::predicates_of, + label_strs::type_of, + // And a big part of compilation (that we eventually want to cache) is type inference + // information: + label_strs::typeck, +]; + +/// DepNodes for Hir, which is pretty much everything +const BASE_HIR: &[&str] = &[ + // hir_owner and hir_owner_nodes should be computed for all nodes + label_strs::hir_owner, + label_strs::hir_owner_nodes, +]; + +/// `impl` implementation of struct/trait +const BASE_IMPL: &[&str] = + &[label_strs::associated_item_def_ids, label_strs::generics_of, label_strs::impl_trait_ref]; + +/// DepNodes for mir_built/Optimized, which is relevant in "executable" +/// code, i.e., functions+methods +const BASE_MIR: &[&str] = &[label_strs::optimized_mir, label_strs::promoted_mir]; + +/// Struct, Enum and Union DepNodes +/// +/// Note that changing the type of a field does not change the type of the struct or enum, but +/// adding/removing fields or changing a fields name or visibility does. +const BASE_STRUCT: &[&str] = + &[label_strs::generics_of, label_strs::predicates_of, label_strs::type_of]; + +/// Trait definition `DepNode`s. +/// Extra `DepNode`s for functions and methods. +const EXTRA_ASSOCIATED: &[&str] = &[label_strs::associated_item]; + +const EXTRA_TRAIT: &[&str] = &[]; + +// Fully Built Labels + +const LABELS_CONST: &[&[&str]] = &[BASE_HIR, BASE_CONST]; + +/// Constant/Typedef in an impl +const LABELS_CONST_IN_IMPL: &[&[&str]] = &[BASE_HIR, BASE_CONST, EXTRA_ASSOCIATED]; + +/// Trait-Const/Typedef DepNodes +const LABELS_CONST_IN_TRAIT: &[&[&str]] = &[BASE_HIR, BASE_CONST, EXTRA_ASSOCIATED, EXTRA_TRAIT]; + +/// Function `DepNode`s. +const LABELS_FN: &[&[&str]] = &[BASE_HIR, BASE_MIR, BASE_FN]; + +/// Method `DepNode`s. +const LABELS_FN_IN_IMPL: &[&[&str]] = &[BASE_HIR, BASE_MIR, BASE_FN, EXTRA_ASSOCIATED]; + +/// Trait method `DepNode`s. +const LABELS_FN_IN_TRAIT: &[&[&str]] = + &[BASE_HIR, BASE_MIR, BASE_FN, EXTRA_ASSOCIATED, EXTRA_TRAIT]; + +/// For generic cases like inline-assembly, modules, etc. +const LABELS_HIR_ONLY: &[&[&str]] = &[BASE_HIR]; + +/// Impl `DepNode`s. +const LABELS_TRAIT: &[&[&str]] = &[ + BASE_HIR, + &[label_strs::associated_item_def_ids, label_strs::predicates_of, label_strs::generics_of], +]; + +/// Impl `DepNode`s. +const LABELS_IMPL: &[&[&str]] = &[BASE_HIR, BASE_IMPL]; + +/// Abstract data type (struct, enum, union) `DepNode`s. +const LABELS_ADT: &[&[&str]] = &[BASE_HIR, BASE_STRUCT]; + +// FIXME: Struct/Enum/Unions Fields (there is currently no way to attach these) +// +// Fields are kind of separate from their containers, as they can change independently from +// them. We should at least check +// +// type_of for these. + +type Labels = FxHashSet<String>; + +/// Represents the requested configuration by rustc_clean/dirty +struct Assertion { + clean: Labels, + dirty: Labels, + loaded_from_disk: Labels, +} + +pub fn check_dirty_clean_annotations(tcx: TyCtxt<'_>) { + if !tcx.sess.opts.unstable_opts.query_dep_graph { + return; + } + + // can't add `#[rustc_clean]` etc without opting in to this feature + if !tcx.features().rustc_attrs { + return; + } + + tcx.dep_graph.with_ignore(|| { + let mut dirty_clean_visitor = DirtyCleanVisitor { tcx, checked_attrs: Default::default() }; + + let crate_items = tcx.hir_crate_items(()); + + for id in crate_items.items() { + dirty_clean_visitor.check_item(id.def_id); + } + + for id in crate_items.trait_items() { + dirty_clean_visitor.check_item(id.def_id); + } + + for id in crate_items.impl_items() { + dirty_clean_visitor.check_item(id.def_id); + } + + for id in crate_items.foreign_items() { + dirty_clean_visitor.check_item(id.def_id); + } + + let mut all_attrs = FindAllAttrs { tcx, found_attrs: vec![] }; + tcx.hir().walk_attributes(&mut all_attrs); + + // Note that we cannot use the existing "unused attribute"-infrastructure + // here, since that is running before codegen. This is also the reason why + // all codegen-specific attributes are `AssumedUsed` in rustc_ast::feature_gate. + all_attrs.report_unchecked_attrs(dirty_clean_visitor.checked_attrs); + }) +} + +pub struct DirtyCleanVisitor<'tcx> { + tcx: TyCtxt<'tcx>, + checked_attrs: FxHashSet<ast::AttrId>, +} + +impl<'tcx> DirtyCleanVisitor<'tcx> { + /// Possibly "deserialize" the attribute into a clean/dirty assertion + fn assertion_maybe(&mut self, item_id: LocalDefId, attr: &Attribute) -> Option<Assertion> { + assert!(attr.has_name(sym::rustc_clean)); + if !check_config(self.tcx, attr) { + // skip: not the correct `cfg=` + return None; + } + let assertion = self.assertion_auto(item_id, attr); + Some(assertion) + } + + /// Gets the "auto" assertion on pre-validated attr, along with the `except` labels. + fn assertion_auto(&mut self, item_id: LocalDefId, attr: &Attribute) -> Assertion { + let (name, mut auto) = self.auto_labels(item_id, attr); + let except = self.except(attr); + let loaded_from_disk = self.loaded_from_disk(attr); + for e in except.iter() { + if !auto.remove(e) { + let msg = format!( + "`except` specified DepNodes that can not be affected for \"{}\": \"{}\"", + name, e + ); + self.tcx.sess.span_fatal(attr.span, &msg); + } + } + Assertion { clean: auto, dirty: except, loaded_from_disk } + } + + /// `loaded_from_disk=` attribute value + fn loaded_from_disk(&self, attr: &Attribute) -> Labels { + for item in attr.meta_item_list().unwrap_or_else(Vec::new) { + if item.has_name(LOADED_FROM_DISK) { + let value = expect_associated_value(self.tcx, &item); + return self.resolve_labels(&item, value); + } + } + // If `loaded_from_disk=` is not specified, don't assert anything + Labels::default() + } + + /// `except=` attribute value + fn except(&self, attr: &Attribute) -> Labels { + for item in attr.meta_item_list().unwrap_or_else(Vec::new) { + if item.has_name(EXCEPT) { + let value = expect_associated_value(self.tcx, &item); + return self.resolve_labels(&item, value); + } + } + // if no `label` or `except` is given, only the node's group are asserted + Labels::default() + } + + /// Return all DepNode labels that should be asserted for this item. + /// index=0 is the "name" used for error messages + fn auto_labels(&mut self, item_id: LocalDefId, attr: &Attribute) -> (&'static str, Labels) { + let node = self.tcx.hir().get_by_def_id(item_id); + let (name, labels) = match node { + HirNode::Item(item) => { + match item.kind { + // note: these are in the same order as hir::Item_; + // FIXME(michaelwoerister): do commented out ones + + // // An `extern crate` item, with optional original crate name, + // HirItem::ExternCrate(..), // intentionally no assertions + + // // `use foo::bar::*;` or `use foo::bar::baz as quux;` + // HirItem::Use(..), // intentionally no assertions + + // A `static` item + HirItem::Static(..) => ("ItemStatic", LABELS_CONST), + + // A `const` item + HirItem::Const(..) => ("ItemConst", LABELS_CONST), + + // A function declaration + HirItem::Fn(..) => ("ItemFn", LABELS_FN), + + // // A module + HirItem::Mod(..) => ("ItemMod", LABELS_HIR_ONLY), + + // // An external module + HirItem::ForeignMod { .. } => ("ItemForeignMod", LABELS_HIR_ONLY), + + // Module-level inline assembly (from global_asm!) + HirItem::GlobalAsm(..) => ("ItemGlobalAsm", LABELS_HIR_ONLY), + + // A type alias, e.g., `type Foo = Bar<u8>` + HirItem::TyAlias(..) => ("ItemTy", LABELS_HIR_ONLY), + + // An enum definition, e.g., `enum Foo<A, B> {C<A>, D<B>}` + HirItem::Enum(..) => ("ItemEnum", LABELS_ADT), + + // A struct definition, e.g., `struct Foo<A> {x: A}` + HirItem::Struct(..) => ("ItemStruct", LABELS_ADT), + + // A union definition, e.g., `union Foo<A, B> {x: A, y: B}` + HirItem::Union(..) => ("ItemUnion", LABELS_ADT), + + // Represents a Trait Declaration + HirItem::Trait(..) => ("ItemTrait", LABELS_TRAIT), + + // An implementation, eg `impl<A> Trait for Foo { .. }` + HirItem::Impl { .. } => ("ItemKind::Impl", LABELS_IMPL), + + _ => self.tcx.sess.span_fatal( + attr.span, + &format!( + "clean/dirty auto-assertions not yet defined \ + for Node::Item.node={:?}", + item.kind + ), + ), + } + } + HirNode::TraitItem(item) => match item.kind { + TraitItemKind::Fn(..) => ("Node::TraitItem", LABELS_FN_IN_TRAIT), + TraitItemKind::Const(..) => ("NodeTraitConst", LABELS_CONST_IN_TRAIT), + TraitItemKind::Type(..) => ("NodeTraitType", LABELS_CONST_IN_TRAIT), + }, + HirNode::ImplItem(item) => match item.kind { + ImplItemKind::Fn(..) => ("Node::ImplItem", LABELS_FN_IN_IMPL), + ImplItemKind::Const(..) => ("NodeImplConst", LABELS_CONST_IN_IMPL), + ImplItemKind::TyAlias(..) => ("NodeImplType", LABELS_CONST_IN_IMPL), + }, + _ => self.tcx.sess.span_fatal( + attr.span, + &format!("clean/dirty auto-assertions not yet defined for {:?}", node), + ), + }; + let labels = + Labels::from_iter(labels.iter().flat_map(|s| s.iter().map(|l| (*l).to_string()))); + (name, labels) + } + + fn resolve_labels(&self, item: &NestedMetaItem, value: Symbol) -> Labels { + let mut out = Labels::default(); + for label in value.as_str().split(',') { + let label = label.trim(); + if DepNode::has_label_string(label) { + if out.contains(label) { + self.tcx.sess.span_fatal( + item.span(), + &format!("dep-node label `{}` is repeated", label), + ); + } + out.insert(label.to_string()); + } else { + self.tcx + .sess + .span_fatal(item.span(), &format!("dep-node label `{}` not recognized", label)); + } + } + out + } + + fn dep_node_str(&self, dep_node: &DepNode) -> String { + if let Some(def_id) = dep_node.extract_def_id(self.tcx) { + format!("{:?}({})", dep_node.kind, self.tcx.def_path_str(def_id)) + } else { + format!("{:?}({:?})", dep_node.kind, dep_node.hash) + } + } + + fn assert_dirty(&self, item_span: Span, dep_node: DepNode) { + debug!("assert_dirty({:?})", dep_node); + + if self.tcx.dep_graph.is_green(&dep_node) { + let dep_node_str = self.dep_node_str(&dep_node); + self.tcx + .sess + .span_err(item_span, &format!("`{}` should be dirty but is not", dep_node_str)); + } + } + + fn assert_clean(&self, item_span: Span, dep_node: DepNode) { + debug!("assert_clean({:?})", dep_node); + + if self.tcx.dep_graph.is_red(&dep_node) { + let dep_node_str = self.dep_node_str(&dep_node); + self.tcx + .sess + .span_err(item_span, &format!("`{}` should be clean but is not", dep_node_str)); + } + } + + fn assert_loaded_from_disk(&self, item_span: Span, dep_node: DepNode) { + debug!("assert_loaded_from_disk({:?})", dep_node); + + if !self.tcx.dep_graph.debug_was_loaded_from_disk(dep_node) { + let dep_node_str = self.dep_node_str(&dep_node); + self.tcx.sess.span_err( + item_span, + &format!("`{}` should have been loaded from disk but it was not", dep_node_str), + ); + } + } + + fn check_item(&mut self, item_id: LocalDefId) { + let item_span = self.tcx.def_span(item_id.to_def_id()); + let def_path_hash = self.tcx.def_path_hash(item_id.to_def_id()); + for attr in self.tcx.get_attrs(item_id.to_def_id(), sym::rustc_clean) { + let Some(assertion) = self.assertion_maybe(item_id, attr) else { + continue; + }; + self.checked_attrs.insert(attr.id); + for label in assertion.clean { + let dep_node = DepNode::from_label_string(self.tcx, &label, def_path_hash).unwrap(); + self.assert_clean(item_span, dep_node); + } + for label in assertion.dirty { + let dep_node = DepNode::from_label_string(self.tcx, &label, def_path_hash).unwrap(); + self.assert_dirty(item_span, dep_node); + } + for label in assertion.loaded_from_disk { + let dep_node = DepNode::from_label_string(self.tcx, &label, def_path_hash).unwrap(); + self.assert_loaded_from_disk(item_span, dep_node); + } + } + } +} + +/// Given a `#[rustc_clean]` attribute, scan for a `cfg="foo"` attribute and check whether we have +/// a cfg flag called `foo`. +fn check_config(tcx: TyCtxt<'_>, attr: &Attribute) -> bool { + debug!("check_config(attr={:?})", attr); + let config = &tcx.sess.parse_sess.config; + debug!("check_config: config={:?}", config); + let mut cfg = None; + for item in attr.meta_item_list().unwrap_or_else(Vec::new) { + if item.has_name(CFG) { + let value = expect_associated_value(tcx, &item); + debug!("check_config: searching for cfg {:?}", value); + cfg = Some(config.contains(&(value, None))); + } else if !(item.has_name(EXCEPT) || item.has_name(LOADED_FROM_DISK)) { + tcx.sess.span_err(attr.span, &format!("unknown item `{}`", item.name_or_empty())); + } + } + + match cfg { + None => tcx.sess.span_fatal(attr.span, "no cfg attribute"), + Some(c) => c, + } +} + +fn expect_associated_value(tcx: TyCtxt<'_>, item: &NestedMetaItem) -> Symbol { + if let Some(value) = item.value_str() { + value + } else { + let msg = if let Some(ident) = item.ident() { + format!("associated value expected for `{}`", ident) + } else { + "expected an associated value".to_string() + }; + + tcx.sess.span_fatal(item.span(), &msg); + } +} + +// A visitor that collects all #[rustc_clean] attributes from +// the HIR. It is used to verify that we really ran checks for all annotated +// nodes. +pub struct FindAllAttrs<'tcx> { + tcx: TyCtxt<'tcx>, + found_attrs: Vec<&'tcx Attribute>, +} + +impl<'tcx> FindAllAttrs<'tcx> { + fn is_active_attr(&mut self, attr: &Attribute) -> bool { + if attr.has_name(sym::rustc_clean) && check_config(self.tcx, attr) { + return true; + } + + false + } + + fn report_unchecked_attrs(&self, mut checked_attrs: FxHashSet<ast::AttrId>) { + for attr in &self.found_attrs { + if !checked_attrs.contains(&attr.id) { + self.tcx.sess.span_err(attr.span, "found unchecked `#[rustc_clean]` attribute"); + checked_attrs.insert(attr.id); + } + } + } +} + +impl<'tcx> intravisit::Visitor<'tcx> for FindAllAttrs<'tcx> { + type NestedFilter = nested_filter::All; + + fn nested_visit_map(&mut self) -> Self::Map { + self.tcx.hir() + } + + fn visit_attribute(&mut self, attr: &'tcx Attribute) { + if self.is_active_attr(attr) { + self.found_attrs.push(attr); + } + } +} |