use clap::ValueHint;
use solar_codegen::{
lower,
mir::{Module, parse_module},
pass::{
DEFAULT_CLEANUP_PIPELINE, DEFAULT_PIPELINE, PASS_REGISTRY, PassInfo, PipelineOptions,
lookup_pass, run_default_pipeline_with_options, run_pass as run_codegen_pass,
},
};
use solar_data_structures::fmt::{self, FmtIteratorExt};
use solar_interface::{Ident, Session, Symbol};
use solar_sema::Compiler;
use std::{ops::ControlFlow, path::Path, process::ExitCode};
fn after_help() -> String {
fn display_pass_help(pass: &PassInfo) -> impl fmt::Display + '_ {
fmt::from_fn(move |f| write!(f, " {:<20} {}", pass.name, pass.description))
}
fn display_pass_list<'a>(passes: &'a [PassInfo], separator: &'a str) -> impl fmt::Display + 'a {
fmt::from_fn(move |f| {
write!(f, "{}", passes.iter().map(|pass| pass.name).format(separator))
})
}
fmt::from_fn(|f| {
write!(
f,
"\
Passes:
{}
{:<20} No transform; just lower/parse and print
Default pipeline:
{}
Default cleanup fixpoint:
{}
Input formats:
*.sol Solidity contract — lowered through the normal compiler pipeline
*.mir Textual MIR — parsed directly via solar_codegen::mir::parse_module",
PASS_REGISTRY.iter().map(display_pass_help).format("\n"),
"none",
display_pass_list(DEFAULT_PIPELINE, " → "),
display_pass_list(DEFAULT_CLEANUP_PIPELINE, " → ")
)
})
.to_string()
}
#[derive(clap::Args)]
#[command(
after_help = after_help(),
arg_required_else_help = true
)]
pub(crate) struct MirOptArgs {
#[arg(
long = "passes",
visible_alias = "pass",
value_name = "NAMES",
value_delimiter = ',',
value_parser = parse_pass,
required_unless_present = "pipeline_default",
conflicts_with = "pipeline_default"
)]
passes: Option<Vec<Option<&'static PassInfo>>>,
#[arg(long)]
print_after_each: bool,
#[arg(long, conflicts_with = "passes")]
pipeline_default: bool,
#[arg(value_hint = ValueHint::FilePath)]
input: String,
}
impl MirOptArgs {
fn selected_passes(&self) -> Vec<Option<&'static PassInfo>> {
self.passes.clone().expect("clap requires passes unless pipeline-default is set")
}
fn pipeline_label(&self, passes: &[Option<&PassInfo>]) -> String {
if self.pipeline_default {
"pipeline-default".to_string()
} else {
selected_pass_list_label(passes, ",")
}
}
}
fn parse_pass(name: &str) -> Result<Option<&'static PassInfo>, String> {
match name {
"none" => Ok(None),
other => lookup_pass(other).map(Some).ok_or_else(|| format!("unknown pass: {other}")),
}
}
fn pass_label(pass: Option<&PassInfo>) -> &'static str {
match pass {
Some(pass) => pass.name,
None => "none",
}
}
fn selected_pass_list_label(passes: &[Option<&PassInfo>], separator: &str) -> String {
passes.iter().copied().map(pass_label).format(separator).to_string()
}
fn print_module(module: &Module, name: &str, after: &str) {
println!("// === {name} (after {after}) ===");
print!("{}", module.to_text());
}
fn run_pipeline(module: &mut Module, name: &str, args: &MirOptArgs) -> Result<(), String> {
if args.pipeline_default {
run_default_pipeline_with_options(
module,
PipelineOptions {
print_after_each: args.print_after_each,
..PipelineOptions::default()
},
);
if !args.print_after_each {
print_module(module, name, "pipeline-default");
}
return Ok(());
}
let passes = args.selected_passes();
if args.print_after_each {
for pass in &passes {
if let Some(pass) = *pass {
run_codegen_pass(module, pass);
}
print_module(module, name, pass_label(*pass));
}
} else {
for &pass in &passes {
if let Some(pass) = pass {
run_codegen_pass(module, pass);
}
}
let label = args.pipeline_label(&passes);
print_module(module, name, &label);
}
Ok(())
}
fn process_mir(args: &MirOptArgs) -> Result<(), String> {
let sess = Session::builder().with_stderr_emitter().build();
let source = sess
.source_map()
.load_file(Path::new(&args.input))
.map_err(|e| format!("failed to read {}: {e}", args.input))?;
let text = source.src.as_str();
let mut result: Result<(), String> = Ok(());
sess.enter(|| {
let mut module = match parse_module(text) {
Ok(m) => m,
Err(e) => {
result = Err(format!("{e}"));
return;
}
};
let name = Ident::with_dummy_span(Symbol::intern(&args.input)).to_string();
if let Err(e) = run_pipeline(&mut module, &name, args) {
result = Err(e);
}
});
result
}
fn process_sol(args: &MirOptArgs) -> Result<(), String> {
let sess = Session::builder().with_stderr_emitter().build();
let mut compiler = Compiler::new(sess);
let parse_result = compiler.enter_mut(|c| -> solar_interface::Result<_> {
let mut pcx = c.parse();
pcx.load_files([Path::new(&args.input)])?;
pcx.parse();
Ok(())
});
if parse_result.is_err() {
return Err("parse error".into());
}
let mut pipeline_err: Option<String> = None;
let result = compiler.enter_mut(|c| -> solar_interface::Result<_> {
let ControlFlow::Continue(()) = c.lower_asts()? else { return Ok(()) };
let ControlFlow::Continue(()) = c.analysis()? else { return Ok(()) };
let gcx = c.gcx();
for id in gcx.hir.contract_ids() {
let contract = gcx.hir.contract(id);
if contract.kind.is_interface() || contract.kind.is_abstract_contract() {
continue;
}
let mut module = lower::lower_contract(gcx, id);
let name = gcx.contract_fully_qualified_name(id).to_string();
if let Err(e) = run_pipeline(&mut module, &name, args) {
pipeline_err = Some(e);
break;
}
}
Ok(())
});
if let Some(e) = pipeline_err {
return Err(e);
}
if result.is_err() || compiler.sess().emitted_errors().is_some_and(|r| r.is_err()) {
return Err("compilation failed".into());
}
Ok(())
}
pub(super) fn run(args: MirOptArgs) -> ExitCode {
let ext = Path::new(&args.input).extension().and_then(|s| s.to_str()).unwrap_or("");
let result = match ext {
"sol" => process_sol(&args),
"mir" => process_mir(&args),
_ => Err(format!("unsupported input file extension `.{ext}` (expected .sol or .mir)")),
};
match result {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {e}");
ExitCode::FAILURE
}
}
}