diff --git a/Cargo.lock b/Cargo.lock index 431c137..7de6757 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4142,6 +4142,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/crates/common/src/types/mode.rs b/crates/common/src/types/mode.rs index c02ccd3..c380209 100644 --- a/crates/common/src/types/mode.rs +++ b/crates/common/src/types/mode.rs @@ -1,11 +1,8 @@ use crate::types::VersionOrRequirement; -use regex::Regex; 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. /// @@ -22,35 +19,29 @@ pub struct Mode { 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, - ) + 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 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() + /// 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 @@ -63,197 +54,17 @@ impl Mode { } } -/// 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 - #[serde(rename = "Y")] ViaYulIR, /// Compile Solidity direct to assembly - #[serde(rename = "E")] ViaEVMAssembly, } impl FromStr for ModePipeline { - type Err = ParseModeError; + type Err = anyhow::Error; fn from_str(s: &str) -> Result { match s { // via Yul IR @@ -261,8 +72,8 @@ impl FromStr for ModePipeline { // Don't go via Yul IR "E" => Ok(ModePipeline::ViaEVMAssembly), // Anything else that we see isn't a mode at all - _ => Err(ParseModeError::UnsupportedPipeline( - s.chars().next().unwrap_or('?'), + _ => Err(anyhow::anyhow!( + "Unsupported pipeline '{s}': expected 'Y' or 'E'" )), } } @@ -307,7 +118,7 @@ pub enum ModeOptimizerSetting { } impl FromStr for ModeOptimizerSetting { - type Err = ParseModeError; + type Err = anyhow::Error; fn from_str(s: &str) -> Result { match s { "M0" => Ok(ModeOptimizerSetting::M0), @@ -316,7 +127,9 @@ impl FromStr for ModeOptimizerSetting { "M3" => Ok(ModeOptimizerSetting::M3), "Ms" => Ok(ModeOptimizerSetting::Ms), "Mz" => Ok(ModeOptimizerSetting::Mz), - _ => Err(ParseModeError::UnsupportedOptimizerSetting(s.to_owned())), + _ => Err(anyhow::anyhow!( + "Unsupported optimizer setting '{s}': expected 'M0', 'M1', 'M2', 'M3', 'Ms' or 'Mz'" + )), } } } @@ -352,102 +165,3 @@ impl ModeOptimizerSetting { !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/format/src/metadata.rs b/crates/format/src/metadata.rs index c9b43fc..a0f9c21 100644 --- a/crates/format/src/metadata.rs +++ b/crates/format/src/metadata.rs @@ -86,7 +86,7 @@ impl Metadata { /// Returns the modes that we should test from this metadata. pub fn solc_modes(&self) -> Vec { match &self.modes { - Some(modes) => Mode::from_parsed_modes(modes.iter()).collect(), + Some(modes) => ParsedMode::many_to_modes(modes.iter()).collect(), None => Mode::all().collect(), } } diff --git a/crates/format/src/mode.rs b/crates/format/src/mode.rs index 3f0d1be..7e6dfc8 100644 --- a/crates/format/src/mode.rs +++ b/crates/format/src/mode.rs @@ -1,4 +1,262 @@ -// Re-export the "format" side of our mode types from the common crate. -pub use revive_dt_common::types::{ - Mode, ModeOptimizerSetting, ModePipeline, ParseModeError, ParsedMode, -}; +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; + +/// 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 = 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() + }); + + let Some(caps) = REGEX.captures(s) else { + anyhow::bail!("Cannot parse mode '{s}' from string"); + }; + + 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| { + 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/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 3a49254..e5d0d1f 100644 --- a/crates/report/src/reporter.rs +++ b/crates/report/src/reporter.rs @@ -12,11 +12,12 @@ use std::{ }; use anyhow::Context; -use revive_dt_compiler::{CompilerInput, CompilerOutput}; 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::Mode}; +use revive_dt_format::corpus::Corpus; use crate::analyzer::CompilerStatistics;