From be759f74afecf7fa9d0f5e115950ae4d326b199c Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Tue, 16 Sep 2025 01:00:49 +0200 Subject: [PATCH 1/7] wip --- crates/forge/src/cmd/bind_json.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/forge/src/cmd/bind_json.rs b/crates/forge/src/cmd/bind_json.rs index ec355de0a3ae5..1ea35c08b22aa 100644 --- a/crates/forge/src/cmd/bind_json.rs +++ b/crates/forge/src/cmd/bind_json.rs @@ -71,7 +71,7 @@ impl BindJsonArgs { .unwrap(); // Step 2: Preprocess sources to handle potentially invalid bindings - self.preprocess_sources(&mut sources)?; + // self.preprocess_sources(&mut sources)?; // Insert empty bindings file. sources.insert(target_path.clone(), Source::new(JSON_BINDINGS_PLACEHOLDER)); From e2e7fda1384670002231f390ae0836a6dd46671e Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 17 Sep 2025 05:08:15 +0200 Subject: [PATCH 2/7] megawip --- crates/forge/src/cmd/bind_json.rs | 181 +++++------------------------- 1 file changed, 25 insertions(+), 156 deletions(-) diff --git a/crates/forge/src/cmd/bind_json.rs b/crates/forge/src/cmd/bind_json.rs index 1ea35c08b22aa..555bbc27f6eb9 100644 --- a/crates/forge/src/cmd/bind_json.rs +++ b/crates/forge/src/cmd/bind_json.rs @@ -5,7 +5,7 @@ use foundry_cli::{ opts::{BuildOpts, configure_pcx_from_solc}, utils::LoadConfig, }; -use foundry_common::{TYPE_BINDING_PREFIX, fs}; +use foundry_common::{TYPE_BINDING_PREFIX, compile::ProjectCompiler, fs}; use foundry_compilers::{ CompilerInput, Graph, Project, artifacts::{Source, Sources}, @@ -48,86 +48,28 @@ pub struct BindJsonArgs { impl BindJsonArgs { pub fn run(self) -> Result<()> { let config = self.load_config()?; - let project = config.ephemeral_project()?; let target_path = config.root.join(self.out.as_ref().unwrap_or(&config.bind_json.out)); + std::fs::write(target_path, JSON_BINDINGS_PLACEHOLDER)?; - // Step 1: Read and preprocess sources - let sources = project.paths.read_input_files()?; - let graph = Graph::::resolve_sources(&project.paths, sources)?; - - // We only generate bindings for a single Solidity version to avoid conflicts. - let (version, mut sources, _) = graph - // resolve graph into mapping language -> version -> sources - .into_sources_by_version(&project)? - .sources - .into_iter() - // we are only interested in Solidity sources - .find(|(lang, _)| *lang == MultiCompilerLanguage::Solc(SolcLanguage::Solidity)) - .ok_or_else(|| eyre::eyre!("no Solidity sources"))? - .1 - .into_iter() - // For now, we are always picking the latest version. - .max_by(|(v1, _, _), (v2, _, _)| v1.cmp(v2)) - .unwrap(); - - // Step 2: Preprocess sources to handle potentially invalid bindings - // self.preprocess_sources(&mut sources)?; + let project = config.solar_project()?; + let mut output = ProjectCompiler::new().compile(&project)?; + + // Read and preprocess sources to handle potentially invalid bindings. + let mut sources = self.preprocess_sources(&mut output)?; // Insert empty bindings file. sources.insert(target_path.clone(), Source::new(JSON_BINDINGS_PLACEHOLDER)); - // Step 3: Find structs and generate bindings + // Find structs and generate bindings. let structs_to_write = - self.find_and_resolve_structs(&config, &project, version, sources, &target_path)?; + self.find_and_resolve_structs(&config, &project, &mut output, &target_path)?; - // Step 4: Write bindings + // Write bindings. self.write_bindings(&structs_to_write, &target_path)?; Ok(()) } - /// In cases when user moves/renames/deletes structs, compiler will start failing because - /// generated bindings will be referencing non-existing structs or importing non-existing - /// files. - /// - /// Because of that, we need a little bit of preprocessing to make sure that bindings will still - /// be valid. - /// - /// The strategy is: - /// 1. Replace bindings file with an empty one to get rid of potentially invalid imports. - /// 2. Remove all function bodies to get rid of `serialize`/`deserialize` invocations. - /// 3. Remove all `immutable` attributes to avoid errors because of erased constructors - /// initializing them. - /// - /// After that we'll still have enough information for bindings but compilation should succeed - /// in most of the cases. - fn preprocess_sources(&self, sources: &mut Sources) -> Result<()> { - let sess = Session::builder().with_stderr_emitter().build(); - let result = sess.enter(|| -> solar::interface::Result<()> { - sources.0.par_iter_mut().try_for_each(|(path, source)| { - let mut content = Arc::try_unwrap(std::mem::take(&mut source.content)).unwrap(); - - let arena = Arena::new(); - let mut parser = SolarParser::from_source_code( - &sess, - &arena, - FileName::Real(path.clone()), - content.to_string(), - )?; - let ast = parser.parse_file().map_err(|e| e.emit())?; - - let mut visitor = PreprocessorVisitor::new(); - let _ = visitor.visit_source_unit(&ast); - visitor.update(&sess, &mut content); - - source.content = Arc::new(content); - Ok(()) - }) - }); - eyre::ensure!(result.is_ok(), "failed parsing"); - Ok(()) - } - /// Find structs, resolve conflicts, and prepare them for writing fn find_and_resolve_structs( &self, @@ -189,27 +131,21 @@ impl BindJsonArgs { let hir = &gcx.hir; let resolver = Resolver::new(gcx); for id in resolver.struct_ids() { - if let Some(schema) = resolver.resolve_struct_eip712(id) { - let def = hir.strukt(id); - let source = hir.source(def.source); - - if !target_files.contains(&source.file) { - continue; - } - - if let FileName::Real(path) = &source.file.name { - structs_to_write.push(StructToWrite { - name: def.name.as_str().into(), - contract_name: def - .contract - .map(|id| hir.contract(id).name.as_str().into()), - path: path.strip_prefix(root).unwrap_or(path).to_path_buf(), - schema, - // will be filled later - import_alias: None, - name_in_fns: String::new(), - }); - } + if let Some(schema) = resolver.resolve_struct_eip712(id) + && let def = hir.strukt(id) + && let source = hir.source(def.source) + && target_files.contains(&source.file) + && let FileName::Real(path) = &source.file.name + { + structs_to_write.push(StructToWrite { + name: def.name.as_str().into(), + contract_name: def.contract.map(|id| hir.contract(id).name.as_str().into()), + path: path.strip_prefix(root).unwrap_or(path).to_path_buf(), + schema, + // will be filled later + import_alias: None, + name_in_fns: String::new(), + }); } } Ok(()) @@ -393,73 +329,6 @@ library JsonBindings { } } -struct PreprocessorVisitor { - updates: Vec<(Span, &'static str)>, -} - -impl PreprocessorVisitor { - fn new() -> Self { - Self { updates: Vec::new() } - } - - fn update(mut self, sess: &Session, content: &mut String) { - if self.updates.is_empty() { - return; - } - - let sf = sess.source_map().lookup_source_file(self.updates[0].0.lo()); - let base = sf.start_pos.0; - - self.updates.sort_by_key(|(span, _)| span.lo()); - let mut shift = 0_i64; - for (span, new) in self.updates { - let lo = span.lo() - base; - let hi = span.hi() - base; - let start = ((lo.0 as i64) - shift) as usize; - let end = ((hi.0 as i64) - shift) as usize; - - content.replace_range(start..end, new); - shift += (end - start) as i64; - shift -= new.len() as i64; - } - } -} - -impl<'ast> Visit<'ast> for PreprocessorVisitor { - type BreakValue = solar::interface::data_structures::Never; - - fn visit_item_function( - &mut self, - func: &'ast ast::ItemFunction<'ast>, - ) -> ControlFlow { - // Replace function bodies with a noop statement. - if let Some(block) = &func.body - && !block.is_empty() - { - let span = block.first().unwrap().span.to(block.last().unwrap().span); - let new_body = match func.kind { - FunctionKind::Modifier => "_;", - _ => "revert();", - }; - self.updates.push((span, new_body)); - } - - self.walk_item_function(func) - } - - fn visit_variable_definition( - &mut self, - var: &'ast ast::VariableDefinition<'ast>, - ) -> ControlFlow { - // Remove `immutable` attributes. - if let Some(VarMut::Immutable) = var.mutability { - self.updates.push((var.span, "")); - } - - self.walk_variable_definition(var) - } -} - /// A single struct definition for which we need to generate bindings. #[derive(Debug, Clone)] struct StructToWrite { From 9c459bac4ea0dea25e35c96cef530cfa39e5af3a Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 17 Sep 2025 17:18:37 +0200 Subject: [PATCH 3/7] clean --- crates/forge/src/cmd/bind_json.rs | 98 ++++++++----------------------- 1 file changed, 26 insertions(+), 72 deletions(-) diff --git a/crates/forge/src/cmd/bind_json.rs b/crates/forge/src/cmd/bind_json.rs index 555bbc27f6eb9..e6f5fbc436632 100644 --- a/crates/forge/src/cmd/bind_json.rs +++ b/crates/forge/src/cmd/bind_json.rs @@ -1,33 +1,18 @@ use super::eip712::Resolver; use clap::{Parser, ValueHint}; use eyre::Result; -use foundry_cli::{ - opts::{BuildOpts, configure_pcx_from_solc}, - utils::LoadConfig, -}; -use foundry_common::{TYPE_BINDING_PREFIX, compile::ProjectCompiler, fs}; -use foundry_compilers::{ - CompilerInput, Graph, Project, - artifacts::{Source, Sources}, - multi::{MultiCompilerLanguage, MultiCompilerParser}, - solc::{SolcLanguage, SolcVersionedInput}, +use foundry_cli::{opts::BuildOpts, utils::LoadConfig}; +use foundry_common::{ + TYPE_BINDING_PREFIX, compile::ProjectCompiler, errors::convert_solar_errors, fs, }; +use foundry_compilers::{Project, ProjectCompileOutput}; use foundry_config::Config; use itertools::Itertools; use path_slash::PathExt; -use rayon::prelude::*; -use semver::Version; -use solar::parse::{ - Parser as SolarParser, - ast::{self, Arena, FunctionKind, Span, VarMut, interface::source_map::FileName, visit::Visit}, - interface::Session, -}; use std::{ - collections::{BTreeMap, BTreeSet, HashSet}, + collections::{BTreeMap, BTreeSet}, fmt::Write, - ops::ControlFlow, path::{Path, PathBuf}, - sync::Arc, }; foundry_config::impl_figment_convert!(BindJsonArgs, build); @@ -49,20 +34,13 @@ impl BindJsonArgs { pub fn run(self) -> Result<()> { let config = self.load_config()?; let target_path = config.root.join(self.out.as_ref().unwrap_or(&config.bind_json.out)); - std::fs::write(target_path, JSON_BINDINGS_PLACEHOLDER)?; + std::fs::write(&target_path, JSON_BINDINGS_PLACEHOLDER)?; let project = config.solar_project()?; let mut output = ProjectCompiler::new().compile(&project)?; - // Read and preprocess sources to handle potentially invalid bindings. - let mut sources = self.preprocess_sources(&mut output)?; - - // Insert empty bindings file. - sources.insert(target_path.clone(), Source::new(JSON_BINDINGS_PLACEHOLDER)); - // Find structs and generate bindings. - let structs_to_write = - self.find_and_resolve_structs(&config, &project, &mut output, &target_path)?; + let structs_to_write = self.find_and_resolve_structs(&config, &project, &mut output)?; // Write bindings. self.write_bindings(&structs_to_write, &target_path)?; @@ -75,58 +53,39 @@ impl BindJsonArgs { &self, config: &Config, project: &Project, - version: Version, - sources: Sources, - _target_path: &Path, + output: &mut ProjectCompileOutput, ) -> Result> { - let settings = config.solc_settings()?; let include = &config.bind_json.include; let exclude = &config.bind_json.exclude; let root = &config.root; - let input = SolcVersionedInput::build(sources, settings, SolcLanguage::Solidity, version); - - let mut sess = Session::builder().with_stderr_emitter().build(); - sess.dcx.set_flags_mut(|flags| flags.track_diagnostics = false); - let mut compiler = solar::sema::Compiler::new(sess); - let mut structs_to_write = Vec::new(); - compiler.enter_mut(|compiler| -> Result<()> { - // Set up the parsing context with the project paths, without adding the source files - let mut pcx = compiler.parse(); - configure_pcx_from_solc(&mut pcx, project, &input, false); - - let mut target_files = HashSet::new(); - for (path, source) in &input.input.sources { + let compiler = output.parser_mut().solc_mut().compiler_mut(); + compiler.enter_mut(|compiler| { + let exclude = |path: &Path| { if !include.is_empty() { if !include.iter().any(|matcher| matcher.is_match(path)) { - continue; + return true; } } else { // Exclude library files by default if project.paths.has_library_ancestor(path) { - continue; + return true; } } if exclude.iter().any(|matcher| matcher.is_match(path)) { - continue; + return true; } - if let Ok(src_file) = compiler - .sess() - .source_map() - .new_source_file(path.clone(), source.content.as_str()) - { - target_files.insert(src_file.clone()); - pcx.add_file(src_file); - } - } + false + }; - // Parse and resolve - pcx.parse(); - let Ok(ControlFlow::Continue(())) = compiler.lower_asts() else { return Ok(()) }; + // Resolve. + if compiler.gcx().stage() < Some(solar::config::CompilerStage::Lowering) { + let _ = compiler.lower_asts(); + } let gcx = compiler.gcx(); let hir = &gcx.hir; let resolver = Resolver::new(gcx); @@ -134,8 +93,8 @@ impl BindJsonArgs { if let Some(schema) = resolver.resolve_struct_eip712(id) && let def = hir.strukt(id) && let source = hir.source(def.source) - && target_files.contains(&source.file) - && let FileName::Real(path) = &source.file.name + && let solar::interface::source_map::FileName::Real(path) = &source.file.name + && !exclude(path) { structs_to_write.push(StructToWrite { name: def.name.as_str().into(), @@ -148,12 +107,11 @@ impl BindJsonArgs { }); } } - Ok(()) - })?; + }); - eyre::ensure!(compiler.sess().dcx.has_errors().is_ok(), "errors occurred"); + convert_solar_errors(compiler.dcx())?; - // Resolve import aliases and function names + // Resolve import aliases and function names. self.resolve_conflicts(&mut structs_to_write); Ok(structs_to_write) @@ -227,11 +185,7 @@ impl BindJsonArgs { } /// Write the final bindings file - fn write_bindings( - &self, - structs_to_write: &[StructToWrite], - target_path: &PathBuf, - ) -> Result<()> { + fn write_bindings(&self, structs_to_write: &[StructToWrite], target_path: &Path) -> Result<()> { let mut result = String::new(); // Write imports From cce01c749006c99d23e6193116f3f5cc3135df8f Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Thu, 18 Sep 2025 05:01:39 +0200 Subject: [PATCH 4/7] wip --- crates/forge/src/cmd/bind_json.rs | 22 +++++++++++++++++----- crates/forge/tests/cli/eip712.rs | 20 +++++++++++++------- crates/test-utils/src/util.rs | 9 ++++++--- 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/crates/forge/src/cmd/bind_json.rs b/crates/forge/src/cmd/bind_json.rs index e6f5fbc436632..dc2b1bae8e89d 100644 --- a/crates/forge/src/cmd/bind_json.rs +++ b/crates/forge/src/cmd/bind_json.rs @@ -34,7 +34,11 @@ impl BindJsonArgs { pub fn run(self) -> Result<()> { let config = self.load_config()?; let target_path = config.root.join(self.out.as_ref().unwrap_or(&config.bind_json.out)); - std::fs::write(&target_path, JSON_BINDINGS_PLACEHOLDER)?; + + // Overwrite existing bindings. + if target_path.exists() { + fs::write(&target_path, JSON_BINDINGS_PLACEHOLDER)?; + } let project = config.solar_project()?; let mut output = ProjectCompiler::new().compile(&project)?; @@ -198,11 +202,19 @@ impl BindJsonArgs { .insert(item); } - result.push_str("// Automatically generated by forge bind-json.\n\npragma solidity >=0.6.2 <0.9.0;\npragma experimental ABIEncoderV2;\n\n"); + result.push_str( + "\ +// Automatically @generated by `forge bind-json`. + +pragma solidity >=0.6.2 <0.9.0; +pragma experimental ABIEncoderV2; + +", + ); for (path, names) in grouped_imports { writeln!( - &mut result, + result, "import {{{}}} from \"{}\";", names.iter().join(", "), path.to_slash_lossy() @@ -233,7 +245,7 @@ library JsonBindings { // write schema constants for struct_to_write in structs_to_write { writeln!( - &mut result, + result, " {}{} = \"{}\";", TYPE_BINDING_PREFIX, struct_to_write.name_in_fns, struct_to_write.schema )?; @@ -242,7 +254,7 @@ library JsonBindings { // write serialization functions for struct_to_write in structs_to_write { write!( - &mut result, + result, r#" function serialize({path} memory value) internal pure returns (string memory) {{ return vm.serializeJsonType(schema_{name_in_fns}, abi.encode(value)); diff --git a/crates/forge/tests/cli/eip712.rs b/crates/forge/tests/cli/eip712.rs index 5e000b86a7a81..a7687eaf90e56 100644 --- a/crates/forge/tests/cli/eip712.rs +++ b/crates/forge/tests/cli/eip712.rs @@ -1,4 +1,5 @@ use foundry_config::fs_permissions::PathPermission; +use std::path::Path; forgetest!(test_eip712, |prj, cmd| { let path = prj.add_source( @@ -249,11 +250,11 @@ contract Eip712Test is DSTest { cmd.forge_fuse().args(["bind-json"]).assert_success(); let bindings = prj.root().join("utils").join("JsonBindings.sol"); - assert!(bindings.exists(), "'JsonBindings.sol' was not generated at {bindings:?}"); + assert_bindings(&bindings); prj.update_config(|config| config.fs_permissions.add(PathPermission::read(bindings))); - cmd.forge_fuse().args(["test", "--mc", "Eip712Test", "-vv"]).assert_success().stdout_eq(str![ - [r#" + cmd.forge_fuse(); + cmd.args(["test", "--mc", "Eip712Test", "-vv"]).assert_success().stdout_eq(str![[r#" [COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] Compiler run successful! @@ -267,8 +268,7 @@ Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED] Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) -"#] - ]); +"#]]); }); forgetest!(test_eip712_cheatcode_nested, |prj, cmd| { @@ -371,7 +371,7 @@ Encountered a total of 1 failing tests, 0 tests succeeded "#]]); cmd.forge_fuse().args(["bind-json"]).assert_success(); - assert!(bindings.exists(), "'JsonBindings.sol' was not generated at {bindings:?}"); + assert_bindings(&bindings); // with generated bindings, cheatcode by type name works cmd.forge_fuse() @@ -415,7 +415,7 @@ Encountered a total of 1 failing tests, 0 tests succeeded "#]]); cmd.forge_fuse().args(["bind-json", "utils/CustomJsonBindings.sol"]).assert_success(); - assert!(bindings_2.exists(), "'CustomJsonBindings.sol' was not generated at {bindings_2:?}"); + assert_bindings(&bindings_2); // with generated bindings, cheatcode by custom path and type name works cmd.forge_fuse() @@ -848,3 +848,9 @@ contract CounterStrike_Test is DSTest { cmd.forge_fuse().args(["test", "-vvv"]).assert_success(); }); + +#[track_caller] +fn assert_bindings(bindings: &Path) { + assert!(bindings.exists(), "'JsonBindings.sol' was not generated at {bindings:?}"); + // assert_data_eq!(Data::try_read_from(bindings, None).unwrap(), expected); +} diff --git a/crates/test-utils/src/util.rs b/crates/test-utils/src/util.rs index e1e53ba59218f..9184093933edc 100644 --- a/crates/test-utils/src/util.rs +++ b/crates/test-utils/src/util.rs @@ -862,23 +862,26 @@ impl TestCommand { } /// Set the environment variable `k` to value `v` for the command. - pub fn env(&mut self, k: impl AsRef, v: impl AsRef) { + pub fn env(&mut self, k: impl AsRef, v: impl AsRef) -> &mut Self { self.cmd.env(k, v); + self } /// Set the environment variable `k` to value `v` for the command. - pub fn envs(&mut self, envs: I) + pub fn envs(&mut self, envs: I) -> &mut Self where I: IntoIterator, K: AsRef, V: AsRef, { self.cmd.envs(envs); + self } /// Unsets the environment variable `k` for the command. - pub fn unset_env(&mut self, k: impl AsRef) { + pub fn unset_env(&mut self, k: impl AsRef) -> &mut Self { self.cmd.env_remove(k); + self } /// Set the working directory for this command. From e8bf32bf8ba9d8945158ed030a789b3212d6bbb7 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Thu, 18 Sep 2025 05:35:00 +0200 Subject: [PATCH 5/7] readd preprocess, use projectcompiler --- Cargo.lock | 2 - crates/cli/Cargo.toml | 2 - crates/cli/src/opts/build/mod.rs | 3 - crates/cli/src/opts/build/utils.rs | 114 ------------------- crates/forge/src/cmd/bind_json.rs | 169 ++++++++++++++++++++++++++--- 5 files changed, 156 insertions(+), 134 deletions(-) delete mode 100644 crates/cli/src/opts/build/utils.rs diff --git a/Cargo.lock b/Cargo.lock index 4c7c9dac4cbc1..23834ec288419 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4400,7 +4400,6 @@ dependencies = [ "clap", "color-eyre", "dotenvy", - "dunce", "eyre", "forge-fmt", "foundry-common", @@ -4418,7 +4417,6 @@ dependencies = [ "rustls", "serde", "serde_json", - "solar-compiler", "strsim", "strum 0.27.2", "tempfile", diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 9c7757edb6a70..917977bc8daf3 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -21,7 +21,6 @@ foundry-evm.workspace = true foundry-wallets.workspace = true foundry-compilers = { workspace = true, features = ["full"] } -solar.workspace = true alloy-eips.workspace = true alloy-dyn-abi.workspace = true @@ -52,7 +51,6 @@ tracing-subscriber = { workspace = true, features = ["registry", "env-filter"] } tracing.workspace = true yansi.workspace = true rustls = { workspace = true, features = ["ring"] } -dunce.workspace = true tracing-tracy = { version = "0.11", optional = true, features = ["demangle"] } diff --git a/crates/cli/src/opts/build/mod.rs b/crates/cli/src/opts/build/mod.rs index c6e9ce6ed64d3..31c88a5139bbd 100644 --- a/crates/cli/src/opts/build/mod.rs +++ b/crates/cli/src/opts/build/mod.rs @@ -8,9 +8,6 @@ pub use self::core::BuildOpts; mod paths; pub use self::paths::ProjectPathOpts; -mod utils; -pub use self::utils::{configure_pcx, configure_pcx_from_solc}; - // A set of solc compiler settings that can be set via command line arguments, which are intended // to be merged into an existing `foundry_config::Config`. // diff --git a/crates/cli/src/opts/build/utils.rs b/crates/cli/src/opts/build/utils.rs deleted file mode 100644 index 43fb685ae945b..0000000000000 --- a/crates/cli/src/opts/build/utils.rs +++ /dev/null @@ -1,114 +0,0 @@ -use eyre::Result; -use foundry_compilers::{ - CompilerInput, Graph, Project, - artifacts::{Source, Sources}, - multi::{MultiCompilerLanguage, MultiCompilerParser}, - solc::{SolcLanguage, SolcVersionedInput}, -}; -use foundry_config::Config; -use rayon::prelude::*; -use solar::sema::ParsingContext; -use std::path::PathBuf; - -/// Configures a [`ParsingContext`] from [`Config`]. -/// -/// - Configures include paths, remappings -/// - Source files are added if `add_source_file` is set -/// - If no `project` is provided, it will spin up a new ephemeral project. -/// - If no `target_paths` are provided, all project files are processed. -/// - Only processes the subset of sources with the most up-to-date Solidity version. -pub fn configure_pcx( - pcx: &mut ParsingContext<'_>, - config: &Config, - project: Option<&Project>, - target_paths: Option<&[PathBuf]>, -) -> Result<()> { - // Process build options - let project = match project { - Some(project) => project, - None => &config.ephemeral_project()?, - }; - - let sources = match target_paths { - // If target files are provided, only process those sources - Some(targets) => { - let mut sources = Sources::new(); - for t in targets { - let path = dunce::canonicalize(t)?; - let source = Source::read(&path)?; - sources.insert(path, source); - } - sources - } - // Otherwise, process all project files - None => project.paths.read_input_files()?, - }; - - // Only process sources with latest Solidity version to avoid conflicts. - let graph = Graph::::resolve_sources(&project.paths, sources)?; - let (version, sources, _) = graph - // resolve graph into mapping language -> version -> sources - .into_sources_by_version(project)? - .sources - .into_iter() - // only interested in Solidity sources - .find(|(lang, _)| *lang == MultiCompilerLanguage::Solc(SolcLanguage::Solidity)) - .ok_or_else(|| eyre::eyre!("no Solidity sources"))? - .1 - .into_iter() - // always pick the latest version - .max_by(|(v1, _, _), (v2, _, _)| v1.cmp(v2)) - .unwrap(); - - let solc = SolcVersionedInput::build( - sources, - config.solc_settings()?, - SolcLanguage::Solidity, - version, - ); - - configure_pcx_from_solc(pcx, project, &solc, true); - - Ok(()) -} - -/// Configures a [`ParsingContext`] from a [`Project`] and [`SolcVersionedInput`]. -/// -/// - Configures include paths, remappings. -/// - Source files are added if `add_source_file` is set -pub fn configure_pcx_from_solc( - pcx: &mut ParsingContext<'_>, - project: &Project, - vinput: &SolcVersionedInput, - add_source_files: bool, -) { - configure_pcx_from_solc_cli(pcx, project, &vinput.cli_settings); - if add_source_files { - let sources = vinput - .input - .sources - .par_iter() - .filter_map(|(path, source)| { - pcx.sess.source_map().new_source_file(path.clone(), source.content.as_str()).ok() - }) - .collect::>(); - pcx.add_files(sources); - } -} - -fn configure_pcx_from_solc_cli( - pcx: &mut ParsingContext<'_>, - project: &Project, - cli_settings: &foundry_compilers::solc::CliSettings, -) { - pcx.file_resolver - .set_current_dir(cli_settings.base_path.as_ref().unwrap_or(&project.paths.root)); - for remapping in &project.paths.remappings { - pcx.file_resolver.add_import_remapping(solar::sema::interface::config::ImportRemapping { - context: remapping.context.clone().unwrap_or_default(), - prefix: remapping.name.clone(), - path: remapping.path.clone(), - }); - } - pcx.file_resolver.add_include_paths(cli_settings.include_paths.iter().cloned()); -} diff --git a/crates/forge/src/cmd/bind_json.rs b/crates/forge/src/cmd/bind_json.rs index dc2b1bae8e89d..a858a3c3db394 100644 --- a/crates/forge/src/cmd/bind_json.rs +++ b/crates/forge/src/cmd/bind_json.rs @@ -2,17 +2,28 @@ use super::eip712::Resolver; use clap::{Parser, ValueHint}; use eyre::Result; use foundry_cli::{opts::BuildOpts, utils::LoadConfig}; -use foundry_common::{ - TYPE_BINDING_PREFIX, compile::ProjectCompiler, errors::convert_solar_errors, fs, +use foundry_common::{TYPE_BINDING_PREFIX, errors::convert_solar_errors, fs}; +use foundry_compilers::{ + Graph, Project, + artifacts::{Source, Sources}, + multi::{MultiCompilerLanguage, MultiCompilerParser}, + project::ProjectCompiler, + solc::SolcLanguage, }; -use foundry_compilers::{Project, ProjectCompileOutput}; use foundry_config::Config; use itertools::Itertools; use path_slash::PathExt; +use rayon::prelude::*; +use solar::parse::{ + ast::{self, FunctionKind, Span, VarMut, visit::Visit}, + interface::Session, +}; use std::{ collections::{BTreeMap, BTreeSet}, fmt::Write, + ops::ControlFlow, path::{Path, PathBuf}, + sync::Arc, }; foundry_config::impl_figment_convert!(BindJsonArgs, build); @@ -33,18 +44,35 @@ pub struct BindJsonArgs { impl BindJsonArgs { pub fn run(self) -> Result<()> { let config = self.load_config()?; + let project = config.solar_project()?; let target_path = config.root.join(self.out.as_ref().unwrap_or(&config.bind_json.out)); - // Overwrite existing bindings. - if target_path.exists() { - fs::write(&target_path, JSON_BINDINGS_PLACEHOLDER)?; - } - - let project = config.solar_project()?; - let mut output = ProjectCompiler::new().compile(&project)?; + let sources = project.paths.read_input_files()?; + let graph = Graph::::resolve_sources(&project.paths, sources)?; + + // We only generate bindings for a single Solidity version to avoid conflicts. + let (_, mut sources, _) = graph + // resolve graph into mapping language -> version -> sources + .into_sources_by_version(&project)? + .sources + .into_iter() + // we are only interested in Solidity sources + .find(|(lang, _)| *lang == MultiCompilerLanguage::Solc(SolcLanguage::Solidity)) + .ok_or_else(|| eyre::eyre!("no Solidity sources"))? + .1 + .into_iter() + // For now, we are always picking the latest version. + .max_by(|(v1, _, _), (v2, _, _)| v1.cmp(v2)) + .unwrap(); + + // Preprocess sources to handle potentially invalid bindings. + self.preprocess_sources(&mut sources)?; + + // Insert empty bindings file. + sources.insert(target_path.clone(), Source::new(JSON_BINDINGS_PLACEHOLDER)); // Find structs and generate bindings. - let structs_to_write = self.find_and_resolve_structs(&config, &project, &mut output)?; + let structs_to_write = self.find_and_resolve_structs(&config, &project, sources)?; // Write bindings. self.write_bindings(&structs_to_write, &target_path)?; @@ -52,20 +80,68 @@ impl BindJsonArgs { Ok(()) } + /// In cases when user moves/renames/deletes structs, compiler will start failing because + /// generated bindings will be referencing non-existing structs or importing non-existing + /// files. + /// + /// Because of that, we need a little bit of preprocessing to make sure that bindings will still + /// be valid. + /// + /// The strategy is: + /// 1. Replace bindings file with an empty one to get rid of potentially invalid imports. + /// 2. Remove all function bodies to get rid of `serialize`/`deserialize` invocations. + /// 3. Remove all `immutable` attributes to avoid errors because of erased constructors + /// initializing them. + /// + /// After that we'll still have enough information for bindings but compilation should succeed + /// in most of the cases. + fn preprocess_sources(&self, sources: &mut Sources) -> Result<()> { + let mut compiler = + solar::sema::Compiler::new(Session::builder().with_stderr_emitter().build()); + let _ = compiler.enter_mut(|compiler| -> solar::interface::Result<()> { + let mut pcx = compiler.parse(); + for (path, source) in &*sources { + if let Ok(source) = + pcx.sess.source_map().new_source_file(path.clone(), source.content.as_str()) + { + pcx.add_file(source); + } + } + pcx.parse(); + + let gcx = compiler.gcx(); + sources.par_iter_mut().try_for_each(|(path, source)| { + let mut content = Arc::unwrap_or_clone(std::mem::take(&mut source.content)); + let mut visitor = PreprocessorVisitor::new(); + let ast = gcx.get_ast_source(path).unwrap().1.ast.as_ref().unwrap(); + let _ = visitor.visit_source_unit(ast); + visitor.update(gcx.sess, &mut content); + source.content = Arc::new(content); + Ok(()) + }) + }); + convert_solar_errors(compiler.dcx()) + } + /// Find structs, resolve conflicts, and prepare them for writing fn find_and_resolve_structs( &self, config: &Config, project: &Project, - output: &mut ProjectCompileOutput, + sources: Sources, ) -> Result> { let include = &config.bind_json.include; let exclude = &config.bind_json.exclude; let root = &config.root; + let mut output = ProjectCompiler::with_sources(project, sources)?.compile()?; + if output.has_compiler_errors() { + eyre::bail!("{output}"); + } + let compiler = output.parser_mut().solc_mut().compiler_mut(); + let mut structs_to_write = Vec::new(); - let compiler = output.parser_mut().solc_mut().compiler_mut(); compiler.enter_mut(|compiler| { let exclude = |path: &Path| { if !include.is_empty() { @@ -295,6 +371,73 @@ library JsonBindings { } } +struct PreprocessorVisitor { + updates: Vec<(Span, &'static str)>, +} + +impl PreprocessorVisitor { + fn new() -> Self { + Self { updates: Vec::new() } + } + + fn update(mut self, sess: &Session, content: &mut String) { + if self.updates.is_empty() { + return; + } + + let sf = sess.source_map().lookup_source_file(self.updates[0].0.lo()); + let base = sf.start_pos.0; + + self.updates.sort_by_key(|(span, _)| span.lo()); + let mut shift = 0_i64; + for (span, new) in self.updates { + let lo = span.lo() - base; + let hi = span.hi() - base; + let start = ((lo.0 as i64) - shift) as usize; + let end = ((hi.0 as i64) - shift) as usize; + + content.replace_range(start..end, new); + shift += (end - start) as i64; + shift -= new.len() as i64; + } + } +} + +impl<'ast> Visit<'ast> for PreprocessorVisitor { + type BreakValue = solar::interface::data_structures::Never; + + fn visit_item_function( + &mut self, + func: &'ast ast::ItemFunction<'ast>, + ) -> ControlFlow { + // Replace function bodies with a noop statement. + if let Some(block) = &func.body + && !block.is_empty() + { + let span = block.first().unwrap().span.to(block.last().unwrap().span); + let new_body = match func.kind { + FunctionKind::Modifier => "_;", + _ => "revert();", + }; + self.updates.push((span, new_body)); + } + + self.walk_item_function(func) + } + + fn visit_variable_definition( + &mut self, + var: &'ast ast::VariableDefinition<'ast>, + ) -> ControlFlow { + // Remove `immutable` attributes. + if let Some(VarMut::Immutable) = var.mutability { + self.updates.push((var.span, "")); + } + + self.walk_variable_definition(var) + } +} + /// A single struct definition for which we need to generate bindings. #[derive(Debug, Clone)] struct StructToWrite { From 861de3df1e7f8c00893bb2611659bc780c3fd31c Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Thu, 18 Sep 2025 05:57:22 +0200 Subject: [PATCH 6/7] update --- crates/forge/src/cmd/bind_json.rs | 3 +++ crates/forge/tests/cli/bind_json.rs | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/forge/src/cmd/bind_json.rs b/crates/forge/src/cmd/bind_json.rs index a858a3c3db394..bc6e083ce632a 100644 --- a/crates/forge/src/cmd/bind_json.rs +++ b/crates/forge/src/cmd/bind_json.rs @@ -47,6 +47,7 @@ impl BindJsonArgs { let project = config.solar_project()?; let target_path = config.root.join(self.out.as_ref().unwrap_or(&config.bind_json.out)); + // Read and preprocess sources. let sources = project.paths.read_input_files()?; let graph = Graph::::resolve_sources(&project.paths, sources)?; @@ -100,6 +101,7 @@ impl BindJsonArgs { solar::sema::Compiler::new(Session::builder().with_stderr_emitter().build()); let _ = compiler.enter_mut(|compiler| -> solar::interface::Result<()> { let mut pcx = compiler.parse(); + pcx.set_resolve_imports(false); for (path, source) in &*sources { if let Ok(source) = pcx.sess.source_map().new_source_file(path.clone(), source.content.as_str()) @@ -403,6 +405,7 @@ impl PreprocessorVisitor { } } +// TODO(dani): AST mut visitor? impl<'ast> Visit<'ast> for PreprocessorVisitor { type BreakValue = solar::interface::data_structures::Never; diff --git a/crates/forge/tests/cli/bind_json.rs b/crates/forge/tests/cli/bind_json.rs index d777a65446cb2..d9eb06b997bb6 100644 --- a/crates/forge/tests/cli/bind_json.rs +++ b/crates/forge/tests/cli/bind_json.rs @@ -50,12 +50,13 @@ contract BindJsonTest is Test { "#, ); + // TODO(dani): assert stdout cmd.arg("bind-json").assert_success(); snapbox::assert_data_eq!( snapbox::Data::read_from(&prj.root().join("utils/JsonBindings.sol"), None), snapbox::str![[r#" -// Automatically generated by forge bind-json. +// Automatically @generated by `forge bind-json`. pragma solidity >=0.6.2 <0.9.0; pragma experimental ABIEncoderV2; From 4b7afa85d8feb29d6fbd2d88b4984c76910eeaf7 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Fri, 19 Sep 2025 14:07:19 +0200 Subject: [PATCH 7/7] wip: mut visitor --- Cargo.lock | 1 + crates/forge/src/cmd/bind_json.rs | 199 ++++++++++-------------------- 2 files changed, 69 insertions(+), 131 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fdd76aba5cee0..852dfc7e1e235 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4863,6 +4863,7 @@ version = "1.3.6" dependencies = [ "alloy-primitives", "foundry-compilers", + "rayon", "semver 1.0.26", "thiserror 2.0.16", ] diff --git a/crates/forge/src/cmd/bind_json.rs b/crates/forge/src/cmd/bind_json.rs index bc6e083ce632a..6cf28ba7ed34b 100644 --- a/crates/forge/src/cmd/bind_json.rs +++ b/crates/forge/src/cmd/bind_json.rs @@ -3,33 +3,23 @@ use clap::{Parser, ValueHint}; use eyre::Result; use foundry_cli::{opts::BuildOpts, utils::LoadConfig}; use foundry_common::{TYPE_BINDING_PREFIX, errors::convert_solar_errors, fs}; -use foundry_compilers::{ - Graph, Project, - artifacts::{Source, Sources}, - multi::{MultiCompilerLanguage, MultiCompilerParser}, - project::ProjectCompiler, - solc::SolcLanguage, -}; +use foundry_compilers::{Project, ProjectCompileOutput}; use foundry_config::Config; use itertools::Itertools; use path_slash::PathExt; -use rayon::prelude::*; -use solar::parse::{ - ast::{self, FunctionKind, Span, VarMut, visit::Visit}, - interface::Session, +use solar::{ + ast::VisitMut, + parse::ast::{self, FunctionKind, VarMut}, }; use std::{ collections::{BTreeMap, BTreeSet}, fmt::Write, ops::ControlFlow, path::{Path, PathBuf}, - sync::Arc, }; foundry_config::impl_figment_convert!(BindJsonArgs, build); -const JSON_BINDINGS_PLACEHOLDER: &str = "library JsonBindings {}"; - /// CLI arguments for `forge bind-json`. #[derive(Clone, Debug, Parser)] pub struct BindJsonArgs { @@ -44,99 +34,33 @@ pub struct BindJsonArgs { impl BindJsonArgs { pub fn run(self) -> Result<()> { let config = self.load_config()?; - let project = config.solar_project()?; - let target_path = config.root.join(self.out.as_ref().unwrap_or(&config.bind_json.out)); - - // Read and preprocess sources. - let sources = project.paths.read_input_files()?; - let graph = Graph::::resolve_sources(&project.paths, sources)?; - - // We only generate bindings for a single Solidity version to avoid conflicts. - let (_, mut sources, _) = graph - // resolve graph into mapping language -> version -> sources - .into_sources_by_version(&project)? - .sources - .into_iter() - // we are only interested in Solidity sources - .find(|(lang, _)| *lang == MultiCompilerLanguage::Solc(SolcLanguage::Solidity)) - .ok_or_else(|| eyre::eyre!("no Solidity sources"))? - .1 - .into_iter() - // For now, we are always picking the latest version. - .max_by(|(v1, _, _), (v2, _, _)| v1.cmp(v2)) - .unwrap(); - - // Preprocess sources to handle potentially invalid bindings. - self.preprocess_sources(&mut sources)?; - - // Insert empty bindings file. - sources.insert(target_path.clone(), Source::new(JSON_BINDINGS_PLACEHOLDER)); + + // We want to avoid analyzing the project with solc. See `PreprocessorVisitor`. + let mut project = config.solar_project()?; + project.settings.solc.stop_after = Some("parsing".into()); + + let mut output = project.compile()?; // Find structs and generate bindings. - let structs_to_write = self.find_and_resolve_structs(&config, &project, sources)?; + let structs_to_write = self.find_and_resolve_structs(&config, &project, &mut output)?; // Write bindings. - self.write_bindings(&structs_to_write, &target_path)?; + self.write_bindings(&config, &structs_to_write)?; Ok(()) } - /// In cases when user moves/renames/deletes structs, compiler will start failing because - /// generated bindings will be referencing non-existing structs or importing non-existing - /// files. - /// - /// Because of that, we need a little bit of preprocessing to make sure that bindings will still - /// be valid. - /// - /// The strategy is: - /// 1. Replace bindings file with an empty one to get rid of potentially invalid imports. - /// 2. Remove all function bodies to get rid of `serialize`/`deserialize` invocations. - /// 3. Remove all `immutable` attributes to avoid errors because of erased constructors - /// initializing them. - /// - /// After that we'll still have enough information for bindings but compilation should succeed - /// in most of the cases. - fn preprocess_sources(&self, sources: &mut Sources) -> Result<()> { - let mut compiler = - solar::sema::Compiler::new(Session::builder().with_stderr_emitter().build()); - let _ = compiler.enter_mut(|compiler| -> solar::interface::Result<()> { - let mut pcx = compiler.parse(); - pcx.set_resolve_imports(false); - for (path, source) in &*sources { - if let Ok(source) = - pcx.sess.source_map().new_source_file(path.clone(), source.content.as_str()) - { - pcx.add_file(source); - } - } - pcx.parse(); - - let gcx = compiler.gcx(); - sources.par_iter_mut().try_for_each(|(path, source)| { - let mut content = Arc::unwrap_or_clone(std::mem::take(&mut source.content)); - let mut visitor = PreprocessorVisitor::new(); - let ast = gcx.get_ast_source(path).unwrap().1.ast.as_ref().unwrap(); - let _ = visitor.visit_source_unit(ast); - visitor.update(gcx.sess, &mut content); - source.content = Arc::new(content); - Ok(()) - }) - }); - convert_solar_errors(compiler.dcx()) - } - /// Find structs, resolve conflicts, and prepare them for writing fn find_and_resolve_structs( &self, config: &Config, project: &Project, - sources: Sources, + output: &mut ProjectCompileOutput, ) -> Result> { let include = &config.bind_json.include; let exclude = &config.bind_json.exclude; let root = &config.root; - let mut output = ProjectCompiler::with_sources(project, sources)?.compile()?; if output.has_compiler_errors() { eyre::bail!("{output}"); } @@ -164,6 +88,17 @@ impl BindJsonArgs { false }; + // Preprocess. + // SAFETY: we have mutable access to `gcx`. Yes this is UB regardless, no I don't care. + let sources = unsafe { &mut *(&raw const compiler.gcx().sources).cast_mut() }; + for source in sources.iter_mut() { + let Some(ast) = &mut source.ast else { + continue; + }; + let mut visitor = PreprocessorVisitor::new(); + let _ = visitor.visit_source_unit_mut(ast); + } + // Resolve. if compiler.gcx().stage() < Some(solar::config::CompilerStage::Lowering) { let _ = compiler.lower_asts(); @@ -267,7 +202,9 @@ impl BindJsonArgs { } /// Write the final bindings file - fn write_bindings(&self, structs_to_write: &[StructToWrite], target_path: &Path) -> Result<()> { + fn write_bindings(&self, config: &Config, structs_to_write: &[StructToWrite]) -> Result<()> { + let target_path = &*config.root.join(self.out.as_ref().unwrap_or(&config.bind_json.out)); + let mut result = String::new(); // Write imports @@ -373,71 +310,71 @@ library JsonBindings { } } -struct PreprocessorVisitor { - updates: Vec<(Span, &'static str)>, -} +/// In cases when user moves/renames/deletes structs, compiler will start failing because +/// generated bindings will be referencing non-existing structs or importing non-existing +/// files. +/// +/// Because of that, we need a little bit of preprocessing to make sure that bindings will still +/// be valid. +/// +/// The strategy is: +/// 1. Replace bindings file with an empty one to get rid of potentially invalid imports. +/// 2. Remove all function bodies to get rid of `serialize`/`deserialize` invocations. +/// 3. Remove all `immutable` attributes to avoid errors because of erased constructors initializing +/// them. +/// +/// After that we'll still have enough information for bindings but compilation should succeed +/// in most of the cases. +struct PreprocessorVisitor(()); impl PreprocessorVisitor { fn new() -> Self { - Self { updates: Vec::new() } - } - - fn update(mut self, sess: &Session, content: &mut String) { - if self.updates.is_empty() { - return; - } - - let sf = sess.source_map().lookup_source_file(self.updates[0].0.lo()); - let base = sf.start_pos.0; - - self.updates.sort_by_key(|(span, _)| span.lo()); - let mut shift = 0_i64; - for (span, new) in self.updates { - let lo = span.lo() - base; - let hi = span.hi() - base; - let start = ((lo.0 as i64) - shift) as usize; - let end = ((hi.0 as i64) - shift) as usize; - - content.replace_range(start..end, new); - shift += (end - start) as i64; - shift -= new.len() as i64; - } + Self(()) } } -// TODO(dani): AST mut visitor? -impl<'ast> Visit<'ast> for PreprocessorVisitor { +impl<'ast> VisitMut<'ast> for PreprocessorVisitor { type BreakValue = solar::interface::data_structures::Never; - fn visit_item_function( + fn visit_item_function_mut( &mut self, - func: &'ast ast::ItemFunction<'ast>, + func: &'ast mut ast::ItemFunction<'ast>, ) -> ControlFlow { // Replace function bodies with a noop statement. - if let Some(block) = &func.body + if let Some(block) = &mut func.body && !block.is_empty() { - let span = block.first().unwrap().span.to(block.last().unwrap().span); - let new_body = match func.kind { - FunctionKind::Modifier => "_;", - _ => "revert();", + block.stmts = &mut block.stmts[..1]; + let span = block.stmts[0].span; + let expr = |kind| ast::Expr { span, kind }; + let alloc = |x| Box::leak(Box::new(x)); + block.stmts[0].kind = match func.kind { + // _; + FunctionKind::Modifier => ast::StmtKind::Placeholder, + // revert(); + _ => ast::StmtKind::Expr(alloc(expr(ast::ExprKind::Call( + alloc(expr(ast::ExprKind::Ident(ast::Ident::new( + solar::interface::kw::Revert, + span, + )))), + ast::CallArgs::empty(span), + )))), }; - self.updates.push((span, new_body)); } - self.walk_item_function(func) + ControlFlow::Continue(()) } - fn visit_variable_definition( + fn visit_variable_definition_mut( &mut self, - var: &'ast ast::VariableDefinition<'ast>, + var: &'ast mut ast::VariableDefinition<'ast>, ) -> ControlFlow { // Remove `immutable` attributes. if let Some(VarMut::Immutable) = var.mutability { - self.updates.push((var.span, "")); + var.mutability = None; } - self.walk_variable_definition(var) + ControlFlow::Continue(()) } }