add formal subcommand

This commit is contained in:
Jacob Lifshay 2024-09-25 01:52:41 -07:00
parent bb860d54cc
commit 45dbb554d0
Signed by: programmerjake
SSH key fingerprint: SHA256:B1iRVvUJkvd7upMIiMqn6OyxvD2SgJkAH3ZnUOj6z+c
4 changed files with 297 additions and 24 deletions

1
Cargo.lock generated
View file

@ -252,6 +252,7 @@ dependencies = [
"os_pipe", "os_pipe",
"serde", "serde",
"serde_json", "serde_json",
"tempfile",
"trybuild", "trybuild",
"which", "which",
] ]

View file

@ -24,6 +24,7 @@ num-traits.workspace = true
os_pipe.workspace = true os_pipe.workspace = true
serde_json.workspace = true serde_json.workspace = true
serde.workspace = true serde.workspace = true
tempfile.workspace = true
which.workspace = true which.workspace = true
[dev-dependencies] [dev-dependencies]

View file

@ -13,6 +13,7 @@ use clap::{
}; };
use eyre::{eyre, Report}; use eyre::{eyre, Report};
use std::{error, ffi::OsString, fmt, io, path::PathBuf, process}; use std::{error, ffi::OsString, fmt, io, path::PathBuf, process};
use tempfile::TempDir;
pub type Result<T = (), E = CliError> = std::result::Result<T, E>; pub type Result<T = (), E = CliError> = std::result::Result<T, E>;
@ -47,21 +48,40 @@ pub trait RunPhase<Arg> {
#[non_exhaustive] #[non_exhaustive]
pub struct BaseArgs { pub struct BaseArgs {
/// the directory to put the generated main output file and associated files in /// the directory to put the generated main output file and associated files in
#[arg(short, long, value_hint = ValueHint::DirPath)] #[arg(short, long, value_hint = ValueHint::DirPath, required = true)]
pub output: PathBuf, pub output: Option<PathBuf>,
/// the stem of the generated main output file, e.g. to get foo.v, pass --file-stem=foo /// the stem of the generated main output file, e.g. to get foo.v, pass --file-stem=foo
#[arg(long)] #[arg(long)]
pub file_stem: Option<String>, pub file_stem: Option<String>,
#[arg(long, env = "FAYALITE_KEEP_TEMP_DIR")]
pub keep_temp_dir: bool,
#[arg(skip = false)] #[arg(skip = false)]
pub redirect_output_for_rust_test: bool, pub redirect_output_for_rust_test: bool,
} }
impl BaseArgs { impl BaseArgs {
pub fn to_firrtl_file_backend(&self) -> firrtl::FileBackend { fn make_firrtl_file_backend(&self) -> Result<(firrtl::FileBackend, Option<TempDir>)> {
firrtl::FileBackend { let (dir_path, temp_dir) = match &self.output {
dir_path: self.output.clone(), Some(output) => (output.clone(), None),
top_fir_file_stem: self.file_stem.clone(), None => {
} let temp_dir = TempDir::new()?;
if self.keep_temp_dir {
let temp_dir = temp_dir.into_path();
println!("created temporary directory: {}", temp_dir.display());
(temp_dir, None)
} else {
(temp_dir.path().to_path_buf(), Some(temp_dir))
}
}
};
Ok((
firrtl::FileBackend {
dir_path,
top_fir_file_stem: self.file_stem.clone(),
circuit_name: None,
},
temp_dir,
))
} }
/// handles possibly redirecting the command's output for Rust tests /// handles possibly redirecting the command's output for Rust tests
pub fn run_external_command( pub fn run_external_command(
@ -106,25 +126,37 @@ pub struct FirrtlArgs {
#[non_exhaustive] #[non_exhaustive]
pub struct FirrtlOutput { pub struct FirrtlOutput {
pub file_stem: String, pub file_stem: String,
pub top_module: String,
pub output_dir: PathBuf,
pub temp_dir: Option<TempDir>,
} }
impl FirrtlOutput { impl FirrtlOutput {
pub fn firrtl_file(&self, args: &FirrtlArgs) -> PathBuf { pub fn file_with_ext(&self, ext: &str) -> PathBuf {
let mut retval = args.base.output.join(&self.file_stem); let mut retval = self.output_dir.join(&self.file_stem);
retval.set_extension("fir"); retval.set_extension(ext);
retval retval
} }
pub fn firrtl_file(&self) -> PathBuf {
self.file_with_ext("fir")
}
} }
impl FirrtlArgs { impl FirrtlArgs {
fn run_impl(&self, top_module: Module<Bundle>) -> Result<FirrtlOutput> { fn run_impl(&self, top_module: Module<Bundle>) -> Result<FirrtlOutput> {
let (file_backend, temp_dir) = self.base.make_firrtl_file_backend()?;
let firrtl::FileBackend { let firrtl::FileBackend {
top_fir_file_stem, .. top_fir_file_stem,
} = firrtl::export(self.base.to_firrtl_file_backend(), &top_module, self.export_options)?; circuit_name,
dir_path,
} = firrtl::export(file_backend, &top_module, self.export_options)?;
Ok(FirrtlOutput { Ok(FirrtlOutput {
file_stem: top_fir_file_stem.expect( file_stem: top_fir_file_stem.expect(
"export is known to set the file stem from the circuit name if not provided", "export is known to set the file stem from the circuit name if not provided",
), ),
top_module: circuit_name.expect("export is known to set the circuit name"),
output_dir: dir_path,
temp_dir,
}) })
} }
} }
@ -155,7 +187,22 @@ pub enum VerilogDialect {
Yosys, Yosys,
} }
impl fmt::Display for VerilogDialect {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl VerilogDialect { impl VerilogDialect {
pub fn as_str(self) -> &'static str {
match self {
VerilogDialect::Questa => "questa",
VerilogDialect::Spyglass => "spyglass",
VerilogDialect::Verilator => "verilator",
VerilogDialect::Vivado => "vivado",
VerilogDialect::Yosys => "yosys",
}
}
pub fn firtool_extra_args(self) -> &'static [&'static str] { pub fn firtool_extra_args(self) -> &'static [&'static str] {
match self { match self {
VerilogDialect::Questa => &["--lowering-options=emitWireInPorts"], VerilogDialect::Questa => &["--lowering-options=emitWireInPorts"],
@ -191,6 +238,8 @@ pub struct VerilogArgs {
/// adapt the generated Verilog for a particular toolchain /// adapt the generated Verilog for a particular toolchain
#[arg(long)] #[arg(long)]
pub verilog_dialect: Option<VerilogDialect>, pub verilog_dialect: Option<VerilogDialect>,
#[arg(long, short = 'g')]
pub debug: bool,
} }
#[derive(Debug)] #[derive(Debug)]
@ -200,28 +249,37 @@ pub struct VerilogOutput {
} }
impl VerilogOutput { impl VerilogOutput {
pub fn verilog_file(&self, args: &VerilogArgs) -> PathBuf { pub fn verilog_file(&self) -> PathBuf {
let mut retval = args.firrtl.base.output.join(&self.firrtl.file_stem); self.firrtl.file_with_ext("v")
retval.set_extension("v");
retval
} }
} }
impl VerilogArgs { impl VerilogArgs {
fn run_impl(&self, firrtl_output: FirrtlOutput) -> Result<VerilogOutput> { fn run_impl(&self, firrtl_output: FirrtlOutput) -> Result<VerilogOutput> {
let Self {
firrtl,
firtool,
firtool_extra_args,
verilog_dialect,
debug,
} = self;
let output = VerilogOutput { let output = VerilogOutput {
firrtl: firrtl_output, firrtl: firrtl_output,
}; };
let mut cmd = process::Command::new(&self.firtool); let mut cmd = process::Command::new(firtool);
cmd.arg(output.firrtl.firrtl_file(&self.firrtl)); cmd.arg(output.firrtl.firrtl_file());
cmd.arg("-o"); cmd.arg("-o");
cmd.arg(output.verilog_file(self)); cmd.arg(output.verilog_file());
if let Some(dialect) = self.verilog_dialect { if *debug {
cmd.arg("-g");
cmd.arg("--preserve-values=named");
}
if let Some(dialect) = verilog_dialect {
cmd.args(dialect.firtool_extra_args()); cmd.args(dialect.firtool_extra_args());
} }
cmd.args(&self.firtool_extra_args); cmd.args(firtool_extra_args);
cmd.current_dir(&self.firrtl.base.output); cmd.current_dir(&output.firrtl.output_dir);
let status = self.firrtl.base.run_external_command(cmd)?; let status = firrtl.base.run_external_command(cmd)?;
if status.success() { if status.success() {
Ok(output) Ok(output)
} else { } else {
@ -244,12 +302,217 @@ where
} }
} }
#[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum FormalMode {
#[default]
BMC,
Prove,
Live,
Cover,
}
impl FormalMode {
pub fn as_str(self) -> &'static str {
match self {
FormalMode::BMC => "bmc",
FormalMode::Prove => "prove",
FormalMode::Live => "live",
FormalMode::Cover => "cover",
}
}
}
impl fmt::Display for FormalMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone)]
struct FormalAdjustArgs;
impl clap::FromArgMatches for FormalAdjustArgs {
fn from_arg_matches(_matches: &clap::ArgMatches) -> Result<Self, clap::Error> {
Ok(Self)
}
fn update_from_arg_matches(&mut self, _matches: &clap::ArgMatches) -> Result<(), clap::Error> {
Ok(())
}
}
impl clap::Args for FormalAdjustArgs {
fn augment_args(cmd: clap::Command) -> clap::Command {
cmd.mut_arg("output", |arg| arg.required(false))
.mut_arg("verilog_dialect", |arg| {
arg.default_value(VerilogDialect::Yosys.to_string())
.hide(true)
})
}
fn augment_args_for_update(cmd: clap::Command) -> clap::Command {
Self::augment_args(cmd)
}
}
#[derive(Parser, Clone)]
#[non_exhaustive]
pub struct FormalArgs {
#[command(flatten)]
pub verilog: VerilogArgs,
#[arg(
long,
default_value = "sby",
env = "SBY",
value_hint = ValueHint::CommandName,
value_parser = OsStringValueParser::new().try_map(which::which)
)]
pub sby: PathBuf,
#[arg(long)]
pub sby_extra_args: Vec<OsString>,
#[arg(long, default_value_t)]
pub mode: FormalMode,
#[arg(long, default_value_t = Self::DEFAULT_DEPTH)]
pub depth: u64,
#[arg(long)]
pub solver: Option<String>,
#[arg(long)]
pub smtbmc_extra_args: Vec<String>,
#[command(flatten)]
_formal_adjust_args: FormalAdjustArgs,
}
impl fmt::Debug for FormalArgs {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self {
verilog,
sby,
sby_extra_args,
mode,
depth,
solver,
smtbmc_extra_args,
_formal_adjust_args: _,
} = self;
f.debug_struct("FormalArgs")
.field("verilog", &verilog)
.field("sby", &sby)
.field("sby_extra_args", &sby_extra_args)
.field("mode", &mode)
.field("depth", &depth)
.field("solver", &solver)
.field("smtbmc_extra_args", &smtbmc_extra_args)
.finish_non_exhaustive()
}
}
impl FormalArgs {
pub const DEFAULT_DEPTH: u64 = 20;
}
#[derive(Debug)]
#[non_exhaustive]
pub struct FormalOutput {
pub verilog: VerilogOutput,
}
impl FormalOutput {
pub fn sby_file(&self) -> PathBuf {
self.verilog.firrtl.file_with_ext("sby")
}
}
impl FormalArgs {
fn sby_contents(&self, output: &FormalOutput) -> String {
let Self {
verilog: _,
sby: _,
sby_extra_args: _,
mode,
depth,
smtbmc_extra_args,
solver,
_formal_adjust_args: _,
} = self;
struct OptArg<T>(Option<T>);
impl<T: fmt::Display> fmt::Display for OptArg<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(v) = &self.0 {
f.write_str(" ")?;
v.fmt(f)
} else {
Ok(())
}
}
}
let space_solver = OptArg(solver.as_ref());
let smtbmc_options = smtbmc_extra_args.join(" ");
let verilog_file = output
.verilog
.verilog_file()
.into_os_string()
.into_string()
.ok()
.expect("verilog file path is not UTF-8");
let top_module = &output.verilog.firrtl.top_module;
format!(
"[options]\n\
mode {mode}\n\
depth {depth}\n\
wait on\n\
\n\
[engines]\n\
smtbmc{space_solver} -- -- {smtbmc_options}\n\
\n\
[script]\n\
read_verilog -sv -formal {verilog_file}\n\
prep -top {top_module}\n
"
)
}
fn run_impl(&self, verilog_output: VerilogOutput) -> Result<FormalOutput> {
let output = FormalOutput {
verilog: verilog_output,
};
let sby_file = output.sby_file();
std::fs::write(&sby_file, self.sby_contents(&output))?;
let mut cmd = process::Command::new(&self.sby);
cmd.arg("-f");
cmd.arg(sby_file);
cmd.args(&self.sby_extra_args);
cmd.current_dir(&output.verilog.firrtl.output_dir);
let status = self.verilog.firrtl.base.run_external_command(cmd)?;
if status.success() {
Ok(output)
} else {
Err(CliError(eyre!(
"running {} failed: {status}",
self.sby.display()
)))
}
}
}
impl<Arg> RunPhase<Arg> for FormalArgs
where
VerilogArgs: RunPhase<Arg, Output = VerilogOutput>,
{
type Output = FormalOutput;
fn run(&self, arg: Arg) -> Result<Self::Output> {
let verilog_output = self.verilog.run(arg)?;
self.run_impl(verilog_output)
}
}
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug)]
enum CliCommand { enum CliCommand {
/// Generate FIRRTL /// Generate FIRRTL
Firrtl(FirrtlArgs), Firrtl(FirrtlArgs),
/// Generate Verilog /// Generate Verilog
Verilog(VerilogArgs), Verilog(VerilogArgs),
/// Run a formal proof
Formal(FormalArgs),
} }
/// a simple CLI /// a simple CLI
@ -335,6 +598,9 @@ where
CliCommand::Verilog(c) => { CliCommand::Verilog(c) => {
c.run(arg)?; c.run(arg)?;
} }
CliCommand::Formal(c) => {
c.run(arg)?;
}
} }
Ok(()) Ok(())
} }

View file

@ -2311,6 +2311,7 @@ impl<T: ?Sized + FileBackendTrait> FileBackendTrait for &'_ mut T {
#[non_exhaustive] #[non_exhaustive]
pub struct FileBackend { pub struct FileBackend {
pub dir_path: PathBuf, pub dir_path: PathBuf,
pub circuit_name: Option<String>,
pub top_fir_file_stem: Option<String>, pub top_fir_file_stem: Option<String>,
} }
@ -2318,6 +2319,7 @@ impl FileBackend {
pub fn new(dir_path: impl AsRef<Path>) -> Self { pub fn new(dir_path: impl AsRef<Path>) -> Self {
Self { Self {
dir_path: dir_path.as_ref().to_owned(), dir_path: dir_path.as_ref().to_owned(),
circuit_name: None,
top_fir_file_stem: None, top_fir_file_stem: None,
} }
} }
@ -2353,7 +2355,10 @@ impl FileBackendTrait for FileBackend {
circuit_name: String, circuit_name: String,
contents: String, contents: String,
) -> Result<(), Self::Error> { ) -> Result<(), Self::Error> {
let top_fir_file_stem = self.top_fir_file_stem.get_or_insert(circuit_name); let top_fir_file_stem = self
.top_fir_file_stem
.get_or_insert_with(|| circuit_name.clone());
self.circuit_name = Some(circuit_name);
let mut path = self.dir_path.join(top_fir_file_stem); let mut path = self.dir_path.join(top_fir_file_stem);
if let Some(parent) = path.parent().filter(|v| !v.as_os_str().is_empty()) { if let Some(parent) = path.parent().filter(|v| !v.as_os_str().is_empty()) {
fs::create_dir_all(parent)?; fs::create_dir_all(parent)?;