diff --git a/.flake8 b/.flake8 index 1d36346c0d..146ae6054d 100644 --- a/.flake8 +++ b/.flake8 @@ -1,2 +1,3 @@ [flake8] -max-line-length = 88 \ No newline at end of file +max-line-length = 88 +exclude = tools/tb/examples/cocotb/doc_examples_quickstart/test_my_design.py diff --git a/.gitignore b/.gitignore index d148072ea7..1990cbd0ff 100644 --- a/.gitignore +++ b/.gitignore @@ -61,5 +61,8 @@ tools/profiler/logs temp/ +tools/tb/**/*.so +tools/tb/**/*.dylib + # for running a venv -.venv \ No newline at end of file +.venv diff --git a/Cargo.lock b/Cargo.lock index ce5c097edc..3136f5bb4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -139,7 +139,7 @@ dependencies = [ "argh_shared", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.69", ] [[package]] @@ -175,7 +175,7 @@ checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.69", ] [[package]] @@ -206,7 +206,7 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.69", ] [[package]] @@ -261,7 +261,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.52", + "syn 2.0.69", "which", ] @@ -410,6 +410,38 @@ dependencies = [ "vast", ] +[[package]] +name = "calyx-ffi" +version = "0.7.1" +dependencies = [ + "calyx-ffi-macro", + "calyx-frontend", + "calyx-ir", + "interp", +] + +[[package]] +name = "calyx-ffi-example" +version = "0.7.1" +dependencies = [ + "calyx-ffi", + "calyx-ir", + "interp", + "rand 0.8.5", +] + +[[package]] +name = "calyx-ffi-macro" +version = "0.7.1" +dependencies = [ + "calyx-frontend", + "calyx-ir", + "calyx-utils", + "proc-macro2", + "quote", + "syn 2.0.69", +] + [[package]] name = "calyx-frontend" version = "0.7.1" @@ -536,6 +568,16 @@ version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +[[package]] +name = "cargo_toml" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4895c018bb228aa6b3ba1a0285543fcb4b704734c3fb1f72afaa75aa769500c1" +dependencies = [ + "serde", + "toml", +] + [[package]] name = "cast" version = "0.3.0" @@ -690,7 +732,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.69", ] [[package]] @@ -975,7 +1017,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.52", + "syn 2.0.69", ] [[package]] @@ -997,7 +1039,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core 0.20.8", "quote", - "syn 2.0.52", + "syn 2.0.69", ] [[package]] @@ -1232,6 +1274,12 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -1322,7 +1370,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.69", ] [[package]] @@ -1574,15 +1622,14 @@ dependencies = [ [[package]] name = "insta" -version = "1.36.0" +version = "1.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a508bf83e6f6f2aa438588ae7ceb558a81030c5762cbfe838180a861cf5dc110" +checksum = "810ae6042d48e2c9e9215043563a58a80b877bc863228a74cf10c49d4620a6f5" dependencies = [ "console", "lazy_static", "linked-hash-map", "similar", - "yaml-rust", ] [[package]] @@ -1694,9 +1741,9 @@ checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libloading" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" dependencies = [ "cfg-if", "windows-targets 0.52.4", @@ -1760,6 +1807,17 @@ dependencies = [ "url", ] +[[package]] +name = "makemake" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe265024d547dbfdae86fdb24b3ea7b96aff8cc71947d08b61d0db2ce2829e05" +dependencies = [ + "cargo_toml", + "insta", + "paste", +] + [[package]] name = "manifest-dir-macros" version = "0.1.18" @@ -1769,7 +1827,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.69", ] [[package]] @@ -2004,6 +2062,12 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pathdiff" version = "0.2.1" @@ -2078,7 +2142,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.69", ] [[package]] @@ -2119,7 +2183,7 @@ checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.69", ] [[package]] @@ -2193,14 +2257,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" dependencies = [ "proc-macro2", - "syn 2.0.52", + "syn 2.0.69", ] [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -2243,9 +2307,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -2454,7 +2518,7 @@ checksum = "59aecf17969c04b9c0c5d21f6bc9da9fec9dd4980e64d1871443a476589d8c86" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.69", ] [[package]] @@ -2544,11 +2608,17 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + [[package]] name = "serde" -version = "1.0.197" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" dependencies = [ "serde_derive", ] @@ -2565,13 +2635,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.69", ] [[package]] @@ -2593,7 +2663,7 @@ checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.69", ] [[package]] @@ -2608,9 +2678,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" dependencies = [ "serde", ] @@ -2664,7 +2734,7 @@ dependencies = [ "darling 0.20.8", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.69", ] [[package]] @@ -2844,7 +2914,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.52", + "syn 2.0.69", ] [[package]] @@ -2887,9 +2957,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.52" +version = "2.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +checksum = "201fcda3845c23e8212cd466bfebf0bd20694490fc0356ae8e428e0824a915a6" dependencies = [ "proc-macro2", "quote", @@ -2908,6 +2978,23 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tb" +version = "0.0.0" +dependencies = [ + "argh", + "env_logger", + "figment", + "fs_extra", + "libloading", + "log", + "makemake", + "semver", + "serde", + "tempdir", + "toml", +] + [[package]] name = "tempdir" version = "0.3.7" @@ -2982,7 +3069,7 @@ checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.69", ] [[package]] @@ -3085,7 +3172,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.69", ] [[package]] @@ -3104,9 +3191,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.10" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290" +checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" dependencies = [ "serde", "serde_spanned", @@ -3116,18 +3203,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.6" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6" +checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" dependencies = [ "indexmap 2.2.4", "serde", @@ -3187,7 +3274,7 @@ checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.69", ] [[package]] @@ -3215,7 +3302,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.69", ] [[package]] @@ -3668,15 +3755,6 @@ 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 = "yansi" version = "1.0.1" @@ -3713,5 +3791,5 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.69", ] diff --git a/Cargo.toml b/Cargo.toml index 94ca7bf447..27b717b95d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,10 @@ members = [ "tools/calyx-pass-explorer", "tools/yxi", "tools/calyx-writer", + "tools/tb", + "tools/calyx-ffi-macro", + "tools/calyx-ffi", + "tools/tb/examples/calyx", ] exclude = ["site"] @@ -58,6 +62,8 @@ calyx-ir = { path = "calyx-ir", version = "0.7.1" } calyx-frontend = { path = "calyx-frontend", version = "0.7.1" } calyx-opt = { path = "calyx-opt", version = "0.7.1" } calyx-backend = { path = "calyx-backend", version = "0.7.1" } +figment = { version = "0.10.12", features = ["toml"] } +semver = "1.0.23" [workspace.dependencies.petgraph] version = "0.6" diff --git a/fud2/README.md b/fud2/README.md index a0f0280308..53b47b3a8a 100644 --- a/fud2/README.md +++ b/fud2/README.md @@ -24,4 +24,13 @@ Add the path to the location of the Calyx compiler: base = "" ``` -[fud]: https://docs.calyxir.org/fud/index.html \ No newline at end of file +[fud]: https://docs.calyxir.org/fud/index.html + +### CLI + +- You can pass/override config variables by passing one or more options of the form `--set variable=value`. + +### Writing a new state + +Given a `bld: &mut DriverBuilder`, call `bld.state`, and define appropriate rules via `bld.rule`. +Each rule may require one or more setups; a setup can be obtained through `bld.setup` and may in addition define variables (including those mapped to config file keys) or rules. diff --git a/fud2/fud-core/Cargo.toml b/fud2/fud-core/Cargo.toml index 7da316f67a..d983d68fa6 100644 --- a/fud2/fud-core/Cargo.toml +++ b/fud2/fud-core/Cargo.toml @@ -13,7 +13,7 @@ description = "Library for building declarative build tools" argh.workspace = true cranelift-entity = "0.103.0" serde.workspace = true -figment = { version = "0.10.12", features = ["toml"] } +figment.workspace = true pathdiff = { version = "0.2.1", features = ["camino"] } camino = "1.1.6" anyhow.workspace = true diff --git a/fud2/fud-core/src/exec/driver.rs b/fud2/fud-core/src/exec/driver.rs index c4805ca7ee..6d69fbadc8 100644 --- a/fud2/fud-core/src/exec/driver.rs +++ b/fud2/fud-core/src/exec/driver.rs @@ -328,6 +328,11 @@ impl DriverBuilder { } } + /// Define a new fud2 state named `name`. Filesnames without an assigned + /// state will be assigned this state if their extension is contained in + /// `extensions`. If two states share an extension, the inferred assignment + /// is undefined. (Assuming this is the behavior; Jeremy said he doesn't + /// remember) pub fn state(&mut self, name: &str, extensions: &[&str]) -> StateRef { self.states.push(State { name: name.to_string(), diff --git a/fud2/fud-core/src/run.rs b/fud2/fud-core/src/run.rs index aa35b8e7d5..504c3d3645 100644 --- a/fud2/fud-core/src/run.rs +++ b/fud2/fud-core/src/run.rs @@ -546,13 +546,13 @@ impl Emitter { } } - /// Emit a Ninja variable declaration for `name` based on the configured value for `key`. + /// Emit a Ninja variable declaration that sets `name` to the value bound by `key` in the config file. pub fn config_var(&mut self, name: &str, key: &str) -> EmitResult { self.var(name, &self.config_val(key)?)?; Ok(()) } - /// Emit a Ninja variable declaration for `name` based on the configured value for `key`, or a + /// Emit a Ninja variable declaration that sets `name` to the value bound by `key` in the config file, or a /// default value if it's missing. pub fn config_var_or( &mut self, @@ -563,7 +563,7 @@ impl Emitter { self.var(name, &self.config_or(key, default)) } - /// Emit a Ninja variable declaration. + /// Emit a Ninja variable declaration `name = value`. pub fn var(&mut self, name: &str, value: &str) -> std::io::Result<()> { writeln!(self.out, "{} = {}", name, value) } diff --git a/fud2/scripts/tb.rhai b/fud2/scripts/tb.rhai new file mode 100644 index 0000000000..c53796e551 --- /dev/null +++ b/fud2/scripts/tb.rhai @@ -0,0 +1,14 @@ +// import "calyx" as calyx; +// +// let tb_state = state("tb", []); +// +// fn tb_setup(e) { +// e.config_var("calyx-tb-exe", "tb.exe"); +// e.config_var("calyx-tb-test", "tb.test"); +// e.config_var("calyx-tb-config-file", "tb.config-file"); +// e.config_var_or("calyx-tb-flags", "tb.flags", ""); +// e.config_var_or("calyx-tb-using", "tb.using"); +// // e.rule("verilog-to-tb", "$calyx-tb-exe $in --test $caller-dir/$calyx-tb-test --using $calyx-tb-using --config $calyx-tb-config-file $calyx-tb-flags"); +// } + +// rule([tb_setup], calyx::verilog_state, tb_state, "verilog-to-tb"); diff --git a/fud2/src/lib.rs b/fud2/src/lib.rs index dcef0adf96..2a0fec9303 100644 --- a/fud2/src/lib.rs +++ b/fud2/src/lib.rs @@ -83,6 +83,21 @@ fn setup_mrxl( (mrxl, mrxl_setup) } +fn setup_tb(bld: &mut DriverBuilder, verilog: StateRef) { + let tb = bld.state("tb", &[]); + let tb_setup = bld.setup("Testbench executable", |e| { + e.var("calyx-tb-exe", "tb")?; + e.config_var("calyx-tb-test", "tb.test")?; // todo multi input op + e.config_var("calyx-tb-config-file", "tb.config-file")?; + e.rule( + "calyx-to-tb", + "$calyx-tb-exe $in --test $calyx-tb-test --using cocotb --config $calyx-tb-config-file", + )?; + Ok(()) + }); + bld.rule(&[tb_setup], verilog, tb, "test"); +} + pub fn build_driver(bld: &mut DriverBuilder) { // The verilog state let verilog = bld.state("verilog", &["sv", "v"]); @@ -93,6 +108,8 @@ pub fn build_driver(bld: &mut DriverBuilder) { // MrXL. setup_mrxl(bld, calyx); + setup_tb(bld, verilog); + // Shared machinery for RTL simulators. let dat = bld.state("dat", &["json"]); let vcd = bld.state("vcd", &["vcd"]); diff --git a/interp/src/flatten/primitives/combinational.rs b/interp/src/flatten/primitives/combinational.rs index e1ef437999..f43f1a943c 100644 --- a/interp/src/flatten/primitives/combinational.rs +++ b/interp/src/flatten/primitives/combinational.rs @@ -123,7 +123,7 @@ comb_primitive!(StdSub(left [0], right [1]) -> (out [2]) { // TODO griffin: the old approach is not possible with the way primitives // work. // this is dubious - let result = Value::from(left.as_signed() - right.as_signed(), left.width()); + let result = Value::from(left.as_unsigned() - right.as_unsigned(), left.width()); Ok(Some(result)) }); diff --git a/tools/calyx-ffi-macro/Cargo.toml b/tools/calyx-ffi-macro/Cargo.toml new file mode 100644 index 0000000000..bff0cd63ae --- /dev/null +++ b/tools/calyx-ffi-macro/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "calyx-ffi-macro" +authors.workspace = true +license-file.workspace = true +keywords.workspace = true +repository.workspace = true +readme.workspace = true +description.workspace = true +categories.workspace = true +homepage.workspace = true +edition.workspace = true +version.workspace = true +rust-version.workspace = true + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0.86" +quote = "1.0.36" +syn = { version = "2.0.69", features = ["full", "visit"] } +calyx-utils.workspace = true +calyx-frontend.workspace = true +calyx-ir.workspace = true +# bigint = "4.4.3" diff --git a/tools/calyx-ffi-macro/src/calyx.rs b/tools/calyx-ffi-macro/src/calyx.rs new file mode 100644 index 0000000000..aee8bbfb52 --- /dev/null +++ b/tools/calyx-ffi-macro/src/calyx.rs @@ -0,0 +1,50 @@ +use std::{env, path::PathBuf, rc::Rc}; + +use proc_macro::TokenStream; + +use crate::{parse::CalyxFFIMacroArgs, util}; + +pub struct CalyxComponent { + ctx: Rc, + index: usize, +} + +impl CalyxComponent { + pub fn get(&self) -> &calyx_ir::Component { + &self.ctx.components[self.index] + } +} + +pub fn parse_calyx_file( + args: &CalyxFFIMacroArgs, +) -> Result { + // there has to be a better way to find lib + let home_dir = env::var("HOME").expect("user home not set"); + let mut lib_path = PathBuf::from(home_dir); + lib_path.push(".calyx"); + let ws = calyx_frontend::Workspace::construct( + &Some(args.src.clone()), + &lib_path, + ) + .map_err(|err| util::compile_error(&args.src_attr_span, err.message()))?; + let ctx = calyx_ir::from_ast::ast_to_ir(ws).map_err(|err| { + util::compile_error(&args.src_attr_span, err.message()) + })?; + + let comp_index = ctx + .components + .iter() + .position(|comp| comp.name == args.comp) + .ok_or(util::compile_error( + &args.comp_attr_span, + format!( + "component '{}' does not exist in '{}'", + args.comp, + args.src.to_string_lossy() + ), + ))?; + Ok(CalyxComponent { + ctx: Rc::new(ctx), + index: comp_index, + }) +} diff --git a/tools/calyx-ffi-macro/src/lib.rs b/tools/calyx-ffi-macro/src/lib.rs new file mode 100644 index 0000000000..39c29666b9 --- /dev/null +++ b/tools/calyx-ffi-macro/src/lib.rs @@ -0,0 +1,335 @@ +use parse::CalyxFFIMacroArgs; +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, spanned::Spanned}; + +mod calyx; +mod parse; +mod util; + +#[proc_macro_attribute] +pub fn calyx_ffi(attrs: TokenStream, item: TokenStream) -> TokenStream { + let args = parse_macro_input!(attrs as CalyxFFIMacroArgs); + let item_struct = parse_macro_input!(item as syn::ItemStruct); + let name = item_struct.ident; + let path = args.src.to_string_lossy().to_string(); + + // + let comp = calyx::parse_calyx_file(&args); + if let Err(error) = comp { + return error; + } + let comp = comp.unwrap(); + let comp = comp.get(); + // + + let comp_name = + syn::parse_str::(&format!("\"{}\"", comp.name)) + .expect("failed to turn quoted name into string"); + let comp_path = syn::parse_str::(&format!("\"{}\"", path)) + .expect("failed to turn quoted path into string"); + + let backend_macro = args.backend; + let mut input_names = Vec::new(); + let mut output_names = Vec::new(); + let mut field_names = vec![]; + let mut fields = vec![]; + let mut getters = vec![]; + + for port in comp.signature.borrow().ports() { + let port_name_str = port.borrow().name.to_string(); + let port_name = syn::parse_str::(&port_name_str) + .expect("failed to turn port name into identifier"); + field_names.push(port_name.clone()); + // let port_width = port.borrow().width; + + // idk why input output ports are being flipped?? + match port.borrow().direction.reverse() { + calyx_ir::Direction::Input => { + fields.push(quote! { + pub #port_name: u64 + }); + input_names.push(port_name); + } + calyx_ir::Direction::Output => { + fields.push(quote! { + #port_name: u64 + }); + getters.push(quote! { + pub fn #port_name(&self) -> u64 { + self.#port_name + } + }); + output_names.push(port_name); + } + calyx_ir::Direction::Inout => { + todo!("inout ports not supported yet") + } + } + } + + let struct_def = quote! { + struct #name { + #(#fields,)* + user_data: std::mem::MaybeUninit<#backend_macro!(@user_data_type)> + } + }; + + let impl_block = quote! { + impl #name { + #(#getters)* + } + + impl std::default::Default for #name { + fn default() -> Self { + Self { + #(#field_names: std::default::Default::default(),)* + user_data: unsafe { std::mem::MaybeUninit::zeroed() } + } + } + } + + impl CalyxFFIComponent for #name { + fn path(&self) -> &'static str { + #comp_path + } + + fn name(&self) -> &'static str { + #comp_name + } + + fn init(&mut self, context: &calyx_ir::Context) { + #backend_macro!(@init self, context; #(#input_names),*; #(#output_names),*); + } + + fn reset(&mut self) { + #backend_macro!(@reset self; #(#input_names),*; #(#output_names),*); + } + + fn can_tick(&self) -> bool { + #backend_macro!(@can_tick self; #(#input_names),*; #(#output_names),*) + } + + fn tick(&mut self) { + #backend_macro!(@tick self; #(#input_names),*; #(#output_names),*); + } + + fn go(&mut self) { + #backend_macro!(@go self; #(#input_names),*; #(#output_names),*); + } + } + }; + + let mut derive_impls = Vec::new(); + + for derive in args.derives { + let trait_name = derive.name; + + let mut getters = Vec::new(); + for output in derive.outputs { + getters.push(quote! { + fn #output(&self) -> u64 { + self.#output + } + }) + } + + let mut setters = Vec::new(); + for input in derive.inputs { + setters.push(quote! { + fn #input(&mut self) -> &mut u64 { + &mut self.#input + } + }) + } + + derive_impls.push(quote! { + impl #trait_name for #name { + #(#getters)* + #(#setters)* + } + }); + } + + quote! { + #struct_def + #impl_block + #(#derive_impls)* + } + .into() +} + +#[derive(Default)] +struct CalyxFFITestModuleVisitor { + pub wrappers: Vec, + pub tests: Vec, +} + +impl syn::visit::Visit<'_> for CalyxFFITestModuleVisitor { + fn visit_item_fn(&mut self, i: &syn::ItemFn) { + let has_calyx_ffi_test = i + .attrs + .iter() + .any(|attr| attr.path().is_ident("calyx_ffi_test")); + if has_calyx_ffi_test { + let fn_name = &i.sig.ident; + let dut_type = get_ffi_test_dut_type(i) + .expect("calyx_ffi_test should enforce this invariant"); + + self.wrappers.push(syn::parse_quote! { + pub(crate) unsafe fn #fn_name(ffi: &mut CalyxFFI) { + let dut = ffi.new_comp::<#dut_type>(); + let dut_ref = &mut *dut.borrow_mut(); + let dut_pointer = dut_ref as *mut dyn CalyxFFIComponent as *mut _ as *mut #dut_type; + let dut_concrete: &mut #dut_type = &mut *dut_pointer; + super::#fn_name(dut_concrete); + } + }); + self.tests.push(syn::parse_quote! { + #[test] + pub(crate) fn #fn_name() { + let mut ffi = CalyxFFI::new(); + unsafe { + super::calyx_ffi_generated_wrappers::#fn_name(&mut ffi); + } + } + }); + } + } +} + +#[proc_macro_attribute] +pub fn calyx_ffi_tests(args: TokenStream, item: TokenStream) -> TokenStream { + if !args.is_empty() { + return util::compile_error( + &args.into_iter().next().unwrap().span().into(), + "#[calyx_ffi_tests] takes no arguments".into(), + ); + } + + let mut module = parse_macro_input!(item as syn::ItemMod); + let module_name = &module.ident; + + let mut visitor = CalyxFFITestModuleVisitor::default(); + syn::visit::visit_item_mod(&mut visitor, &module); + let wrappers = visitor.wrappers; + let tests = visitor.tests; + + let test_names = wrappers.iter().map(|test| test.sig.ident.clone()); + let generated_wrappers = quote! { + pub(crate) mod calyx_ffi_generated_wrappers { + use super::*; + + pub(crate) const CALYX_FFI_TESTS: &'static [unsafe fn(&mut CalyxFFI) -> ()] = &[ + #(#test_names),* + ]; + + #(#wrappers)* + } + }; + let generated_wrappers_item: syn::Item = + syn::parse2(generated_wrappers).unwrap(); + + let generated_tests = quote! { + pub(crate) mod calyx_ffi_generated_tests { + use super::*; + + #(#tests)* + } + }; + let generated_tests_item: syn::Item = syn::parse2(generated_tests).unwrap(); + + let items_to_add = vec![generated_wrappers_item, generated_tests_item]; + if let Some((_, ref mut items)) = module.content { + items.extend(items_to_add); + } else { + module.content = Some((syn::token::Brace::default(), items_to_add)); + } + + quote! { + #module + + pub mod calyx_ffi_generated_top { + use super::*; + + pub unsafe fn run_tests(ffi: &mut CalyxFFI) { + for test in #module_name::calyx_ffi_generated_wrappers::CALYX_FFI_TESTS { + test(ffi); + } + } + } + } + .into() +} + +#[proc_macro_attribute] +pub fn calyx_ffi_test(args: TokenStream, item: TokenStream) -> TokenStream { + if !args.is_empty() { + return util::compile_error( + &args.into_iter().next().unwrap().span().into(), + "#[calyx_ffi_test] takes no arguments".into(), + ); + } + + let mut func = parse_macro_input!(item as syn::ItemFn); + let dut_type = get_ffi_test_dut_type(&func); + let Ok(dut_type) = dut_type else { + return dut_type.err().unwrap(); + }; + + let check_trait_impl = quote! { + { + fn assert_is_calyx_ffi_component() {} + assert_is_calyx_ffi_component::<#dut_type>(); + } + }; + + let check_trait_impl_stmts: syn::Block = syn::parse2(check_trait_impl) + .expect("Failed to parse check_trait_impl as a block"); + + let new_stmts: Vec = check_trait_impl_stmts + .stmts + .iter() + .chain(func.block.stmts.iter()) + .cloned() + .collect(); + + let new_block = syn::Block { + brace_token: func.block.brace_token, + stmts: new_stmts, + }; + func.block = Box::new(new_block); + + quote! { + #func + } + .into() +} + +fn get_ffi_test_dut_type( + func: &syn::ItemFn, +) -> Result<&syn::Type, TokenStream> { + let inputs: Vec<&syn::FnArg> = func.sig.inputs.iter().collect(); + + let bad_sig_msg = "#[calyx_ffi_test] tests must take exactly one argument, namely, a mutable reference to the DUT".into(); + + if inputs.len() != 1 { + return Err(util::compile_error(&func.span(), bad_sig_msg)); + } + let input = inputs.first().unwrap(); + + let syn::FnArg::Typed(pat_ty) = input else { + return Err(util::compile_error(&func.span(), bad_sig_msg)); + }; + + let syn::Type::Reference(syn::TypeReference { + mutability: Some(syn::token::Mut { span: _ }), + ref elem, + .. + }) = *pat_ty.ty + else { + return Err(util::compile_error(&func.span(), bad_sig_msg)); + }; + + Ok(elem) +} diff --git a/tools/calyx-ffi-macro/src/parse.rs b/tools/calyx-ffi-macro/src/parse.rs new file mode 100644 index 0000000000..f0642e5754 --- /dev/null +++ b/tools/calyx-ffi-macro/src/parse.rs @@ -0,0 +1,100 @@ +use std::path::PathBuf; + +use proc_macro2::{Span, TokenTree}; +use syn::{ + bracketed, parenthesized, + parse::{Parse, ParseStream}, +}; + +pub struct CalyxInterface { + pub name: syn::Ident, + pub inputs: Vec, + pub outputs: Vec, +} + +impl Parse for CalyxInterface { + fn parse(input: ParseStream) -> syn::Result { + let name = input.parse::()?; + let inputs; + let outputs; + parenthesized!(inputs in input); + let inputs = inputs + .parse_terminated(syn::Ident::parse, syn::Token![,])? + .into_iter() + .collect(); + input.parse::]>()?; + parenthesized!(outputs in input); + let outputs = outputs + .parse_terminated(syn::Ident::parse, syn::Token![,])? + .into_iter() + .collect(); + Ok(Self { + name, + inputs, + outputs, + }) + } +} + +pub struct CalyxFFIMacroArgs { + pub src_attr_span: Span, + pub src: PathBuf, + pub comp_attr_span: Span, + pub comp: String, + pub backend: syn::Path, + pub derives: Vec, +} + +impl Parse for CalyxFFIMacroArgs { + fn parse(input: ParseStream) -> syn::Result { + syn::custom_keyword!(src); + syn::custom_keyword!(comp); + syn::custom_keyword!(backend); + syn::custom_keyword!(derive); + + let src_ident = input.parse::()?; + input.parse::()?; + let src_lit = input.parse::()?.value(); + + input.parse::()?; + + let comp_ident = input.parse::()?; + input.parse::()?; + let comp_lit = input.parse::()?.value(); + + input.parse::()?; + input.parse::()?; + input.parse::()?; + let backend_path = input.parse::()?; + + let _ = input.parse::(); + + let derives = if input.parse::().is_ok() { + input.parse::()?; + let content; + bracketed!(content in input); + content + .parse_terminated(CalyxInterface::parse, syn::Token![,])? + .into_iter() + .collect() + } else { + vec![] + }; + + if !input.is_empty() { + return Err(syn::Error::new_spanned( + input.parse::()?, + "Invalid `calyx_ffi` argument syntax: expected 'src = \"...\", comp = \"...\", extern = ...", + )); + } + + Ok(Self { + src_attr_span: src_ident.span, + src: src_lit.into(), + comp_attr_span: comp_ident.span, + comp: comp_lit, + backend: backend_path, + derives, + }) + } +} diff --git a/tools/calyx-ffi-macro/src/util.rs b/tools/calyx-ffi-macro/src/util.rs new file mode 100644 index 0000000000..882a9e0e6b --- /dev/null +++ b/tools/calyx-ffi-macro/src/util.rs @@ -0,0 +1,6 @@ +use proc_macro::TokenStream; +use proc_macro2::Span; + +pub fn compile_error(span: &Span, msg: String) -> TokenStream { + syn::Error::new(*span, msg).to_compile_error().into() +} diff --git a/tools/calyx-ffi/Cargo.toml b/tools/calyx-ffi/Cargo.toml new file mode 100644 index 0000000000..862e34cbce --- /dev/null +++ b/tools/calyx-ffi/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "calyx-ffi" +authors.workspace = true +license-file.workspace = true +keywords.workspace = true +repository.workspace = true +readme.workspace = true +description.workspace = true +categories.workspace = true +homepage.workspace = true +edition.workspace = true +version.workspace = true +rust-version.workspace = true + +[dependencies] +calyx-ffi-macro = { path = "../calyx-ffi-macro" } +calyx-frontend.workspace = true +calyx-ir.workspace = true +interp = { path = "../../interp" } diff --git a/tools/calyx-ffi/src/backend.rs b/tools/calyx-ffi/src/backend.rs new file mode 100644 index 0000000000..759f64d89b --- /dev/null +++ b/tools/calyx-ffi/src/backend.rs @@ -0,0 +1,2 @@ +pub mod cider; +pub mod useless; diff --git a/tools/calyx-ffi/src/backend/cider.rs b/tools/calyx-ffi/src/backend/cider.rs new file mode 100644 index 0000000000..9f7e3438db --- /dev/null +++ b/tools/calyx-ffi/src/backend/cider.rs @@ -0,0 +1,99 @@ +use calyx_ir::Context; +use interp::{ + flatten::{ + flat_ir, + structures::{ + context::Context as CiderContext, environment::Simulator, + }, + }, + values::Value, +}; +use std::rc::Rc; + +pub struct CiderFFIBackend { + simulator: Simulator>, +} + +impl CiderFFIBackend { + pub fn from(ctx: &Context, _name: &'static str) -> Self { + // TODO(ethan, maybe griffin): use _name to select the component somehow + let ctx = flat_ir::translate(ctx); + let simulator = Simulator::build_simulator(Rc::new(ctx), &None) + .expect("we live on the edge"); + Self { simulator } + } + + pub fn write_port(&mut self, name: &'static str, value: u64) { + if name == "go" { + return; + } + self.simulator.pin_value(name, Value::from(value, 64)); + } + + pub fn read_port(&self, name: &'static str) -> u64 { + self.simulator + .lookup_port_from_string(&String::from(name)) + .expect("wrong port name") + .as_u64() + } + + pub fn step(&mut self) { + self.simulator.step().expect( + "this function isn't documented so don't know what went wrong", + ); + } + + pub fn go(&mut self) { + self.simulator.run_program().expect("failed to run program"); + self.step(); // since griffin said so + } +} + +/// Runs the component using cider2. +#[macro_export] +macro_rules! cider_ffi_backend { + (@user_data_type) => { + $crate::backend::cider::CiderFFIBackend + }; + (@init $dut:ident, $ctx:expr; $($input:ident),*; $($output:ident),*) => { + $dut.user_data + .write($crate::backend::cider::CiderFFIBackend::from( + $ctx, + $dut.name(), + )); + }; + (@reset $dut:ident; $($input:ident),*; $($output:ident),*) => { + println!("cider_ffi_backend reset. doesn't work LOL"); + // $dut.done = 0; + // $dut.reset = 1; + // for i in 0..5 { + // $dut.tick(); + // } + // $dut.reset = 0; + }; + (@can_tick $dut:ident; $($input:ident),*; $($output:ident),*) => { + true + }; + (@tick $dut:ident; $($input:ident),*; $($output:ident),*) => { + println!("cider_ffi_backend tick"); + let cider = unsafe { $dut.user_data.assume_init_mut() }; + $( + cider.write_port(stringify!($input), $dut.$input); + )* + cider.step(); + $( + $dut.$output = cider.read_port(stringify!($output)); + )* + }; + (@go $dut:ident; $($input:ident),*; $($output:ident),*) => { + println!("cider_ffi_backend go"); + let cider = unsafe { $dut.user_data.assume_init_mut() }; + $( + cider.write_port(stringify!($input), $dut.$input); + )* + cider.go(); + $( + $dut.$output = cider.read_port(stringify!($output)); + )* + }; +} diff --git a/tools/calyx-ffi/src/backend/useless.rs b/tools/calyx-ffi/src/backend/useless.rs new file mode 100644 index 0000000000..c890fdfce2 --- /dev/null +++ b/tools/calyx-ffi/src/backend/useless.rs @@ -0,0 +1,35 @@ +/// Example FFI backend. +#[macro_export] +macro_rules! useless_ffi_backend { + (@user_data_type) => { + () // unit type + }; + (@init $dut:ident, $ctx:expr; $($input:ident),*; $($output:ident),*) => { + println!("useless_ffi_backend init"); + }; + (@reset $dut:ident; $($input:ident),*; $($output:ident),*) => { + println!("useless_ffi_backend reset"); + $dut.done = 0; + $dut.reset = 1; + for i in 0..5 { + $dut.tick(); + } + $dut.reset = 0; + }; + (@can_tick $dut:ident; $($input:ident),*; $($output:ident),*) => { + true + }; + (@tick $dut:ident; $($input:ident),*; $($output:ident),*) => { + println!("useless_ffi_backend tick"); + if $dut.done == 1 { + $dut.done = 0; + } + }; + (@go $dut:ident; $($input:ident),*; $($output:ident),*) => { + println!("useless_ffi_backend go"); + $dut.go = 1; + $dut.go = 0; + $dut.done = 1; + $dut.tick(); + }; +} diff --git a/tools/calyx-ffi/src/lib.rs b/tools/calyx-ffi/src/lib.rs new file mode 100644 index 0000000000..b4644d545a --- /dev/null +++ b/tools/calyx-ffi/src/lib.rs @@ -0,0 +1,76 @@ +use calyx_ir::Context; +use std::{ + any, cell::RefCell, collections::HashMap, env, path::PathBuf, rc::Rc, +}; + +pub mod backend; +pub mod prelude; + +/// A non-combinational calyx component. +pub trait CalyxFFIComponent: any::Any { + /// The path to the component source file. Must be a constant expression. + fn path(&self) -> &'static str; + + /// The in-source name of this component. Must be a constant expression. + fn name(&self) -> &'static str; + + /// Internal initialization routine. Do not call! + fn init(&mut self, context: &Context); + + /// Resets this component. + fn reset(&mut self); + + /// Whether this component's backend supports ticking. + fn can_tick(&self) -> bool; + + /// Advances this component by one clock cycle. May not always be available, so check [`has_tick`]([CalyxFFIComponent::has_tick]). + fn tick(&mut self); + + /// Calls this component, blocking until it is done executing. + fn go(&mut self); +} + +pub type CalyxFFIComponentRef = Rc>; + +fn box_calyx_ffi_component( + comp: T, +) -> CalyxFFIComponentRef { + Rc::new(RefCell::new(comp)) +} + +#[derive(Default)] +pub struct CalyxFFI { + contexts: HashMap<&'static str, Context>, +} + +impl CalyxFFI { + pub fn new() -> Self { + Self::default() + } + + /// Constructs a new calyx component of the given type. + /// + /// The `path` implementation for `CalyxFFIComponent` must be a constant + /// expression and should derived via the `calyx_ffi` procedural macro. + pub fn new_comp( + &mut self, + ) -> CalyxFFIComponentRef { + let mut comp = T::default(); + let path = comp.path(); + let context = self.contexts.entry(path).or_insert_with_key(|path| { + // there has to be a better way to find lib + let home_dir = env::var("HOME").expect("user home not set"); + let mut lib_path = PathBuf::from(home_dir); + lib_path.push(".calyx"); + let ws = calyx_frontend::Workspace::construct( + &Some(path.into()), + &lib_path, + ) + .expect("couldn't parse calyx"); + calyx_ir::from_ast::ast_to_ir(ws) + .expect("couldn't construct calyx ir") + }); + comp.init(context); + box_calyx_ffi_component(comp) + } +} diff --git a/tools/calyx-ffi/src/prelude.rs b/tools/calyx-ffi/src/prelude.rs new file mode 100644 index 0000000000..93bc88911a --- /dev/null +++ b/tools/calyx-ffi/src/prelude.rs @@ -0,0 +1,16 @@ +pub use super::{CalyxFFI, CalyxFFIComponent, CalyxFFIComponentRef}; +pub use calyx_ffi_macro::{calyx_ffi, calyx_ffi_test, calyx_ffi_tests}; + +#[macro_export] +macro_rules! declare_calyx_interface { + ($name:ident($($input:ident),*) -> ($($output:ident),*)) => { + pub trait $name: CalyxFFIComponent { + $( + fn $input(&mut self) -> &mut u64; + )* + $( + fn $output(&self) -> u64; + )* + } + }; +} diff --git a/tools/calyx-ffi/tests/file.futil b/tools/calyx-ffi/tests/file.futil new file mode 100644 index 0000000000..f1b20be0ff --- /dev/null +++ b/tools/calyx-ffi/tests/file.futil @@ -0,0 +1,5 @@ +component main(foo: 1) -> () { + cells {} + wires {} + control {} +} diff --git a/tools/calyx-ffi/tests/test.rs b/tools/calyx-ffi/tests/test.rs new file mode 100644 index 0000000000..1e025264a0 --- /dev/null +++ b/tools/calyx-ffi/tests/test.rs @@ -0,0 +1,19 @@ +use calyx_ffi::prelude::*; + +use calyx_ffi::useless_ffi_backend; + +#[calyx_ffi( + src = "/home/runner/work/calyx/calyx/tools/calyx-ffi/tests/file.futil", + comp = "main", + backend = useless_ffi_backend +)] +struct Main; + +#[test] +fn test() { + let mut main = Main::default(); + assert!(main.name() == "main"); + main.reset(); + assert!(main.reset == 0); + main.tick(); +} diff --git a/tools/tb/.gitignore b/tools/tb/.gitignore new file mode 100644 index 0000000000..5beb86d02e --- /dev/null +++ b/tools/tb/.gitignore @@ -0,0 +1 @@ +tb diff --git a/tools/tb/Cargo.toml b/tools/tb/Cargo.toml new file mode 100644 index 0000000000..fea9e34fc5 --- /dev/null +++ b/tools/tb/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "tb" +authors.workspace = true +license-file.workspace = true +keywords.workspace = true +repository.workspace = true +readme.workspace = true +description.workspace = true +categories.workspace = true +homepage.workspace = true +edition.workspace = true +version = "0.0.0" +rust-version.workspace = true + +[dependencies] +argh.workspace = true +tempdir = "0.3.7" +figment.workspace = true +serde.workspace = true +semver.workspace = true +libloading = "0.8.4" +log.workspace = true +env_logger.workspace = true +fs_extra = "1.3.0" +makemake = "0.1.3" +toml = "0.8.14" diff --git a/tools/tb/Makefile b/tools/tb/Makefile new file mode 100644 index 0000000000..2dc33d17c0 --- /dev/null +++ b/tools/tb/Makefile @@ -0,0 +1,11 @@ +TARGET = tb + +.PHONY: $(TARGET) +$(TARGET): plugins + cargo build --manifest-path Cargo.toml + printf "../../target/debug/tb \$$@\n" > $@ + chmod u+x $@ + +.PHONY: plugins +plugins: + $(shell which python3 || which python || which pypy3 || which pypy) build_plugins.py diff --git a/tools/tb/README.md b/tools/tb/README.md new file mode 100644 index 0000000000..19a34aabe5 --- /dev/null +++ b/tools/tb/README.md @@ -0,0 +1,62 @@ +# tb: The Calyx Testbench Tool + +## Contents + +1. Setup +2. Usage +3. Writing a Plugin + +## Setup + +Run `make plugins` to build the builtin plugins (cocotb, verilator, etc.). + +## Usage + +There are two ways to use `tb`: + +### Directly + +For example, if you make sure to follow the instructions under [`examples/cocotb/doc_examples_quickstart/`](examples/cocotb/doc_examples_quickstart/), +``` +make +./tb examples/cocotb/doc_examples_quickstart/my_design.sv -t examples/cocotb/doc_examples_quickstart/test_my_design.py --using cocotb +``` +should run `cocotb` on the input file and harness. + +### Via `fud2`: + +You can follow the above steps but invoke the following command instead. +``` +fud2 my_design.sv -s tb.test=test_my_design.py -s tb.using=cocotb --to tb +``` + +### Makefile + +I've provided a [Makefile](Makefile) in this directory for local testing. Use `make` to build the `tb` executable locally. + +## Writing a Plugin + +First, setup a simple rust library as you would any other, but **ensure that `lib.crate-type` is `cdylib`**. +Here, we're writing the plugin in `lib.rs`. +Remember to update the `path` in the `dependencies.tb` dependency! + +```toml +[package] +name = "my-tb-plugin" +edition = "2021" # or `edition.workspace = true` + +[lib] +path = "lib.rs" +crate-type = ["cdylib"] + +[dependencies] +tb = { path = "path/to/tb/crate", version = "0.0.0" } +``` + +In the crate, you can write any auxillary code. +However, you'll need to define at least two things: + +1. A type implementing `tb::plugin::Plugin`. +2. A `declare_plugin!` declaration to expose the plugin and its constructor to the outside world. + +It may be helpful to look at the existing plugins for reference. diff --git a/tools/tb/build_plugins.py b/tools/tb/build_plugins.py new file mode 100644 index 0000000000..99d9235095 --- /dev/null +++ b/tools/tb/build_plugins.py @@ -0,0 +1,28 @@ +import os +import subprocess + +for dir_name in os.listdir("plugins"): + dir_path = os.path.join("plugins", dir_name) + if os.path.isdir(dir_path): + subprocess.run( + [ + "cargo", + "build", + "--manifest-path", + os.path.join(dir_path, "Cargo.toml"), + "--release", + "--target-dir", + os.path.join(dir_path, "target"), + ], + check=True, + ) + release_dir = os.path.join(dir_path, "target", "release") + for file_name in os.listdir(release_dir): + file_path = os.path.join(release_dir, file_name) + if os.path.isfile(file_path) and ( + file_name.endswith(".dylib") or file_name.endswith(".so") + ): + dest_path = os.path.join("plugins", file_name) + if os.path.isfile(dest_path): + os.remove(dest_path) + os.rename(file_path, dest_path) diff --git a/tools/tb/examples/calyx/Cargo.toml b/tools/tb/examples/calyx/Cargo.toml new file mode 100644 index 0000000000..581eaff62e --- /dev/null +++ b/tools/tb/examples/calyx/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "calyx-ffi-example" +authors.workspace = true +license-file.workspace = true +keywords.workspace = true +repository.workspace = true +readme.workspace = true +description.workspace = true +categories.workspace = true +homepage.workspace = true +edition.workspace = true +version.workspace = true +rust-version.workspace = true + +[lib] +path = "test.rs" + +[dependencies] +calyx-ffi = { path = "../../../calyx-ffi" } +interp = { path = "../../../../interp" } +calyx-ir.workspace = true +rand = "0.8.5" diff --git a/tools/tb/examples/calyx/adder.futil b/tools/tb/examples/calyx/adder.futil new file mode 100644 index 0000000000..1730a7c5fd --- /dev/null +++ b/tools/tb/examples/calyx/adder.futil @@ -0,0 +1,23 @@ +import "primitives/core.futil"; + +component main(lhs: 64, rhs: 64) -> (result: 64) { + cells { + adder = std_add(64); + temp = std_reg(64); + } + wires { + group add { + adder.left = lhs; + adder.right = rhs; + temp.in = adder.out; + temp.write_en = 1'b1; + add[done] = temp.done; + } + result = temp.out; + } + control { + add; + } +} + + diff --git a/tools/tb/examples/calyx/subber.futil b/tools/tb/examples/calyx/subber.futil new file mode 100644 index 0000000000..260456f938 --- /dev/null +++ b/tools/tb/examples/calyx/subber.futil @@ -0,0 +1,21 @@ +import "primitives/core.futil"; + +component main(lhs: 64, rhs: 64) -> (result: 64) { + cells { + subber = std_sub(64); + temp = std_reg(64); + } + wires { + group sub { + subber.left = lhs; + subber.right = rhs; + temp.in = subber.out; + temp.write_en = 1'b1; + sub[done] = temp.done; + } + result = temp.out; + } + control { + sub; + } +} diff --git a/tools/tb/examples/calyx/test.rs b/tools/tb/examples/calyx/test.rs new file mode 100644 index 0000000000..b0a9135ddc --- /dev/null +++ b/tools/tb/examples/calyx/test.rs @@ -0,0 +1,67 @@ +use calyx_ffi::declare_calyx_interface; +use calyx_ffi::prelude::*; + +use calyx_ffi::cider_ffi_backend; + +// not necessary, just to show it off +declare_calyx_interface! { + In2Out1(lhs, rhs) -> (result) +} + +#[calyx_ffi( + src = "/home/runner/work/calyx/calyx/tools/tb/examples/calyx/adder.futil", + comp = "main", + backend = cider_ffi_backend, + derive = [ + In2Out1(lhs, rhs) -> (result) + ] +)] +struct Adder; + +#[calyx_ffi( + src = "/home/runner/work/calyx/calyx/tools/tb/examples/calyx/subber.futil", + comp = "main", + backend = cider_ffi_backend, + derive = [ + In2Out1(lhs, rhs) -> (result) + ] +)] +struct Subber; + +#[cfg(test)] +#[calyx_ffi_tests] +mod tests { + use super::*; + use rand::Rng; + use std::mem; + + // inv: the left argument will always be greater than the right + fn fuzz_in2out1 u64>( + comp: &mut I, + oracle: &F, + ) { + comp.reset(); + let mut rng = rand::thread_rng(); + for (mut x, mut y) in (0..100).map(|_| (rng.gen(), rng.gen())) { + if y > x { + mem::swap(&mut x, &mut y); + } + *comp.lhs() = x; + *comp.rhs() = y; + comp.go(); + assert_eq!(oracle(x, y), comp.result(), "testing f({}, {})", x, y); + } + } + + #[calyx_ffi_test] + fn test_add(adder: &mut Adder) { + println!("testing adder"); + fuzz_in2out1(adder, &|x, y| x.wrapping_add(y)) + } + + #[calyx_ffi_test] + fn test_sub(subber: &mut Subber) { + println!("testing subber"); + fuzz_in2out1(subber, &|x, y| x - y) + } +} diff --git a/tools/tb/examples/cocotb/doc_examples_quickstart/README.md b/tools/tb/examples/cocotb/doc_examples_quickstart/README.md new file mode 100644 index 0000000000..b7547d5738 --- /dev/null +++ b/tools/tb/examples/cocotb/doc_examples_quickstart/README.md @@ -0,0 +1,2 @@ +Taken from https://github.com/cocotb/cocotb/tree/master/examples/doc_examples/quickstart. +Make sure to checkout `v1.8.1` for `cocotb`. diff --git a/tools/tb/examples/cocotb/doc_examples_quickstart/my_design.sv b/tools/tb/examples/cocotb/doc_examples_quickstart/my_design.sv new file mode 100644 index 0000000000..bfa912c018 --- /dev/null +++ b/tools/tb/examples/cocotb/doc_examples_quickstart/my_design.sv @@ -0,0 +1,17 @@ +// https://github.com/cocotb/cocotb/tree/master/examples/doc_examples/quickstart + +// This file is public domain, it can be freely copied without restrictions. +// SPDX-License-Identifier: CC0-1.0 + +module my_design(input logic clk); + +timeunit 1ns; +timeprecision 1ns; + +logic my_signal_1; +logic my_signal_2; + +assign my_signal_1 = 1'bx; +assign my_signal_2 = 0; + +endmodule diff --git a/tools/tb/examples/cocotb/doc_examples_quickstart/test_my_design.py b/tools/tb/examples/cocotb/doc_examples_quickstart/test_my_design.py new file mode 100644 index 0000000000..daaa9996db --- /dev/null +++ b/tools/tb/examples/cocotb/doc_examples_quickstart/test_my_design.py @@ -0,0 +1,52 @@ +# https://github.com/cocotb/cocotb/tree/master/examples/doc_examples/quickstart + +# This file is public domain, it can be freely copied without restrictions. +# SPDX-License-Identifier: CC0-1.0 + +# test_my_design.py (simple) + +import cocotb +from cocotb.triggers import Timer + + +@cocotb.test() +async def my_first_test(dut): + """Try accessing the design.""" + + for cycle in range(10): + dut.clk.value = 0 + await Timer(1, units="ns") + dut.clk.value = 1 + await Timer(1, units="ns") + + dut._log.info("my_signal_1 is %s", dut.my_signal_1.value) + assert dut.my_signal_2.value[0] == 0, "my_signal_2[0] is not 0!" + + +# test_my_design.py (extended) + +import cocotb +from cocotb.triggers import FallingEdge, Timer + + +async def generate_clock(dut): + """Generate clock pulses.""" + + for cycle in range(10): + dut.clk.value = 0 + await Timer(1, units="ns") + dut.clk.value = 1 + await Timer(1, units="ns") + + +@cocotb.test() +async def my_second_test(dut): + """Try accessing the design.""" + + await cocotb.start(generate_clock(dut)) # run the clock "in the background" + + await Timer(5, units="ns") # wait a bit + await FallingEdge(dut.clk) # wait for falling edge/"negedge" + + dut._log.info("my_signal_1 is %s", dut.my_signal_1.value) + assert dut.my_signal_2.value[0] == 0, "my_signal_2[0] is not 0!" diff --git a/tools/tb/examples/verilator/test_our/README.md b/tools/tb/examples/verilator/test_our/README.md new file mode 100644 index 0000000000..43e5c6763d --- /dev/null +++ b/tools/tb/examples/verilator/test_our/README.md @@ -0,0 +1 @@ +Taken from https://veripool.org/guide/latest/example_cc.html#example-c-execution. diff --git a/tools/tb/examples/verilator/test_our/our.v b/tools/tb/examples/verilator/test_our/our.v new file mode 100644 index 0000000000..e124a796f6 --- /dev/null +++ b/tools/tb/examples/verilator/test_our/our.v @@ -0,0 +1,4 @@ +// https://veripool.org/guide/latest/example_cc.html#example-c-execution +module our; + initial begin $display("Hello World"); $finish; end + endmodule diff --git a/tools/tb/examples/verilator/test_our/sim_main.cpp b/tools/tb/examples/verilator/test_our/sim_main.cpp new file mode 100644 index 0000000000..c881991c46 --- /dev/null +++ b/tools/tb/examples/verilator/test_our/sim_main.cpp @@ -0,0 +1,14 @@ +// https://veripool.org/guide/latest/example_cc.html#example-c-execution +#include "Vour.h" +#include "verilated.h" +int main(int argc, char** argv) { + VerilatedContext* contextp = new VerilatedContext; + contextp->commandArgs(argc, argv); + Vour* top = new Vour{contextp}; + while (!contextp->gotFinish()) { + top->eval(); + } + delete top; + delete contextp; + return 0; +} diff --git a/tools/tb/plugins/.gitkeep b/tools/tb/plugins/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tools/tb/src/builtin_plugins.rs b/tools/tb/src/builtin_plugins.rs new file mode 100644 index 0000000000..3eb32c3d02 --- /dev/null +++ b/tools/tb/src/builtin_plugins.rs @@ -0,0 +1,3 @@ +pub mod calyx; +pub mod cocotb; +pub mod verilator; diff --git a/tools/tb/src/builtin_plugins/calyx.rs b/tools/tb/src/builtin_plugins/calyx.rs new file mode 100644 index 0000000000..a4bff0e64b --- /dev/null +++ b/tools/tb/src/builtin_plugins/calyx.rs @@ -0,0 +1,105 @@ +use std::fs; +use std::io::{self, Write}; +use std::path::PathBuf; +use std::process::Command; + +use crate::{ + config::Config, error::LocalResult, plugin::Plugin, semver, tempdir, +}; + +#[derive(Default)] +pub struct CalyxTB; + +mod config_keys {} + +const DRIVER_CODE: &str = include_str!("resources/driver.rs"); + +impl Plugin for CalyxTB { + fn name(&self) -> &'static str { + "calyx" + } + + fn version(&self) -> semver::Version { + semver::Version::new(0, 0, 0) + } + + fn setup(&self, _config: &mut Config) -> LocalResult<()> { + Ok(()) + } + + fn run( + &self, + input: String, + tests: &[String], + work_dir: tempdir::TempDir, + _config: &Config, + ) -> LocalResult<()> { + println!( + "recommendation: Run the #[calyx_ffi_tests] as Rust tests directly" + ); + let mut calyx_ffi_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + calyx_ffi_path.push("../../../calyx-ffi"); + + let mut main_file = PathBuf::from(work_dir.path()); + main_file.push("main.rs"); + fs::write(&main_file, DRIVER_CODE)?; + + let mut manifest_path = PathBuf::from(work_dir.path()); + manifest_path.push("Cargo.toml"); + + let mut lib_path = PathBuf::from(work_dir.path()); + lib_path.push(input); + + for test in tests { + let mut test_path = PathBuf::from(work_dir.path()); + test_path.push(test); + + let mut manifest = toml::Table::new(); + manifest.insert( + "package".into(), + toml::Value::Table(toml::map::Map::from_iter([ + ("name".to_string(), "test_crate".into()), + ("edition".to_string(), "2021".into()), + ])), + ); + manifest.insert( + "lib".into(), + toml::Value::Table(toml::map::Map::from_iter([( + "path".to_string(), + "lib.rs".into(), + )])), + ); + manifest.insert( + "bin".into(), + vec![toml::Value::Table(toml::map::Map::from_iter([ + ("name".to_string(), "test".into()), + ("path".to_string(), "main.rs".into()), + ]))] + .into(), + ); + manifest.insert( + "dependencies".into(), + toml::Value::Table(toml::map::Map::from_iter([( + "calyx-ffi".to_string(), + toml::Value::Table(toml::map::Map::from_iter([( + "path".to_string(), + calyx_ffi_path.to_string_lossy().to_string().into(), + )])), + )])), + ); + + fs::write(&manifest_path, manifest.to_string())?; + fs::rename(&test_path, &lib_path)?; + + let output = Command::new("cargo") + .arg("run") + .arg("--quiet") + .current_dir(work_dir.path()) + .output()?; + io::stderr().write_all(&output.stderr)?; + io::stdout().write_all(&output.stdout)?; + } + + Ok(()) + } +} diff --git a/tools/tb/src/builtin_plugins/cocotb.rs b/tools/tb/src/builtin_plugins/cocotb.rs new file mode 100644 index 0000000000..2cbd27181e --- /dev/null +++ b/tools/tb/src/builtin_plugins/cocotb.rs @@ -0,0 +1,145 @@ +use std::io::{self, Write}; +use std::process::Command; +use std::{fs, path::Path}; + +use crate::{ + config::{Config, ConfigVarValidator}, + error::{LocalError, LocalResult}, + plugin::Plugin, + semver, tempdir, +}; +use makemake::{emitter::Emitter, makefile::Makefile}; + +/// v1.8.1 cocotb +#[derive(Default)] +pub struct CocoTB; + +mod config_keys { + pub const EXE: &str = "cocotb-config.exe"; + pub const SIM: &str = "sim"; +} + +fn filestem(path_str: &str) -> &str { + let path = Path::new(path_str); + path.file_stem() + .expect("invalid filename") + .to_str() + .expect("invalid unicode") +} + +impl Plugin for CocoTB { + fn name(&self) -> &'static str { + "cocotb" + } + + fn version(&self) -> semver::Version { + semver::Version::new(0, 0, 0) + } + + fn setup(&self, config: &mut Config) -> LocalResult<()> { + config.require( + config_keys::EXE, + Some("cocotb-config"), + "path to cocotb-config executable", + ConfigVarValidator::when(|value| { + if let Some(cmd) = value.as_str() { + let output = Command::new(cmd) + .arg("--version") + .output() + .map_err(LocalError::from).map_err(|_| LocalError::other(format!("{} is not the cocotb-config executable", cmd)))?; + let version = String::from_utf8(output.stdout) + .map_err(|_| LocalError::other(format!("{} is not the cocotb-config executable", cmd)))?; + if version.trim() != "1.8.1" { + Err(LocalError::other("cocotb-config must be version 1.8.1")) + } else { + Ok(()) + } + } else { + Err(LocalError::other( + "the cocotb-config executable path must be specified as a string", + )) + } + }), + ); + + config.require( + config_keys::SIM, + Some("icarus"), + "cocotb simulator", + ConfigVarValidator::when(|value| { + if let Some(sim) = value.as_str() { + let simulators = [ + "icarus", + "verilator", + "vcs", + "riviera", + "activehdl", + "questa", + "modelsim", + "ius", + "xcelium", + "ghdl", + "cvc", + ]; + if simulators.contains(&sim) { + Ok(()) + } else { + Err(LocalError::other("unsupported simulator: see https://docs.cocotb.org/en/stable/simulator_support.html for details")) + } + } else { + Err(LocalError::other( + "the cocotb simulator must be a string", + )) + } + }), + ); + + Ok(()) + } + + fn run( + &self, + input: String, + tests: &[String], + work_dir: tempdir::TempDir, + config: &Config, + ) -> LocalResult<()> { + for test in tests { + // copied from https://github.com/cocotb/cocotb/blob/v1.8.1/examples/doc_examples/quickstart/Makefile + let mut makefile = Makefile::new(); + makefile.comment("This file is public domain, it can be freely copied without restrictions."); + makefile.comment("SPDX-License-Identifier: CC0-1.0"); + makefile.newline(); + makefile.comment("Makefile"); + makefile.newline(); + makefile.comment("defaults"); + makefile.assign_without_overwrite("SIM", "icarus"); + makefile.assign_without_overwrite("TOPLEVEL_LANG", "verilog"); + makefile.append("VERILOG_SOURCES", &input); + makefile.comment("use VHDL_SOURCES for VHDL files"); + makefile.newline(); + makefile.comment("TOPLEVEL is the name of the toplevel module in your Verilog or VHDL file"); + makefile.assign("TOPLEVEL", filestem(&input)); + makefile.newline(); + makefile.comment("MODULE is the basename of the Python test file"); + makefile.assign("MODULE", filestem(test)); + makefile.newline(); + makefile.comment("include cocotb's make rules to take care of the simulator setup"); + makefile.include(format!( + "$(shell {} --makefiles)/Makefile.sim", + config.get(config_keys::EXE)?.as_str().unwrap() + )); + + let mut makefile_path = work_dir.path().to_path_buf(); + makefile_path.push("Makefile"); + fs::write(makefile_path, makefile.build())?; + + let output = + Command::new("make").current_dir(work_dir.path()).output()?; + io::stdout().write_all(&output.stdout)?; + io::stderr().write_all(&output.stderr)?; + } + + Ok(()) + } +} diff --git a/tools/tb/src/builtin_plugins/resources/driver.rs b/tools/tb/src/builtin_plugins/resources/driver.rs new file mode 100644 index 0000000000..bce3015661 --- /dev/null +++ b/tools/tb/src/builtin_plugins/resources/driver.rs @@ -0,0 +1,9 @@ +use calyx_ffi::prelude::*; +use test_crate::calyx_ffi_generated_top::run_tests; + +fn main() { + let mut ffi = CalyxFFI::default(); + unsafe { + run_tests(&mut ffi); + } +} diff --git a/tools/tb/src/builtin_plugins/verilator.rs b/tools/tb/src/builtin_plugins/verilator.rs new file mode 100644 index 0000000000..8f1a3a3457 --- /dev/null +++ b/tools/tb/src/builtin_plugins/verilator.rs @@ -0,0 +1,136 @@ +use std::{ + io::{self, Write}, + process::Command, +}; + +use crate::{ + config::{Config, ConfigVarValidator}, + error::{LocalError, LocalResult}, + plugin::Plugin, + semver, tempdir, +}; + +mod config_keys { + pub const EXE: &str = "exe"; + pub const CFLAGS: &str = "cflags"; + pub const TOP: &str = "top"; + pub const USE_SV: &str = "use-sv"; +} + +#[derive(Default)] +pub struct Verilator; + +impl Verilator { + fn create_build_files( + &self, + input: &str, + test: &str, + work_dir: &tempdir::TempDir, + config: &Config, + ) -> LocalResult { + let mut cmd = + Command::new(config.get(config_keys::EXE)?.as_str().unwrap()); + cmd.current_dir(work_dir.path()); + cmd.args([ + "--cc", "--exe", "--build", "--timing", "-j", "0", "-Wall", input, + test, + ]); + cmd.args([ + "--top-module", + config.get(config_keys::TOP)?.as_str().unwrap(), + ]); + + let cflags = config.get(config_keys::CFLAGS)?; + let cflags = cflags.as_str().unwrap(); + cmd.args(["-CFLAGS", if cflags.is_empty() { "\"\"" } else { cflags }]); + if config.get(config_keys::USE_SV)?.as_str().unwrap() == "true" { + cmd.arg("-sv"); + } + + let output = cmd.output()?; + io::stdout().write_all(&output.stdout)?; + io::stderr().write_all(&output.stderr)?; + + Ok(format!( + "obj_dir/V{}", + config.get(config_keys::TOP)?.as_str().unwrap() + )) + } + + fn execute_harness( + &self, + executable: String, + work_dir: &tempdir::TempDir, + ) -> LocalResult<()> { + let output = Command::new(executable) + .current_dir(work_dir.path()) + .output()?; + io::stdout().write_all(&output.stdout)?; + io::stderr().write_all(&output.stderr)?; + + Ok(()) + } +} + +impl Plugin for Verilator { + fn name(&self) -> &'static str { + "verilator" + } + + fn version(&self) -> semver::Version { + semver::Version::new(0, 0, 0) + } + + fn setup(&self, config: &mut Config) -> LocalResult<()> { + config.require( + config_keys::EXE, + Some("verilator"), + "path to verilator executable", + ConfigVarValidator::default(), + ); + + config.require( + config_keys::CFLAGS, + Some(""), + "passed via -CFLAGS", + ConfigVarValidator::default(), + ); + + config.require( + config_keys::TOP, + Some("main"), + "name of top-level module", + ConfigVarValidator::default(), + ); + + config.require( + config_keys::USE_SV, + Some("true"), + "whether the input is SystemVerilog", + ConfigVarValidator::when(|value| { + value + .as_str() + .filter(|value| ["true", "false"].contains(value)) + .ok_or(LocalError::other("must be true or false")) + .map(|_| ()) + }), + ); + + Ok(()) + } + + fn run( + &self, + input: String, + tests: &[String], + work_dir: tempdir::TempDir, + config: &Config, + ) -> LocalResult<()> { + for test in tests { + let exec = + self.create_build_files(&input, test, &work_dir, config)?; + self.execute_harness(exec, &work_dir)?; + } + Ok(()) + } +} diff --git a/tools/tb/src/cli.rs b/tools/tb/src/cli.rs new file mode 100644 index 0000000000..c04aa8de5c --- /dev/null +++ b/tools/tb/src/cli.rs @@ -0,0 +1,69 @@ +use std::{env, path::PathBuf, str::FromStr}; + +use argh::FromArgs; + +use crate::error::{LocalError, LocalResult}; + +pub struct ConfigSet { + pub key: String, + pub value: String, +} + +impl FromStr for ConfigSet { + type Err = LocalError; + + fn from_str(s: &str) -> LocalResult { + s.find('=') + .map(|index| { + let (key, value) = s.split_at(index); + let value = value.chars().skip(1).collect(); + ConfigSet { + key: key.to_string(), + value, + } + }) + .ok_or(LocalError::other("expected syntax 'key=value'")) + } +} + +#[derive(FromArgs, Default)] +/// Test verilog files under various harnesses. +pub struct CLI { + #[argh(positional)] + /// verilog file + pub input: String, + + #[argh(option, short = 't', long = "test")] + /// test harness + pub tests: Vec, + + #[argh(option, short = 's')] + /// set a config option + pub set: Vec, + + #[argh(option, short = 'u')] + /// the testbench to invoke + pub using: String, + + /// path to the config file + #[argh(option, short = 'c')] + pub config: Option, + + #[argh(switch)] + /// displays version information + pub version: bool, +} + +impl CLI { + pub fn from_env() -> Self { + let args: Vec<_> = env::args().collect(); + if args.len() == 2 && matches!(args[1].as_str(), "-v" | "--version") { + Self { + version: true, + ..Default::default() + } + } else { + argh::from_env() + } + } +} diff --git a/tools/tb/src/config.rs b/tools/tb/src/config.rs new file mode 100644 index 0000000000..7bea9ab6bb --- /dev/null +++ b/tools/tb/src/config.rs @@ -0,0 +1,177 @@ +use crate::error::{LocalError, LocalResult}; +use figment::providers::Format; +use figment::value::Value; +use figment::Figment; +use std::fmt::Debug; +use std::path::Path; +use std::rc::Rc; + +pub type ConfigVarValidatorPredicate = fn(&Value) -> LocalResult<()>; + +/// TODO: make this declarative, allow building complex things in some sort of +/// eDSL fashion, with helpers for like "this must be a string", "this must be a +/// command and running it yields this output", etc. +pub struct ConfigVarValidator { + predicates: Vec, +} + +impl ConfigVarValidator { + pub fn when(predicate: ConfigVarValidatorPredicate) -> Self { + Self { + predicates: vec![predicate], + } + } + + pub fn and(mut self, predicate: ConfigVarValidatorPredicate) -> Self { + self.predicates.push(predicate); + self + } + + pub(crate) fn run(&self, value: &Value) -> LocalResult<()> { + self.predicates + .iter() + .try_for_each(|predicate| predicate(value)) + } +} + +impl Default for ConfigVarValidator { + fn default() -> Self { + Self::when(|_| Ok(())) + } +} + +impl Debug for ConfigVarValidator { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "ConfigVarValidator {{ predicates: vec![{}] }}", + self.predicates + .iter() + .map(|p| format!("{:p}", p)) + .collect::>() + .join(", ") + ) + } +} + +#[derive(Debug, Clone)] +pub struct ConfigVar { + key: String, + description: String, + validator: Rc, +} + +impl ConfigVar { + pub(crate) fn from, T: AsRef>( + key: S, + description: T, + validator: ConfigVarValidator, + ) -> Self { + Self { + key: key.as_ref().to_string(), + description: description.as_ref().to_string(), + validator: Rc::new(validator), + } + } + + pub fn key(&self) -> &String { + &self.key + } + + pub fn description(&self) -> &String { + &self.description + } + + pub fn validate(&self, value: &Value) -> LocalResult<()> { + self.validator.run(value) + } +} + +#[derive(Debug)] +pub enum InvalidConfigVar { + Missing(ConfigVar, Box), + Incorrect(ConfigVar, Box), +} + +pub struct Config { + /// DO NOT USE DIRECTLY. use [`Config::get`] instead. + figment: Figment, + profile: String, // since figment doesn't want to work + required: Vec, +} + +impl Config { + pub fn from, S: AsRef>( + path: P, + profile: S, + ) -> LocalResult { + use figment::providers::Toml; + let toml = Toml::file(path); + Ok(Self { + figment: Figment::from(toml), + profile: profile.as_ref().to_string(), + required: Vec::new(), + }) + } + + pub fn get>(&self, key: S) -> LocalResult { + self.figment + .find_value(&self.fix_key(key)) + .map_err(Into::into) + } + + pub fn require, T: AsRef, V: Into>( + &mut self, + key: S, + default: Option, + description: T, + validator: ConfigVarValidator, + ) { + if let Some(default) = default { + if self.get(key.as_ref()).is_err() { + self.set(&key, default); + } + } + self.required + .push(ConfigVar::from(key, description, validator)); + } + + pub(crate) fn doctor(&self) -> LocalResult<()> { + let mut errors = vec![]; + for required_key in &self.required { + match self.get(&required_key.key) { + Ok(value) => { + if let Err(error) = required_key.validate(&value) { + errors.push(InvalidConfigVar::Incorrect( + required_key.clone(), + Box::new(error), + )) + } + } + Err(error) => errors.push(InvalidConfigVar::Missing( + required_key.clone(), + Box::new(error), + )), + } + } + if errors.is_empty() { + Ok(()) + } else { + Err(LocalError::InvalidConfig(errors)) + } + } + + pub(crate) fn set, V: Into>( + &mut self, + key: S, + value: V, + ) { + let new_figment = std::mem::take(&mut self.figment); + self.figment = + new_figment.join((self.fix_key(key.as_ref()), value.into())); + } + + fn fix_key>(&self, key: S) -> String { + format!("{}.{}", self.profile, key.as_ref()) + } +} diff --git a/tools/tb/src/driver.rs b/tools/tb/src/driver.rs new file mode 100644 index 0000000000..9859003986 --- /dev/null +++ b/tools/tb/src/driver.rs @@ -0,0 +1,176 @@ +use std::{ + collections::HashMap, + io, + path::{Path, PathBuf}, +}; + +use crate::{ + builtin_plugins::{calyx::CalyxTB, cocotb::CocoTB, verilator::Verilator}, + cli::ConfigSet, + config::Config, + error::{LocalError, LocalResult}, + plugin::{Plugin, PluginCreate, PluginRef}, +}; +use libloading::{Library, Symbol}; +use semver::VersionReq; +use tempdir::TempDir; + +#[derive(Default)] +pub struct Driver { + plugins: HashMap, + loaded_libraries: Vec, +} + +impl Driver { + pub fn load(plugin_dirs: &[PathBuf]) -> LocalResult { + let mut new_self = Self::default(); + + let cocotb = Box::new(CocoTB); + let verilator = Box::new(Verilator); + let calyx = Box::new(CalyxTB); + new_self.register(cocotb.name(), cocotb); + new_self.register(verilator.name(), verilator); + new_self.register(calyx.name(), calyx); + + for plugin_dir in plugin_dirs { + match plugin_dir.read_dir().map_err(LocalError::from) { + Ok(library_paths) => { + for library_path in library_paths { + let library_path = + library_path.map_err(LocalError::from)?.path(); + if library_path.is_file() + && library_path + .extension() + .map(|e| e == "so" || e == "dylib") + .unwrap_or_default() + { + let library = + unsafe { Library::new(&library_path).unwrap() }; + new_self.load_plugin(&library_path, library)?; + } + } + } + Err(error) => { + log::warn!( + "Error processing plugin directory {}: {}", + plugin_dir.to_string_lossy(), + error + ) + } + } + } + Ok(new_self) + } + + pub fn register>(&mut self, name: S, tb: PluginRef) { + assert!( + self.plugins.insert(name.as_ref().to_string(), tb).is_none(), + "cannot re-register the same testbench name for a different testbench" + ); + } + + fn load_plugin( + &mut self, + path: &Path, + library: Library, + ) -> LocalResult<()> { + // todo: better way to do this + let req = + VersionReq::parse(&format!(">={}", env!("CARGO_PKG_VERSION"))) + .unwrap(); + + let create_plugin: Symbol = + unsafe { library.get(b"_plugin_create") }.map_err(|_| { + LocalError::other(format!( + "Plugin '{}' must `declare_plugin!`.", + extract_plugin_name(path) + )) + })?; + let boxed_raw = unsafe { create_plugin() }; + let plugin = unsafe { Box::from_raw(boxed_raw) }; + let plugin_version = plugin.version(); + if !req.matches(&plugin_version) { + log::warn!("Skipping loading {} because its version ({}) is not compatible with {}", plugin.name(), plugin_version, req); + return Ok(()); + } + self.register(plugin.name(), plugin); + self.loaded_libraries.push(library); + Ok(()) + } + + pub fn run, P: AsRef>( + &self, + name: S, + config_path: P, + config_sets: Vec, + input: String, + tests: &[String], + ) -> LocalResult<()> { + if let Some(plugin) = self.plugins.get(name.as_ref()) { + let work_dir = + TempDir::new(".calyx-tb").map_err(LocalError::from)?; + let mut config = Config::from(config_path, name)?; + for config_set in config_sets { + config.set(config_set.key, config_set.value); + } + let input = + copy_into(input, &work_dir).map_err(LocalError::from)?; + let mut test_basenames = vec![]; + for test in tests { + test_basenames.push( + copy_into(test, &work_dir).map_err(LocalError::from)?, + ); + } + plugin.setup(&mut config)?; + config.doctor()?; + plugin.run(input, &test_basenames, work_dir, &config) + } else { + Err(LocalError::Other(format!( + "Unknown testbench '{}'", + name.as_ref() + ))) + } + } +} + +fn copy_into>(file: S, work_dir: &TempDir) -> io::Result { + let from_path = PathBuf::from(file.as_ref()); + let basename = from_path + .file_name() + .ok_or_else(|| { + io::Error::new(io::ErrorKind::Other, "path ended with '..'") + })? + .to_str() + .ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidData, "invalid unicode") + })? + .to_string(); + let to_path = work_dir.path().join(&basename); + + if from_path.is_dir() { + fs_extra::dir::copy( + from_path, + work_dir.path(), + &fs_extra::dir::CopyOptions::new(), + ) + .map_err(io::Error::other)?; + } else { + fs_extra::file::copy( + from_path, + to_path, + &fs_extra::file::CopyOptions::new(), + ) + .map_err(io::Error::other)?; + } + + Ok(basename) +} + +fn extract_plugin_name(path: &Path) -> &str { + let stem = path + .file_stem() + .expect("invalid library path") + .to_str() + .expect("invalid unicode"); + stem.strip_prefix("lib").unwrap_or(stem) +} diff --git a/tools/tb/src/error.rs b/tools/tb/src/error.rs new file mode 100644 index 0000000000..3f34330505 --- /dev/null +++ b/tools/tb/src/error.rs @@ -0,0 +1,64 @@ +use crate::config::InvalidConfigVar; +use std::{fmt::Display, io}; + +#[derive(Debug)] +pub enum LocalError { + IO(io::Error), + Figment(figment::Error), + InvalidConfig(Vec), + Other(String), +} + +impl LocalError { + pub fn other>(msg: S) -> Self { + Self::Other(msg.as_ref().to_string()) + } +} + +impl From for LocalError { + fn from(value: io::Error) -> Self { + Self::IO(value) + } +} + +impl From for LocalError { + fn from(value: figment::Error) -> Self { + Self::Figment(value) + } +} + +impl Display for LocalError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + LocalError::IO(io_err) => io_err.fmt(f), + LocalError::Figment(figment_err) => figment_err.fmt(f), + LocalError::InvalidConfig(errors) => { + writeln!(f, "We detected some errors in your config:")?; + for error in errors { + match error { + InvalidConfigVar::Missing(config_var, _) => { + writeln!( + f, + "- missing key '{}': {}", + config_var.key(), + config_var.description() + )?; + } + InvalidConfigVar::Incorrect(config_var, error) => { + writeln!( + f, + "- incorrect key '{}': {}", + config_var.key(), + error + )?; + } + } + } + Ok(()) + } + LocalError::Other(msg) => msg.fmt(f), + } + } +} + +pub type LocalResult = Result; diff --git a/tools/tb/src/lib.rs b/tools/tb/src/lib.rs new file mode 100644 index 0000000000..e943f2b7bd --- /dev/null +++ b/tools/tb/src/lib.rs @@ -0,0 +1,11 @@ +//! Author: Ethan Uppal + +pub mod builtin_plugins; +pub mod cli; +pub mod config; +pub mod driver; +pub mod error; +pub mod plugin; + +pub use semver; +pub use tempdir; diff --git a/tools/tb/src/main.rs b/tools/tb/src/main.rs new file mode 100644 index 0000000000..2b5e453810 --- /dev/null +++ b/tools/tb/src/main.rs @@ -0,0 +1,72 @@ +use std::{ + env, + io::{self, Write}, + path::PathBuf, +}; +use tb::{ + cli::CLI, + driver::Driver, + error::{LocalError, LocalResult}, +}; + +const CONFIG_FILE_NAME: &str = "calyx-tb.toml"; + +fn setup_logging() { + if env::var("RUST_LOG").is_err() { + env::set_var("RUST_LOG", "warn"); + } + if env::var("NO_COLOR").is_err() { + env::set_var("RUST_LOG_STYLE", "always"); + } + + env_logger::builder().format_target(false).init(); +} + +fn run_app(args: CLI) -> LocalResult<()> { + let config_path = match args.config { + Some(config_path) => config_path, + None => { + log::info!( + "No config file specified, using default: {}", + CONFIG_FILE_NAME + ); + let mut config_path = + PathBuf::from(env::var("HOME").expect("user has no $HOME :(")); + config_path.push(".config"); + config_path.push(CONFIG_FILE_NAME); + config_path + } + }; + + if !config_path.exists() { + return Err(LocalError::other(format!( + "missing config file {}", + config_path.to_string_lossy() + ))); + } + + let default_loc = { + let mut default_loc = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + default_loc.push("plugins"); + default_loc + }; + let driver = Driver::load(&[default_loc])?; + driver.run(args.using, config_path, args.set, args.input, &args.tests) +} + +fn main() -> io::Result<()> { + setup_logging(); + + let args = CLI::from_env(); + + if args.version { + println!("{}", env!("CARGO_PKG_VERSION")); + return Ok(()); + } + + if let Err(error) = run_app(args) { + write!(&mut io::stderr(), "{}", error)?; + } + + Ok(()) +} diff --git a/tools/tb/src/plugin.rs b/tools/tb/src/plugin.rs new file mode 100644 index 0000000000..a5471102db --- /dev/null +++ b/tools/tb/src/plugin.rs @@ -0,0 +1,42 @@ +use crate::{config::Config, error::LocalResult}; +use semver::Version; +use tempdir::TempDir; + +pub trait Plugin: Send + Sync { + /// A unique name for this plugin. + fn name(&self) -> &'static str; + + /// The version of tb this plugin was built for. + fn version(&self) -> Version; + + /// Declares the configuration for this plugin. + fn setup(&self, config: &mut Config) -> LocalResult<()>; + + /// Runs this plugin's testbench. + /// - `input` is a relative path to the input file in `work_dir`. + /// - `tests` are a relative paths to the testing harnesses in `work_dir`. + fn run( + &self, + input: String, + tests: &[String], + work_dir: TempDir, + config: &Config, + ) -> LocalResult<()>; +} + +pub type PluginRef = Box; + +// https://www.michaelfbryan.com/rust-ffi-guide/dynamic_loading.html +pub type PluginCreate = unsafe fn() -> *mut dyn Plugin; + +/// `declare_plugin!(MyPlugin, MyPlugin::constructor)` exposes `MyPlugin` to the +/// world as constructable by the zero-arity `MyPlugin::constructor`. +#[macro_export] +macro_rules! declare_plugin { + ($plugin_type:ty, $constructor:path) => { + #[no_mangle] + pub extern "C" fn _plugin_create() -> *mut dyn $crate::plugin::Plugin { + Box::into_raw(Box::new($constructor())) + } + }; +}