Skip to content

Commit 9d4f7eb

Browse files
committed
wip
1 parent cff3dec commit 9d4f7eb

File tree

13 files changed

+327
-61
lines changed

13 files changed

+327
-61
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cheatcodes/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ foundry-evm-traces.workspace = true
2525
foundry-wallets.workspace = true
2626
forge-script-sequence.workspace = true
2727

28+
solar.workspace = true
29+
2830
alloy-dyn-abi.workspace = true
2931
alloy-evm.workspace = true
3032
alloy-json-abi.workspace = true

crates/cheatcodes/src/inspector.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use crate::{
88
mock::{MockCallDataContext, MockCallReturnData},
99
prank::Prank,
1010
},
11-
inspector::utils::CommonCreateInput,
11+
inspector::{analysis::CheatcodeAnalysis, utils::CommonCreateInput},
1212
script::{Broadcast, Wallets},
1313
test::{
1414
assume::AssumeNoRevert,
@@ -75,6 +75,7 @@ use std::{
7575
sync::Arc,
7676
};
7777

78+
mod analysis;
7879
mod utils;
7980

8081
pub type Ecx<'a, 'b, 'c> = &'a mut EthEvmContext<&'b mut (dyn DatabaseExt + 'c)>;
@@ -367,6 +368,9 @@ pub type BroadcastableTransactions = VecDeque<BroadcastableTransaction>;
367368
/// allowed to execute cheatcodes
368369
#[derive(Clone, Debug)]
369370
pub struct Cheatcodes {
371+
/// Solar compiler instance, to grant syntactic and semantic analysis capabilities
372+
pub analysis: Option<CheatcodeAnalysis>,
373+
370374
/// The block environment
371375
///
372376
/// Used in the cheatcode handler to overwrite the block environment separately from the
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
//! Cheatcode information, extracted from the syntactic and semantic analysis of the sources.
2+
3+
use eyre::{OptionExt, Result};
4+
use solar::sema::{self, Compiler, Gcx, hir};
5+
use std::{cell::OnceCell, collections::BTreeMap, sync::Arc};
6+
7+
/// Provides cached, on-demand syntactic and semantic analysis of a completed `Compiler` instance.
8+
///
9+
/// This struct acts as a facade over the `Compiler`, offering lazy-loaded analysis
10+
/// for tools like cheatcode inspectors. It assumes the compiler has already
11+
/// completed parsing and lowering.
12+
///
13+
/// # Extending with New Analyses
14+
///
15+
/// To add support for a new type of cached analysis, follow this pattern:
16+
///
17+
/// 1. Add a new `pub OnceCell<Option<T>>` field to `CheatcodeAnalysis`, where `T` is the type of
18+
/// the data that you are adding support for.
19+
///
20+
/// 2. Implement a getter method for the new field. Inside the getter, use
21+
/// `self.field.get_or_init()` to compute and cache the value on the first call.
22+
///
23+
/// 3. Inside the closure passed to `get_or_init()`, create a dedicated visitor to traverse the HIR
24+
/// using `self.compiler.enter()` and collect the required data.
25+
///
26+
/// This ensures all analyses remain lazy, efficient, and consistent with the existing design.
27+
#[derive(Clone)]
28+
pub struct CheatcodeAnalysis {
29+
/// A shared, thread-safe reference to solar's `Compiler` instance.
30+
pub compiler: Arc<Compiler>,
31+
32+
/// Cached struct definitions in the sources.
33+
/// Used to keep field order when parsing JSON values.
34+
pub struct_defs: OnceCell<Option<StructDefinitions>>,
35+
}
36+
37+
pub type StructDefinitions = BTreeMap<String, Vec<(String, String)>>;
38+
39+
impl std::fmt::Debug for CheatcodeAnalysis {
40+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41+
f.debug_struct("CheatcodeAnalysis")
42+
.field("compiler", &"<compiler>")
43+
.field("struct_defs", &self.struct_defs)
44+
.finish()
45+
}
46+
}
47+
48+
impl CheatcodeAnalysis {
49+
pub fn new(compiler: Arc<solar::sema::Compiler>) -> Self {
50+
Self { compiler, struct_defs: OnceCell::new() }
51+
}
52+
53+
/// Lazily initializes and returns the struct definitions.
54+
pub fn struct_defs(&self) -> Result<&StructDefinitions> {
55+
self.struct_defs
56+
.get_or_init(|| {
57+
self.compiler.enter(|compiler| {
58+
let gcx = compiler.gcx();
59+
60+
StructDefinitionResolver::new(gcx).process().ok()
61+
})
62+
})
63+
.as_ref()
64+
.ok_or_eyre("unable to resolve struct definitions")
65+
}
66+
}
67+
68+
/// Generates a map of all struct definitions from the HIR using the resolved `Ty` system.
69+
pub struct StructDefinitionResolver<'hir> {
70+
gcx: Gcx<'hir>,
71+
struct_defs: StructDefinitions,
72+
}
73+
74+
impl<'hir> StructDefinitionResolver<'hir> {
75+
/// Constructs a new generator.
76+
pub fn new(gcx: Gcx<'hir>) -> Self {
77+
Self { gcx, struct_defs: BTreeMap::new() }
78+
}
79+
80+
/// Processes the HIR to generate all the struct definitions.
81+
pub fn process(mut self) -> Result<StructDefinitions> {
82+
for id in self.hir().strukt_ids() {
83+
self.resolve_struct_definition(id)?;
84+
}
85+
Ok(self.struct_defs)
86+
}
87+
88+
#[inline]
89+
fn hir(&self) -> &'hir hir::Hir<'hir> {
90+
&self.gcx.hir
91+
}
92+
93+
/// The recursive core of the generator. Resolves a single struct and adds it to the cache.
94+
fn resolve_struct_definition(&mut self, id: hir::StructId) -> Result<()> {
95+
let qualified_name = self.get_fully_qualified_name(id);
96+
if self.struct_defs.contains_key(&qualified_name) {
97+
return Ok(());
98+
}
99+
100+
let hir = self.hir();
101+
let strukt = hir.strukt(id);
102+
let mut fields = Vec::with_capacity(strukt.fields.len());
103+
104+
for &field_id in strukt.fields {
105+
let var = hir.variable(field_id);
106+
let name =
107+
var.name.ok_or_else(|| eyre::eyre!("struct field is missing a name"))?.to_string();
108+
if let Some(ty_str) = self.ty_to_string(self.gcx.type_of_hir_ty(&var.ty)) {
109+
fields.push((name, ty_str));
110+
}
111+
}
112+
113+
// Only insert if there are fields, to avoid adding empty entries
114+
if !fields.is_empty() {
115+
self.struct_defs.insert(qualified_name, fields);
116+
}
117+
118+
Ok(())
119+
}
120+
121+
/// Converts a resolved `Ty` into its canonical string representation.
122+
fn ty_to_string(&mut self, ty: sema::Ty<'hir>) -> Option<String> {
123+
let ty = ty.peel_refs();
124+
let res = match ty.kind {
125+
sema::ty::TyKind::Elementary(e) => e.to_string(),
126+
sema::ty::TyKind::Array(ty, size) => {
127+
let inner_type = self.ty_to_string(ty)?;
128+
format!("{inner_type}[{size}]")
129+
}
130+
sema::ty::TyKind::DynArray(ty) => {
131+
let inner_type = self.ty_to_string(ty)?;
132+
format!("{inner_type}[]")
133+
}
134+
sema::ty::TyKind::Struct(id) => {
135+
// Ensure the nested struct is resolved before proceeding.
136+
self.resolve_struct_definition(id).ok()?;
137+
self.get_fully_qualified_name(id)
138+
}
139+
sema::ty::TyKind::Udvt(ty, _) => self.ty_to_string(ty)?,
140+
// For now, map enums to `uint8`
141+
sema::ty::TyKind::Enum(_) => "uint8".to_string(),
142+
// For now, map contracts to `address`
143+
sema::ty::TyKind::Contract(_) => "address".to_string(),
144+
// Explicitly disallow unsupported types
145+
_ => return None,
146+
};
147+
148+
Some(res)
149+
}
150+
151+
/// Helper to get the fully qualified name `Contract.Struct`.
152+
fn get_fully_qualified_name(&self, id: hir::StructId) -> String {
153+
let hir = self.hir();
154+
let strukt = hir.strukt(id);
155+
if let Some(contract_id) = strukt.contract {
156+
format!("{}.{}", hir.contract(contract_id).name.as_str(), strukt.name.as_str())
157+
} else {
158+
strukt.name.as_str().into()
159+
}
160+
}
161+
}

crates/evm/evm/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ foundry-evm-coverage.workspace = true
2323
foundry-evm-fuzz.workspace = true
2424
foundry-evm-traces.workspace = true
2525

26+
solar.workspace = true
27+
2628
alloy-dyn-abi = { workspace = true, features = ["arbitrary", "eip712"] }
2729
alloy-evm.workspace = true
2830
alloy-json-abi.workspace = true

crates/evm/evm/src/executors/builder.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@ use crate::{executors::Executor, inspectors::InspectorStackBuilder};
22
use foundry_evm_core::{Env, backend::Backend};
33
use revm::primitives::hardfork::SpecId;
44

5+
// TODO(rusowsky): impl dummy `Debug` trait on `solar::sema::Compiler`
6+
// #[derive(Clone, Debug)]
7+
58
/// The builder that allows to configure an evm [`Executor`] which a stack of optional
69
/// [`revm::Inspector`]s, such as [`Cheatcodes`].
710
///
811
/// By default, the [`Executor`] will be configured with an empty [`InspectorStack`].
912
///
1013
/// [`Cheatcodes`]: super::Cheatcodes
1114
/// [`InspectorStack`]: super::InspectorStack
12-
#[derive(Clone, Debug)]
15+
#[derive(Clone)]
1316
#[must_use = "builders do nothing unless you call `build` on them"]
1417
pub struct ExecutorBuilder {
1518
/// The configuration used to build an `InspectorStack`.

crates/evm/evm/src/inspectors/stack.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,13 @@ use std::{
3434
sync::Arc,
3535
};
3636

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

87+
/// Set the solar compiler instance that grants syntactic and semantic analysis capabilities
88+
#[inline]
89+
pub fn set_analysis(mut self, analysis: Arc<solar::sema::Compiler>) -> Self {
90+
self.analysis = Some(analysis);
91+
self
92+
}
93+
8394
/// Set the block environment.
8495
#[inline]
8596
pub fn block(mut self, block: BlockEnv) -> Self {
@@ -178,6 +189,7 @@ impl InspectorStackBuilder {
178189
/// Builds the stack of inspectors to use when transacting/committing on the EVM.
179190
pub fn build(self) -> InspectorStack {
180191
let Self {
192+
analysis,
181193
block,
182194
gas_price,
183195
cheatcodes,
@@ -204,6 +216,9 @@ impl InspectorStackBuilder {
204216
stack.set_cheatcodes(cheatcodes);
205217
}
206218

219+
if let Some(analysis) = analysis {
220+
stack.set_analysis(analysis);
221+
}
207222
if let Some(fuzzer) = fuzzer {
208223
stack.set_fuzzer(fuzzer);
209224
}

crates/forge/src/cmd/coverage.rs

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use super::{install, test::TestArgs, watch::WatchArgs};
22
use crate::{
3-
MultiContractRunnerBuilder,
3+
ConfigAndProject, MultiContractRunnerBuilder,
44
coverage::{
55
BytecodeReporter, ContractId, CoverageReport, CoverageReporter, CoverageSummaryReporter,
66
DebugReporter, ItemAnchor, LcovReporter,
@@ -103,18 +103,21 @@ impl CoverageArgs {
103103
// Coverage analysis requires the Solc AST output.
104104
config.ast = true;
105105

106-
let (paths, output) = {
107-
let (project, output) = self.build(&config)?;
108-
(project.paths, output)
109-
};
106+
let (project, output) = self.build(&config)?;
110107

111-
self.populate_reporters(&paths.root);
108+
self.populate_reporters(&project.paths.root);
112109

113110
sh_println!("Analysing contracts...")?;
114-
let report = self.prepare(&paths, &output)?;
111+
let report = self.prepare(&project.paths, &output)?;
115112

116113
sh_println!("Running tests...")?;
117-
self.collect(&paths.root, &output, report, Arc::new(config), evm_opts).await
114+
self.collect(
115+
&output,
116+
report,
117+
ConfigAndProject::new(Arc::new(config), Arc::new(project)),
118+
evm_opts,
119+
)
120+
.await
118121
}
119122

120123
fn populate_reporters(&mut self, root: &Path) {
@@ -261,23 +264,23 @@ impl CoverageArgs {
261264
#[instrument(name = "Coverage::collect", skip_all)]
262265
async fn collect(
263266
mut self,
264-
root: &Path,
265267
output: &ProjectCompileOutput,
266268
mut report: CoverageReport,
267-
config: Arc<Config>,
269+
config_and_project: ConfigAndProject,
268270
evm_opts: EvmOpts,
269271
) -> Result<()> {
270272
let verbosity = evm_opts.verbosity;
273+
let config = config_and_project.config.clone();
271274

272275
// Build the contract runner
273276
let env = evm_opts.evm_env().await?;
274-
let runner = MultiContractRunnerBuilder::new(config.clone())
277+
let runner = MultiContractRunnerBuilder::new(config_and_project)
275278
.initial_balance(evm_opts.initial_balance)
276279
.evm_spec(config.evm_spec_id())
277280
.sender(evm_opts.sender)
278281
.with_fork(evm_opts.get_fork(&config, env.clone()))
279282
.set_coverage(true)
280-
.build::<MultiCompiler>(root, output, env, evm_opts)?;
283+
.build::<MultiCompiler>(output, env, evm_opts)?;
281284

282285
let known_contracts = runner.known_contracts.clone();
283286

0 commit comments

Comments
 (0)