diff --git a/Cargo.lock b/Cargo.lock index ff5c492..431c137 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4029,7 +4029,10 @@ name = "revive-dt-common" version = "0.1.0" dependencies = [ "anyhow", + "regex", "semver 1.0.26", + "serde", + "thiserror 2.0.12", "tokio", ] @@ -4044,7 +4047,6 @@ dependencies = [ "revive-common", "revive-dt-common", "revive-dt-config", - "revive-dt-format", "revive-dt-solc-binaries", "revive-solc-json-interface", "semver 1.0.26", @@ -4103,7 +4105,6 @@ dependencies = [ "semver 1.0.26", "serde", "serde_json", - "thiserror 2.0.12", "tokio", "tracing", ] diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 516b1be..ce78f29 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -10,5 +10,8 @@ rust-version.workspace = true [dependencies] anyhow = { workspace = true } +regex = { workspace = true } semver = { workspace = true } +serde = { workspace = true } +thiserror = { 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..cf2f50a 100644 --- a/crates/common/src/types/mod.rs +++ b/crates/common/src/types/mod.rs @@ -1,3 +1,5 @@ mod version_or_requirement; +mod mode; pub use version_or_requirement::*; +pub use mode::*; \ No newline at end of file diff --git a/crates/common/src/types/mode.rs b/crates/common/src/types/mode.rs new file mode 100644 index 0000000..8e2f5f2 --- /dev/null +++ b/crates/common/src/types/mode.rs @@ -0,0 +1,451 @@ +use regex::Regex; +use crate::types::VersionOrRequirement; +use semver::Version; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; +use std::fmt::Display; +use std::str::FromStr; +use std::sync::LazyLock; + +/// 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 { + fmt_mode_parts( + Some(&self.pipeline), + None, + Some(&self.optimize_setting), + self.version.as_ref(), + f, + ) + } +} + +impl Mode { + /// Return a set of [`TestMode`]s that correspond to the given [`ParsedMode`]s. + /// This avoids any duplicate entries. + pub fn from_parsed_modes<'a>( + parsed: impl Iterator, + ) -> impl Iterator { + let modes: HashSet<_> = parsed.flat_map(|p| p.to_test_modes()).collect(); + modes.into_iter() + } + + /// Return all of the test modes that we want to run when no specific [`ParsedMode`] is specified. + pub fn all() -> impl Iterator { + ParsedMode { + pipeline: None, + optimize_flag: None, + optimize_setting: None, + version: None, + } + .to_test_modes() + } + + /// 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(), + } + } +} + +/// 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, +} + +impl FromStr for ParsedMode { + type Err = ParseModeError; + 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() + }); + + let Some(caps) = REGEX.captures(s) else { + return Err(ParseModeError::CannotParse); + }; + + let pipeline = match caps.name("pipeline") { + Some(m) => Some(ModePipeline::from_str(m.as_str())?), + None => None, + }; + + let optimize_flag = caps.name("optimize_flag").map(|m| m.as_str() == "+"); + + let optimize_setting = match caps.name("optimize_setting") { + Some(m) => Some(ModeOptimizerSetting::from_str(m.as_str())?), + None => None, + }; + + let version = match caps.name("version") { + Some(m) => Some( + semver::VersionReq::parse(m.as_str()) + .map_err(|e| ParseModeError::InvalidVersion(e.to_string()))?, + ), + 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 { + fmt_mode_parts( + self.pipeline.as_ref(), + self.optimize_flag, + self.optimize_setting.as_ref(), + self.version.as_ref(), + f, + ) + } +} + +impl From for String { + fn from(parsed_mode: ParsedMode) -> Self { + parsed_mode.to_string() + } +} + +impl TryFrom for ParsedMode { + type Error = ParseModeError; + fn try_from(value: String) -> Result { + ParsedMode::from_str(&value) + } +} + +fn fmt_mode_parts( + pipeline: Option<&ModePipeline>, + optimize_flag: Option, + optimize_setting: Option<&ModeOptimizerSetting>, + version: Option<&semver::VersionReq>, + f: &mut std::fmt::Formatter<'_>, +) -> std::fmt::Result { + let mut has_written = false; + + if let Some(pipeline) = pipeline { + pipeline.fmt(f)?; + if let Some(optimize_flag) = optimize_flag { + f.write_str(if optimize_flag { "+" } else { "-" })?; + } + has_written = true; + } + + if let Some(optimize_setting) = optimize_setting { + if has_written { + f.write_str(" ")?; + } + optimize_setting.fmt(f)?; + has_written = true; + } + + if let Some(version) = version { + if has_written { + f.write_str(" ")?; + } + version.fmt(f)?; + } + + Ok(()) +} + +impl ParsedMode { + /// This takes a [`ParsedMode`] and expands it into a list of [`TestMode`]s that we should try. + pub fn to_test_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(), + }) + }) + } +} + +#[derive(thiserror::Error, Debug, Clone)] +pub enum ParseModeError { + #[error( + "Cannot parse compiler mode via regex: expecting something like 'Y', 'E+ <0.8', 'Y Mz =0.8'" + )] + CannotParse, + #[error("Unsupported compiler pipeline mode: {0}. We support Y and E modes only.")] + UnsupportedPipeline(char), + #[error("Unsupported optimizer setting: {0}. We support M0, M1, M2, M3, Ms, and Mz only.")] + UnsupportedOptimizerSetting(String), + #[error("Invalid semver specifier: {0}")] + InvalidVersion(String), +} + +/// 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 + Y, + /// Compile Solidity direct to assembly + E, +} + +impl FromStr for ModePipeline { + type Err = ParseModeError; + fn from_str(s: &str) -> Result { + match s { + // via Yul IR + "Y" => Ok(ModePipeline::Y), + // Don't go via Yul IR + "E" => Ok(ModePipeline::E), + // Anything else that we see isn't a mode at all + _ => Err(ParseModeError::UnsupportedPipeline( + s.chars().next().unwrap_or('?'), + )), + } + } +} + +impl Display for ModePipeline { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ModePipeline::Y => f.write_str("Y"), + ModePipeline::E => f.write_str("E"), + } + } +} + +impl ModePipeline { + /// Should we go via Yul IR? + pub fn via_yul_ir(&self) -> bool { + matches!(self, ModePipeline::Y) + } + + /// 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::Y, ModePipeline::E].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 = ParseModeError; + 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(ParseModeError::UnsupportedOptimizerSetting(s.to_owned())), + } + } +} + +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) + } +} + +/// 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_test_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/compiler/Cargo.toml b/crates/compiler/Cargo.toml index ee69f39..9e10a10 100644 --- a/crates/compiler/Cargo.toml +++ b/crates/compiler/Cargo.toml @@ -12,7 +12,6 @@ rust-version.workspace = true revive-solc-json-interface = { workspace = true } revive-dt-common = { workspace = true } revive-dt-config = { workspace = true } -revive-dt-format = { workspace = true } revive-dt-solc-binaries = { workspace = true } revive-common = { workspace = true } diff --git a/crates/compiler/src/lib.rs b/crates/compiler/src/lib.rs index 5b6d247..9d35476 100644 --- a/crates/compiler/src/lib.rs +++ b/crates/compiler/src/lib.rs @@ -20,7 +20,7 @@ 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_format::mode::{Mode, ModeOptimizerSetting, ModePipeline}; +pub use revive_dt_common::types::{Mode, ModeOptimizerSetting, ModePipeline}; pub mod revive_js; pub mod revive_resolc; diff --git a/crates/compiler/src/revive_resolc.rs b/crates/compiler/src/revive_resolc.rs index e9e2bf3..8209a48 100644 --- a/crates/compiler/src/revive_resolc.rs +++ b/crates/compiler/src/revive_resolc.rs @@ -39,7 +39,6 @@ impl SolidityCompiler for Resolc { async fn build( &self, CompilerInput { - // Ignored as we only support one pipeline (Y) pipeline, optimization, evm_version, @@ -53,6 +52,10 @@ impl SolidityCompiler for Resolc { }: CompilerInput, additional_options: Self::Options, ) -> anyhow::Result { + if !matches!(pipeline, None | Some(ModePipeline::Y)) { + 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 diff --git a/crates/core/src/main.rs b/crates/core/src/main.rs index bb99b0d..3d854ca 100644 --- a/crates/core/src/main.rs +++ b/crates/core/src/main.rs @@ -25,6 +25,7 @@ use tracing_subscriber::{EnvFilter, FmtSubscriber}; use revive_dt_compiler::SolidityCompiler; use revive_dt_compiler::{Compiler, CompilerOutput}; use revive_dt_config::*; +use revive_dt_common::types::Mode; use revive_dt_core::{ Geth, Kitchensink, Platform, driver::{CaseDriver, CaseState}, @@ -34,7 +35,6 @@ use revive_dt_format::{ corpus::Corpus, input::{Input, Step}, metadata::{ContractInstance, ContractPathAndIdent, Metadata, MetadataFile}, - mode::Mode, }; use revive_dt_node::pool::NodePool; use revive_dt_report::reporter::{Report, Span}; diff --git a/crates/format/Cargo.toml b/crates/format/Cargo.toml index e4e5173..0e5745e 100644 --- a/crates/format/Cargo.toml +++ b/crates/format/Cargo.toml @@ -18,7 +18,6 @@ alloy-primitives = { workspace = true } alloy-sol-types = { workspace = true } anyhow = { workspace = true } regex = { workspace = true } -thiserror = { workspace = true } tracing = { workspace = true } semver = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/crates/format/src/metadata.rs b/crates/format/src/metadata.rs index 7089bb7..be1a78b 100644 --- a/crates/format/src/metadata.rs +++ b/crates/format/src/metadata.rs @@ -11,11 +11,11 @@ use std::{ use serde::{Deserialize, Serialize}; use revive_common::EVMVersion; -use revive_dt_common::{iterators::FilesWithExtensionIterator, macros::define_wrapper_type}; +use revive_dt_common::{iterators::FilesWithExtensionIterator, macros::define_wrapper_type, types::Mode}; use crate::{ case::Case, - mode::{Mode, ParsedMode}, + mode::ParsedMode, }; pub const METADATA_FILE_EXTENSION: &str = "json"; diff --git a/crates/format/src/mode.rs b/crates/format/src/mode.rs index 41289d2..2812bfd 100644 --- a/crates/format/src/mode.rs +++ b/crates/format/src/mode.rs @@ -1,451 +1,8 @@ -use regex::Regex; -use revive_dt_common::types::VersionOrRequirement; -use semver::Version; -use serde::{Deserialize, Serialize}; -use std::collections::HashSet; -use std::fmt::Display; -use std::str::FromStr; -use std::sync::LazyLock; - -/// 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 { - fmt_mode_parts( - Some(&self.pipeline), - None, - Some(&self.optimize_setting), - self.version.as_ref(), - f, - ) - } -} - -impl Mode { - /// Return a set of [`TestMode`]s that correspond to the given [`ParsedMode`]s. - /// This avoids any duplicate entries. - pub fn from_parsed_modes<'a>( - parsed: impl Iterator, - ) -> impl Iterator { - let modes: HashSet<_> = parsed.flat_map(|p| p.to_test_modes()).collect(); - modes.into_iter() - } - - /// Return all of the test modes that we want to run when no specific [`ParsedMode`] is specified. - pub fn all() -> impl Iterator { - ParsedMode { - pipeline: None, - optimize_flag: None, - optimize_setting: None, - version: None, - } - .to_test_modes() - } - - /// 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(), - } - } -} - -/// 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, -} - -impl FromStr for ParsedMode { - type Err = ParseModeError; - 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() - }); - - let Some(caps) = REGEX.captures(s) else { - return Err(ParseModeError::CannotParse); - }; - - let pipeline = match caps.name("pipeline") { - Some(m) => Some(ModePipeline::from_str(m.as_str())?), - None => None, - }; - - let optimize_flag = caps.name("optimize_flag").map(|m| m.as_str() == "+"); - - let optimize_setting = match caps.name("optimize_setting") { - Some(m) => Some(ModeOptimizerSetting::from_str(m.as_str())?), - None => None, - }; - - let version = match caps.name("version") { - Some(m) => Some( - semver::VersionReq::parse(m.as_str()) - .map_err(|e| ParseModeError::InvalidVersion(e.to_string()))?, - ), - 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 { - fmt_mode_parts( - self.pipeline.as_ref(), - self.optimize_flag, - self.optimize_setting.as_ref(), - self.version.as_ref(), - f, - ) - } -} - -impl From for String { - fn from(parsed_mode: ParsedMode) -> Self { - parsed_mode.to_string() - } -} - -impl TryFrom for ParsedMode { - type Error = ParseModeError; - fn try_from(value: String) -> Result { - ParsedMode::from_str(&value) - } -} - -fn fmt_mode_parts( - pipeline: Option<&ModePipeline>, - optimize_flag: Option, - optimize_setting: Option<&ModeOptimizerSetting>, - version: Option<&semver::VersionReq>, - f: &mut std::fmt::Formatter<'_>, -) -> std::fmt::Result { - let mut has_written = false; - - if let Some(pipeline) = pipeline { - pipeline.fmt(f)?; - if let Some(optimize_flag) = optimize_flag { - f.write_str(if optimize_flag { "+" } else { "-" })?; - } - has_written = true; - } - - if let Some(optimize_setting) = optimize_setting { - if has_written { - f.write_str(" ")?; - } - optimize_setting.fmt(f)?; - has_written = true; - } - - if let Some(version) = version { - if has_written { - f.write_str(" ")?; - } - version.fmt(f)?; - } - - Ok(()) -} - -impl ParsedMode { - /// This takes a [`ParsedMode`] and expands it into a list of [`TestMode`]s that we should try. - pub fn to_test_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(), - }) - }) - } -} - -#[derive(thiserror::Error, Debug, Clone)] -pub enum ParseModeError { - #[error( - "Cannot parse compiler mode via regex: expecting something like 'Y', 'E+ <0.8', 'Y Mz =0.8'" - )] - CannotParse, - #[error("Unsupported compiler pipeline mode: {0}. We support Y and E modes only.")] - UnsupportedPipeline(char), - #[error("Unsupported optimizer setting: {0}. We support M0, M1, M2, M3, Ms, and Mz only.")] - UnsupportedOptimizerSetting(String), - #[error("Invalid semver specifier: {0}")] - InvalidVersion(String), -} - -/// 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 - Y, - /// Compile Solidity direct to assembly - E, -} - -impl FromStr for ModePipeline { - type Err = ParseModeError; - fn from_str(s: &str) -> Result { - match s { - // via Yul IR - "Y" => Ok(ModePipeline::Y), - // Don't go via Yul IR - "E" => Ok(ModePipeline::E), - // Anything else that we see isn't a mode at all - _ => Err(ParseModeError::UnsupportedPipeline( - s.chars().next().unwrap_or('?'), - )), - } - } -} - -impl Display for ModePipeline { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ModePipeline::Y => f.write_str("Y"), - ModePipeline::E => f.write_str("E"), - } - } -} - -impl ModePipeline { - /// Should we go via Yul IR? - pub fn via_yul_ir(&self) -> bool { - matches!(self, ModePipeline::Y) - } - - /// 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::Y, ModePipeline::E].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 = ParseModeError; - 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(ParseModeError::UnsupportedOptimizerSetting(s.to_owned())), - } - } -} - -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) - } -} - -/// 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_test_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:?}'" - ); - } - } -} +// Re-export the "format" side of our mode types from the common crate. +pub use revive_dt_common::types::{ + Mode, + ModeOptimizerSetting, + ModePipeline, + ParsedMode, + ParseModeError +}; \ No newline at end of file