diff --git a/Cargo.lock b/Cargo.lock index 501d257..0b375d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4093,6 +4093,7 @@ dependencies = [ "moka", "once_cell", "semver 1.0.26", + "serde", "tokio", ] @@ -4159,6 +4160,7 @@ dependencies = [ "alloy-primitives", "alloy-sol-types", "anyhow", + "regex", "revive-common", "revive-dt-common", "semver 1.0.26", @@ -4201,6 +4203,7 @@ name = "revive-dt-report" version = "0.1.0" dependencies = [ "anyhow", + "revive-dt-common", "revive-dt-compiler", "revive-dt-config", "revive-dt-format", diff --git a/Cargo.toml b/Cargo.toml index 34ae899..8669e9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ clap = { version = "4", features = ["derive"] } foundry-compilers-artifacts = { version = "0.18.0" } futures = { version = "0.3.31" } hex = "0.4.3" +regex = "1" moka = "0.12.10" reqwest = { version = "0.12.15", features = ["json"] } once_cell = "1.21" @@ -44,6 +45,7 @@ sp-core = "36.1.0" sp-runtime = "41.1.0" temp-dir = { version = "0.1.16" } tempfile = "3.3" +thiserror = "2" tokio = { version = "1.47.0", default-features = false, features = [ "rt-multi-thread", "process", diff --git a/assets/test_metadata.json b/assets/test_metadata.json index 127e808..14c1fde 100644 --- a/assets/test_metadata.json +++ b/assets/test_metadata.json @@ -1,8 +1,7 @@ { "modes": [ "Y >=0.8.9", - "E", - "I" + "E" ], "cases": [ { diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 52d7d8a..128e464 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -13,4 +13,5 @@ anyhow = { workspace = true } moka = { workspace = true, features = ["sync"] } once_cell = { workspace = true } semver = { workspace = true } +serde = { workspace = true } tokio = { workspace = true, default-features = false, features = ["time"] } diff --git a/crates/common/src/types/mod.rs b/crates/common/src/types/mod.rs index 4cd063a..0e1c34f 100644 --- a/crates/common/src/types/mod.rs +++ b/crates/common/src/types/mod.rs @@ -1,3 +1,5 @@ +mod mode; mod version_or_requirement; +pub use mode::*; pub use version_or_requirement::*; diff --git a/crates/common/src/types/mode.rs b/crates/common/src/types/mode.rs new file mode 100644 index 0000000..c380209 --- /dev/null +++ b/crates/common/src/types/mode.rs @@ -0,0 +1,167 @@ +use crate::types::VersionOrRequirement; +use semver::Version; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; +use std::str::FromStr; + +/// This represents a mode that a given test should be run with, if possible. +/// +/// We obtain this by taking a [`ParsedMode`], which may be looser or more strict +/// in its requirements, and then expanding it out into a list of [`Mode`]s. +/// +/// Use [`ParsedMode::to_test_modes()`] to do this. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)] +pub struct Mode { + pub pipeline: ModePipeline, + pub optimize_setting: ModeOptimizerSetting, + pub version: Option, +} + +impl Display for Mode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.pipeline.fmt(f)?; + f.write_str(" ")?; + self.optimize_setting.fmt(f)?; + + if let Some(version) = &self.version { + f.write_str(" ")?; + version.fmt(f)?; + } + + Ok(()) + } +} + +impl Mode { + /// Return all of the available mode combinations. + pub fn all() -> impl Iterator { + ModePipeline::test_cases().flat_map(|pipeline| { + ModeOptimizerSetting::test_cases().map(move |optimize_setting| Mode { + pipeline, + optimize_setting, + version: None, + }) + }) + } + + /// Resolves the [`Mode`]'s solidity version requirement into a [`VersionOrRequirement`] if + /// the requirement is present on the object. Otherwise, the passed default version is used. + pub fn compiler_version_to_use(&self, default: Version) -> VersionOrRequirement { + match self.version { + Some(ref requirement) => requirement.clone().into(), + None => default.into(), + } + } +} + +/// What do we want the compiler to do? +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +pub enum ModePipeline { + /// Compile Solidity code via Yul IR + ViaYulIR, + /// Compile Solidity direct to assembly + ViaEVMAssembly, +} + +impl FromStr for ModePipeline { + type Err = anyhow::Error; + fn from_str(s: &str) -> Result { + match s { + // via Yul IR + "Y" => Ok(ModePipeline::ViaYulIR), + // Don't go via Yul IR + "E" => Ok(ModePipeline::ViaEVMAssembly), + // Anything else that we see isn't a mode at all + _ => Err(anyhow::anyhow!( + "Unsupported pipeline '{s}': expected 'Y' or 'E'" + )), + } + } +} + +impl Display for ModePipeline { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ModePipeline::ViaYulIR => f.write_str("Y"), + ModePipeline::ViaEVMAssembly => f.write_str("E"), + } + } +} + +impl ModePipeline { + /// Should we go via Yul IR? + pub fn via_yul_ir(&self) -> bool { + matches!(self, ModePipeline::ViaYulIR) + } + + /// An iterator over the available pipelines that we'd like to test, + /// when an explicit pipeline was not specified. + pub fn test_cases() -> impl Iterator + Clone { + [ModePipeline::ViaYulIR, ModePipeline::ViaEVMAssembly].into_iter() + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +pub enum ModeOptimizerSetting { + /// 0 / -: Don't apply any optimizations + M0, + /// 1: Apply less than default optimizations + M1, + /// 2: Apply the default optimizations + M2, + /// 3 / +: Apply aggressive optimizations + M3, + /// s: Optimize for size + Ms, + /// z: Aggressively optimize for size + Mz, +} + +impl FromStr for ModeOptimizerSetting { + type Err = anyhow::Error; + fn from_str(s: &str) -> Result { + match s { + "M0" => Ok(ModeOptimizerSetting::M0), + "M1" => Ok(ModeOptimizerSetting::M1), + "M2" => Ok(ModeOptimizerSetting::M2), + "M3" => Ok(ModeOptimizerSetting::M3), + "Ms" => Ok(ModeOptimizerSetting::Ms), + "Mz" => Ok(ModeOptimizerSetting::Mz), + _ => Err(anyhow::anyhow!( + "Unsupported optimizer setting '{s}': expected 'M0', 'M1', 'M2', 'M3', 'Ms' or 'Mz'" + )), + } + } +} + +impl Display for ModeOptimizerSetting { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ModeOptimizerSetting::M0 => f.write_str("M0"), + ModeOptimizerSetting::M1 => f.write_str("M1"), + ModeOptimizerSetting::M2 => f.write_str("M2"), + ModeOptimizerSetting::M3 => f.write_str("M3"), + ModeOptimizerSetting::Ms => f.write_str("Ms"), + ModeOptimizerSetting::Mz => f.write_str("Mz"), + } + } +} + +impl ModeOptimizerSetting { + /// An iterator over the available optimizer settings that we'd like to test, + /// when an explicit optimizer setting was not specified. + pub fn test_cases() -> impl Iterator + Clone { + [ + // No optimizations: + ModeOptimizerSetting::M0, + // Aggressive optimizations: + ModeOptimizerSetting::M3, + ] + .into_iter() + } + + /// Are any optimizations enabled? + pub fn optimizations_enabled(&self) -> bool { + !matches!(self, ModeOptimizerSetting::M0) + } +} diff --git a/crates/compiler/src/constants.rs b/crates/compiler/src/constants.rs new file mode 100644 index 0000000..bdb87f1 --- /dev/null +++ b/crates/compiler/src/constants.rs @@ -0,0 +1,4 @@ +use semver::Version; + +/// This is the first version of solc that supports the `--via-ir` flag / "viaIR" input JSON. +pub const SOLC_VERSION_SUPPORTING_VIA_YUL_IR: Version = Version::new(0, 8, 13); diff --git a/crates/compiler/src/lib.rs b/crates/compiler/src/lib.rs index dc00113..551d101 100644 --- a/crates/compiler/src/lib.rs +++ b/crates/compiler/src/lib.rs @@ -3,6 +3,8 @@ //! - Polkadot revive resolc compiler //! - Polkadot revive Wasm compiler +mod constants; + use std::{ collections::HashMap, hash::Hash, @@ -19,6 +21,9 @@ use revive_dt_common::cached_fs::read_to_string; use revive_dt_common::types::VersionOrRequirement; use revive_dt_config::Arguments; +// Re-export this as it's a part of the compiler interface. +pub use revive_dt_common::types::{Mode, ModeOptimizerSetting, ModePipeline}; + pub mod revive_js; pub mod revive_resolc; pub mod solc; @@ -43,13 +48,20 @@ pub trait SolidityCompiler { ) -> impl Future>; fn version(&self) -> anyhow::Result; + + /// Does the compiler support the provided mode and version settings? + fn supports_mode( + compiler_version: &Version, + optimize_setting: ModeOptimizerSetting, + pipeline: ModePipeline, + ) -> bool; } /// The generic compilation input configuration. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CompilerInput { - pub enable_optimization: Option, - pub via_ir: Option, + pub pipeline: Option, + pub optimization: Option, pub evm_version: Option, pub allow_paths: Vec, pub base_path: Option, @@ -85,8 +97,8 @@ where pub fn new() -> Self { Self { input: CompilerInput { - enable_optimization: Default::default(), - via_ir: Default::default(), + pipeline: Default::default(), + optimization: Default::default(), evm_version: Default::default(), allow_paths: Default::default(), base_path: Default::default(), @@ -98,13 +110,13 @@ where } } - pub fn with_optimization(mut self, value: impl Into>) -> Self { - self.input.enable_optimization = value.into(); + pub fn with_optimization(mut self, value: impl Into>) -> Self { + self.input.optimization = value.into(); self } - pub fn with_via_ir(mut self, value: impl Into>) -> Self { - self.input.via_ir = value.into(); + pub fn with_pipeline(mut self, value: impl Into>) -> Self { + self.input.pipeline = value.into(); self } diff --git a/crates/compiler/src/revive_resolc.rs b/crates/compiler/src/revive_resolc.rs index 0261de8..efa0812 100644 --- a/crates/compiler/src/revive_resolc.rs +++ b/crates/compiler/src/revive_resolc.rs @@ -14,7 +14,8 @@ use revive_solc_json_interface::{ SolcStandardJsonOutput, }; -use crate::{CompilerInput, CompilerOutput, SolidityCompiler}; +use super::constants::SOLC_VERSION_SUPPORTING_VIA_YUL_IR; +use crate::{CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler}; use alloy::json_abi::JsonAbi; use anyhow::Context; @@ -39,9 +40,8 @@ impl SolidityCompiler for Resolc { async fn build( &self, CompilerInput { - enable_optimization, - // Ignored and not honored since this is required for the resolc compilation. - via_ir: _via_ir, + pipeline, + optimization, evm_version, allow_paths, base_path, @@ -53,6 +53,12 @@ impl SolidityCompiler for Resolc { }: CompilerInput, additional_options: Self::Options, ) -> anyhow::Result { + if !matches!(pipeline, None | Some(ModePipeline::ViaYulIR)) { + anyhow::bail!( + "Resolc only supports the Y (via Yul IR) pipeline, but the provided pipeline is {pipeline:?}" + ); + } + let input = SolcStandardJsonInput { language: SolcStandardJsonInputLanguage::Solidity, sources: sources @@ -81,7 +87,9 @@ impl SolidityCompiler for Resolc { output_selection: Some(SolcStandardJsonInputSettingsSelection::new_required()), via_ir: Some(true), optimizer: SolcStandardJsonInputSettingsOptimizer::new( - enable_optimization.unwrap_or(false), + optimization + .unwrap_or(ModeOptimizerSetting::M0) + .optimizations_enabled(), None, &Version::new(0, 0, 0), false, @@ -232,6 +240,18 @@ impl SolidityCompiler for Resolc { Version::parse(version_string).map_err(Into::into) } + + fn supports_mode( + compiler_version: &Version, + _optimize_setting: ModeOptimizerSetting, + pipeline: ModePipeline, + ) -> bool { + // We only support the Y (IE compile via Yul IR) mode here, which also means that we can + // only use solc version 0.8.13 and above. We must always compile via Yul IR as resolc + // needs this to translate to LLVM IR and then RISCV. + pipeline == ModePipeline::ViaYulIR + && compiler_version >= &SOLC_VERSION_SUPPORTING_VIA_YUL_IR + } } #[cfg(test)] diff --git a/crates/compiler/src/solc.rs b/crates/compiler/src/solc.rs index f714857..b785b06 100644 --- a/crates/compiler/src/solc.rs +++ b/crates/compiler/src/solc.rs @@ -10,7 +10,8 @@ use revive_dt_common::types::VersionOrRequirement; use revive_dt_config::Arguments; use revive_dt_solc_binaries::download_solc; -use crate::{CompilerInput, CompilerOutput, SolidityCompiler}; +use super::constants::SOLC_VERSION_SUPPORTING_VIA_YUL_IR; +use crate::{CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler}; use anyhow::Context; use foundry_compilers_artifacts::{ @@ -35,8 +36,8 @@ impl SolidityCompiler for Solc { async fn build( &self, CompilerInput { - enable_optimization, - via_ir, + pipeline, + optimization, evm_version, allow_paths, base_path, @@ -46,6 +47,17 @@ impl SolidityCompiler for Solc { }: CompilerInput, _: Self::Options, ) -> anyhow::Result { + let compiler_supports_via_ir = self.version()? >= SOLC_VERSION_SUPPORTING_VIA_YUL_IR; + + // Be careful to entirely omit the viaIR field if the compiler does not support it, + // as it will error if you provide fields it does not know about. Because + // `supports_mode` is called prior to instantiating a compiler, we should never + // ask for something which is invalid. + let via_ir = match (pipeline, compiler_supports_via_ir) { + (pipeline, true) => pipeline.map(|p| p.via_yul_ir()), + (_pipeline, false) => None, + }; + let input = SolcInput { language: SolcLanguage::Solidity, sources: Sources( @@ -56,7 +68,7 @@ impl SolidityCompiler for Solc { ), settings: Settings { optimizer: Optimizer { - enabled: enable_optimization, + enabled: optimization.map(|o| o.optimizations_enabled()), details: Some(Default::default()), ..Default::default() }, @@ -222,6 +234,18 @@ impl SolidityCompiler for Solc { Version::parse(version_string).map_err(Into::into) } + + fn supports_mode( + compiler_version: &Version, + _optimize_setting: ModeOptimizerSetting, + pipeline: ModePipeline, + ) -> bool { + // solc 0.8.13 and above supports --via-ir, and less than that does not. Thus, we support mode E + // (ie no Yul IR) in either case, but only support Y (via Yul IR) if the compiler is new enough. + pipeline == ModePipeline::ViaEVMAssembly + || (pipeline == ModePipeline::ViaYulIR + && compiler_version >= &SOLC_VERSION_SUPPORTING_VIA_YUL_IR) + } } #[cfg(test)] diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index b7871fb..e4fb409 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -58,10 +58,6 @@ pub struct Arguments { #[arg(long = "geth-start-timeout", default_value = "5000")] pub geth_start_timeout: u64, - /// The test network chain ID. - #[arg(short, long = "network-id", default_value = "420420420")] - pub network_id: u64, - /// Configure nodes according to this genesis.json file. #[arg(long = "genesis", default_value = "genesis.json")] pub genesis_file: PathBuf, diff --git a/crates/core/src/main.rs b/crates/core/src/main.rs index 8cad37a..a1a9429 100644 --- a/crates/core/src/main.rs +++ b/crates/core/src/main.rs @@ -13,7 +13,8 @@ use alloy::{ }; use anyhow::Context; use clap::Parser; -use futures::StreamExt; +use futures::stream::futures_unordered::FuturesUnordered; +use futures::{Stream, StreamExt}; use revive_dt_common::iterators::FilesWithExtensionIterator; use revive_dt_node_interaction::EthereumNode; use semver::Version; @@ -22,6 +23,7 @@ use tokio::sync::{Mutex, RwLock, mpsc}; use tracing::{Instrument, Level}; use tracing_subscriber::{EnvFilter, FmtSubscriber}; +use revive_dt_common::types::Mode; use revive_dt_compiler::SolidityCompiler; use revive_dt_compiler::{Compiler, CompilerOutput}; use revive_dt_config::*; @@ -34,7 +36,6 @@ use revive_dt_format::{ corpus::Corpus, input::{Input, Step}, metadata::{ContractInstance, ContractPathAndIdent, Metadata, MetadataFile}, - mode::SolcMode, }; use revive_dt_node::pool::NodePool; use revive_dt_report::reporter::{Report, Span}; @@ -44,7 +45,7 @@ static TEMP_DIR: LazyLock = LazyLock::new(|| TempDir::new().unwrap()); type CompilationCache = Arc< RwLock< HashMap< - (PathBuf, SolcMode, TestingPlatform), + (PathBuf, Mode, TestingPlatform), Arc>>>, >, >, @@ -55,8 +56,8 @@ type CompilationCache = Arc< struct Test { metadata: Metadata, path: PathBuf, - mode: SolcMode, - case_idx: usize, + mode: Mode, + case_idx: CaseIdx, case: Case, } @@ -144,7 +145,7 @@ where { let (report_tx, report_rx) = mpsc::unbounded_channel::<(Test, CaseResult)>(); - let tests = prepare_tests::(metadata_files); + let tests = prepare_tests::(args, metadata_files); let driver_task = start_driver_task::(args, tests, span, report_tx)?; let status_reporter_task = start_reporter_task(report_rx); @@ -153,7 +154,10 @@ where Ok(()) } -fn prepare_tests(metadata_files: &[MetadataFile]) -> impl Iterator +fn prepare_tests( + args: &Arguments, + metadata_files: &[MetadataFile], +) -> impl Stream where L: Platform, F: Platform, @@ -231,15 +235,53 @@ where metadata: metadata.clone(), path: metadata_file_path.to_path_buf(), mode: solc_mode, - case_idx, + case_idx: case_idx.into(), case: case.clone(), } }) + .map(async |test| test) + .collect::>() + .filter_map(async move |test| { + // Check that both compilers support this test, else we skip it + let is_supported = does_compiler_support_mode::(args, &test.mode).await.ok().unwrap_or(false) && + does_compiler_support_mode::(args, &test.mode).await.ok().unwrap_or(false); + + tracing::warn!( + metadata_file_path = %test.path.display(), + case_idx = %test.case_idx, + case_name = ?test.case.name, + mode = %test.mode, + "Skipping test as one or both of the compilers don't support it" + ); + + // We filter_map to avoid needing to clone `test`, but return it as-is. + if is_supported { + Some(test) + } else { + None + } + }) +} + +async fn does_compiler_support_mode( + args: &Arguments, + mode: &Mode, +) -> anyhow::Result { + let compiler_version_or_requirement = mode.compiler_version_to_use(args.solc.clone()); + let compiler_path = + P::Compiler::get_compiler_executable(args, compiler_version_or_requirement).await?; + let compiler_version = P::Compiler::new(compiler_path.clone()).version()?; + + Ok(P::Compiler::supports_mode( + &compiler_version, + mode.optimize_setting, + mode.pipeline, + )) } fn start_driver_task( args: &Arguments, - tests: impl Iterator, + tests: impl Stream, span: Span, report_tx: mpsc::UnboundedSender<(Test, CaseResult)>, ) -> anyhow::Result> @@ -254,7 +296,7 @@ where let compilation_cache = Arc::new(RwLock::new(HashMap::new())); let number_concurrent_tasks = args.number_of_concurrent_tasks(); - Ok(futures::stream::iter(tests).for_each_concurrent( + Ok(tests.for_each_concurrent( // We want to limit the concurrent tasks here because: // // 1. We don't want to overwhelm the nodes with too many requests, leading to responses timing out. @@ -284,7 +326,7 @@ where let result = handle_case_driver::( &test.path, &test.metadata, - test.case_idx.into(), + test.case_idx, &test.case, test.mode.clone(), args, @@ -328,13 +370,13 @@ async fn start_reporter_task(mut report_rx: mpsc::UnboundedReceiver<(Test, CaseR Ok(_inputs) => { number_of_successes += 1; eprintln!( - "{GREEN}Case Succeeded:{COLOUR_RESET} {test_path} -> {case_name}:{case_idx} (mode: {test_mode:?})" + "{GREEN}Case Succeeded:{COLOUR_RESET} {test_path} -> {case_name}:{case_idx} (mode: {test_mode})" ); } Err(err) => { number_of_failures += 1; eprintln!( - "{RED}Case Failed:{COLOUR_RESET} {test_path} -> {case_name}:{case_idx} (mode: {test_mode:?})" + "{RED}Case Failed:{COLOUR_RESET} {test_path} -> {case_name}:{case_idx} (mode: {test_mode})" ); failures.push((test, err)); } @@ -357,7 +399,7 @@ async fn start_reporter_task(mut report_rx: mpsc::UnboundedReceiver<(Test, CaseR let test_mode = test.mode.clone(); eprintln!( - "---- {RED}Case Failed:{COLOUR_RESET} {test_path} -> {case_name}:{case_idx} (mode: {test_mode:?}) ----\n\n{err}\n" + "---- {RED}Case Failed:{COLOUR_RESET} {test_path} -> {case_name}:{case_idx} (mode: {test_mode}) ----\n\n{err}\n" ); } } @@ -376,7 +418,7 @@ async fn handle_case_driver( metadata: &Metadata, case_idx: CaseIdx, case: &Case, - mode: SolcMode, + mode: Mode, config: &Arguments, compilation_cache: CompilationCache, leader_node: &L::Blockchain, @@ -617,7 +659,7 @@ where async fn get_or_build_contracts( metadata: &Metadata, metadata_file_path: &Path, - mode: SolcMode, + mode: Mode, config: &Arguments, compilation_cache: CompilationCache, deployed_libraries: &HashMap, @@ -636,16 +678,16 @@ async fn get_or_build_contracts( } None => { tracing::debug!(?key, "Compiled contracts cache miss"); - let compiled_contracts = Arc::new( - compile_contracts::

( - metadata, - metadata_file_path, - &mode, - config, - deployed_libraries, - ) - .await?, - ); + let compiled_contracts = compile_contracts::

( + metadata, + metadata_file_path, + &mode, + config, + deployed_libraries, + ) + .await?; + let compiled_contracts = Arc::new(compiled_contracts); + *compilation_artifact = Some(compiled_contracts.clone()); return Ok(compiled_contracts.clone()); } @@ -660,16 +702,17 @@ async fn get_or_build_contracts( mutex }; let mut compilation_artifact = mutex.lock().await; - let compiled_contracts = Arc::new( - compile_contracts::

( - metadata, - metadata_file_path, - &mode, - config, - deployed_libraries, - ) - .await?, - ); + + let compiled_contracts = compile_contracts::

( + metadata, + metadata_file_path, + &mode, + config, + deployed_libraries, + ) + .await?; + let compiled_contracts = Arc::new(compiled_contracts); + *compilation_artifact = Some(compiled_contracts.clone()); Ok(compiled_contracts.clone()) } @@ -677,7 +720,7 @@ async fn get_or_build_contracts( async fn compile_contracts( metadata: &Metadata, metadata_file_path: &Path, - mode: &SolcMode, + mode: &Mode, config: &Arguments, deployed_libraries: &HashMap, ) -> anyhow::Result<(Version, CompilerOutput)> { @@ -695,7 +738,8 @@ async fn compile_contracts( let compiler = Compiler::::new() .with_allow_path(metadata.directory()?) - .with_optimization(mode.solc_optimize()); + .with_optimization(mode.optimize_setting) + .with_pipeline(mode.pipeline); let mut compiler = metadata .files_to_compile()? .try_fold(compiler, |compiler, path| compiler.with_source(&path))?; diff --git a/crates/format/Cargo.toml b/crates/format/Cargo.toml index 0f50758..0e5745e 100644 --- a/crates/format/Cargo.toml +++ b/crates/format/Cargo.toml @@ -17,6 +17,7 @@ alloy = { workspace = true } alloy-primitives = { workspace = true } alloy-sol-types = { workspace = true } anyhow = { workspace = true } +regex = { workspace = true } tracing = { workspace = true } semver = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/crates/format/src/case.rs b/crates/format/src/case.rs index aafe914..b1bd234 100644 --- a/crates/format/src/case.rs +++ b/crates/format/src/case.rs @@ -4,7 +4,7 @@ use revive_dt_common::macros::define_wrapper_type; use crate::{ input::{Expected, Step}, - mode::Mode, + mode::ParsedMode, }; #[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq)] @@ -16,7 +16,7 @@ pub struct Case { pub comment: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub modes: Option>, + pub modes: Option>, #[serde(rename = "inputs")] pub steps: Vec, @@ -67,3 +67,9 @@ define_wrapper_type!( #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct CaseIdx(usize); ); + +impl std::fmt::Display for CaseIdx { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/crates/format/src/metadata.rs b/crates/format/src/metadata.rs index d5c55be..5e6e07d 100644 --- a/crates/format/src/metadata.rs +++ b/crates/format/src/metadata.rs @@ -13,12 +13,10 @@ use serde::{Deserialize, Serialize}; use revive_common::EVMVersion; use revive_dt_common::{ cached_fs::read_to_string, iterators::FilesWithExtensionIterator, macros::define_wrapper_type, + types::Mode, }; -use crate::{ - case::Case, - mode::{Mode, SolcMode}, -}; +use crate::{case::Case, mode::ParsedMode}; pub const METADATA_FILE_EXTENSION: &str = "json"; pub const SOLIDITY_CASE_FILE_EXTENSION: &str = "sol"; @@ -68,7 +66,7 @@ pub struct Metadata { pub libraries: Option>>, #[serde(skip_serializing_if = "Option::is_none")] - pub modes: Option>, + pub modes: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub file_path: Option, @@ -86,21 +84,12 @@ pub struct Metadata { } impl Metadata { - /// Returns the solc modes of this metadata, inserting a default mode if not present. - pub fn solc_modes(&self) -> Vec { - self.modes - .to_owned() - .unwrap_or_else(|| vec![Mode::Solidity(Default::default())]) - .iter() - .filter_map(|mode| match mode { - Mode::Solidity(solc_mode) => Some(solc_mode), - Mode::Unknown(mode) => { - tracing::debug!("compiler: ignoring unknown mode '{mode}'"); - None - } - }) - .cloned() - .collect() + /// Returns the modes that we should test from this metadata. + pub fn solc_modes(&self) -> Vec { + match &self.modes { + Some(modes) => ParsedMode::many_to_modes(modes.iter()).collect(), + None => Mode::all().collect(), + } } /// Returns the base directory of this metadata. diff --git a/crates/format/src/mode.rs b/crates/format/src/mode.rs index 8b1f4c0..7e6dfc8 100644 --- a/crates/format/src/mode.rs +++ b/crates/format/src/mode.rs @@ -1,123 +1,262 @@ -use revive_dt_common::types::VersionOrRequirement; -use semver::Version; -use serde::de::Deserializer; +use regex::Regex; +use revive_dt_common::types::{Mode, ModeOptimizerSetting, ModePipeline}; use serde::{Deserialize, Serialize}; +use std::collections::HashSet; +use std::fmt::Display; +use std::str::FromStr; +use std::sync::LazyLock; -/// Specifies the compilation mode of the test artifact. -#[derive(Hash, Debug, Clone, Eq, PartialEq)] -pub enum Mode { - Solidity(SolcMode), - Unknown(String), +/// This represents a mode that has been parsed from test metadata. +/// +/// Mode strings can take the following form (in pseudo-regex): +/// +/// ```text +/// [YEILV][+-]? (M[0123sz])? ? +/// ``` +/// +/// We can parse valid mode strings into [`ParsedMode`] using [`ParsedMode::from_str`]. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)] +#[serde(try_from = "String", into = "String")] +pub struct ParsedMode { + pub pipeline: Option, + pub optimize_flag: Option, + pub optimize_setting: Option, + pub version: Option, } -/// Specify Solidity specific compiler options. -#[derive(Hash, Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)] -pub struct SolcMode { - pub solc_version: Option, - solc_optimize: Option, - pub llvm_optimizer_settings: Vec, - mode_string: String, -} +impl FromStr for ParsedMode { + type Err = anyhow::Error; + fn from_str(s: &str) -> Result { + static REGEX: LazyLock = LazyLock::new(|| { + Regex::new(r"(?x) + ^ + (?:(?P[YEILV])(?P[+-])?)? # Pipeline to use eg Y, E+, E- + \s* + (?PM[a-zA-Z0-9])? # Optimize setting eg M0, Ms, Mz + \s* + (?P[>=<]*\d+(?:\.\d+)*)? # Optional semver version eg >=0.8.0, 0.7, <0.8 + $ + ").unwrap() + }); -impl SolcMode { - /// Try to parse a mode string into a solc mode. - /// Returns `None` if the string wasn't a solc YUL mode string. - /// - /// The mode string is expected to start with the `Y` ID (YUL ID), - /// optionally followed by `+` or `-` for the solc optimizer settings. - /// - /// Options can be separated by a whitespace contain the following - /// - A solc `SemVer version requirement` string - /// - One or more `-OX` where X is a supposed to be an LLVM opt mode - pub fn parse_from_mode_string(mode_string: &str) -> Option { - let mut result = Self { - mode_string: mode_string.to_string(), - ..Default::default() + let Some(caps) = REGEX.captures(s) else { + anyhow::bail!("Cannot parse mode '{s}' from string"); }; - let mut parts = mode_string.trim().split(" "); - - match parts.next()? { - "Y" => {} - "Y+" => result.solc_optimize = Some(true), - "Y-" => result.solc_optimize = Some(false), - _ => return None, - } - - for part in parts { - if let Ok(solc_version) = semver::VersionReq::parse(part) { - result.solc_version = Some(solc_version); - continue; - } - if let Some(level) = part.strip_prefix("-O") { - result.llvm_optimizer_settings.push(level.to_string()); - continue; - } - panic!("the YUL mode string {mode_string} failed to parse, invalid part: {part}") - } - - Some(result) - } - - /// Returns whether to enable the solc optimizer. - pub fn solc_optimize(&self) -> bool { - self.solc_optimize.unwrap_or(true) - } - - /// Calculate the latest matching solc patch version. Returns: - /// - `latest_supported` if no version request was specified. - /// - A matching version with the same minor version as `latest_supported`, if any. - /// - `None` if no minor version of the `latest_supported` version matches. - pub fn last_patch_version(&self, latest_supported: &Version) -> Option { - let Some(version_req) = self.solc_version.as_ref() else { - return Some(latest_supported.to_owned()); + let pipeline = match caps.name("pipeline") { + Some(m) => Some(ModePipeline::from_str(m.as_str())?), + None => None, }; - // lgtm - for patch in (0..latest_supported.patch + 1).rev() { - let version = Version::new(0, latest_supported.minor, patch); - if version_req.matches(&version) { - return Some(version); - } - } + let optimize_flag = caps.name("optimize_flag").map(|m| m.as_str() == "+"); - None - } - - /// Resolves the [`SolcMode`]'s solidity version requirement into a [`VersionOrRequirement`] if - /// the requirement is present on the object. Otherwise, the passed default version is used. - pub fn compiler_version_to_use(&self, default: Version) -> VersionOrRequirement { - match self.solc_version { - Some(ref requirement) => requirement.clone().into(), - None => default.into(), - } - } -} - -impl<'de> Deserialize<'de> for Mode { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let mode_string = String::deserialize(deserializer)?; - - if let Some(solc_mode) = SolcMode::parse_from_mode_string(&mode_string) { - return Ok(Self::Solidity(solc_mode)); - } - - Ok(Self::Unknown(mode_string)) - } -} - -impl Serialize for Mode { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let string = match self { - Mode::Solidity(solc_mode) => &solc_mode.mode_string, - Mode::Unknown(string) => string, + let optimize_setting = match caps.name("optimize_setting") { + Some(m) => Some(ModeOptimizerSetting::from_str(m.as_str())?), + None => None, }; - string.serialize(serializer) + + let version = match caps.name("version") { + Some(m) => Some(semver::VersionReq::parse(m.as_str()).map_err(|e| { + anyhow::anyhow!("Cannot parse the version requirement '{}': {e}", m.as_str()) + })?), + None => None, + }; + + Ok(ParsedMode { + pipeline, + optimize_flag, + optimize_setting, + version, + }) + } +} + +impl Display for ParsedMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut has_written = false; + + if let Some(pipeline) = self.pipeline { + pipeline.fmt(f)?; + if let Some(optimize_flag) = self.optimize_flag { + f.write_str(if optimize_flag { "+" } else { "-" })?; + } + has_written = true; + } + + if let Some(optimize_setting) = self.optimize_setting { + if has_written { + f.write_str(" ")?; + } + optimize_setting.fmt(f)?; + has_written = true; + } + + if let Some(version) = &self.version { + if has_written { + f.write_str(" ")?; + } + version.fmt(f)?; + } + + Ok(()) + } +} + +impl From for String { + fn from(parsed_mode: ParsedMode) -> Self { + parsed_mode.to_string() + } +} + +impl TryFrom for ParsedMode { + type Error = anyhow::Error; + fn try_from(value: String) -> Result { + ParsedMode::from_str(&value) + } +} + +impl ParsedMode { + /// This takes a [`ParsedMode`] and expands it into a list of [`Mode`]s that we should try. + pub fn to_modes(&self) -> impl Iterator { + let pipeline_iter = self.pipeline.as_ref().map_or_else( + || EitherIter::A(ModePipeline::test_cases()), + |p| EitherIter::B(std::iter::once(*p)), + ); + + let optimize_flag_setting = self.optimize_flag.map(|flag| { + if flag { + ModeOptimizerSetting::M3 + } else { + ModeOptimizerSetting::M0 + } + }); + + let optimize_flag_iter = match optimize_flag_setting { + Some(setting) => EitherIter::A(std::iter::once(setting)), + None => EitherIter::B(ModeOptimizerSetting::test_cases()), + }; + + let optimize_settings_iter = self.optimize_setting.as_ref().map_or_else( + || EitherIter::A(optimize_flag_iter), + |s| EitherIter::B(std::iter::once(*s)), + ); + + pipeline_iter.flat_map(move |pipeline| { + optimize_settings_iter + .clone() + .map(move |optimize_setting| Mode { + pipeline, + optimize_setting, + version: self.version.clone(), + }) + }) + } + + /// Return a set of [`Mode`]s that correspond to the given [`ParsedMode`]s. + /// This avoids any duplicate entries. + pub fn many_to_modes<'a>( + parsed: impl Iterator, + ) -> impl Iterator { + let modes: HashSet<_> = parsed.flat_map(|p| p.to_modes()).collect(); + modes.into_iter() + } +} + +/// An iterator that could be either of two iterators. +#[derive(Clone, Debug)] +enum EitherIter { + A(A), + B(B), +} + +impl Iterator for EitherIter +where + A: Iterator, + B: Iterator, +{ + type Item = A::Item; + fn next(&mut self) -> Option { + match self { + EitherIter::A(iter) => iter.next(), + EitherIter::B(iter) => iter.next(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parsed_mode_from_str() { + let strings = vec![ + ("Mz", "Mz"), + ("Y", "Y"), + ("Y+", "Y+"), + ("Y-", "Y-"), + ("E", "E"), + ("E+", "E+"), + ("E-", "E-"), + ("Y M0", "Y M0"), + ("Y M1", "Y M1"), + ("Y M2", "Y M2"), + ("Y M3", "Y M3"), + ("Y Ms", "Y Ms"), + ("Y Mz", "Y Mz"), + ("E M0", "E M0"), + ("E M1", "E M1"), + ("E M2", "E M2"), + ("E M3", "E M3"), + ("E Ms", "E Ms"), + ("E Mz", "E Mz"), + // When stringifying semver again, 0.8.0 becomes ^0.8.0 (same meaning) + ("Y 0.8.0", "Y ^0.8.0"), + ("E+ 0.8.0", "E+ ^0.8.0"), + ("Y M3 >=0.8.0", "Y M3 >=0.8.0"), + ("E Mz <0.7.0", "E Mz <0.7.0"), + // We can parse +- _and_ M1/M2 but the latter takes priority. + ("Y+ M1 0.8.0", "Y+ M1 ^0.8.0"), + ("E- M2 0.7.0", "E- M2 ^0.7.0"), + // We don't see this in the wild but it is parsed. + ("<=0.8", "<=0.8"), + ]; + + for (actual, expected) in strings { + let parsed = ParsedMode::from_str(actual) + .expect(format!("Failed to parse mode string '{actual}'").as_str()); + assert_eq!( + expected, + parsed.to_string(), + "Mode string '{actual}' did not parse to '{expected}': got '{parsed}'" + ); + } + } + + #[test] + fn test_parsed_mode_to_test_modes() { + let strings = vec![ + ("Mz", vec!["Y Mz", "E Mz"]), + ("Y", vec!["Y M0", "Y M3"]), + ("E", vec!["E M0", "E M3"]), + ("Y+", vec!["Y M3"]), + ("Y-", vec!["Y M0"]), + ("Y <=0.8", vec!["Y M0 <=0.8", "Y M3 <=0.8"]), + ( + "<=0.8", + vec!["Y M0 <=0.8", "Y M3 <=0.8", "E M0 <=0.8", "E M3 <=0.8"], + ), + ]; + + for (actual, expected) in strings { + let parsed = ParsedMode::from_str(actual) + .expect(format!("Failed to parse mode string '{actual}'").as_str()); + let expected_set: HashSet<_> = expected.into_iter().map(|s| s.to_owned()).collect(); + let actual_set: HashSet<_> = parsed.to_modes().map(|m| m.to_string()).collect(); + + assert_eq!( + expected_set, actual_set, + "Mode string '{actual}' did not expand to '{expected_set:?}': got '{actual_set:?}'" + ); + } } } diff --git a/crates/node/src/geth.rs b/crates/node/src/geth.rs index 09cf6c6..6617351 100644 --- a/crates/node/src/geth.rs +++ b/crates/node/src/geth.rs @@ -60,7 +60,6 @@ pub struct GethNode { geth: PathBuf, id: u32, handle: Option, - network_id: u64, start_timeout: u64, wallet: EthereumWallet, nonce_manager: CachedNonceManager, @@ -165,8 +164,6 @@ impl GethNode { .arg(&self.data_directory) .arg("--ipcpath") .arg(&self.connection_string) - .arg("--networkid") - .arg(self.network_id.to_string()) .arg("--nodiscover") .arg("--maxpeers") .arg("0") @@ -213,6 +210,7 @@ impl GethNode { let maximum_wait_time = Duration::from_millis(self.start_timeout); let mut stderr = BufReader::new(logs_file).lines(); + let mut lines = vec![]; loop { if let Some(Ok(line)) = stderr.next() { if line.contains(Self::ERROR_MARKER) { @@ -221,9 +219,14 @@ impl GethNode { if line.contains(Self::READY_MARKER) { return Ok(self); } + lines.push(line); } if Instant::now().duration_since(start_time) > maximum_wait_time { - anyhow::bail!("Timeout in starting geth"); + anyhow::bail!( + "Timeout in starting geth: took longer than {}ms. stdout:\n\n{}\n", + self.start_timeout, + lines.join("\n") + ); } } } @@ -257,7 +260,11 @@ impl GethNode { Box::pin(async move { ProviderBuilder::new() .disable_recommended_fillers() - .filler(FallbackGasFiller::new(500_000_000, 500_000_000, 1)) + .filler(FallbackGasFiller::new( + 25_000_000, + 1_000_000_000, + 1_000_000_000, + )) .filler(ChainIdFiller::default()) .filler(NonceFiller::new(nonce_manager)) .wallet(wallet) @@ -517,7 +524,6 @@ impl Node for GethNode { geth: config.geth.clone(), id, handle: None, - network_id: config.network_id, start_timeout: config.geth_start_timeout, wallet, // We know that we only need to be storing 2 files so we can specify that when creating diff --git a/crates/node/src/kitchensink.rs b/crates/node/src/kitchensink.rs index 03a86ff..f5a9e0a 100644 --- a/crates/node/src/kitchensink.rs +++ b/crates/node/src/kitchensink.rs @@ -367,9 +367,9 @@ impl KitchensinkNode { .disable_recommended_fillers() .network::() .filler(FallbackGasFiller::new( - 30_000_000, - 200_000_000_000, - 3_000_000_000, + 25_000_000, + 1_000_000_000, + 1_000_000_000, )) .filler(ChainIdFiller::default()) .filler(NonceFiller::new(nonce_manager)) diff --git a/crates/report/Cargo.toml b/crates/report/Cargo.toml index 1ffb7b8..d18caab 100644 --- a/crates/report/Cargo.toml +++ b/crates/report/Cargo.toml @@ -8,6 +8,7 @@ repository.workspace = true rust-version.workspace = true [dependencies] +revive-dt-common = { workspace = true } revive-dt-config = { workspace = true } revive-dt-format = { workspace = true } revive-dt-compiler = { workspace = true } diff --git a/crates/report/src/reporter.rs b/crates/report/src/reporter.rs index 9b9303d..e5d0d1f 100644 --- a/crates/report/src/reporter.rs +++ b/crates/report/src/reporter.rs @@ -12,18 +12,19 @@ use std::{ }; use anyhow::Context; -use revive_dt_compiler::{CompilerInput, CompilerOutput}; -use serde::{Deserialize, Serialize}; +use serde::Serialize; +use revive_dt_common::types::Mode; +use revive_dt_compiler::{CompilerInput, CompilerOutput}; use revive_dt_config::{Arguments, TestingPlatform}; -use revive_dt_format::{corpus::Corpus, mode::SolcMode}; +use revive_dt_format::corpus::Corpus; use crate::analyzer::CompilerStatistics; pub(crate) static REPORTER: OnceLock> = OnceLock::new(); /// The `Report` datastructure stores all relevant inforamtion required for generating reports. -#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, Serialize)] pub struct Report { /// The configuration used during the test. pub config: Arguments, @@ -41,14 +42,14 @@ pub struct Report { } /// Contains a compiled contract. -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize)] pub struct CompilationTask { /// The observed compiler input. pub json_input: CompilerInput, /// The observed compiler output. pub json_output: Option, /// The observed compiler mode. - pub mode: SolcMode, + pub mode: Mode, /// The observed compiler version. pub compiler_version: String, /// The observed error, if any. @@ -56,7 +57,7 @@ pub struct CompilationTask { } /// Represents a report about a compilation task. -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize)] pub struct CompilationResult { /// The observed compilation task. pub compilation_task: CompilationTask, @@ -65,7 +66,7 @@ pub struct CompilationResult { } /// The [Span] struct indicates the context of what is being reported. -#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, Serialize)] pub struct Span { /// The corpus index this belongs to. corpus: usize,