Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
abd521f
wip
klkvr Sep 11, 2024
fb64ca7
wip
klkvr Sep 11, 2024
0876b01
add preprocessor
klkvr Sep 11, 2024
314f284
wip
klkvr Sep 11, 2024
86f0562
wip
klkvr Sep 11, 2024
af6550f
fixes
klkvr Sep 13, 2024
276bc94
helper libs
klkvr Sep 13, 2024
fa5d7bf
some docs
klkvr Sep 13, 2024
15ce120
clean up
klkvr Sep 13, 2024
a379db3
fixes
klkvr Sep 16, 2024
25e5ef9
Merge branch 'main' into klkvr/pp-test-rebase
grandizzy Feb 18, 2025
3faaa02
Port to solar
grandizzy Feb 19, 2025
0f78197
Clippy
grandizzy Feb 19, 2025
594c09b
Changes after review
grandizzy Feb 19, 2025
3105272
Patch solar to get HIR visitor impl
grandizzy Feb 19, 2025
7d0edb8
Review changes
grandizzy Feb 19, 2025
4025e5a
reuse replace source fn
grandizzy Feb 19, 2025
0c940d1
Clippy
grandizzy Feb 19, 2025
79d0a0f
Cleanup, move Update type in lib
grandizzy Feb 19, 2025
d37cafb
Change replace_source_content sig, move apply_updates
grandizzy Feb 20, 2025
c526df6
Port to solar HIR, refactor
grandizzy Feb 26, 2025
e691a96
add preprocessing parse constructors
grandizzy Feb 26, 2025
4241d2b
Contract id cleanup
grandizzy Feb 26, 2025
c86c4aa
Cleanup, filter collected dependencies to be source contracts
grandizzy Feb 27, 2025
f4a109d
Cleanup, remove Hir:: usage, use ContractIds
grandizzy Feb 28, 2025
f0818ea
Optional preprocessed cache
grandizzy Mar 3, 2025
2d1e59f
Review cleanup
grandizzy Mar 3, 2025
c354a7c
Simplify find and remove branches
grandizzy Mar 3, 2025
719d4e8
Autodetect and recompile mocks
grandizzy Mar 4, 2025
4342f82
Fix description
grandizzy Mar 4, 2025
d8ece14
Cleanup autodetect and update cached mocks:
grandizzy Mar 5, 2025
c4ec52c
Invalidate cache on preprocess option toggle
grandizzy Mar 5, 2025
15ec56f
Bump solar rev
grandizzy Mar 14, 2025
2f80366
Move preproc tests
grandizzy Mar 14, 2025
1a3de9f
Preprocess by input reference
grandizzy Mar 14, 2025
5c4becb
Merge branch 'main' into klkvr/pp-test-rebase
grandizzy Mar 14, 2025
0fd4caa
Update crates/compilers/src/preprocessor/deps.rs
grandizzy Mar 20, 2025
6259e8d
Merge branch 'main' into klkvr/pp-test-rebase
grandizzy Mar 24, 2025
c41692d
Bump solar
grandizzy Mar 24, 2025
8138172
Rust backtrace to debug win failure
grandizzy Mar 24, 2025
2947ec6
Update crates/compilers/src/lib.rs
grandizzy Mar 24, 2025
af7cc59
Update crates/compilers/src/preprocessor/deps.rs
grandizzy Mar 24, 2025
cb0f1d8
Handle vars without name in ctor
grandizzy Mar 24, 2025
ebda056
Better way to determine constructor call
grandizzy Mar 24, 2025
64c5ecd
Use Path, add named args test
grandizzy Mar 25, 2025
67ac483
Ensure / for win
grandizzy Mar 25, 2025
6b3e887
clean
DaniPopes Mar 25, 2025
c04627d
Use solar main
grandizzy Mar 25, 2025
0d0f5d9
Ensure / for win in import
grandizzy Mar 25, 2025
6a81147
Support value and salt in constructors
grandizzy Mar 26, 2025
322e80a
Handle named args with call args and offset
grandizzy Mar 26, 2025
5d02555
cleaning
DaniPopes Mar 26, 2025
8dd35c3
chore: replace SourceMapLocation with Range<usize>
DaniPopes Mar 26, 2025
a7bc520
fmt
DaniPopes Mar 26, 2025
6b907d1
Cleanup BytecodeDependencyKind::New
grandizzy Mar 26, 2025
446a5d8
Use sources that are already read
grandizzy Mar 26, 2025
4eefc93
fix: correctly set paths in ParsingContext
DaniPopes Mar 27, 2025
9b15b7a
clippy
grandizzy Mar 28, 2025
d27f6f4
Merge branch 'main' into klkvr/pp-test-rebase
grandizzy Mar 28, 2025
fb5ebc2
bump
DaniPopes Mar 28, 2025
59ebda6
feat: avoid reparsing for interface_repr_hash
DaniPopes Mar 31, 2025
4451c71
perf: cache interface_hash in ArtifactsCacheInner
DaniPopes Mar 31, 2025
73ae218
chore: change &PathBuf to &Path
DaniPopes Apr 1, 2025
a9341af
com
DaniPopes Apr 1, 2025
a48b275
fix: correctly create Session and ParsingContext from solc input
DaniPopes Apr 2, 2025
49b65bf
fix: better check for 'is_source_file'
DaniPopes Apr 3, 2025
de0bebc
chore: rm &PathBuf
DaniPopes Apr 3, 2025
14cabe3
test: dapptools instead of hardhat paths
DaniPopes Apr 3, 2025
93b3c3f
Continue compiling if preprocessor fails parsing
grandizzy Apr 3, 2025
5377589
chore: move preprocessor to foundry
DaniPopes Apr 3, 2025
48df36d
check
DaniPopes Apr 3, 2025
743ff47
clippy
DaniPopes Apr 3, 2025
a3f4c4d
nit
DaniPopes Apr 3, 2025
87b9346
Bump solar version
grandizzy Apr 7, 2025
2527d65
Merge branch 'main' into klkvr/pp-test-rebase
grandizzy Apr 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,6 @@ futures-util = "0.3"
tokio = { version = "1.35", features = ["rt-multi-thread"] }

snapbox = "0.6.9"

[patch.crates-io]
solar-parse = { git = "https://github.com/paradigmxyz/solar", rev = "6e8f4a1" }
2 changes: 1 addition & 1 deletion crates/artifacts/solc/src/ast/misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::{fmt, fmt::Write, str::FromStr};
/// Represents the source location of a node: `<start byte>:<length>:<source index>`.
///
/// The `start`, `length` and `index` can be -1 which is represented as `None`
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SourceLocation {
pub start: Option<usize>,
pub length: Option<usize>,
Expand Down
165 changes: 107 additions & 58 deletions crates/compilers/src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::{
buildinfo::RawBuildInfo,
compilers::{Compiler, CompilerSettings, Language},
output::Builds,
preprocessor::interface_representation_hash,
resolver::GraphEdges,
ArtifactFile, ArtifactOutput, Artifacts, ArtifactsMap, Graph, OutputContext, Project,
ProjectPaths, ProjectPathsConfig, SourceCompilationKind,
Expand Down Expand Up @@ -411,6 +412,8 @@ pub struct CacheEntry {
pub last_modification_date: u64,
/// hash to identify whether the content of the file changed
pub content_hash: String,
/// hash of the interface representation of the file, if it's a source file
pub interface_repr_hash: Option<String>,
Comment on lines +420 to +421
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: noticed interfaceReprHash is sometimes null in the cache, would it make sense to skip this field when it is None during serialization w/ #[serde(skip_serializing_if = "Option::is_none")]?

Screenshot from 2025-03-05 12-45-41

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, that's null for all files under test / script dirs, will check if any side effect if we don't serialize them but shouldn't

/// identifier name see [`foundry_compilers_core::utils::source_name()`]
pub source_name: PathBuf,
/// fully resolved imports of the file
Expand Down Expand Up @@ -654,11 +657,20 @@ pub(crate) struct ArtifactsCacheInner<

/// The file hashes.
pub content_hashes: HashMap<PathBuf, String>,

/// The interface representations for source files.
pub interface_repr_hashes: HashMap<PathBuf, String>,
}

impl<T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
ArtifactsCacheInner<'_, T, C>
{
/// Whether given file is a source file or a test/script file.
fn is_source_file(&self, file: &Path) -> bool {
!file.starts_with(&self.project.paths.tests)
&& !file.starts_with(&self.project.paths.scripts)
}

/// Creates a new cache entry for the file
fn create_cache_entry(&mut self, file: PathBuf, source: &Source) {
let imports = self
Expand All @@ -668,10 +680,14 @@ impl<T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
.map(|import| strip_prefix(import, self.project.root()).into())
.collect();

let interface_repr_hash =
self.is_source_file(&file).then(|| interface_representation_hash(source, &file));

let entry = CacheEntry {
last_modification_date: CacheEntry::read_last_modification_date(&file)
.unwrap_or_default(),
content_hash: source.content_hash(),
interface_repr_hash,
source_name: strip_prefix(&file, self.project.root()).into(),
imports,
version_requirement: self.edges.version_requirement(&file).map(|v| v.to_string()),
Expand Down Expand Up @@ -765,26 +781,23 @@ impl<T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
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() {
for artifacts in artifacts.values() {
for artifact_file in artifacts {
if self.project.artifacts_handler().is_dirty(artifact_file).unwrap_or(true) {
return true;
}
}
}
}

false
}

// Walks over all cache entires, detects dirty files and removes them from cache.
fn find_and_remove_dirty(&mut self) {
fn populate_dirty_files<D>(
file: &Path,
dirty_files: &mut HashSet<PathBuf>,
edges: &GraphEdges<D>,
) {
for file in edges.importers(file) {
// If file is marked as dirty we either have already visited it or it was marked as
// dirty initially and will be visited at some point later.
if !dirty_files.contains(file) {
dirty_files.insert(file.to_path_buf());
populate_dirty_files(file, dirty_files, edges);
}
}
}

let existing_profiles = self.project.settings_profiles().collect::<BTreeMap<_, _>>();

let mut dirty_profiles = HashSet::new();
Expand Down Expand Up @@ -821,76 +834,103 @@ impl<T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
}
}

// Iterate over existing cache entries.
let files = self.cache.files.keys().cloned().collect::<HashSet<_>>();

let mut sources = Sources::new();

// Read all sources, marking entries as dirty on I/O errors.
for file in &files {
let Ok(source) = Source::read(file) else {
self.dirty_sources.insert(file.clone());
// Read all sources, removing entries on I/O errors.
for file in self.cache.files.keys().cloned().collect::<Vec<_>>() {
let Ok(source) = Source::read(&file) else {
self.cache.files.remove(&file);
continue;
};
sources.insert(file.clone(), source);
}

// Build a temporary graph for walking imports. We need this because `self.edges`
// only contains graph data for in-scope sources but we are operating on cache entries.
if let Ok(graph) = Graph::<C::ParsedSource>::resolve_sources(&self.project.paths, sources) {
let (sources, edges) = graph.into_sources();
// Calculate content hashes for later comparison.
self.fill_hashes(&sources);

// Calculate content hashes for later comparison.
self.fill_hashes(&sources);
// Pre-add all sources that are guaranteed to be dirty
for file in self.cache.files.keys() {
if self.is_dirty_impl(file, false) {
self.dirty_sources.insert(file.clone());
}
}

// Pre-add all sources that are guaranteed to be dirty
for file in sources.keys() {
if self.is_dirty_impl(file) {
// Build a temporary graph for populating cache. We want to ensure that we preserve all just
// removed entries with updated data. We need separate graph for this because
// `self.edges` only contains graph data for in-scope sources but we are operating on cache
// entries.
let Ok(graph) = Graph::<C::ParsedSource>::resolve_sources(&self.project.paths, sources)
else {
// Purge all sources on graph resolution error.
self.cache.files.clear();
return;
};

let (sources, edges) = graph.into_sources();

// Mark sources as dirty based on their imports
for file in sources.keys() {
if self.dirty_sources.contains(file) {
continue;
}
let is_src = self.is_source_file(file);
for import in edges.imports(file) {
// Any source file importing dirty source file is dirty.
if is_src && self.dirty_sources.contains(import) {
self.dirty_sources.insert(file.clone());
break;
// For non-src files we mark them as dirty only if they import dirty non-src file
// or src file for which interface representation changed.
} else if !is_src
&& self.dirty_sources.contains(import)
&& (!self.is_source_file(import) || self.is_dirty_impl(import, true))
{
self.dirty_sources.insert(file.clone());
}
}

// Perform DFS to find direct/indirect importers of dirty files.
for file in self.dirty_sources.clone().iter() {
populate_dirty_files(file, &mut self.dirty_sources, &edges);
}
} else {
// Purge all sources on graph resolution error.
self.dirty_sources.extend(files);
}

// Remove all dirty files from cache.
for file in &self.dirty_sources {
debug!("removing dirty file from cache: {}", file.display());
self.cache.remove(file);
}
}

fn is_dirty_impl(&self, file: &Path) -> bool {
let Some(hash) = self.content_hashes.get(file) else {
trace!("missing content hash");
return true;
};
// Create new entries for all source files
for (file, source) in sources {
if self.cache.files.contains_key(&file) {
continue;
}

self.create_cache_entry(file.clone(), &source);
}
}

fn is_dirty_impl(&self, file: &Path, use_interface_repr: bool) -> bool {
let Some(entry) = self.cache.entry(file) else {
trace!("missing cache entry");
return true;
};

if entry.content_hash != *hash {
trace!("content hash changed");
return true;
}
if use_interface_repr {
let Some(interface_hash) = self.interface_repr_hashes.get(file) else {
trace!("missing interface hash");
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() {
for artifacts in artifacts.values() {
for artifact_file in artifacts {
if self.project.artifacts_handler().is_dirty(artifact_file).unwrap_or(true) {
return true;
}
}
if entry.interface_repr_hash.as_ref() != Some(interface_hash) {
trace!("interface hash changed");
return true;
};
} else {
let Some(content_hash) = self.content_hashes.get(file) else {
trace!("missing content hash");
return true;
};

if entry.content_hash != *content_hash {
trace!("content hash changed");
return true;
}
}

Expand All @@ -904,6 +944,14 @@ impl<T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
if let hash_map::Entry::Vacant(entry) = self.content_hashes.entry(file.clone()) {
entry.insert(source.content_hash());
}
// Fill interface representation hashes for source files
if self.is_source_file(file) {
if let hash_map::Entry::Vacant(entry) =
self.interface_repr_hashes.entry(file.clone())
{
entry.insert(interface_representation_hash(source, file));
}
}
}
}
}
Expand Down Expand Up @@ -993,6 +1041,7 @@ impl<'a, T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
dirty_sources: Default::default(),
content_hashes: Default::default(),
sources_in_scope: Default::default(),
interface_repr_hashes: Default::default(),
};

ArtifactsCache::Cached(cache)
Expand Down
39 changes: 32 additions & 7 deletions crates/compilers/src/compile/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,16 +109,27 @@ use crate::{
output::{AggregatedCompilerOutput, Builds},
report,
resolver::{GraphEdges, ResolvedSources},
ArtifactOutput, CompilerSettings, Graph, Project, ProjectCompileOutput, Sources,
ArtifactOutput, CompilerSettings, Graph, Project, ProjectCompileOutput, ProjectPathsConfig,
Sources,
};
use foundry_compilers_core::error::Result;
use rayon::prelude::*;
use semver::Version;
use std::{collections::HashMap, path::PathBuf, time::Instant};
use std::{collections::HashMap, fmt::Debug, path::PathBuf, time::Instant};

/// A set of different Solc installations with their version and the sources to be compiled
pub(crate) type VersionedSources<'a, L, S> = HashMap<L, Vec<(Version, Sources, (&'a str, &'a S))>>;

/// Invoked before the actual compiler invocation and can override the input.
pub trait Preprocessor<C: Compiler>: Debug {
fn preprocess(
&self,
compiler: &C,
input: C::Input,
paths: &ProjectPathsConfig<C::Language>,
) -> Result<C::Input>;
}

#[derive(Debug)]
pub struct ProjectCompiler<
'a,
Expand All @@ -132,6 +143,8 @@ pub struct ProjectCompiler<
primary_profiles: HashMap<PathBuf, &'a str>,
/// how to compile all the sources
sources: CompilerSources<'a, C::Language, C::Settings>,
/// Optional preprocessor
preprocessor: Option<Box<dyn Preprocessor<C>>>,
}

impl<'a, T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
Expand Down Expand Up @@ -165,7 +178,11 @@ impl<'a, T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
sources,
};

Ok(Self { edges, primary_profiles, project, sources })
Ok(Self { edges, primary_profiles, project, sources, preprocessor: None })
}

pub fn with_preprocessor(self, preprocessor: impl Preprocessor<C> + 'static) -> Self {
Self { preprocessor: Some(Box::new(preprocessor)), ..self }
}

/// Compiles all the sources of the `Project` in the appropriate mode
Expand Down Expand Up @@ -202,7 +219,7 @@ impl<'a, T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
/// - check cache
fn preprocess(self) -> Result<PreprocessedState<'a, T, C>> {
trace!("preprocessing");
let Self { edges, project, mut sources, primary_profiles } = self;
let Self { edges, project, mut sources, primary_profiles, preprocessor } = self;

// convert paths on windows to ensure consistency with the `CompilerOutput` `solc` emits,
// which is unix style `/`
Expand All @@ -212,7 +229,7 @@ impl<'a, T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
// retain and compile only dirty sources and all their imports
sources.filter(&mut cache);

Ok(PreprocessedState { sources, cache, primary_profiles })
Ok(PreprocessedState { sources, cache, primary_profiles, preprocessor })
}
}

Expand All @@ -230,6 +247,9 @@ struct PreprocessedState<'a, T: ArtifactOutput<CompilerContract = C::CompilerCon

/// A mapping from a source file path to the primary profile name selected for it.
primary_profiles: HashMap<PathBuf, &'a str>,

/// Optional preprocessor
preprocessor: Option<Box<dyn Preprocessor<C>>>,
}

impl<'a, T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
Expand All @@ -238,9 +258,9 @@ impl<'a, T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
/// advance to the next state by compiling all sources
fn compile(self) -> Result<CompiledState<'a, T, C>> {
trace!("compiling");
let PreprocessedState { sources, mut cache, primary_profiles } = self;
let PreprocessedState { sources, mut cache, primary_profiles, preprocessor } = self;

let mut output = sources.compile(&mut cache)?;
let mut output = sources.compile(&mut cache, preprocessor)?;

// source paths get stripped before handing them over to solc, so solc never uses absolute
// paths, instead `--base-path <root dir>` is set. this way any metadata that's derived from
Expand Down Expand Up @@ -435,6 +455,7 @@ impl<L: Language, S: CompilerSettings> CompilerSources<'_, L, S> {
>(
self,
cache: &mut ArtifactsCache<'_, T, C>,
preprocessor: Option<Box<dyn Preprocessor<C>>>,
) -> Result<AggregatedCompilerOutput<C>> {
let project = cache.project();
let graph = cache.graph();
Expand Down Expand Up @@ -481,6 +502,10 @@ impl<L: Language, S: CompilerSettings> CompilerSources<'_, L, S> {

input.strip_prefix(project.paths.root.as_path());

if let Some(preprocessor) = preprocessor.as_ref() {
input = preprocessor.preprocess(&project.compiler, input, &project.paths)?;
}

jobs.push((input, profile, actually_dirty));
}
}
Expand Down
Loading
Loading