Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/cheatcodes/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ foundry-evm-traces.workspace = true
foundry-wallets.workspace = true
forge-script-sequence.workspace = true

solar.workspace = true

alloy-dyn-abi.workspace = true
alloy-evm.workspace = true
alloy-json-abi.workspace = true
Expand Down
6 changes: 5 additions & 1 deletion crates/cheatcodes/src/inspector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::{
mock::{MockCallDataContext, MockCallReturnData},
prank::Prank,
},
inspector::utils::CommonCreateInput,
inspector::{analysis::CheatcodeAnalysis, utils::CommonCreateInput},
script::{Broadcast, Wallets},
test::{
assume::AssumeNoRevert,
Expand Down Expand Up @@ -75,6 +75,7 @@ use std::{
sync::Arc,
};

mod analysis;
mod utils;

pub type Ecx<'a, 'b, 'c> = &'a mut EthEvmContext<&'b mut (dyn DatabaseExt + 'c)>;
Expand Down Expand Up @@ -367,6 +368,9 @@ pub type BroadcastableTransactions = VecDeque<BroadcastableTransaction>;
/// allowed to execute cheatcodes
#[derive(Clone, Debug)]
pub struct Cheatcodes {
/// Solar compiler instance, to grant syntactic and semantic analysis capabilities
pub analysis: Option<CheatcodeAnalysis>,

/// The block environment
///
/// Used in the cheatcode handler to overwrite the block environment separately from the
Expand Down
161 changes: 161 additions & 0 deletions crates/cheatcodes/src/inspector/analysis.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
//! Cheatcode information, extracted from the syntactic and semantic analysis of the sources.

use eyre::{OptionExt, Result};
use solar::sema::{self, Compiler, Gcx, hir};
use std::{cell::OnceCell, collections::BTreeMap, sync::Arc};

/// Provides cached, on-demand syntactic and semantic analysis of a completed `Compiler` instance.
///
/// This struct acts as a facade over the `Compiler`, offering lazy-loaded analysis
/// for tools like cheatcode inspectors. It assumes the compiler has already
/// completed parsing and lowering.
///
/// # Extending with New Analyses
///
/// To add support for a new type of cached analysis, follow this pattern:
///
/// 1. Add a new `pub OnceCell<Option<T>>` field to `CheatcodeAnalysis`, where `T` is the type of
/// the data that you are adding support for.
///
/// 2. Implement a getter method for the new field. Inside the getter, use
/// `self.field.get_or_init()` to compute and cache the value on the first call.
///
/// 3. Inside the closure passed to `get_or_init()`, create a dedicated visitor to traverse the HIR
/// using `self.compiler.enter()` and collect the required data.
///
/// This ensures all analyses remain lazy, efficient, and consistent with the existing design.
#[derive(Clone)]
pub struct CheatcodeAnalysis {
/// A shared, thread-safe reference to solar's `Compiler` instance.
pub compiler: Arc<Compiler>,

/// Cached struct definitions in the sources.
/// Used to keep field order when parsing JSON values.
pub struct_defs: OnceCell<Option<StructDefinitions>>,
}

pub type StructDefinitions = BTreeMap<String, Vec<(String, String)>>;

impl std::fmt::Debug for CheatcodeAnalysis {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CheatcodeAnalysis")
.field("compiler", &"<compiler>")
.field("struct_defs", &self.struct_defs)
.finish()
}
}

impl CheatcodeAnalysis {
pub fn new(compiler: Arc<solar::sema::Compiler>) -> Self {
Self { compiler, struct_defs: OnceCell::new() }
}

/// Lazily initializes and returns the struct definitions.
pub fn struct_defs(&self) -> Result<&StructDefinitions> {
self.struct_defs
.get_or_init(|| {
self.compiler.enter(|compiler| {
let gcx = compiler.gcx();

StructDefinitionResolver::new(gcx).process().ok()
})
})
.as_ref()
.ok_or_eyre("unable to resolve struct definitions")
}
}

/// Generates a map of all struct definitions from the HIR using the resolved `Ty` system.
pub struct StructDefinitionResolver<'hir> {
gcx: Gcx<'hir>,
struct_defs: StructDefinitions,
}

impl<'hir> StructDefinitionResolver<'hir> {
/// Constructs a new generator.
pub fn new(gcx: Gcx<'hir>) -> Self {
Self { gcx, struct_defs: BTreeMap::new() }
}

/// Processes the HIR to generate all the struct definitions.
pub fn process(mut self) -> Result<StructDefinitions> {
for id in self.hir().strukt_ids() {
self.resolve_struct_definition(id)?;
}
Ok(self.struct_defs)
}

#[inline]
fn hir(&self) -> &'hir hir::Hir<'hir> {
&self.gcx.hir
}

/// The recursive core of the generator. Resolves a single struct and adds it to the cache.
fn resolve_struct_definition(&mut self, id: hir::StructId) -> Result<()> {
let qualified_name = self.get_fully_qualified_name(id);
if self.struct_defs.contains_key(&qualified_name) {
return Ok(());
}

let hir = self.hir();
let strukt = hir.strukt(id);
let mut fields = Vec::with_capacity(strukt.fields.len());

for &field_id in strukt.fields {
let var = hir.variable(field_id);
let name =
var.name.ok_or_else(|| eyre::eyre!("struct field is missing a name"))?.to_string();
if let Some(ty_str) = self.ty_to_string(self.gcx.type_of_hir_ty(&var.ty)) {
fields.push((name, ty_str));
}
}

// Only insert if there are fields, to avoid adding empty entries
if !fields.is_empty() {
self.struct_defs.insert(qualified_name, fields);
}

Ok(())
}

/// Converts a resolved `Ty` into its canonical string representation.
fn ty_to_string(&mut self, ty: sema::Ty<'hir>) -> Option<String> {
let ty = ty.peel_refs();
let res = match ty.kind {
sema::ty::TyKind::Elementary(e) => e.to_string(),
sema::ty::TyKind::Array(ty, size) => {
let inner_type = self.ty_to_string(ty)?;
format!("{inner_type}[{size}]")
}
sema::ty::TyKind::DynArray(ty) => {
let inner_type = self.ty_to_string(ty)?;
format!("{inner_type}[]")
}
sema::ty::TyKind::Struct(id) => {
// Ensure the nested struct is resolved before proceeding.
self.resolve_struct_definition(id).ok()?;
self.get_fully_qualified_name(id)
}
sema::ty::TyKind::Udvt(ty, _) => self.ty_to_string(ty)?,
// For now, map enums to `uint8`
sema::ty::TyKind::Enum(_) => "uint8".to_string(),
// For now, map contracts to `address`
sema::ty::TyKind::Contract(_) => "address".to_string(),
// Explicitly disallow unsupported types
_ => return None,
};

Some(res)
}

/// Helper to get the fully qualified name `Contract.Struct`.
fn get_fully_qualified_name(&self, id: hir::StructId) -> String {
let hir = self.hir();
let strukt = hir.strukt(id);
if let Some(contract_id) = strukt.contract {
format!("{}.{}", hir.contract(contract_id).name.as_str(), strukt.name.as_str())
} else {
strukt.name.as_str().into()
}
}
}
2 changes: 2 additions & 0 deletions crates/evm/evm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ foundry-evm-coverage.workspace = true
foundry-evm-fuzz.workspace = true
foundry-evm-traces.workspace = true

solar.workspace = true

alloy-dyn-abi = { workspace = true, features = ["arbitrary", "eip712"] }
alloy-evm.workspace = true
alloy-json-abi.workspace = true
Expand Down
5 changes: 4 additions & 1 deletion crates/evm/evm/src/executors/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ use crate::{executors::Executor, inspectors::InspectorStackBuilder};
use foundry_evm_core::{Env, backend::Backend};
use revm::primitives::hardfork::SpecId;

// TODO(rusowsky): impl dummy `Debug` trait on `solar::sema::Compiler`
// #[derive(Clone, Debug)]

/// The builder that allows to configure an evm [`Executor`] which a stack of optional
/// [`revm::Inspector`]s, such as [`Cheatcodes`].
///
/// By default, the [`Executor`] will be configured with an empty [`InspectorStack`].
///
/// [`Cheatcodes`]: super::Cheatcodes
/// [`InspectorStack`]: super::InspectorStack
#[derive(Clone, Debug)]
#[derive(Clone)]
#[must_use = "builders do nothing unless you call `build` on them"]
pub struct ExecutorBuilder {
/// The configuration used to build an `InspectorStack`.
Expand Down
17 changes: 16 additions & 1 deletion crates/evm/evm/src/inspectors/stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,13 @@ use std::{
sync::Arc,
};

#[derive(Clone, Debug, Default)]
// TODO(rusowsky): impl dummy `Debug` trait for solar `Compiler`
// #[derive(Clone, Debug, Default)]
#[derive(Clone, Default)]
#[must_use = "builders do nothing unless you call `build` on them"]
pub struct InspectorStackBuilder {
/// Solar compiler instance, to grant syntactic and semantic analysis capabilities
pub analysis: Option<Arc<solar::sema::Compiler>>,
/// The block environment.
///
/// Used in the cheatcode handler to overwrite the block environment separately from the
Expand Down Expand Up @@ -80,6 +84,13 @@ impl InspectorStackBuilder {
Self::default()
}

/// Set the solar compiler instance that grants syntactic and semantic analysis capabilities
#[inline]
pub fn set_analysis(mut self, analysis: Arc<solar::sema::Compiler>) -> Self {
self.analysis = Some(analysis);
self
}

/// Set the block environment.
#[inline]
pub fn block(mut self, block: BlockEnv) -> Self {
Expand Down Expand Up @@ -178,6 +189,7 @@ impl InspectorStackBuilder {
/// Builds the stack of inspectors to use when transacting/committing on the EVM.
pub fn build(self) -> InspectorStack {
let Self {
analysis,
block,
gas_price,
cheatcodes,
Expand All @@ -204,6 +216,9 @@ impl InspectorStackBuilder {
stack.set_cheatcodes(cheatcodes);
}

if let Some(analysis) = analysis {
stack.set_analysis(analysis);
}
if let Some(fuzzer) = fuzzer {
stack.set_fuzzer(fuzzer);
}
Expand Down
27 changes: 15 additions & 12 deletions crates/forge/src/cmd/coverage.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::{install, test::TestArgs, watch::WatchArgs};
use crate::{
MultiContractRunnerBuilder,
ConfigAndProject, MultiContractRunnerBuilder,
coverage::{
BytecodeReporter, ContractId, CoverageReport, CoverageReporter, CoverageSummaryReporter,
DebugReporter, ItemAnchor, LcovReporter,
Expand Down Expand Up @@ -103,18 +103,21 @@ impl CoverageArgs {
// Coverage analysis requires the Solc AST output.
config.ast = true;

let (paths, output) = {
let (project, output) = self.build(&config)?;
(project.paths, output)
};
let (project, output) = self.build(&config)?;

self.populate_reporters(&paths.root);
self.populate_reporters(&project.paths.root);

sh_println!("Analysing contracts...")?;
let report = self.prepare(&paths, &output)?;
let report = self.prepare(&project.paths, &output)?;

sh_println!("Running tests...")?;
self.collect(&paths.root, &output, report, Arc::new(config), evm_opts).await
self.collect(
&output,
report,
ConfigAndProject::new(Arc::new(config), Arc::new(project)),
evm_opts,
)
.await
}

fn populate_reporters(&mut self, root: &Path) {
Expand Down Expand Up @@ -261,23 +264,23 @@ impl CoverageArgs {
#[instrument(name = "Coverage::collect", skip_all)]
async fn collect(
mut self,
root: &Path,
output: &ProjectCompileOutput,
mut report: CoverageReport,
config: Arc<Config>,
config_and_project: ConfigAndProject,
evm_opts: EvmOpts,
) -> Result<()> {
let verbosity = evm_opts.verbosity;
let config = config_and_project.config.clone();

// Build the contract runner
let env = evm_opts.evm_env().await?;
let runner = MultiContractRunnerBuilder::new(config.clone())
let runner = MultiContractRunnerBuilder::new(config_and_project)
.initial_balance(evm_opts.initial_balance)
.evm_spec(config.evm_spec_id())
.sender(evm_opts.sender)
.with_fork(evm_opts.get_fork(&config, env.clone()))
.set_coverage(true)
.build::<MultiCompiler>(root, output, env, evm_opts)?;
.build::<MultiCompiler>(output, env, evm_opts)?;

let known_contracts = runner.known_contracts.clone();

Expand Down
Loading
Loading