From ec266a0b3bbcda83e3ffccdef50075a937d5c8df Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 4 Mar 2024 15:45:46 -0500 Subject: [PATCH] [fud2] Basic testing setup (#1954) * Skeleton for the simplest possible test * Reusable test scaffolding * Test config setup * Control executable name for tests * Split main/lib/test Now we have separate files for the main driver build, the actual `main` function which is very short, and the tests. * Add missing tests file * Include through in test description * A second test! * Change config option name We use `exe` everywhere, not `exec`, and I keep forgetting this fact. * Be friendlier to Insta By default, snapshot names are based on the call site... so let's try separating the assertion lines? * Factor out test function * Accept snapshots! * Add CI action to test fud2 * Appease clippy --- .github/workflows/fud2.yml | 16 + Cargo.lock | 47 ++ fud2/Cargo.toml | 7 + fud2/fud-core/src/config.rs | 20 +- fud2/fud-core/src/run.rs | 26 +- fud2/src/lib.rs | 461 +++++++++++++++++ fud2/src/main.rs | 464 +----------------- .../tests__emit@calyx_firrtl_verilog.snap | 29 ++ .../snapshots/tests__emit@calyx_verilog.snap | 19 + fud2/tests/tests.rs | 93 ++++ 10 files changed, 704 insertions(+), 478 deletions(-) create mode 100644 .github/workflows/fud2.yml create mode 100644 fud2/src/lib.rs create mode 100644 fud2/tests/snapshots/tests__emit@calyx_firrtl_verilog.snap create mode 100644 fud2/tests/snapshots/tests__emit@calyx_verilog.snap create mode 100644 fud2/tests/tests.rs diff --git a/.github/workflows/fud2.yml b/.github/workflows/fud2.yml new file mode 100644 index 0000000000..8101c772ec --- /dev/null +++ b/.github/workflows/fud2.yml @@ -0,0 +1,16 @@ +name: fud2 tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + - run: cargo test -p fud diff --git a/Cargo.lock b/Cargo.lock index 5e04b57fa9..f769fbf6f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -471,6 +471,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -778,6 +790,12 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "endian-type" version = "0.1.2" @@ -891,6 +909,7 @@ dependencies = [ "anyhow", "fud-core", "include_dir", + "insta", "manifest-dir-macros", ] @@ -1175,6 +1194,19 @@ dependencies = [ "serde", ] +[[package]] +name = "insta" +version = "1.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a508bf83e6f6f2aa438588ae7ceb558a81030c5762cbfe838180a861cf5dc110" +dependencies = [ + "console", + "lazy_static", + "linked-hash-map", + "similar", + "yaml-rust", +] + [[package]] name = "interp" version = "0.1.1" @@ -2129,6 +2161,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "similar" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21" + [[package]] name = "slab" version = "0.4.9" @@ -3004,6 +3042,15 @@ dependencies = [ "tap", ] +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "zerocopy" version = "0.7.32" diff --git a/fud2/Cargo.toml b/fud2/Cargo.toml index f77cd7a363..85d9af2a51 100644 --- a/fud2/Cargo.toml +++ b/fud2/Cargo.toml @@ -18,6 +18,13 @@ anyhow.workspace = true manifest-dir-macros = "0.1" include_dir = "0.7" +[lib] +name = "fud2" +path = "src/lib.rs" + [[bin]] name = "fud2" path = "src/main.rs" + +[dev-dependencies] +insta = "1.36.0" diff --git a/fud2/fud-core/src/config.rs b/fud2/fud-core/src/config.rs index 3e90d83ceb..ef3b45692f 100644 --- a/fud2/fud-core/src/config.rs +++ b/fud2/fud-core/src/config.rs @@ -15,6 +15,9 @@ pub struct GlobalConfig { /// Enable verbose output. pub verbose: bool, + + /// The path to the build tool executable. + pub exe: String, } impl Default for GlobalConfig { @@ -23,12 +26,17 @@ impl Default for GlobalConfig { ninja: "ninja".to_string(), keep_build_dir: false, verbose: false, + exe: std::env::current_exe() + .expect("executable path unknown") + .to_str() + .expect("invalid executable name") + .into(), } } } /// Location of the configuration file -pub(crate) fn config_path(name: &str) -> std::path::PathBuf { +pub fn config_path(name: &str) -> std::path::PathBuf { // The configuration is usually at `~/.config/driver_name.toml`. let config_base = env::var("XDG_CONFIG_HOME").unwrap_or_else(|_| { let home = env::var("HOME").expect("$HOME not set"); @@ -39,11 +47,15 @@ pub(crate) fn config_path(name: &str) -> std::path::PathBuf { config_path } +/// Get raw configuration data with some default options. +pub fn default_config() -> Figment { + Figment::from(Serialized::defaults(GlobalConfig::default())) +} + /// Load configuration data from the standard config file location. -pub(crate) fn load_config(name: &str) -> Figment { +pub fn load_config(name: &str) -> Figment { let config_path = config_path(name); // Use our defaults, overridden by the TOML config file. - Figment::from(Serialized::defaults(GlobalConfig::default())) - .merge(Toml::file(config_path)) + default_config().merge(Toml::file(config_path)) } diff --git a/fud2/fud-core/src/run.rs b/fud2/fud-core/src/run.rs index ba731e3183..6f8b2ce551 100644 --- a/fud2/fud-core/src/run.rs +++ b/fud2/fud-core/src/run.rs @@ -98,6 +98,14 @@ pub struct Run<'a> { impl<'a> Run<'a> { pub fn new(driver: &'a Driver, plan: Plan) -> Self { let config_data = config::load_config(&driver.name); + Self::with_config(driver, plan, config_data) + } + + pub fn with_config( + driver: &'a Driver, + plan: Plan, + config_data: figment::Figment, + ) -> Self { let global_config: config::GlobalConfig = config_data.extract().expect("failed to load config"); Self { @@ -226,7 +234,7 @@ impl<'a> Run<'a> { Ok(()) } - fn emit(&self, out: T) -> EmitResult { + pub fn emit(&self, out: T) -> EmitResult { let mut emitter = Emitter::new( out, self.config_data.clone(), @@ -234,13 +242,7 @@ impl<'a> Run<'a> { ); // Emit preamble. - emitter.var( - "build-tool", - std::env::current_exe() - .expect("executable path unknown") - .to_str() - .expect("invalid executable name"), - )?; + emitter.var("build-tool", &self.global_config.exe)?; emitter.rule("get-rsrc", "$build-tool get-rsrc $out")?; writeln!(emitter.out)?; @@ -278,14 +280,14 @@ impl<'a> Run<'a> { } } -pub struct Emitter { - pub out: Box, +pub struct Emitter<'a> { + pub out: Box, pub config_data: figment::Figment, pub workdir: Utf8PathBuf, } -impl Emitter { - fn new( +impl<'a> Emitter<'a> { + fn new( out: T, config_data: figment::Figment, workdir: Utf8PathBuf, diff --git a/fud2/src/lib.rs b/fud2/src/lib.rs new file mode 100644 index 0000000000..a4029d9369 --- /dev/null +++ b/fud2/src/lib.rs @@ -0,0 +1,461 @@ +use fud_core::{ + exec::{SetupRef, StateRef}, + run::{EmitResult, Emitter}, + DriverBuilder, +}; + +fn setup_calyx( + bld: &mut DriverBuilder, + verilog: StateRef, +) -> (StateRef, SetupRef) { + let calyx = bld.state("calyx", &["futil"]); + let calyx_setup = bld.setup("Calyx compiler", |e| { + e.config_var("calyx-base", "calyx.base")?; + e.config_var_or( + "calyx-exe", + "calyx.exe", + "$calyx-base/target/debug/calyx", + )?; + e.rule( + "calyx", + "$calyx-exe -l $calyx-base -b $backend $args $in > $out", + )?; + Ok(()) + }); + bld.op( + "calyx-to-verilog", + &[calyx_setup], + calyx, + verilog, + |e, input, output| { + e.build_cmd(&[output], "calyx", &[input], &[])?; + e.arg("backend", "verilog")?; + Ok(()) + }, + ); + (calyx, calyx_setup) +} + +fn setup_dahlia( + bld: &mut DriverBuilder, + calyx: StateRef, +) -> (StateRef, SetupRef) { + let dahlia = bld.state("dahlia", &["fuse"]); + let dahlia_setup = bld.setup("Dahlia compiler", |e| { + e.config_var("dahlia-exe", "dahlia")?; + e.rule( + "dahlia-to-calyx", + "$dahlia-exe -b calyx --lower -l error $in -o $out", + )?; + Ok(()) + }); + bld.rule(&[dahlia_setup], dahlia, calyx, "dahlia-to-calyx"); + (dahlia, dahlia_setup) +} + +fn setup_mrxl( + bld: &mut DriverBuilder, + calyx: StateRef, +) -> (StateRef, SetupRef) { + let mrxl = bld.state("mrxl", &["mrxl"]); + let mrxl_setup = bld.setup("MrXL compiler", |e| { + e.var("mrxl-exe", "mrxl")?; + e.rule("mrxl-to-calyx", "$mrxl-exe $in > $out")?; + Ok(()) + }); + bld.rule(&[mrxl_setup], mrxl, calyx, "mrxl-to-calyx"); + (mrxl, mrxl_setup) +} + +pub fn build_driver(bld: &mut DriverBuilder) { + // The verilog state + let verilog = bld.state("verilog", &["sv", "v"]); + // Calyx. + let (calyx, calyx_setup) = setup_calyx(bld, verilog); + // Dahlia. + setup_dahlia(bld, calyx); + // MrXL. + setup_mrxl(bld, calyx); + + // Shared machinery for RTL simulators. + let dat = bld.state("dat", &["json"]); + let vcd = bld.state("vcd", &["vcd"]); + let simulator = bld.state("sim", &["exe"]); + let sim_setup = bld.setup("RTL simulation", |e| { + // Data conversion to and from JSON. + e.config_var_or("python", "python", "python3")?; + e.rsrc("json-dat.py")?; + e.rule("hex-data", "$python json-dat.py --from-json $in $out")?; + e.rule("json-data", "$python json-dat.py --to-json $out $in")?; + + // The Verilog testbench. + e.rsrc("tb.sv")?; + + // The input data file. `sim.data` is required. + let data_name = e.config_val("sim.data")?; + let data_path = e.external_path(data_name.as_ref()); + e.var("sim_data", data_path.as_str())?; + + // Produce the data directory. + e.var("datadir", "sim_data")?; + e.build_cmd( + &["$datadir"], + "hex-data", + &["$sim_data"], + &["json-dat.py"], + )?; + + // Rule for simulation execution. + e.rule( + "sim-run", + "./$bin +DATA=$datadir +CYCLE_LIMIT=$cycle-limit $args > $out", + )?; + + // More shared configuration. + e.config_var_or("cycle-limit", "sim.cycle_limit", "500000000")?; + + Ok(()) + }); + bld.op( + "simulate", + &[sim_setup], + simulator, + dat, + |e, input, output| { + e.build_cmd(&["sim.log"], "sim-run", &[input, "$datadir"], &[])?; + e.arg("bin", input)?; + e.arg("args", "+NOTRACE=1")?; + e.build_cmd( + &[output], + "json-data", + &["$datadir", "sim.log"], + &["json-dat.py"], + )?; + Ok(()) + }, + ); + bld.op("trace", &[sim_setup], simulator, vcd, |e, input, output| { + e.build_cmd( + &["sim.log", output], + "sim-run", + &[input, "$datadir"], + &[], + )?; + e.arg("bin", input)?; + e.arg("args", &format!("+NOTRACE=0 +OUT={}", output))?; + Ok(()) + }); + + // Icarus Verilog. + let verilog_noverify = bld.state("verilog-noverify", &["sv"]); + let icarus_setup = bld.setup("Icarus Verilog", |e| { + e.var("iverilog", "iverilog")?; + e.rule("icarus-compile", "$iverilog -g2012 -o $out tb.sv $in")?; + Ok(()) + }); + bld.op( + "calyx-noverify", + &[calyx_setup], + calyx, + verilog_noverify, + |e, input, output| { + // Icarus requires a special --disable-verify version of Calyx code. + e.build_cmd(&[output], "calyx", &[input], &[])?; + e.arg("backend", "verilog")?; + e.arg("args", "--disable-verify")?; + Ok(()) + }, + ); + bld.op( + "icarus", + &[sim_setup, icarus_setup], + verilog_noverify, + simulator, + |e, input, output| { + e.build_cmd(&[output], "icarus-compile", &[input], &["tb.sv"])?; + Ok(()) + }, + ); + + // Calyx to FIRRTL. + let firrtl = bld.state("firrtl", &["fir"]); + bld.op( + "calyx-to-firrtl", + &[calyx_setup], + calyx, + firrtl, + |e, input, output| { + e.build_cmd(&[output], "calyx", &[input], &[])?; + e.arg("backend", "firrtl")?; + Ok(()) + }, + ); + + // The FIRRTL compiler. + let firrtl_setup = bld.setup("Firrtl to Verilog compiler", |e| { + e.config_var("firrtl-exe", "firrtl.exe")?; + e.rule("firrtl", "$firrtl-exe -i $in -o $out -X sverilog")?; + + e.rsrc("primitives-for-firrtl.sv")?; + e.rule( + "add-firrtl-prims", + "cat primitives-for-firrtl.sv $in > $out", + )?; + + Ok(()) + }); + fn firrtl_compile( + e: &mut Emitter, + input: &str, + output: &str, + ) -> EmitResult { + let tmp_verilog = "partial.sv"; + e.build_cmd(&[tmp_verilog], "firrtl", &[input], &[])?; + e.build_cmd( + &[output], + "add-firrtl-prims", + &[tmp_verilog], + &["primitives-for-firrtl.sv"], + )?; + Ok(()) + } + bld.op("firrtl", &[firrtl_setup], firrtl, verilog, firrtl_compile); + // This is a bit of a hack, but the Icarus-friendly "noverify" state is identical for this path + // (since FIRRTL compilation doesn't come with verification). + bld.op( + "firrtl-noverify", + &[firrtl_setup], + firrtl, + verilog_noverify, + firrtl_compile, + ); + + // primitive-uses backend + let primitive_uses_json = bld.state("primitive-uses-json", &["json"]); + bld.op( + "primitive-uses", + &[calyx_setup], + calyx, + primitive_uses_json, + |e, input, output| { + e.build_cmd(&[output], "calyx", &[input], &[])?; + e.arg("backend", "primitive-uses")?; + Ok(()) + }, + ); + + // Verilator. + let verilator_setup = bld.setup("Verilator", |e| { + e.config_var_or("verilator", "verilator.exe", "verilator")?; + e.config_var_or("cycle-limit", "sim.cycle_limit", "500000000")?; + e.rule( + "verilator-compile", + "$verilator $in tb.sv --trace --binary --top-module TOP -fno-inline -Mdir $out-dir", + )?; + e.rule("cp", "cp $in $out")?; + Ok(()) + }); + bld.op( + "verilator", + &[sim_setup, verilator_setup], + verilog, + simulator, + |e, input, output| { + let out_dir = "verilator-out"; + let sim_bin = format!("{}/VTOP", out_dir); + e.build_cmd( + &[&sim_bin], + "verilator-compile", + &[input], + &["tb.sv"], + )?; + e.arg("out-dir", out_dir)?; + e.build("cp", &sim_bin, output)?; + Ok(()) + }, + ); + + // Interpreter. + let debug = bld.state("debug", &[]); // A pseudo-state. + let cider_setup = bld.setup("Cider interpreter", |e| { + e.config_var_or( + "cider-exe", + "cider.exe", + "$calyx-base/target/debug/cider", + )?; + e.rule( + "cider", + "$cider-exe -l $calyx-base --raw --data data.json $in > $out", + )?; + e.rule( + "cider-debug", + "$cider-exe -l $calyx-base --data data.json $in debug || true", + )?; + e.arg("pool", "console")?; + + // TODO Can we reduce the duplication around and `$python`? + e.rsrc("interp-dat.py")?; + e.config_var_or("python", "python", "python3")?; + e.rule("dat-to-interp", "$python interp-dat.py --to-interp $in")?; + e.rule( + "interp-to-dat", + "$python interp-dat.py --from-interp $in $sim_data > $out", + )?; + e.build_cmd( + &["data.json"], + "dat-to-interp", + &["$sim_data"], + &["interp-dat.py"], + )?; + Ok(()) + }); + bld.op( + "interp", + &[sim_setup, calyx_setup, cider_setup], + calyx, + dat, + |e, input, output| { + let out_file = "interp_out.json"; + e.build_cmd(&[out_file], "cider", &[input], &["data.json"])?; + e.build_cmd( + &[output], + "interp-to-dat", + &[out_file], + &["$sim_data", "interp-dat.py"], + )?; + Ok(()) + }, + ); + bld.op( + "debug", + &[sim_setup, calyx_setup, cider_setup], + calyx, + debug, + |e, input, output| { + e.build_cmd(&[output], "cider-debug", &[input], &["data.json"])?; + Ok(()) + }, + ); + + // Xilinx compilation. + let xo = bld.state("xo", &["xo"]); + let xclbin = bld.state("xclbin", &["xclbin"]); + let xilinx_setup = bld.setup("Xilinx tools", |e| { + // Locations for Vivado and Vitis installations. + e.config_var("vivado-dir", "xilinx.vivado")?; + e.config_var("vitis-dir", "xilinx.vitis")?; + + // Package a Verilog program as an `.xo` file. + e.rsrc("gen_xo.tcl")?; + e.rsrc("get-ports.py")?; + e.config_var_or("python", "python", "python3")?; + e.rule("gen-xo", "$vivado-dir/bin/vivado -mode batch -source gen_xo.tcl -tclargs $out `$python get-ports.py kernel.xml`")?; + e.arg("pool", "console")?; // Lets Ninja stream the tool output "live." + + // Compile an `.xo` file to an `.xclbin` file, which is where the actual EDA work occurs. + e.config_var_or("xilinx-mode", "xilinx.mode", "hw_emu")?; + e.config_var_or("platform", "xilinx.device", "xilinx_u50_gen3x16_xdma_201920_3")?; + e.rule("compile-xclbin", "$vitis-dir/bin/v++ -g -t $xilinx-mode --platform $platform --save-temps --profile.data all:all:all --profile.exec all:all:all -lo $out $in")?; + e.arg("pool", "console")?; + + Ok(()) + }); + bld.op( + "xo", + &[calyx_setup, xilinx_setup], + calyx, + xo, + |e, input, output| { + // Emit the Verilog itself in "synthesis mode." + e.build_cmd(&["main.sv"], "calyx", &[input], &[])?; + e.arg("backend", "verilog")?; + e.arg("args", "--synthesis -p external")?; + + // Extra ingredients for the `.xo` package. + e.build_cmd(&["toplevel.v"], "calyx", &[input], &[])?; + e.arg("backend", "xilinx")?; + e.build_cmd(&["kernel.xml"], "calyx", &[input], &[])?; + e.arg("backend", "xilinx-xml")?; + + // Package the `.xo`. + e.build_cmd( + &[output], + "gen-xo", + &[], + &[ + "main.sv", + "toplevel.v", + "kernel.xml", + "gen_xo.tcl", + "get-ports.py", + ], + )?; + Ok(()) + }, + ); + bld.op("xclbin", &[xilinx_setup], xo, xclbin, |e, input, output| { + e.build_cmd(&[output], "compile-xclbin", &[input], &[])?; + Ok(()) + }); + + // Xilinx execution. + // TODO Only does `hw_emu` for now... + let xrt_setup = bld.setup("Xilinx execution via XRT", |e| { + // Generate `emconfig.json`. + e.rule("emconfig", "$vitis-dir/bin/emconfigutil --platform $platform")?; + e.build_cmd(&["emconfig.json"], "emconfig", &[], &[])?; + + // Execute via the `xclrun` tool. + e.config_var("xrt-dir", "xilinx.xrt")?; + e.rule("xclrun", "bash -c 'source $vitis-dir/settings64.sh ; source $xrt-dir/setup.sh ; XRT_INI_PATH=$xrt_ini EMCONFIG_PATH=. XCL_EMULATION_MODE=$xilinx-mode $python -m fud.xclrun --out $out $in'")?; + e.arg("pool", "console")?; + + // "Pre-sim" and "post-sim" scripts for simulation. + e.rule("echo", "echo $contents > $out")?; + e.build_cmd(&["pre_sim.tcl"], "echo", &[""], &[""])?; + e.arg("contents", "open_vcd\\\\nlog_vcd *\\\\n")?; + e.build_cmd(&["post_sim.tcl"], "echo", &[""], &[""])?; + e.arg("contents", "close_vcd\\\\n")?; + + Ok(()) + }); + bld.op( + "xrt", + &[xilinx_setup, sim_setup, xrt_setup], + xclbin, + dat, + |e, input, output| { + e.rsrc("xrt.ini")?; + e.build_cmd( + &[output], + "xclrun", + &[input, "$sim_data"], + &["emconfig.json", "xrt.ini"], + )?; + e.arg("xrt_ini", "xrt.ini")?; + Ok(()) + }, + ); + bld.op( + "xrt-trace", + &[xilinx_setup, sim_setup, xrt_setup], + xclbin, + vcd, + |e, input, output| { + e.rsrc("xrt_trace.ini")?; + e.build_cmd( + &[output], // TODO not the VCD, yet... + "xclrun", + &[input, "$sim_data"], + &[ + "emconfig.json", + "pre_sim.tcl", + "post_sim.tcl", + "xrt_trace.ini", + ], + )?; + e.arg("xrt_ini", "xrt_trace.ini")?; + Ok(()) + }, + ); +} diff --git a/fud2/src/main.rs b/fud2/src/main.rs index 200952953e..d84f687d29 100644 --- a/fud2/src/main.rs +++ b/fud2/src/main.rs @@ -1,465 +1,5 @@ -use fud_core::{ - cli, - exec::{SetupRef, StateRef}, - run::{EmitResult, Emitter}, - DriverBuilder, -}; - -fn setup_calyx( - bld: &mut DriverBuilder, - verilog: StateRef, -) -> (StateRef, SetupRef) { - let calyx = bld.state("calyx", &["futil"]); - let calyx_setup = bld.setup("Calyx compiler", |e| { - e.config_var("calyx-base", "calyx.base")?; - e.config_var_or( - "calyx-exe", - "calyx.exe", - "$calyx-base/target/debug/calyx", - )?; - e.rule( - "calyx", - "$calyx-exe -l $calyx-base -b $backend $args $in > $out", - )?; - Ok(()) - }); - bld.op( - "calyx-to-verilog", - &[calyx_setup], - calyx, - verilog, - |e, input, output| { - e.build_cmd(&[output], "calyx", &[input], &[])?; - e.arg("backend", "verilog")?; - Ok(()) - }, - ); - (calyx, calyx_setup) -} - -fn setup_dahlia( - bld: &mut DriverBuilder, - calyx: StateRef, -) -> (StateRef, SetupRef) { - let dahlia = bld.state("dahlia", &["fuse"]); - let dahlia_setup = bld.setup("Dahlia compiler", |e| { - e.config_var("dahlia-exe", "dahlia")?; - e.rule( - "dahlia-to-calyx", - "$dahlia-exe -b calyx --lower -l error $in -o $out", - )?; - Ok(()) - }); - bld.rule(&[dahlia_setup], dahlia, calyx, "dahlia-to-calyx"); - (dahlia, dahlia_setup) -} - -fn setup_mrxl( - bld: &mut DriverBuilder, - calyx: StateRef, -) -> (StateRef, SetupRef) { - let mrxl = bld.state("mrxl", &["mrxl"]); - let mrxl_setup = bld.setup("MrXL compiler", |e| { - e.var("mrxl-exe", "mrxl")?; - e.rule("mrxl-to-calyx", "$mrxl-exe $in > $out")?; - Ok(()) - }); - bld.rule(&[mrxl_setup], mrxl, calyx, "mrxl-to-calyx"); - (mrxl, mrxl_setup) -} - -fn build_driver(bld: &mut DriverBuilder) { - // The verilog state - let verilog = bld.state("verilog", &["sv", "v"]); - // Calyx. - let (calyx, calyx_setup) = setup_calyx(bld, verilog); - // Dahlia. - setup_dahlia(bld, calyx); - // MrXL. - setup_mrxl(bld, calyx); - - // Shared machinery for RTL simulators. - let dat = bld.state("dat", &["json"]); - let vcd = bld.state("vcd", &["vcd"]); - let simulator = bld.state("sim", &["exe"]); - let sim_setup = bld.setup("RTL simulation", |e| { - // Data conversion to and from JSON. - e.config_var_or("python", "python", "python3")?; - e.rsrc("json-dat.py")?; - e.rule("hex-data", "$python json-dat.py --from-json $in $out")?; - e.rule("json-data", "$python json-dat.py --to-json $out $in")?; - - // The Verilog testbench. - e.rsrc("tb.sv")?; - - // The input data file. `sim.data` is required. - let data_name = e.config_val("sim.data")?; - let data_path = e.external_path(data_name.as_ref()); - e.var("sim_data", data_path.as_str())?; - - // Produce the data directory. - e.var("datadir", "sim_data")?; - e.build_cmd( - &["$datadir"], - "hex-data", - &["$sim_data"], - &["json-dat.py"], - )?; - - // Rule for simulation execution. - e.rule( - "sim-run", - "./$bin +DATA=$datadir +CYCLE_LIMIT=$cycle-limit $args > $out", - )?; - - // More shared configuration. - e.config_var_or("cycle-limit", "sim.cycle_limit", "500000000")?; - - Ok(()) - }); - bld.op( - "simulate", - &[sim_setup], - simulator, - dat, - |e, input, output| { - e.build_cmd(&["sim.log"], "sim-run", &[input, "$datadir"], &[])?; - e.arg("bin", input)?; - e.arg("args", "+NOTRACE=1")?; - e.build_cmd( - &[output], - "json-data", - &["$datadir", "sim.log"], - &["json-dat.py"], - )?; - Ok(()) - }, - ); - bld.op("trace", &[sim_setup], simulator, vcd, |e, input, output| { - e.build_cmd( - &["sim.log", output], - "sim-run", - &[input, "$datadir"], - &[], - )?; - e.arg("bin", input)?; - e.arg("args", &format!("+NOTRACE=0 +OUT={}", output))?; - Ok(()) - }); - - // Icarus Verilog. - let verilog_noverify = bld.state("verilog-noverify", &["sv"]); - let icarus_setup = bld.setup("Icarus Verilog", |e| { - e.var("iverilog", "iverilog")?; - e.rule("icarus-compile", "$iverilog -g2012 -o $out tb.sv $in")?; - Ok(()) - }); - bld.op( - "calyx-noverify", - &[calyx_setup], - calyx, - verilog_noverify, - |e, input, output| { - // Icarus requires a special --disable-verify version of Calyx code. - e.build_cmd(&[output], "calyx", &[input], &[])?; - e.arg("backend", "verilog")?; - e.arg("args", "--disable-verify")?; - Ok(()) - }, - ); - bld.op( - "icarus", - &[sim_setup, icarus_setup], - verilog_noverify, - simulator, - |e, input, output| { - e.build_cmd(&[output], "icarus-compile", &[input], &["tb.sv"])?; - Ok(()) - }, - ); - - // Calyx to FIRRTL. - let firrtl = bld.state("firrtl", &["fir"]); - bld.op( - "calyx-to-firrtl", - &[calyx_setup], - calyx, - firrtl, - |e, input, output| { - e.build_cmd(&[output], "calyx", &[input], &[])?; - e.arg("backend", "firrtl")?; - Ok(()) - }, - ); - - // The FIRRTL compiler. - let firrtl_setup = bld.setup("Firrtl to Verilog compiler", |e| { - e.config_var("firrtl-exe", "firrtl.exe")?; - e.rule("firrtl", "$firrtl-exe -i $in -o $out -X sverilog")?; - - e.rsrc("primitives-for-firrtl.sv")?; - e.rule( - "add-firrtl-prims", - "cat primitives-for-firrtl.sv $in > $out", - )?; - - Ok(()) - }); - fn firrtl_compile( - e: &mut Emitter, - input: &str, - output: &str, - ) -> EmitResult { - let tmp_verilog = "partial.sv"; - e.build_cmd(&[tmp_verilog], "firrtl", &[input], &[])?; - e.build_cmd( - &[output], - "add-firrtl-prims", - &[tmp_verilog], - &["primitives-for-firrtl.sv"], - )?; - Ok(()) - } - bld.op("firrtl", &[firrtl_setup], firrtl, verilog, firrtl_compile); - // This is a bit of a hack, but the Icarus-friendly "noverify" state is identical for this path - // (since FIRRTL compilation doesn't come with verification). - bld.op( - "firrtl-noverify", - &[firrtl_setup], - firrtl, - verilog_noverify, - firrtl_compile, - ); - - // primitive-uses backend - let primitive_uses_json = bld.state("primitive-uses-json", &["json"]); - bld.op( - "primitive-uses", - &[calyx_setup], - calyx, - primitive_uses_json, - |e, input, output| { - e.build_cmd(&[output], "calyx", &[input], &[])?; - e.arg("backend", "primitive-uses")?; - Ok(()) - }, - ); - - // Verilator. - let verilator_setup = bld.setup("Verilator", |e| { - e.config_var_or("verilator", "verilator.exe", "verilator")?; - e.config_var_or("cycle-limit", "sim.cycle_limit", "500000000")?; - e.rule( - "verilator-compile", - "$verilator $in tb.sv --trace --binary --top-module TOP -fno-inline -Mdir $out-dir", - )?; - e.rule("cp", "cp $in $out")?; - Ok(()) - }); - bld.op( - "verilator", - &[sim_setup, verilator_setup], - verilog, - simulator, - |e, input, output| { - let out_dir = "verilator-out"; - let sim_bin = format!("{}/VTOP", out_dir); - e.build_cmd( - &[&sim_bin], - "verilator-compile", - &[input], - &["tb.sv"], - )?; - e.arg("out-dir", out_dir)?; - e.build("cp", &sim_bin, output)?; - Ok(()) - }, - ); - - // Interpreter. - let debug = bld.state("debug", &[]); // A pseudo-state. - let cider_setup = bld.setup("Cider interpreter", |e| { - e.config_var_or( - "cider-exe", - "cider.exe", - "$calyx-base/target/debug/cider", - )?; - e.rule( - "cider", - "$cider-exe -l $calyx-base --raw --data data.json $in > $out", - )?; - e.rule( - "cider-debug", - "$cider-exe -l $calyx-base --data data.json $in debug || true", - )?; - e.arg("pool", "console")?; - - // TODO Can we reduce the duplication around and `$python`? - e.rsrc("interp-dat.py")?; - e.config_var_or("python", "python", "python3")?; - e.rule("dat-to-interp", "$python interp-dat.py --to-interp $in")?; - e.rule( - "interp-to-dat", - "$python interp-dat.py --from-interp $in $sim_data > $out", - )?; - e.build_cmd( - &["data.json"], - "dat-to-interp", - &["$sim_data"], - &["interp-dat.py"], - )?; - Ok(()) - }); - bld.op( - "interp", - &[sim_setup, calyx_setup, cider_setup], - calyx, - dat, - |e, input, output| { - let out_file = "interp_out.json"; - e.build_cmd(&[out_file], "cider", &[input], &["data.json"])?; - e.build_cmd( - &[output], - "interp-to-dat", - &[out_file], - &["$sim_data", "interp-dat.py"], - )?; - Ok(()) - }, - ); - bld.op( - "debug", - &[sim_setup, calyx_setup, cider_setup], - calyx, - debug, - |e, input, output| { - e.build_cmd(&[output], "cider-debug", &[input], &["data.json"])?; - Ok(()) - }, - ); - - // Xilinx compilation. - let xo = bld.state("xo", &["xo"]); - let xclbin = bld.state("xclbin", &["xclbin"]); - let xilinx_setup = bld.setup("Xilinx tools", |e| { - // Locations for Vivado and Vitis installations. - e.config_var("vivado-dir", "xilinx.vivado")?; - e.config_var("vitis-dir", "xilinx.vitis")?; - - // Package a Verilog program as an `.xo` file. - e.rsrc("gen_xo.tcl")?; - e.rsrc("get-ports.py")?; - e.config_var_or("python", "python", "python3")?; - e.rule("gen-xo", "$vivado-dir/bin/vivado -mode batch -source gen_xo.tcl -tclargs $out `$python get-ports.py kernel.xml`")?; - e.arg("pool", "console")?; // Lets Ninja stream the tool output "live." - - // Compile an `.xo` file to an `.xclbin` file, which is where the actual EDA work occurs. - e.config_var_or("xilinx-mode", "xilinx.mode", "hw_emu")?; - e.config_var_or("platform", "xilinx.device", "xilinx_u50_gen3x16_xdma_201920_3")?; - e.rule("compile-xclbin", "$vitis-dir/bin/v++ -g -t $xilinx-mode --platform $platform --save-temps --profile.data all:all:all --profile.exec all:all:all -lo $out $in")?; - e.arg("pool", "console")?; - - Ok(()) - }); - bld.op( - "xo", - &[calyx_setup, xilinx_setup], - calyx, - xo, - |e, input, output| { - // Emit the Verilog itself in "synthesis mode." - e.build_cmd(&["main.sv"], "calyx", &[input], &[])?; - e.arg("backend", "verilog")?; - e.arg("args", "--synthesis -p external")?; - - // Extra ingredients for the `.xo` package. - e.build_cmd(&["toplevel.v"], "calyx", &[input], &[])?; - e.arg("backend", "xilinx")?; - e.build_cmd(&["kernel.xml"], "calyx", &[input], &[])?; - e.arg("backend", "xilinx-xml")?; - - // Package the `.xo`. - e.build_cmd( - &[output], - "gen-xo", - &[], - &[ - "main.sv", - "toplevel.v", - "kernel.xml", - "gen_xo.tcl", - "get-ports.py", - ], - )?; - Ok(()) - }, - ); - bld.op("xclbin", &[xilinx_setup], xo, xclbin, |e, input, output| { - e.build_cmd(&[output], "compile-xclbin", &[input], &[])?; - Ok(()) - }); - - // Xilinx execution. - // TODO Only does `hw_emu` for now... - let xrt_setup = bld.setup("Xilinx execution via XRT", |e| { - // Generate `emconfig.json`. - e.rule("emconfig", "$vitis-dir/bin/emconfigutil --platform $platform")?; - e.build_cmd(&["emconfig.json"], "emconfig", &[], &[])?; - - // Execute via the `xclrun` tool. - e.config_var("xrt-dir", "xilinx.xrt")?; - e.rule("xclrun", "bash -c 'source $vitis-dir/settings64.sh ; source $xrt-dir/setup.sh ; XRT_INI_PATH=$xrt_ini EMCONFIG_PATH=. XCL_EMULATION_MODE=$xilinx-mode $python -m fud.xclrun --out $out $in'")?; - e.arg("pool", "console")?; - - // "Pre-sim" and "post-sim" scripts for simulation. - e.rule("echo", "echo $contents > $out")?; - e.build_cmd(&["pre_sim.tcl"], "echo", &[""], &[""])?; - e.arg("contents", "open_vcd\\\\nlog_vcd *\\\\n")?; - e.build_cmd(&["post_sim.tcl"], "echo", &[""], &[""])?; - e.arg("contents", "close_vcd\\\\n")?; - - Ok(()) - }); - bld.op( - "xrt", - &[xilinx_setup, sim_setup, xrt_setup], - xclbin, - dat, - |e, input, output| { - e.rsrc("xrt.ini")?; - e.build_cmd( - &[output], - "xclrun", - &[input, "$sim_data"], - &["emconfig.json", "xrt.ini"], - )?; - e.arg("xrt_ini", "xrt.ini")?; - Ok(()) - }, - ); - bld.op( - "xrt-trace", - &[xilinx_setup, sim_setup, xrt_setup], - xclbin, - vcd, - |e, input, output| { - e.rsrc("xrt_trace.ini")?; - e.build_cmd( - &[output], // TODO not the VCD, yet... - "xclrun", - &[input, "$sim_data"], - &[ - "emconfig.json", - "pre_sim.tcl", - "post_sim.tcl", - "xrt_trace.ini", - ], - )?; - e.arg("xrt_ini", "xrt_trace.ini")?; - Ok(()) - }, - ); -} +use fud2::build_driver; +use fud_core::{cli, DriverBuilder}; fn main() -> anyhow::Result<()> { let mut bld = DriverBuilder::new("fud2"); diff --git a/fud2/tests/snapshots/tests__emit@calyx_firrtl_verilog.snap b/fud2/tests/snapshots/tests__emit@calyx_firrtl_verilog.snap new file mode 100644 index 0000000000..e956c8ff43 --- /dev/null +++ b/fud2/tests/snapshots/tests__emit@calyx_firrtl_verilog.snap @@ -0,0 +1,29 @@ +--- +source: fud2/tests/tests.rs +description: emit calyx -> verilog through firrtl +--- +build-tool = fud2 +rule get-rsrc + command = $build-tool get-rsrc $out + +# Calyx compiler +calyx-base = /test/calyx +calyx-exe = $calyx-base/target/debug/calyx +rule calyx + command = $calyx-exe -l $calyx-base -b $backend $args $in > $out + +# Firrtl to Verilog compiler +firrtl-exe = /test/bin/firrtl +rule firrtl + command = $firrtl-exe -i $in -o $out -X sverilog +build primitives-for-firrtl.sv: get-rsrc +rule add-firrtl-prims + command = cat primitives-for-firrtl.sv $in > $out + +# build targets +build stdin.fir: calyx stdin + backend = firrtl +build partial.sv: firrtl stdin.fir +build stdin.sv: add-firrtl-prims partial.sv | primitives-for-firrtl.sv + +default stdin.sv diff --git a/fud2/tests/snapshots/tests__emit@calyx_verilog.snap b/fud2/tests/snapshots/tests__emit@calyx_verilog.snap new file mode 100644 index 0000000000..e91debcdd9 --- /dev/null +++ b/fud2/tests/snapshots/tests__emit@calyx_verilog.snap @@ -0,0 +1,19 @@ +--- +source: fud2/tests/tests.rs +description: emit calyx -> verilog +--- +build-tool = fud2 +rule get-rsrc + command = $build-tool get-rsrc $out + +# Calyx compiler +calyx-base = /test/calyx +calyx-exe = $calyx-base/target/debug/calyx +rule calyx + command = $calyx-exe -l $calyx-base -b $backend $args $in > $out + +# build targets +build stdin.sv: calyx stdin + backend = verilog + +default stdin.sv diff --git a/fud2/tests/tests.rs b/fud2/tests/tests.rs new file mode 100644 index 0000000000..6baff4d526 --- /dev/null +++ b/fud2/tests/tests.rs @@ -0,0 +1,93 @@ +use fud2::build_driver; +use fud_core::{ + config::default_config, exec::Request, run::Run, Driver, DriverBuilder, +}; + +fn test_driver() -> Driver { + let mut bld = DriverBuilder::new("fud2"); + build_driver(&mut bld); + bld.build() +} + +fn request( + driver: &Driver, + start: &str, + end: &str, + through: &[&str], +) -> Request { + fud_core::exec::Request { + start_file: None, + start_state: driver.get_state(start).unwrap(), + end_file: None, + end_state: driver.get_state(end).unwrap(), + through: through.iter().map(|s| driver.get_op(s).unwrap()).collect(), + workdir: ".".into(), + } +} + +fn emit_ninja(driver: &Driver, req: Request) -> String { + let plan = driver.plan(req).unwrap(); + let config = default_config() + .merge(("exe", "fud2")) + .merge(("calyx.base", "/test/calyx")) + .merge(("firrtl.exe", "/test/bin/firrtl")); + let run = Run::with_config(driver, plan, config); + let mut buf = vec![]; + run.emit(&mut buf).unwrap(); + String::from_utf8(buf).unwrap() +} + +/// Get a human-readable description of a request. +fn req_desc(driver: &Driver, req: &Request) -> String { + let mut desc = format!( + "emit {} -> {}", + driver.states[req.start_state].name, driver.states[req.end_state].name + ); + if !req.through.is_empty() { + desc.push_str(" through"); + for op in &req.through { + desc.push(' '); + desc.push_str(&driver.ops[*op].name); + } + } + desc +} + +/// Get a short string uniquely identifying a request. +fn req_slug(driver: &Driver, req: &Request) -> String { + let mut desc = driver.states[req.start_state].name.to_string(); + for op in &req.through { + desc.push('_'); + desc.push_str(&driver.ops[*op].name); + } + desc.push('_'); + desc.push_str(&driver.states[req.end_state].name); + desc +} + +fn test_emit(driver: Driver, req: Request) { + let desc = req_desc(&driver, &req); + let slug = req_slug(&driver, &req); + let ninja = emit_ninja(&driver, req); + insta::with_settings!({ + description => desc, + omit_expression => true, + snapshot_suffix => slug, + }, { + insta::assert_snapshot!(ninja); + }); +} + +#[test] +fn calyx_to_verilog() { + let driver = test_driver(); + let req = request(&driver, "calyx", "verilog", &[]); + test_emit(driver, req); +} + +#[test] +fn calyx_via_firrtl() { + let driver = test_driver(); + let req = request(&driver, "calyx", "verilog", &["firrtl"]); + test_emit(driver, req); +}