diff --git a/Cargo.lock b/Cargo.lock index 7d1655e2..badd3f8d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -671,27 +671,6 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[package]] -name = "const_format" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" -dependencies = [ - "const_format_proc_macros", - "konst", -] - -[[package]] -name = "const_format_proc_macros" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - [[package]] name = "convert_case" version = "0.6.0" @@ -1464,21 +1443,6 @@ dependencies = [ "sha3-asm", ] -[[package]] -name = "konst" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330f0e13e6483b8c34885f7e6c9f19b1a7bd449c673fbb948a51c99d66ef74f4" -dependencies = [ - "konst_macro_rules", -] - -[[package]] -name = "konst_macro_rules" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" - [[package]] name = "lalrpop" version = "0.20.2" @@ -2583,7 +2547,6 @@ dependencies = [ "alloy-primitives", "cfg-if", "clap", - "const_format", "libc", "solar-config", "solar-interface", @@ -2593,7 +2556,6 @@ dependencies = [ "tracing-chrome", "tracing-subscriber", "tracing-tracy", - "vergen", ] [[package]] @@ -2610,17 +2572,17 @@ dependencies = [ "solar-parse", "solar-sema", "solar-tester", - "vergen", ] [[package]] name = "solar-config" version = "0.1.0" dependencies = [ - "clap_builder", + "clap", "serde", "serde_json", "strum", + "vergen", ] [[package]] diff --git a/benches/src/lib.rs b/benches/src/lib.rs index b00d18fe..9e3993ed 100644 --- a/benches/src/lib.rs +++ b/benches/src/lib.rs @@ -97,7 +97,7 @@ impl Parser for Solar { } fn lex(&self, src: &str) { - let sess = Session::builder().with_stderr_emitter().build(); + let sess = session(); for token in solar_parse::Lexer::new(&sess, src) { black_box(token); } @@ -105,7 +105,7 @@ impl Parser for Solar { } fn parse(&self, src: &str) { - let sess = Session::builder().with_stderr_emitter().build(); + let sess = session(); sess.enter(|| -> solar_parse::interface::Result { let arena = solar_parse::ast::Arena::new(); let filename = PathBuf::from("test.sol"); @@ -120,6 +120,13 @@ impl Parser for Solar { } } +fn session() -> Session { + Session::builder() + .with_stderr_emitter_and_color(solar_parse::interface::ColorChoice::Always) + .single_threaded() + .build() +} + pub struct Solang; impl Parser for Solang { fn name(&self) -> &'static str { diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 7be15b18..6e3d0fc7 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -15,9 +15,6 @@ categories.workspace = true [lints] workspace = true -[build-dependencies] -vergen = { workspace = true, features = ["build", "git", "gitcl", "cargo"] } - [dependencies] solar-config = { workspace = true, features = ["clap"] } solar-interface = { workspace = true, features = ["json"] } @@ -26,7 +23,6 @@ solar-sema.workspace = true alloy-primitives.workspace = true cfg-if.workspace = true clap = { workspace = true, features = ["derive"] } -const_format = { workspace = true, features = ["rust_1_64"] } tracing.workspace = true tracing-subscriber = { workspace = true, features = ["registry", "env-filter"] } diff --git a/crates/cli/build.rs b/crates/cli/build.rs deleted file mode 100644 index 19152b47..00000000 --- a/crates/cli/build.rs +++ /dev/null @@ -1,24 +0,0 @@ -use std::env; - -fn main() { - vergen::EmitBuilder::builder() - .git_describe(false, true, None) - .git_dirty(true) - .git_sha(false) - .build_timestamp() - .cargo_features() - .cargo_target_triple() - .emit_and_set() - .unwrap(); - - let sha = env::var("VERGEN_GIT_SHA").unwrap(); - let sha_short = &sha[0..7]; - - let is_dirty = env::var("VERGEN_GIT_DIRTY").unwrap() == "true"; - // > git describe --always --tags - // if not on a tag: v0.2.0-beta.3-82-g1939939b - // if on a tag: v0.2.0-beta.3 - let not_on_tag = env::var("VERGEN_GIT_DESCRIBE").unwrap().ends_with(&format!("-g{sha_short}")); - let is_dev = is_dirty || not_on_tag; - println!("cargo:rustc-env=VERSION_SUFFIX={}", if is_dev { "-dev" } else { "" }); -} diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index f9ad5a96..a868e664 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -6,16 +6,16 @@ #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] use clap::Parser as _; -use cli::Args; +use solar_config::{ErrorFormat, ImportMap}; use solar_interface::{ diagnostics::{DiagCtxt, DynEmitter, HumanEmitter, JsonEmitter}, Result, Session, SourceMap, }; -use std::{collections::BTreeSet, num::NonZeroUsize, path::Path, sync::Arc}; +use std::{path::Path, sync::Arc}; + +pub use solar_config::{self as config, version, Opts, UnstableOpts}; -pub mod cli; pub mod utils; -pub mod version; #[cfg(all(unix, any(target_env = "gnu", target_os = "macos")))] pub mod sigsegv_handler; @@ -34,30 +34,29 @@ use alloy_primitives as _; use tracing as _; -pub fn parse_args(itr: I) -> Result +pub fn parse_args(itr: I) -> Result where I: IntoIterator, T: Into + Clone, { - let mut args = Args::try_parse_from(itr)?; - args.finish()?; - Ok(args) + let mut opts = Opts::try_parse_from(itr)?; + opts.finish()?; + Ok(opts) } -pub fn run_compiler_args(args: Args) -> Result<()> { - run_compiler_with(args, Compiler::run_default) +pub fn run_compiler_args(opts: Opts) -> Result<()> { + run_compiler_with(opts, Compiler::run_default) } pub struct Compiler { pub sess: Session, - pub args: Args, } impl Compiler { pub fn run_default(&self) -> Result<()> { - let Self { sess, args } = self; + let Self { sess } = self; - if sess.language.is_yul() && !args.unstable.parse_yul { + if sess.opts.language.is_yul() && !sess.opts.unstable.parse_yul { return Err(sess.dcx.err("Yul is not supported yet").emit()); } @@ -65,20 +64,20 @@ impl Compiler { // - `stdin`: `-`, occurrences after the first are ignored // - remappings: `path=mapped` // - paths: everything else - let stdin = args.input.iter().any(|arg| *arg == Path::new("-")); - let non_stdin_args = args.input.iter().filter(|arg| *arg != Path::new("-")); + let stdin = sess.opts.input.iter().any(|arg| *arg == Path::new("-")); + let non_stdin_args = sess.opts.input.iter().filter(|arg| *arg != Path::new("-")); let arg_remappings = non_stdin_args .clone() - .filter_map(|arg| arg.to_str().unwrap_or("").parse::().ok()); + .filter_map(|arg| arg.to_str().unwrap_or("").parse::().ok()); let paths = non_stdin_args.filter(|arg| !arg.as_os_str().as_encoded_bytes().contains(&b'=')); let mut pcx = solar_sema::ParsingContext::new(sess); - let remappings = arg_remappings.chain(args.import_map.iter().cloned()); + let remappings = arg_remappings.chain(sess.opts.import_map.iter().cloned()); for map in remappings { pcx.file_resolver.add_import_map(map.map, map.path); } - for path in &args.import_path { + for path in &sess.opts.import_path { let new = pcx.file_resolver.add_import_path(path.clone()); if !new { let msg = format!("import path {} already specified", path.display()); @@ -101,12 +100,12 @@ impl Compiler { } } -fn run_compiler_with(args: Args, f: impl FnOnce(&Compiler) -> Result + Send) -> Result { - let ui_testing = args.unstable.ui_testing; +fn run_compiler_with(opts: Opts, f: impl FnOnce(&Compiler) -> Result + Send) -> Result { + let ui_testing = opts.unstable.ui_testing; let source_map = Arc::new(SourceMap::empty()); - let emitter: Box = match args.error_format { - cli::ErrorFormat::Human => { - let color = match args.color { + let emitter: Box = match opts.error_format { + ErrorFormat::Human => { + let color = match opts.color { clap::ColorChoice::Always => solar_interface::ColorChoice::Always, clap::ColorChoice::Auto => solar_interface::ColorChoice::Auto, clap::ColorChoice::Never => solar_interface::ColorChoice::Never, @@ -116,12 +115,12 @@ fn run_compiler_with(args: Args, f: impl FnOnce(&Compiler) -> Result + Send) -> .ui_testing(ui_testing); Box::new(human) } - cli::ErrorFormat::Json | cli::ErrorFormat::RustcJson => { + ErrorFormat::Json | ErrorFormat::RustcJson => { // `io::Stderr` is not buffered. let writer = Box::new(std::io::BufWriter::new(std::io::stderr())); let json = JsonEmitter::new(writer, source_map.clone()) - .pretty(args.pretty_json_err) - .rustc_like(matches!(args.error_format, cli::ErrorFormat::RustcJson)) + .pretty(opts.pretty_json_err) + .rustc_like(matches!(opts.error_format, ErrorFormat::RustcJson)) .ui_testing(ui_testing); Box::new(json) } @@ -129,36 +128,14 @@ fn run_compiler_with(args: Args, f: impl FnOnce(&Compiler) -> Result + Send) -> let dcx = DiagCtxt::new(emitter).set_flags(|flags| { flags.deduplicate_diagnostics &= !ui_testing; flags.track_diagnostics &= !ui_testing; - flags.track_diagnostics |= args.unstable.track_diagnostics; + flags.track_diagnostics |= opts.unstable.track_diagnostics; }); - let mut sess = Session::new(dcx, source_map); - sess.evm_version = args.evm_version; - sess.language = args.language; - sess.stop_after = args.stop_after; - sess.dump = args.unstable.dump.clone(); - sess.ast_stats = args.unstable.ast_stats; - sess.jobs = NonZeroUsize::new(args.threads) - .unwrap_or_else(|| std::thread::available_parallelism().unwrap_or(NonZeroUsize::MIN)); - if !args.input.is_empty() - && args.input.iter().all(|arg| arg.extension() == Some("yul".as_ref())) - { - sess.language = solar_config::Language::Yul; - } - sess.emit = { - let mut set = BTreeSet::default(); - for &emit in &args.emit { - if !set.insert(emit) { - let msg = format!("cannot specify `--emit {emit}` twice"); - return Err(sess.dcx.err(msg).emit()); - } - } - set - }; - sess.out_dir = args.out_dir.clone(); - sess.pretty_json = args.pretty_json; + let mut sess = Session::builder().dcx(dcx).source_map(source_map).opts(opts).build(); + sess.infer_language(); + sess.validate()?; - let compiler = Compiler { sess, args }; + let compiler = Compiler { sess }; compiler.sess.enter(|| { let mut r = f(&compiler); r = compiler.finish_diagnostics().and(r); diff --git a/crates/cli/src/version.rs b/crates/cli/src/version.rs deleted file mode 100644 index 26ed8f30..00000000 --- a/crates/cli/src/version.rs +++ /dev/null @@ -1,47 +0,0 @@ -/// The short version information. -pub const SHORT_VERSION: &str = const_format::concatcp!( - env!("CARGO_PKG_VERSION"), - env!("VERSION_SUFFIX"), - " (", - VERGEN_GIT_SHA, - " ", - env!("VERGEN_BUILD_TIMESTAMP"), - ")", -); - -/// The long version information. -pub const LONG_VERSION: &str = const_format::concatcp!( - "Version: ", - env!("CARGO_PKG_VERSION"), - "\n", - "Commit SHA: ", - VERGEN_GIT_SHA_LONG, - "\n", - "Build Timestamp: ", - env!("VERGEN_BUILD_TIMESTAMP"), - "\n", - "Build Features: ", - env!("VERGEN_CARGO_FEATURES"), - "\n", - "Build Profile: ", - BUILD_PROFILE_NAME, -); - -/// The build profile name. -const BUILD_PROFILE_NAME: &str = { - // https://stackoverflow.com/questions/73595435/how-to-get-profile-from-cargo-toml-in-build-rs-or-at-runtime - const OUT_DIR: &str = env!("OUT_DIR"); - let unix_parts = const_format::str_split!(OUT_DIR, '/'); - if unix_parts.len() >= 4 { - unix_parts[unix_parts.len() - 4] - } else { - let win_parts = const_format::str_split!(OUT_DIR, '\\'); - win_parts[win_parts.len() - 4] - } -}; - -/// The full SHA of the latest commit. -const VERGEN_GIT_SHA_LONG: &str = env!("VERGEN_GIT_SHA"); - -/// The 8 character short SHA of the latest commit. -const VERGEN_GIT_SHA: &str = const_format::str_index!(VERGEN_GIT_SHA_LONG, ..8); diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index 28f19dd1..de0cab83 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -15,13 +15,18 @@ categories.workspace = true [lints] workspace = true +[build-dependencies] +vergen = { workspace = true, optional = true, features = [ + "build", + "git", + "gitcl", + "cargo", +] } + [dependencies] strum = { workspace = true, features = ["derive"] } -# clap -clap_builder = { workspace = true, optional = true } - -# serde +clap = { workspace = true, optional = true, features = ["derive"] } serde = { workspace = true, optional = true } [dev-dependencies] @@ -30,5 +35,6 @@ serde_json.workspace = true [features] nightly = [] -clap = ["dep:clap_builder"] +clap = ["version", "dep:clap"] +version = ["dep:vergen"] serde = ["dep:serde"] diff --git a/crates/config/build.rs b/crates/config/build.rs new file mode 100644 index 00000000..b2299a7c --- /dev/null +++ b/crates/config/build.rs @@ -0,0 +1,51 @@ +#[cfg(feature = "version")] +fn main() { + use std::env; + + vergen::EmitBuilder::builder() + .git_describe(false, true, None) + .git_dirty(true) + .git_sha(false) + .build_timestamp() + .cargo_features() + .cargo_target_triple() + .emit_and_set() + .unwrap(); + + let sha = env::var("VERGEN_GIT_SHA").unwrap(); + let sha_short = &sha[..7]; + + let is_dev = { + let is_dirty = env::var("VERGEN_GIT_DIRTY").unwrap() == "true"; + + // > git describe --always --tags + // if not on a tag: v0.2.0-beta.3-82-g1939939b + // if on a tag: v0.2.0-beta.3 + let not_on_tag = + env::var("VERGEN_GIT_DESCRIBE").unwrap().ends_with(&format!("-g{sha_short}")); + + is_dirty || not_on_tag + }; + let version_suffix = if is_dev { "-dev" } else { "" }; + + let version = env::var("CARGO_PKG_VERSION").unwrap(); + let version_suffixed = format!("{version}{version_suffix}"); + + let timestamp = env::var("VERGEN_BUILD_TIMESTAMP").unwrap(); + + let short_version = format!("{version_suffixed} ({sha_short} {timestamp})"); + println!("cargo:rustc-env=SHORT_VERSION={short_version}"); + + let long_version = format!( + "Version: {version}\nCommit SHA: {sha}\nBuild Timestamp: {timestamp}\nBuild Features: {}\nBuild Profile: {}", + env::var("VERGEN_CARGO_FEATURES").unwrap(), + env::var("PROFILE").unwrap(), + ); + assert_eq!(long_version.lines().count(), 5); // `version.rs` must be updated as well. + for (i, line) in long_version.lines().enumerate() { + println!("cargo:rustc-env=LONG_VERSION{i}={line}"); + } +} + +#[cfg(not(feature = "version"))] +fn main() {} diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 20ff3d50..eb9047fa 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -5,13 +5,20 @@ )] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +use std::{num::NonZeroUsize, path::PathBuf}; use strum::EnumIs; #[macro_use] mod macros; +mod opts; +pub use opts::{Opts, UnstableOpts}; + mod utils; +#[cfg(feature = "version")] +pub mod version; + str_enum! { /// Compiler stage. #[derive(strum::EnumIs)] @@ -119,7 +126,6 @@ pub struct Dump { pub paths: Option>, } -#[cfg(feature = "clap")] impl std::str::FromStr for Dump { type Err = String; @@ -130,8 +136,7 @@ impl std::str::FromStr for Dump { } else { (s, None) }; - let kind = ::from_str(kind, false)?; - Ok(Self { kind, paths }) + Ok(Self { kind: kind.parse::().map_err(|e| e.to_string())?, paths }) } } @@ -147,6 +152,99 @@ str_enum! { } } +str_enum! { + /// How errors and other messages are produced. + #[derive(Default)] + #[strum(serialize_all = "kebab-case")] + pub enum ErrorFormat { + /// Human-readable output. + #[default] + Human, + /// Solc-like JSON output. + Json, + /// Rustc-like JSON output. + RustcJson, + } +} + +/// A single import map, AKA remapping: `map=path`. +#[derive(Clone, Debug)] +pub struct ImportMap { + pub map: PathBuf, + pub path: PathBuf, +} + +impl std::str::FromStr for ImportMap { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + if let Some((a, b)) = s.split_once('=') { + Ok(Self { map: a.into(), path: b.into() }) + } else { + Err("missing '='") + } + } +} + +/// Wrapper to implement a custom `Default` value for the number of threads. +#[derive(Clone, Copy)] +pub struct Threads(pub NonZeroUsize); + +impl From for NonZeroUsize { + fn from(threads: Threads) -> Self { + threads.0 + } +} + +impl From for Threads { + fn from(n: NonZeroUsize) -> Self { + Self(n) + } +} + +impl From for Threads { + fn from(n: usize) -> Self { + Self::resolve(n) + } +} + +impl Default for Threads { + fn default() -> Self { + Self(NonZeroUsize::new(8).unwrap()) + } +} + +impl std::str::FromStr for Threads { + type Err = ::Err; + + fn from_str(s: &str) -> Result { + s.parse::().map(Self::resolve) + } +} + +impl std::fmt::Display for Threads { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl std::fmt::Debug for Threads { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl Threads { + /// Resolves the number of threads to use. + pub fn resolve(n: usize) -> Self { + Self( + NonZeroUsize::new(n) + .or_else(|| std::thread::available_parallelism().ok()) + .unwrap_or(NonZeroUsize::MIN), + ) + } +} + #[cfg(test)] mod tests { use super::*; @@ -161,6 +259,7 @@ mod tests { let s = value.to_str(); assert_eq!(value.to_string(), s); assert_eq!(value, s.parse().unwrap()); + #[cfg(feature = "serde")] { let json_s = format!("\"{value}\""); diff --git a/crates/config/src/macros.rs b/crates/config/src/macros.rs index b3d4a48f..950df6aa 100644 --- a/crates/config/src/macros.rs +++ b/crates/config/src/macros.rs @@ -11,26 +11,24 @@ macro_rules! str_enum { } impl std::fmt::Display for $name { - #[inline] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(self.to_str()) } } #[cfg(feature = "clap")] - impl clap_builder::ValueEnum for $name { + impl clap::ValueEnum for $name { fn value_variants<'a>() -> &'a [Self] { &[$(Self::$var),*] } - fn to_possible_value(&self) -> Option { - Some(clap_builder::builder::PossibleValue::new(self.to_str())) + fn to_possible_value(&self) -> Option { + Some(clap::builder::PossibleValue::new(self.to_str())) } } #[cfg(feature = "serde")] impl serde::Serialize for $name { - #[inline] fn serialize(&self, serializer: S) -> Result { serializer.serialize_str(self.to_str()) } @@ -38,7 +36,6 @@ macro_rules! str_enum { #[cfg(feature = "serde")] impl<'de> serde::Deserialize<'de> for $name { - #[inline] fn deserialize>(deserializer: D) -> Result { deserializer.deserialize_any(crate::utils::StrumVisitor::::new()) } @@ -46,7 +43,6 @@ macro_rules! str_enum { impl $name { /// Returns the string representation of `self`. - #[inline] pub fn to_str(self) -> &'static str { self.into() } diff --git a/crates/cli/src/cli.rs b/crates/config/src/opts.rs similarity index 56% rename from crates/cli/src/cli.rs rename to crates/config/src/opts.rs index f5534871..8d579b86 100644 --- a/crates/cli/src/cli.rs +++ b/crates/config/src/opts.rs @@ -1,97 +1,128 @@ //! Solar CLI arguments. +use crate::{ + CompilerOutput, CompilerStage, Dump, ErrorFormat, EvmVersion, ImportMap, Language, Threads, +}; +use std::{num::NonZeroUsize, path::PathBuf}; + +#[cfg(feature = "clap")] use clap::{ColorChoice, Parser, ValueHint}; -use solar_config::{CompilerOutput, CompilerStage, Dump, EvmVersion, Language}; -use std::path::PathBuf; /// Blazingly fast Solidity compiler. -#[derive(Parser)] -#[command( +#[derive(Clone, Debug, Default)] +#[cfg_attr(feature = "clap", derive(Parser))] +#[cfg_attr(feature = "clap", command( name = "solar", version = crate::version::SHORT_VERSION, long_version = crate::version::LONG_VERSION, arg_required_else_help = true, -)] -#[non_exhaustive] -pub struct Args { +))] +#[allow(clippy::manual_non_exhaustive)] +pub struct Opts { /// Files to compile or import remappings. - #[arg(value_hint = ValueHint::FilePath)] + #[cfg_attr(feature = "clap", arg(value_hint = ValueHint::FilePath))] pub input: Vec, /// Directory to search for files. - #[arg(help_heading = "Input options", long, short = 'I', visible_alias = "base-path", value_hint = ValueHint::FilePath)] + #[cfg_attr(feature = "clap", arg(help_heading = "Input options", long, short = 'I', visible_alias = "base-path", value_hint = ValueHint::FilePath))] pub import_path: Vec, /// Map to search for files. Can also be provided as a positional argument. - #[arg(help_heading = "Input options", long, short = 'm', value_name = "MAP=PATH")] + #[cfg_attr( + feature = "clap", + arg(help_heading = "Input options", long, short = 'm', value_name = "MAP=PATH") + )] pub import_map: Vec, /// Source code language. Only Solidity is currently implemented. - #[arg(help_heading = "Input options", long, value_enum, default_value_t, hide = true)] + #[cfg_attr( + feature = "clap", + arg(help_heading = "Input options", long, value_enum, default_value_t, hide = true) + )] pub language: Language, /// Number of threads to use. Zero specifies the number of logical cores. - #[arg(long, short = 'j', visible_alias = "jobs", default_value = "8")] - pub threads: usize, + #[cfg_attr(feature = "clap", arg(long, short = 'j', visible_alias = "jobs", default_value_t))] + pub threads: Threads, /// EVM version. - #[arg(long, value_enum, default_value_t)] + #[cfg_attr(feature = "clap", arg(long, value_enum, default_value_t))] pub evm_version: EvmVersion, /// Stop execution after the given compiler stage. - #[arg(long, value_enum)] + #[cfg_attr(feature = "clap", arg(long, value_enum))] pub stop_after: Option, /// Directory to write output files. - #[arg(long, value_hint = ValueHint::DirPath)] + #[cfg_attr(feature = "clap", arg(long, value_hint = ValueHint::DirPath))] pub out_dir: Option, /// Comma separated list of types of output for the compiler to emit. - #[arg(long, value_delimiter = ',')] + #[cfg_attr(feature = "clap", arg(long, value_delimiter = ','))] pub emit: Vec, /// Coloring. - #[arg(help_heading = "Display options", long, value_enum, default_value = "auto")] + #[cfg(feature = "clap")] // TODO + #[cfg_attr( + feature = "clap", + arg(help_heading = "Display options", long, value_enum, default_value = "auto") + )] pub color: ColorChoice, /// Use verbose output. - #[arg(help_heading = "Display options", long, short)] + #[cfg_attr(feature = "clap", arg(help_heading = "Display options", long, short))] pub verbose: bool, /// Pretty-print JSON output. /// /// Does not include errors. See `--pretty-json-err`. - #[arg(help_heading = "Display options", long)] + #[cfg_attr(feature = "clap", arg(help_heading = "Display options", long))] pub pretty_json: bool, /// Pretty-print error JSON output. - #[arg(help_heading = "Display options", long)] + #[cfg_attr(feature = "clap", arg(help_heading = "Display options", long))] pub pretty_json_err: bool, /// How errors and other messages are produced. - #[arg(help_heading = "Display options", long, value_enum, default_value_t)] + #[cfg_attr( + feature = "clap", + arg(help_heading = "Display options", long, value_enum, default_value_t) + )] pub error_format: ErrorFormat, /// Unstable flags. WARNING: these are completely unstable, and may change at any time. /// /// See `-Zhelp` for more details. #[doc(hidden)] - #[arg(id = "unstable-features", value_name = "FLAG", short = 'Z')] + #[cfg_attr(feature = "clap", arg(id = "unstable-features", value_name = "FLAG", short = 'Z'))] pub _unstable: Vec, /// Parsed unstable flags. - #[arg(skip)] - pub unstable: UnstableFeatures, + #[cfg_attr(feature = "clap", arg(skip))] + pub unstable: UnstableOpts, + + // Allows `Opts { x: y, ..Default::default() }`. + #[doc(hidden)] + #[cfg_attr(feature = "clap", arg(skip))] + pub _non_exhaustive: (), } -impl Args { +impl Opts { + /// Returns the number of threads to use. + #[inline] + pub fn threads(&self) -> NonZeroUsize { + self.threads.0 + } + /// Finishes argument parsing. /// /// This currently only parses the `-Z` arguments into the `unstable` field, but may be extended /// in the future. + #[cfg(feature = "clap")] pub fn finish(&mut self) -> Result<(), clap::Error> { if !self._unstable.is_empty() { let hack = self._unstable.iter().map(|s| format!("--{s}")); self.unstable = - UnstableFeatures::try_parse_from(std::iter::once(String::new()).chain(hack))?; + UnstableOpts::try_parse_from(std::iter::once(String::new()).chain(hack))?; } Ok(()) } } /// Internal options. -#[derive(Clone, Debug, Default, Parser)] -#[clap( +#[derive(Clone, Debug, Default)] +#[cfg_attr(feature = "clap", derive(Parser))] +#[cfg_attr(feature = "clap", clap( disable_help_flag = true, before_help = concat!( "List of all unstable flags.\n", @@ -100,92 +131,66 @@ impl Args { " NOTE: the following flags should be passed on the command-line using `-Z`, not `--`", ), help_template = "{before-help}{all-args}" -)] -#[non_exhaustive] +))] #[allow(clippy::manual_non_exhaustive)] -pub struct UnstableFeatures { +pub struct UnstableOpts { /// Enables UI testing mode. - #[arg(long)] + #[cfg_attr(feature = "clap", arg(long))] pub ui_testing: bool, /// Prints a note for every diagnostic that is emitted with the creation and emission location. /// /// This is enabled by default on debug builds. - #[arg(long)] + #[cfg_attr(feature = "clap", arg(long))] pub track_diagnostics: bool, /// Enables parsing Yul files for testing. - #[arg(long)] + #[cfg_attr(feature = "clap", arg(long))] pub parse_yul: bool, /// Print additional information about the compiler's internal state. /// /// Valid kinds are `ast` and `hir`. - #[arg(long, value_name = "KIND[=PATHS...]")] + #[cfg_attr(feature = "clap", arg(long, value_name = "KIND[=PATHS...]"))] pub dump: Option, /// Print AST stats. - #[arg(long)] + #[cfg_attr(feature = "clap", arg(long))] pub ast_stats: bool, /// Print help. - #[arg(long, action = clap::ArgAction::Help)] - help: (), + #[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Help))] + pub help: (), + + // Allows `UnstableOpts { x: y, ..Default::default() }`. + #[doc(hidden)] + #[cfg_attr(feature = "clap", arg(skip))] + pub _non_exhaustive: (), #[cfg(test)] - #[arg(long)] - test_bool: bool, + #[cfg_attr(feature = "clap", arg(long))] + pub test_bool: bool, #[cfg(test)] - #[arg(long)] - test_value: Option, -} - -/// How errors and other messages are produced. -#[derive(Clone, Debug, Default, clap::ValueEnum)] -#[value(rename_all = "kebab-case")] -pub enum ErrorFormat { - /// Human-readable output. - #[default] - Human, - /// Solc-like JSON output. - Json, - /// Rustc-like JSON output. - RustcJson, -} - -/// A single import map, AKA remapping: `map=path`. -#[derive(Clone, Debug)] -pub struct ImportMap { - pub map: PathBuf, - pub path: PathBuf, -} - -impl std::str::FromStr for ImportMap { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - if let Some((a, b)) = s.split_once('=') { - Ok(Self { map: a.into(), path: b.into() }) - } else { - Err("missing '='") - } - } + #[cfg_attr(feature = "clap", arg(long))] + pub test_value: Option, } -#[cfg(test)] +#[cfg(all(test, feature = "clap"))] mod tests { use super::*; use clap::CommandFactory; #[test] fn verify_cli() { - Args::command().debug_assert(); - UnstableFeatures::command().debug_assert(); + Opts::command().debug_assert(); + let _ = Opts::default(); + UnstableOpts::command().debug_assert(); + let _ = UnstableOpts::default(); } #[test] fn unstable_features() { - fn parse(args: &[&str]) -> Result { + fn parse(args: &[&str]) -> Result { struct UnwrapDisplay(T); impl std::fmt::Debug for UnwrapDisplay { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -193,9 +198,9 @@ mod tests { } } (|| { - let mut args = Args::try_parse_from(args)?; - args.finish()?; - Ok::<_, clap::Error>(args.unstable) + let mut opts = Opts::try_parse_from(args)?; + opts.finish()?; + Ok::<_, clap::Error>(opts.unstable) })() .map_err(|e| UnwrapDisplay(e.render().ansi().to_string())) } diff --git a/crates/config/src/utils.rs b/crates/config/src/utils.rs index 32b4211e..6f6a89a3 100644 --- a/crates/config/src/utils.rs +++ b/crates/config/src/utils.rs @@ -1,3 +1,4 @@ +/// [`strum`] -> [`serde`] adapter. #[cfg(feature = "serde")] pub(crate) struct StrumVisitor(std::marker::PhantomData); diff --git a/crates/config/src/version.rs b/crates/config/src/version.rs new file mode 100644 index 00000000..eb8052e2 --- /dev/null +++ b/crates/config/src/version.rs @@ -0,0 +1,17 @@ +//! Solar version information. + +/// The short version information. +pub const SHORT_VERSION: &str = env!("SHORT_VERSION"); + +/// The long version information. +pub const LONG_VERSION: &str = concat!( + env!("LONG_VERSION0"), + "\n", + env!("LONG_VERSION1"), + "\n", + env!("LONG_VERSION2"), + "\n", + env!("LONG_VERSION3"), + "\n", + env!("LONG_VERSION4"), +); diff --git a/crates/interface/src/session.rs b/crates/interface/src/session.rs index faa2576c..2e6bf3e0 100644 --- a/crates/interface/src/session.rs +++ b/crates/interface/src/session.rs @@ -2,8 +2,8 @@ use crate::{ diagnostics::{DiagCtxt, EmittedDiagnostics}, ColorChoice, SessionGlobals, SourceMap, }; -use solar_config::{CompilerOutput, CompilerStage, Dump, EvmVersion, Language}; -use std::{collections::BTreeSet, num::NonZeroUsize, path::PathBuf, sync::Arc}; +use solar_config::{CompilerOutput, CompilerStage, Opts, UnstableOpts}; +use std::sync::Arc; /// Information about the current compiler session. #[derive(derive_builder::Builder)] @@ -15,36 +15,9 @@ pub struct Session { #[builder(default)] source_map: Arc, - /// EVM version. + /// The compiler options. #[builder(default)] - pub evm_version: EvmVersion, - /// Source code language. - #[builder(default)] - pub language: Language, - /// Stop execution after the given compiler stage. - #[builder(default)] - pub stop_after: Option, - /// Types of output to emit. - #[builder(default)] - pub emit: BTreeSet, - /// Output directory. - #[builder(default)] - pub out_dir: Option, - /// Internal state to dump to stdout. - #[builder(default)] - pub dump: Option, - /// Pretty-print any JSON output. - #[builder(default)] - pub pretty_json: bool, - /// Number of threads to use. Already resolved to a non-zero value. - /// - /// Note that this defaults to 1. If you wish to use parallelism, you must manually set this to - /// a value greater than 1. - #[builder(default = "NonZeroUsize::MIN")] - pub jobs: NonZeroUsize, - /// Whether to emit AST stats. - #[builder(default)] - pub ast_stats: bool, + pub opts: Opts, } impl SessionBuilder { @@ -81,9 +54,28 @@ impl SessionBuilder { self.dcx(DiagCtxt::with_silent_emitter(fatal_note)) } + /// Sets the number of threads to use for parallelism to 1. + #[inline] + pub fn single_threaded(self) -> Self { + self.threads(1) + } + + /// Sets the number of threads to use for parallelism. Zero specifies the number of logical + /// cores. + #[inline] + pub fn threads(mut self, threads: usize) -> Self { + self.opts_mut().threads = threads.into(); + self + } + /// Gets the source map from the diagnostics context. fn get_source_map(&mut self) -> Arc { - self.source_map.get_or_insert_with(Default::default).clone() + self.source_map.get_or_insert_default().clone() + } + + /// Returns a mutable reference to the options. + fn opts_mut(&mut self) -> &mut Opts { + self.opts.get_or_insert_default() } /// Consumes the builder to create a new session. @@ -133,6 +125,44 @@ impl Session { SessionBuilder::default() } + /// Infers the language from the input files. + pub fn infer_language(&mut self) { + if !self.opts.input.is_empty() + && self.opts.input.iter().all(|arg| arg.extension() == Some("yul".as_ref())) + { + self.opts.language = solar_config::Language::Yul; + } + } + + /// Validates the session options. + pub fn validate(&self) -> crate::Result<()> { + let mut result = Ok(()); + result = result.and(self.check_unique("emit", &self.opts.emit)); + result + } + + fn check_unique( + &self, + name: &str, + list: &[T], + ) -> crate::Result<()> { + let mut result = Ok(()); + let mut seen = std::collections::HashSet::new(); + for item in list { + if !seen.insert(item) { + let msg = format!("cannot specify `--{name} {item}` twice"); + result = Err(self.dcx.err(msg).emit()); + } + } + result + } + + /// Returns the unstable options. + #[inline] + pub fn unstable(&self) -> &UnstableOpts { + &self.opts.unstable + } + /// Returns the emitted diagnostics. Can be empty. /// /// Returns `None` if the underlying emitter is not a human buffer emitter created with @@ -166,13 +196,19 @@ impl Session { /// Returns `true` if compilation should stop after the given stage. #[inline] pub fn stop_after(&self, stage: CompilerStage) -> bool { - self.stop_after >= Some(stage) + self.opts.stop_after >= Some(stage) + } + + /// Returns the number of threads to use for parallelism. + #[inline] + pub fn threads(&self) -> usize { + self.opts.threads().get() } /// Returns `true` if parallelism is not enabled. #[inline] pub fn is_sequential(&self) -> bool { - self.jobs == NonZeroUsize::MIN + self.threads() == 1 } /// Returns `true` if parallelism is enabled. @@ -184,7 +220,7 @@ impl Session { /// Returns `true` if the given output should be emitted. #[inline] pub fn do_emit(&self, output: CompilerOutput) -> bool { - self.emit.contains(&output) + self.opts.emit.contains(&output) } /// Spawns the given closure on the thread pool or executes it immediately if parallelism is not @@ -237,7 +273,7 @@ impl Session { pub fn enter(&self, f: impl FnOnce() -> R + Send) -> R { SessionGlobals::with_or_default(|session_globals| { SessionGlobals::with_source_map(self.clone_source_map(), || { - run_in_thread_pool_with_globals(self.jobs.get(), session_globals, f) + run_in_thread_pool_with_globals(self.threads(), session_globals, f) }) }) } diff --git a/crates/sema/src/emit.rs b/crates/sema/src/emit.rs index a3535b8a..8e9ff69f 100644 --- a/crates/sema/src/emit.rs +++ b/crates/sema/src/emit.rs @@ -31,7 +31,7 @@ pub(crate) fn emit(gcx: Gcx<'_>) { for id in gcx.hir.contract_ids() { let name = gcx.contract_fully_qualified_name(id).to_string(); let contract_output = output.contracts.entry(name).or_default(); - for &emit in &gcx.sess.emit { + for &emit in &gcx.sess.opts.emit { match emit { CompilerOutput::Abi => contract_output.abi = Some(gcx.contract_abi(id)), CompilerOutput::Hashes => { @@ -48,9 +48,9 @@ pub(crate) fn emit(gcx: Gcx<'_>) { } } let _ = (|| { - let out_path = gcx.sess.out_dir.as_deref().map(|dir| dir.join("combined.json")); + let out_path = gcx.sess.opts.out_dir.as_deref().map(|dir| dir.join("combined.json")); let mut writer = out_writer(out_path.as_deref())?; - to_json(&mut writer, &output, gcx.sess.pretty_json)?; + to_json(&mut writer, &output, gcx.sess.opts.pretty_json)?; writer.flush()?; Ok::<_, io::Error>(()) })() diff --git a/crates/sema/src/lib.rs b/crates/sema/src/lib.rs index c0417f80..b312ea40 100644 --- a/crates/sema/src/lib.rs +++ b/crates/sema/src/lib.rs @@ -54,19 +54,19 @@ pub fn parse_and_resolve(pcx: ParsingContext<'_>) -> Result<()> { }); let mut sources = pcx.parse(&ast_arenas); - if let Some(dump) = &sess.dump { + if let Some(dump) = &sess.opts.unstable.dump { if dump.kind.is_ast() { dump_ast(sess, &sources, dump.paths.as_deref())?; } } - if sess.ast_stats { + if sess.opts.unstable.ast_stats { for source in sources.asts() { stats::print_ast_stats(source, "AST STATS", "ast-stats"); } } - if sess.language.is_yul() || sess.stop_after(CompilerStage::Parsed) { + if sess.opts.language.is_yul() || sess.stop_after(CompilerStage::Parsed) { return Ok(()); } @@ -119,7 +119,7 @@ fn lower<'sess, 'hir>( #[instrument(level = "debug", skip_all)] fn analysis(gcx: Gcx<'_>) -> Result<()> { - if let Some(dump) = &gcx.sess.dump { + if let Some(dump) = &gcx.sess.opts.unstable.dump { if dump.kind.is_hir() { dump_hir(gcx, dump.paths.as_deref())?; } @@ -139,7 +139,7 @@ fn analysis(gcx: Gcx<'_>) -> Result<()> { typeck::check(gcx); gcx.sess.dcx.has_errors()?; - if !gcx.sess.emit.is_empty() { + if !gcx.sess.opts.emit.is_empty() { emit::emit(gcx); gcx.sess.dcx.has_errors()?; } diff --git a/crates/sema/src/parse.rs b/crates/sema/src/parse.rs index 2d1a6dc8..ccb596c7 100644 --- a/crates/sema/src/parse.rs +++ b/crates/sema/src/parse.rs @@ -180,7 +180,7 @@ impl<'sess> ParsingContext<'sess> { ) -> Option> { let lexer = Lexer::from_source_file(self.sess, file); let mut parser = Parser::from_lexer(arena, lexer); - let r = if self.sess.language.is_yul() { + let r = if self.sess.opts.language.is_yul() { let _file = parser.parse_yul_file_object().map_err(|e| e.emit()); None } else { diff --git a/crates/solar/Cargo.toml b/crates/solar/Cargo.toml index fff3ca4e..d1c7205d 100644 --- a/crates/solar/Cargo.toml +++ b/crates/solar/Cargo.toml @@ -29,9 +29,6 @@ name = "tests" path = "./tests.rs" harness = false -[build-dependencies] -vergen = { workspace = true, features = ["build", "git", "gitcl"] } - [dependencies] solar-ast.workspace = true solar-config.workspace = true