use base64::display::Base64Display;
use base64::engine::general_purpose::STANDARD;
use clap::{Arg, ArgAction, Command};
use simplicityhl::ast::ElementsJetHinter;
use simplicityhl::error::should_color;
use simplicityhl::version::SimcDirective;
use simplicityhl::{
resolution::DependencyMapBuilder, source::CanonPath, source::CanonSourceFile, AbiMeta,
TemplateProgram,
};
use simplicityhl::{UnstableFeature, UnstableFeatures};
use std::path::Path;
use std::{env, fmt, io};
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
struct Output {
program: String,
witness: Option<String>,
abi_meta: Option<AbiMeta>,
cmr: String,
compiler_version: &'static str,
}
impl fmt::Display for Output {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Program:\n{}", self.program)?;
writeln!(f, "CMR:\n{}", self.cmr)?;
writeln!(f, "Compiler version:\n{}", self.compiler_version)?;
if let Some(witness) = &self.witness {
writeln!(f, "Witness:\n{}", witness)?;
}
if let Some(witness) = &self.abi_meta {
writeln!(f, "ABI meta:\n{:?}", witness)?;
}
Ok(())
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let command = {
Command::new(env!("CARGO_BIN_NAME"))
.about(
"\
Compile the given SimplicityHL program and print the resulting Simplicity base64 string.\n\
If a SimplicityHL witness is provided, then use it to satisfy the program (requires \
feature 'serde' to be enabled).\
",
)
.arg(
Arg::new("prog_file")
.required(true)
.value_name("PROGRAM_FILE")
.action(ArgAction::Set)
.help("SimplicityHL program file to build"),
)
.arg(
Arg::new("dependencies")
.long("dep")
.value_name("[CONTEXT:]ALIAS=PATH")
.action(ArgAction::Append)
.help("Link a dependency, optionally scoped to a specific module (e.g., --dep ./libs/merkle:math=./libs/math)"),
)
.arg(
Arg::new("wit_file")
.long("wit")
.short('w')
.value_name("WITNESS_FILE")
.action(ArgAction::Set)
.help("File containing the witness data"),
)
.arg(
Arg::new("args_file")
.long("args")
.short('a')
.value_name("ARGUMENTS_FILE")
.action(ArgAction::Set)
.help("File containing the arguments data"),
)
.arg(
Arg::new("debug")
.long("debug")
.action(ArgAction::SetTrue)
.help("Include debug symbols in the output"),
)
.arg(
Arg::new("json")
.long("json")
.action(ArgAction::SetTrue)
.help("Output in JSON"),
)
.arg(
Arg::new("abi")
.long("abi")
.action(ArgAction::SetTrue)
.help("Additional ABI .simf contract types"),
)
.arg(
Arg::new("unstable_features")
.long("unstable-feature")
.short('Z')
.value_name("FEATURE")
.action(ArgAction::Append)
.value_parser(clap::value_parser!(UnstableFeature))
.help(simplicityhl::UnstableFeature::help_message()),
)
};
let matches = command.get_matches();
let prog_file = matches.get_one::<String>("prog_file").unwrap();
let main_path = CanonPath::canonicalize(Path::new(prog_file))?;
let main_text = std::fs::read_to_string(main_path.as_path()).map_err(|e| e.to_string())?;
if let Some(warning) = SimcDirective::missing_warning(&main_text) {
eprintln!("Warning: {warning}");
}
let include_debug_symbols = matches.get_flag("debug");
let output_json = matches.get_flag("json");
let abi_param = matches.get_flag("abi");
let unstable_features = matches
.get_many::<UnstableFeature>("unstable_features")
.map_or_else(UnstableFeatures::none, |features| {
UnstableFeatures::new(features.copied())
});
#[cfg(feature = "serde")]
let unresolved_args: Option<simplicityhl::UnresolvedValues> =
match matches.get_one::<String>("args_file") {
None => None,
Some(args_file) => {
let args_path = Path::new(&args_file);
let args_text = std::fs::read_to_string(args_path).map_err(|e| e.to_string())?;
Some(serde_json::from_str(&args_text)?)
}
};
#[cfg(not(feature = "serde"))]
if matches.contains_id("args_file") {
return Err(
"Program was compiled without the 'serde' feature and cannot process .args files."
.into(),
);
}
let dep_args = matches
.get_many::<String>("dependencies")
.unwrap_or_default();
let canon_root = main_path
.as_path()
.parent()
.and_then(|p| CanonPath::canonicalize(p).ok())
.ok_or("Failed to determine project root directory from entry file")?;
let mut builder = DependencyMapBuilder::new();
for arg in dep_args {
let (left_side, path_str) = arg.split_once('=').unwrap_or_else(|| {
eprintln!(
"Error: Library argument must be in format [CONTEXT:]ALIAS=PATH, got '{arg}'"
);
std::process::exit(1);
});
let (context_path, alias) = if let Some((ctx_str, alias_str)) = left_side.rsplit_once(':') {
let canon_path = CanonPath::canonicalize(Path::new(ctx_str))?;
(canon_path, alias_str)
} else {
(canon_root.clone(), left_side)
};
let target_path = CanonPath::canonicalize(Path::new(path_str))?;
builder.add_dependency(context_path, alias.to_string(), target_path);
}
let dependencies = match builder.build(canon_root.clone()) {
Ok(map) => map,
Err(e) => {
eprintln!("Error: {e}");
std::process::exit(1);
}
};
let source = CanonSourceFile::new(main_path.clone(), std::sync::Arc::from(main_text));
let template = match TemplateProgram::new_with_dep(
source,
&dependencies,
&unstable_features,
Box::new(ElementsJetHinter::new()),
) {
Ok(program) => program,
Err(diags) => {
let stderr = io::stderr();
let with_color = should_color(&stderr);
let mut lock = stderr.lock();
diags
.render(with_color, &mut lock)
.expect("writing to stderr");
std::process::exit(1);
}
};
#[cfg(feature = "serde")]
let args_opt: simplicityhl::Arguments = match unresolved_args {
None => simplicityhl::Arguments::default(),
Some(unresolved) => unresolved.resolve(template.parameters())?,
};
#[cfg(not(feature = "serde"))]
let args_opt = simplicityhl::Arguments::default();
let compiled = match template.instantiate(args_opt, include_debug_symbols) {
Ok(program) => program,
Err(e) => {
eprintln!("{e}");
std::process::exit(1);
}
};
#[cfg(feature = "serde")]
let witness_opt = matches
.get_one::<String>("wit_file")
.map(|wit_file| -> Result<simplicityhl::WitnessValues, String> {
let wit_path = Path::new(wit_file);
let wit_text = std::fs::read_to_string(wit_path).map_err(|e| e.to_string())?;
let unresolved = serde_json::from_str::<simplicityhl::UnresolvedValues>(&wit_text)
.map_err(|e| e.to_string())?;
unresolved.resolve(compiled.witness_types())
})
.transpose()?;
#[cfg(not(feature = "serde"))]
let witness_opt = if matches.contains_id("wit_file") {
return Err(
"Program was compiled without the 'serde' feature and cannot process .wit files."
.into(),
);
} else {
None
};
let (program_bytes, witness_bytes) = match witness_opt {
Some(witness) => {
let satisfied = match compiled.satisfy(witness) {
Ok(s) => s,
Err(e) => {
eprintln!("{e}");
std::process::exit(1);
}
};
let (program_bytes, witness_bytes) = satisfied.redeem().to_vec_with_witness();
(program_bytes, Some(witness_bytes))
}
None => {
let program_bytes = compiled.commit().to_vec_without_witness();
(program_bytes, None)
}
};
let abi_opt = if abi_param {
Some(compiled.generate_abi_meta()?)
} else {
None
};
let cmr_hex = compiled.commit().cmr().to_string();
let output = Output {
program: Base64Display::new(&program_bytes, &STANDARD).to_string(),
witness: witness_bytes.map(|bytes| Base64Display::new(&bytes, &STANDARD).to_string()),
abi_meta: abi_opt,
cmr: cmr_hex,
compiler_version: compiled.compiler_version(),
};
if output_json {
#[cfg(not(feature = "serde"))]
{
return Err(
"Program was compiled without the 'serde' feature and cannot output JSON.".into(),
);
}
#[cfg(feature = "serde")]
{
println!("{}", serde_json::to_string(&output)?);
return Ok(());
}
}
println!("{}", output);
Ok(())
}