diff --git a/src/cache.rs b/src/cache.rs index 0cbe48efd..6e13be94f 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -4,7 +4,7 @@ use crate::{ artifacts::Sources, config::{ProjectPaths, SolcConfig}, error::{Result, SolcError}, - filter::{FilteredSource, FilteredSourceInfo, FilteredSources}, + filter::{FilteredSources, SourceCompilationKind}, resolver::GraphEdges, utils, ArtifactFile, ArtifactOutput, Artifacts, ArtifactsMap, OutputContext, Project, ProjectPathsConfig, Source, @@ -12,10 +12,7 @@ use crate::{ use semver::Version; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::{ - collections::{ - btree_map::{BTreeMap, Entry}, - hash_map, BTreeSet, HashMap, HashSet, - }, + collections::{btree_map::BTreeMap, hash_map, BTreeSet, HashMap, HashSet}, fs, path::{Path, PathBuf}, time::{Duration, UNIX_EPOCH}, @@ -51,6 +48,16 @@ impl SolFilesCache { self.files.is_empty() } + /// Returns `true` if the cache contains any artifacts for the given file and version. + pub fn contains(&self, file: &Path, version: &Version) -> bool { + self.files.get(file).map_or(true, |entry| !entry.contains_version(version)) + } + + /// Removes entry for the given file + pub fn remove(&mut self, file: &Path) -> Option { + self.files.remove(file) + } + /// How many entries the cache contains where each entry represents a sourc file pub fn len(&self) -> usize { self.files.len() @@ -302,51 +309,6 @@ impl SolFilesCache { .collect::>>()?; Ok(Artifacts(artifacts)) } - - /// Retains only the `CacheEntry` specified by the file + version combination. - /// - /// In other words, only keep those cache entries with the paths (keys) that the iterator yields - /// and only keep the versions in the cache entry that the version iterator yields. - pub fn retain<'a, I, V>(&mut self, files: I) - where - I: IntoIterator, - V: IntoIterator, - { - let mut files: HashMap<_, _> = files.into_iter().collect(); - - self.files.retain(|file, entry| { - if entry.artifacts.is_empty() { - // keep entries that didn't emit any artifacts in the first place, such as a - // solidity file that only includes error definitions - return true; - } - - if let Some(versions) = files.remove(file.as_path()) { - entry.retain_versions(versions); - } else { - return false; - } - !entry.artifacts.is_empty() - }); - } - - /// Inserts the provided cache entries, if there is an existing `CacheEntry` it will be updated - /// but versions will be merged. - pub fn extend(&mut self, entries: I) - where - I: IntoIterator, - { - for (file, entry) in entries.into_iter() { - match self.files.entry(file) { - Entry::Vacant(e) => { - e.insert(entry); - } - Entry::Occupied(mut other) => { - other.get_mut().merge_artifacts(entry); - } - } - } - } } // async variants for read and write @@ -484,47 +446,21 @@ impl CacheEntry { Ok(artifacts) } - pub(crate) fn insert_artifacts<'a, I, T: 'a>(&mut self, artifacts: I) + pub(crate) fn merge_artifacts<'a, A, I, T: 'a>(&mut self, artifacts: I) where - I: IntoIterator>)>, + I: IntoIterator, + A: IntoIterator>, { - for (name, artifacts) in artifacts.into_iter().filter(|(_, a)| !a.is_empty()) { - let entries: BTreeMap<_, _> = artifacts - .into_iter() - .map(|artifact| (artifact.version.clone(), artifact.file.clone())) - .collect(); - self.artifacts.insert(name.clone(), entries); - } - } - - /// Merges another `CacheEntries` artifacts into the existing set - fn merge_artifacts(&mut self, other: CacheEntry) { - for (name, artifacts) in other.artifacts { - match self.artifacts.entry(name) { - Entry::Vacant(entry) => { - entry.insert(artifacts); - } - Entry::Occupied(mut entry) => { - entry.get_mut().extend(artifacts); - } + for (name, artifacts) in artifacts.into_iter() { + for artifact in artifacts { + self.artifacts + .entry(name.clone()) + .or_default() + .insert(artifact.version.clone(), artifact.file.clone()); } } } - /// Retains only those artifacts that match the provided versions. - /// - /// Removes an artifact entry if none of its versions is included in the `versions` set. - pub fn retain_versions<'a, I>(&mut self, versions: I) - where - I: IntoIterator, - { - let versions = versions.into_iter().collect::>(); - self.artifacts.retain(|_, artifacts| { - artifacts.retain(|version, _| versions.contains(version)); - !artifacts.is_empty() - }) - } - /// Returns `true` if the artifacts set contains the given version pub fn contains_version(&self, version: &Version) -> bool { self.artifacts_versions().any(|(v, _)| v == version) @@ -580,6 +516,31 @@ impl CacheEntry { } } +/// Collection of source file paths mapped to versions. +#[derive(Debug, Clone, Default)] +pub struct GroupedSources { + pub inner: HashMap>, +} + +impl GroupedSources { + /// Inserts provided source and version into the collection. + pub fn insert(&mut self, file: PathBuf, version: Version) { + match self.inner.entry(file) { + hash_map::Entry::Occupied(mut entry) => { + entry.get_mut().insert(version); + } + hash_map::Entry::Vacant(entry) => { + entry.insert(HashSet::from([version])); + } + } + } + + /// Returns true if the file was included with the given version. + pub fn contains(&self, file: &Path, version: &Version) -> bool { + self.inner.get(file).map_or(false, |versions| versions.contains(version)) + } +} + /// A helper abstraction over the [`SolFilesCache`] used to determine what files need to compiled /// and which `Artifacts` can be reused. #[derive(Debug)] @@ -596,18 +557,14 @@ pub(crate) struct ArtifactsCacheInner<'a, T: ArtifactOutput> { /// The project. pub project: &'a Project, - /// All the files that were filtered because they haven't changed. - pub filtered: HashMap)>, + /// Files that were invalidated and removed from cache. + /// Those are not grouped by version and purged completely. + pub dirty_sources: HashSet, - /// The corresponding cache entries for all sources that were deemed to be dirty. + /// Artifact+version pairs which are in scope for each solc version. /// - /// `CacheEntry` are grouped by their Solidity file. - /// During preprocessing the `artifacts` field of a new `CacheEntry` is left blank, because in - /// order to determine the artifacts of the solidity file, the file needs to be compiled first. - /// Only after the `CompilerOutput` is received and all compiled contracts are handled, see - /// [`crate::ArtifactOutput::on_output`] all artifacts, their disk paths, are determined and - /// can be populated before the updated [`crate::SolFilesCache`] is finally written to disk. - pub dirty_source_files: HashMap)>, + /// Only those files will be included into cached artifacts list for each version. + pub sources_in_scope: GroupedSources, /// The file hashes. pub content_hashes: HashMap, @@ -615,125 +572,133 @@ pub(crate) struct ArtifactsCacheInner<'a, T: ArtifactOutput> { impl<'a, T: ArtifactOutput> ArtifactsCacheInner<'a, T> { /// Creates a new cache entry for the file - fn create_cache_entry(&self, file: &Path, source: &Source) -> CacheEntry { + fn create_cache_entry(&mut self, file: PathBuf, source: &Source) { let imports = self .edges - .imports(file) + .imports(&file) .into_iter() .map(|import| utils::source_name(import, self.project.root()).to_path_buf()) .collect(); let entry = CacheEntry { - last_modification_date: CacheEntry::read_last_modification_date(file) + last_modification_date: CacheEntry::read_last_modification_date(&file) .unwrap_or_default(), content_hash: source.content_hash(), - source_name: utils::source_name(file, self.project.root()).into(), + source_name: utils::source_name(&file, self.project.root()).into(), solc_config: self.project.solc_config.clone(), imports, - version_requirement: self.edges.version_requirement(file).map(|v| v.to_string()), + version_requirement: self.edges.version_requirement(&file).map(|v| v.to_string()), // artifacts remain empty until we received the compiler output artifacts: Default::default(), }; - entry + self.cache.files.insert(file, entry.clone()); } - /// inserts a new cache entry for the given file + /// Returns the set of [Source]s that need to be compiled to produce artifacts for requested + /// input. /// - /// If there is already an entry available for the file the given version is added to the set - fn insert_new_cache_entry(&mut self, file: &Path, source: &Source, version: Version) { - if let Some((_, versions)) = self.dirty_source_files.get_mut(file) { - versions.insert(version); - } else { - let entry = self.create_cache_entry(file, source); - self.dirty_source_files.insert(file.to_path_buf(), (entry, HashSet::from([version]))); - } - } + /// Source file may have one of the two [SourceCompilationKind]s: + /// 1. [SourceCompilationKind::Complete] - the file has been modified or compiled with different + /// settings and its cache is invalidated. For such sources we request full data needed for + /// artifact construction. + /// 2. [SourceCompilationKind::Optimized] - the file is not dirty, but is imported by a dirty + /// file and thus will be processed by solc. For such files we don't need full data, so we + /// are marking them as clean to optimize output selection later. + fn filter(&mut self, sources: Sources, version: &Version) -> FilteredSources { + // sources that should be passed to compiler. + let mut compile_complete = BTreeSet::new(); + let mut compile_optimized = BTreeSet::new(); + + // Collect files which cache is invalidated. + self.dirty_sources.extend(self.get_dirty_files(&sources)); + + for (file, source) in sources.iter() { + self.sources_in_scope.insert(file.clone(), version.clone()); + if self.dirty_sources.contains(file) { + compile_complete.insert(file.clone()); + + // If file is dirty, its data should be invalidated and all artifacts for all + // versions should be removed. + self.cache.remove(file.as_path()); + } else if self.is_missing_artifacts(file, version) { + // If source is not dirty, but we are missing artifacts for this version, we + // should compile it to populate the cache. + compile_complete.insert(file.clone()); + } - /// inserts the filtered source with the given version - fn insert_filtered_source(&mut self, file: PathBuf, source: Source, version: Version) { - match self.filtered.entry(file) { - hash_map::Entry::Occupied(mut entry) => { - entry.get_mut().1.insert(version); + // Ensure that we have a cache entry for all sources. + if !self.cache.files.contains_key(file) { + self.create_cache_entry(file.clone(), source); } - hash_map::Entry::Vacant(entry) => { - entry.insert((source, HashSet::from([version]))); + } + + // Prepare optimization by collecting sources which are imported by files requiring complete + // compilation. + for source in &compile_complete { + for import in self.edges.imports(source) { + if !compile_complete.contains(import) { + compile_optimized.insert(import.clone()); + } } } - } - /// Returns the set of [Source]s that need to be included in the `CompilerOutput` in order to - /// recompile the project. - /// - /// We define _dirty_ sources as files that: - /// - are new - /// - were changed - /// - their imports were changed - /// - their artifact is missing - /// - /// A _dirty_ file is always included in the `CompilerInput`. - /// A _dirty_ file can also include clean files - files that do not match any of the above - /// criteria - which solc also requires in order to compile a dirty file. - /// - /// Therefore, these files will also be included in the filtered output but not marked as dirty, - /// so that their `OutputSelection` can be optimized in the `CompilerOutput` and their (empty) - /// artifacts ignored. - fn filter(&mut self, sources: Sources, version: &Version) -> FilteredSources { - // all files that are not dirty themselves, but are pulled from a dirty file - let mut imports_of_dirty = HashSet::new(); + let filtered = sources + .into_iter() + .filter_map(|(file, source)| { + if compile_complete.contains(&file) { + Some((file, SourceCompilationKind::Complete(source))) + } else if compile_optimized.contains(&file) { + Some((file, SourceCompilationKind::Optimized(source))) + } else { + None + } + }) + .collect(); - // separates all source files that fit the criteria (dirty) from those that don't (clean) - let mut dirty_sources = BTreeMap::new(); - let mut clean_sources = Vec::with_capacity(sources.len()); - let dirty_files = self.get_dirty_files(&sources, version); + FilteredSources(filtered) + } - for (file, source) in sources { - let source = self.filter_source(file, source, &dirty_files); - if source.dirty { - // mark all files that are imported by a dirty file - imports_of_dirty.extend(self.edges.all_imported_nodes(source.idx)); - dirty_sources.insert(source.file, FilteredSource::Dirty(source.source)); - } else { - clean_sources.push(source); - } + /// Returns whether we are missing artifacts for the given file and version. + fn is_missing_artifacts(&self, file: &Path, version: &Version) -> bool { + let Some(entry) = self.cache.entry(file) else { + trace!("missing cache entry"); + return true; + }; + + // only check artifact's existence if the file generated artifacts. + // e.g. a solidity file consisting only of import statements (like interfaces that + // re-export) do not create artifacts + if entry.artifacts.is_empty() { + trace!("no artifacts"); + return false; } - // track new cache entries for dirty files - for (file, filtered) in dirty_sources.iter() { - self.insert_new_cache_entry(file, filtered.source(), version.clone()); + if !entry.contains_version(version) { + trace!("missing linked artifacts",); + return true; } - for clean_source in clean_sources { - let FilteredSourceInfo { file, source, idx, .. } = clean_source; - if imports_of_dirty.contains(&idx) { - // file is pulled in by a dirty file - dirty_sources.insert(file.clone(), FilteredSource::Clean(source.clone())); + if entry.artifacts_for_version(version).any(|artifact_path| { + let missing_artifact = !self.cached_artifacts.has_artifact(artifact_path); + if missing_artifact { + trace!("missing artifact \"{}\"", artifact_path.display()); } - self.insert_filtered_source(file, source, version.clone()); + missing_artifact + }) { + return true; } - dirty_sources.into() - } - - /// Returns the state of the given source file. - fn filter_source( - &self, - file: PathBuf, - source: Source, - dirty_files: &HashSet, - ) -> FilteredSourceInfo { - let idx = self.edges.node_id(&file); - let dirty = dirty_files.contains(&file); - FilteredSourceInfo { file, source, idx, dirty } + false } /// Returns a set of files that are dirty itself or import dirty file directly or indirectly. - fn get_dirty_files(&self, sources: &Sources, version: &Version) -> HashSet { + fn get_dirty_files(&self, sources: &Sources) -> HashSet { let mut dirty_files = HashSet::new(); // Pre-add all sources that are guaranteed to be dirty for file in sources.keys() { - if self.is_dirty_impl(file, version) { + if self.is_dirty_impl(file) { dirty_files.insert(file.to_path_buf()); } } @@ -760,14 +725,14 @@ impl<'a, T: ArtifactOutput> ArtifactsCacheInner<'a, T> { } } - fn is_dirty_impl(&self, file: &Path, version: &Version) -> bool { + fn is_dirty_impl(&self, file: &Path) -> bool { let Some(hash) = self.content_hashes.get(file) else { - trace!("missing cache entry"); + trace!("missing content hash"); return true; }; let Some(entry) = self.cache.entry(file) else { - trace!("missing content hash"); + trace!("missing cache entry"); return true; }; @@ -781,29 +746,6 @@ impl<'a, T: ArtifactOutput> ArtifactsCacheInner<'a, T> { return true; } - // only check artifact's existence if the file generated artifacts. - // e.g. a solidity file consisting only of import statements (like interfaces that - // re-export) do not create artifacts - if entry.artifacts.is_empty() { - trace!("no artifacts"); - return false; - } - - if !entry.contains_version(version) { - trace!("missing linked artifacts",); - return true; - } - - if entry.artifacts_for_version(version).any(|artifact_path| { - let missing_artifact = !self.cached_artifacts.has_artifact(artifact_path); - if missing_artifact { - trace!("missing artifact \"{}\"", artifact_path.display()); - } - missing_artifact - }) { - return true; - } - // If any requested extra files are missing for any artifact, mark source as dirty to // generate them for artifacts in self.cached_artifacts.values() { @@ -892,9 +834,9 @@ impl<'a, T: ArtifactOutput> ArtifactsCache<'a, T> { cached_artifacts, edges, project, - filtered: Default::default(), - dirty_source_files: Default::default(), + dirty_sources: Default::default(), content_hashes: Default::default(), + sources_in_scope: Default::default(), }; ArtifactsCache::Cached(cache) @@ -973,76 +915,49 @@ impl<'a, T: ArtifactOutput> ArtifactsCache<'a, T> { let ArtifactsCacheInner { mut cache, mut cached_artifacts, - mut dirty_source_files, - filtered, + dirty_sources, + sources_in_scope, project, .. } = cache; - // keep only those files that were previously filtered (not dirty, reused) - cache.retain(filtered.iter().map(|(p, (_, v))| (p.as_path(), v))); - - // add the written artifacts to the cache entries, this way we can keep a mapping - // from solidity file to its artifacts - // this step is necessary because the concrete artifacts are only known after solc - // was invoked and received as output, before that we merely know the file and - // the versions, so we add the artifacts on a file by file basis - for (file, written_artifacts) in written_artifacts.as_ref() { - let file_path = Path::new(file); - if let Some((cache_entry, versions)) = dirty_source_files.get_mut(file_path) { - cache_entry.insert_artifacts(written_artifacts.iter().map(|(name, artifacts)| { - let artifacts = artifacts - .iter() - .filter(|artifact| versions.contains(&artifact.version)) - .collect::>(); - (name, artifacts) - })); - } + // Remove cached artifacts which are out of scope, dirty or appear in `written_artifacts`. + cached_artifacts.0.retain(|file, artifacts| { + let file = Path::new(file); + artifacts.retain(|name, artifacts| { + artifacts.retain(|artifact| { + let version = &artifact.version; - // cached artifacts that were overwritten also need to be removed from the - // `cached_artifacts` set - if let Some((f, mut cached)) = cached_artifacts.0.remove_entry(file) { - trace!(file, "checking for obsolete cached artifact entries"); - cached.retain(|name, cached_artifacts| { - let Some(written_files) = written_artifacts.get(name) else { + if !sources_in_scope.contains(file, version) { + return false; + } + if dirty_sources.contains(file) { return false; - }; - - // written artifact clashes with a cached artifact, so we need to decide whether - // to keep or to remove the cached - cached_artifacts.retain(|f| { - // we only keep those artifacts that don't conflict with written artifacts - // and which version was a compiler target - let same_version = - written_files.iter().all(|other| other.version != f.version); - let is_filtered = filtered - .get(file_path) - .map(|(_, versions)| versions.contains(&f.version)) - .unwrap_or_default(); - let retain = same_version && is_filtered; - if !retain { - trace!( - artifact=%f.file.display(), - contract=%name, - version=%f.version, - "purging obsolete cached artifact", - ); - } - retain - }); - - !cached_artifacts.is_empty() + } + if written_artifacts + .find_artifact(&file.to_string_lossy(), name, version) + .is_some() + { + return false; + } + true }); + !artifacts.is_empty() + }); + !artifacts.is_empty() + }); - if !cached.is_empty() { - cached_artifacts.0.insert(f, cached); - } + // Update cache entries with newly written artifacts. We update data for any artifacts as + // `written_artifacts` always contain the most recent data. + for (file, artifacts) in written_artifacts.as_ref() { + let file_path = Path::new(file); + // Only update data for existing entries, we should have entries for all in-scope files + // by now. + if let Some(entry) = cache.files.get_mut(file_path) { + entry.merge_artifacts(artifacts); } } - // add the new cache entries to the cache file - cache.extend(dirty_source_files.into_iter().map(|(file, (entry, _))| (file, entry))); - // write to disk if write_to_disk { // make all `CacheEntry` paths relative to the project root and all artifact diff --git a/src/compile/project.rs b/src/compile/project.rs index 34a4f4d6a..690494e8c 100644 --- a/src/compile/project.rs +++ b/src/compile/project.rs @@ -329,13 +329,6 @@ impl<'a, T: ArtifactOutput> CompiledState<'a, T> { ctx, )?; - match cache { - ArtifactsCache::Cached(ref cache) => { - project.artifacts_handler().handle_cached_artifacts(&cache.cached_artifacts)?; - } - ArtifactsCache::Ephemeral(..) => {} - } - // emits all the build infos, if they exist output.write_build_infos(project.build_info_path())?; @@ -370,6 +363,9 @@ impl<'a, T: ArtifactOutput> ArtifactsState<'a, T> { trace!(has_error, project.no_artifacts, skip_write_to_disk, cache_path=?project.cache_path(),"prepare writing cache file"); let cached_artifacts = cache.consume(&compiled_artifacts, !skip_write_to_disk)?; + + project.artifacts_handler().handle_cached_artifacts(&cached_artifacts)?; + Ok(ProjectCompileOutput { compiler_output: output, compiled_artifacts, @@ -437,13 +433,13 @@ impl CompilerSources { .into_iter() .map(|(solc, (version, sources))| { trace!("Filtering {} sources for {}", sources.len(), version); - let sources = cache.filter(sources, &version); + let sources_to_compile = cache.filter(sources, &version); trace!( - "Detected {} dirty sources {:?}", - sources.dirty().count(), - sources.dirty_files().collect::>() + "Detected {} sources to compile {:?}", + sources_to_compile.dirty().count(), + sources_to_compile.dirty_files().collect::>() ); - (solc, (version, sources)) + (solc, (version, sources_to_compile)) }) .collect() } @@ -708,9 +704,7 @@ mod tests { let prep = compiler.preprocess().unwrap(); let cache = prep.cache.as_cached().unwrap(); // 3 contracts - assert_eq!(cache.dirty_source_files.len(), 3); - assert!(cache.filtered.is_empty()); - assert!(cache.cache.is_empty()); + assert_eq!(cache.dirty_sources.len(), 3); let compiled = prep.compile().unwrap(); assert_eq!(compiled.output.contracts.files().count(), 3); @@ -728,7 +722,7 @@ mod tests { let inner = project.project(); let compiler = ProjectCompiler::new(inner).unwrap(); let prep = compiler.preprocess().unwrap(); - assert!(prep.cache.as_cached().unwrap().dirty_source_files.is_empty()) + assert!(prep.cache.as_cached().unwrap().dirty_sources.is_empty()) } #[test] @@ -789,6 +783,13 @@ mod tests { let state = compiler.preprocess().unwrap(); let sources = state.sources.sources(); + let cache = state.cache.as_cached().unwrap(); + + // 2 clean sources + assert_eq!(cache.cache.artifacts_len(), 2); + assert!(cache.cache.all_artifacts_exist()); + assert_eq!(cache.dirty_sources.len(), 1); + // single solc assert_eq!(sources.len(), 1); diff --git a/src/filter.rs b/src/filter.rs index 4122066fe..cf4abb98e 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -57,7 +57,7 @@ pub enum SparseOutputFilter { /// In other words, we request the output of solc only for files that have been detected as /// _dirty_. #[default] - AllDirty, + Optimized, /// Apply an additional filter to [FilteredSources] to Custom(Box), } @@ -79,9 +79,9 @@ impl SparseOutputFilter { graph: &GraphEdges, ) -> Sources { match self { - SparseOutputFilter::AllDirty => { + SparseOutputFilter::Optimized => { if !sources.all_dirty() { - Self::all_dirty(&sources, settings) + Self::optimize(&sources, settings) } } SparseOutputFilter::Custom(f) => { @@ -137,7 +137,7 @@ impl SparseOutputFilter { } /// prunes all clean sources and only selects an output for dirty sources - fn all_dirty(sources: &FilteredSources, settings: &mut Settings) { + fn optimize(sources: &FilteredSources, settings: &mut Settings) { // settings can be optimized trace!( "optimizing output selection for {}/{} sources", @@ -151,18 +151,21 @@ impl SparseOutputFilter { .remove("*") .unwrap_or_else(OutputSelection::default_file_output_selection); - for (file, source) in sources.0.iter() { - if source.is_dirty() { - settings - .output_selection - .as_mut() - .insert(format!("{}", file.display()), selection.clone()); - } else { - trace!("using pruned output selection for {}", file.display()); - settings.output_selection.as_mut().insert( - format!("{}", file.display()), - OutputSelection::empty_file_output_select(), - ); + for (file, kind) in sources.0.iter() { + match kind { + SourceCompilationKind::Complete(_) => { + settings + .output_selection + .as_mut() + .insert(format!("{}", file.display()), selection.clone()); + } + SourceCompilationKind::Optimized(_) => { + trace!("using pruned output selection for {}", file.display()); + settings.output_selection.as_mut().insert( + format!("{}", file.display()), + OutputSelection::empty_file_output_select(), + ); + } } } } @@ -177,15 +180,15 @@ impl From> for SparseOutputFilter { impl fmt::Debug for SparseOutputFilter { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { - SparseOutputFilter::AllDirty => f.write_str("AllDirty"), + SparseOutputFilter::Optimized => f.write_str("Optimized"), SparseOutputFilter::Custom(_) => f.write_str("Custom"), } } } -/// Container type for a set of [FilteredSource] +/// Container type for a mapping from source path to [SourceCompilationKind] #[derive(Debug, Clone, Eq, PartialEq)] -pub struct FilteredSources(pub BTreeMap); +pub struct FilteredSources(pub BTreeMap); impl FilteredSources { pub fn is_empty(&self) -> bool { @@ -196,22 +199,22 @@ impl FilteredSources { self.0.len() } - /// Returns `true` if all files are dirty + /// Returns `true` if no sources should have optimized output selection. pub fn all_dirty(&self) -> bool { self.0.values().all(|s| s.is_dirty()) } - /// Returns all entries that are dirty - pub fn dirty(&self) -> impl Iterator + '_ { + /// Returns all entries that should not be optimized. + pub fn dirty(&self) -> impl Iterator + '_ { self.0.iter().filter(|(_, s)| s.is_dirty()) } - /// Returns all entries that are clean - pub fn clean(&self) -> impl Iterator + '_ { + /// Returns all entries that should be optimized. + pub fn clean(&self) -> impl Iterator + '_ { self.0.iter().filter(|(_, s)| !s.is_dirty()) } - /// Returns all dirty files + /// Returns all files that should not be optimized. pub fn dirty_files(&self) -> impl Iterator + fmt::Debug + '_ { self.0.iter().filter_map(|(k, s)| s.is_dirty().then_some(k)) } @@ -225,75 +228,59 @@ impl From for Sources { impl From for FilteredSources { fn from(s: Sources) -> Self { - FilteredSources(s.into_iter().map(|(key, val)| (key, FilteredSource::Dirty(val))).collect()) + FilteredSources( + s.into_iter().map(|(key, val)| (key, SourceCompilationKind::Complete(val))).collect(), + ) } } -impl From> for FilteredSources { - fn from(s: BTreeMap) -> Self { +impl From> for FilteredSources { + fn from(s: BTreeMap) -> Self { FilteredSources(s) } } -impl AsRef> for FilteredSources { - fn as_ref(&self) -> &BTreeMap { +impl AsRef> for FilteredSources { + fn as_ref(&self) -> &BTreeMap { &self.0 } } -impl AsMut> for FilteredSources { - fn as_mut(&mut self) -> &mut BTreeMap { +impl AsMut> for FilteredSources { + fn as_mut(&mut self) -> &mut BTreeMap { &mut self.0 } } /// Represents the state of a filtered [Source] #[derive(Debug, Clone, Eq, PartialEq)] -pub enum FilteredSource { - /// A source that fits the _dirty_ criteria - Dirty(Source), - /// A source that does _not_ fit the _dirty_ criteria but is included in the filtered set - /// because a _dirty_ file pulls it in, either directly on indirectly. - Clean(Source), +pub enum SourceCompilationKind { + /// We need a complete compilation output for the source. + Complete(Source), + /// A source for which we don't need a complete output and want to optimize its compilation by + /// reducing output selection. + Optimized(Source), } -impl FilteredSource { +impl SourceCompilationKind { /// Returns the underlying source pub fn source(&self) -> &Source { match self { - FilteredSource::Dirty(s) => s, - FilteredSource::Clean(s) => s, + SourceCompilationKind::Complete(s) => s, + SourceCompilationKind::Optimized(s) => s, } } /// Consumes the type and returns the underlying source pub fn into_source(self) -> Source { match self { - FilteredSource::Dirty(s) => s, - FilteredSource::Clean(s) => s, + SourceCompilationKind::Complete(s) => s, + SourceCompilationKind::Optimized(s) => s, } } - /// Whether this file is actually dirt + /// Whether this file should be compiled with full output selection pub fn is_dirty(&self) -> bool { - matches!(self, FilteredSource::Dirty(_)) + matches!(self, SourceCompilationKind::Complete(_)) } } - -/// Helper type that determines the state of a source file -#[derive(Debug)] -pub struct FilteredSourceInfo { - /// Path to the source file. - pub file: PathBuf, - - /// Contents of the file. - pub source: Source, - - /// Index in the [GraphEdges]. - pub idx: usize, - - /// Whether this file is actually dirty. - /// - /// See also `ArtifactsCacheInner::is_dirty` - pub dirty: bool, -} diff --git a/tests/project.rs b/tests/project.rs index 025832080..f85f7b6dc 100644 --- a/tests/project.rs +++ b/tests/project.rs @@ -3519,8 +3519,8 @@ fn can_detect_config_changes() { let compiled = project.compile().unwrap(); compiled.assert_success(); - let cache = SolFilesCache::read(&project.paths().cache).unwrap(); - assert_eq!(cache.files.len(), 2); + let cache_before = SolFilesCache::read(&project.paths().cache).unwrap(); + assert_eq!(cache_before.files.len(), 2); // nothing to compile let compiled = project.compile().unwrap(); @@ -3531,6 +3531,9 @@ fn can_detect_config_changes() { let compiled = project.compile().unwrap(); compiled.assert_success(); assert!(!compiled.is_unchanged()); + + let cache_after = SolFilesCache::read(&project.paths().cache).unwrap(); + assert_ne!(cache_before, cache_after); } #[test]