From 6b8390e3115251ed792a40a7d0580687815061a4 Mon Sep 17 00:00:00 2001 From: Samuel Thomas Date: Sun, 2 Jun 2024 18:06:49 -0500 Subject: [PATCH] [fud2] Rhai Plugins (#2078) * initial implementation of rhai based plugin system * cleanup EmitSetup trait situation + appease clippy * fix tests by emitting everything to an intermediate buffer first * use thread_local + once_cell::Lazy to avoid constructing the engine over and over * remove unused import * DriverBuilder::find_state + define plugins in fud2.toml * try building more complicated plugins * unused annotation for add_file * nicer errors for rhai * remove unused imports * clean reporting code slightly * create a new emitter for each rhai function * RhaiEmitter::with to simplifies buffering + unbuffering the emitter * separate plugins.rs into multiple files + rename things * fix lib.rs * error reporting for setup_refs * small string change * change Into to From --- Cargo.lock | 109 ++++++++++++ fud2/fud-core/Cargo.toml | 3 + fud2/fud-core/src/exec/driver.rs | 71 ++++++-- fud2/fud-core/src/lib.rs | 2 + fud2/fud-core/src/run.rs | 49 ++++-- fud2/fud-core/src/script/error.rs | 46 +++++ fud2/fud-core/src/script/exec_scripts.rs | 211 +++++++++++++++++++++++ fud2/fud-core/src/script/mod.rs | 6 + fud2/fud-core/src/script/plugin.rs | 150 ++++++++++++++++ fud2/fud-core/src/script/report.rs | 100 +++++++++++ fud2/plugins/axi.rhai | 49 ++++++ fud2/plugins/calyx.rhai | 18 ++ fud2/plugins/dahlia-to-calyx.rhai | 8 + fud2/plugins/mrxl.rhai | 8 + fud2/plugins/sim.rhai | 50 ++++++ fud2/src/lib.rs | 8 +- fud2/src/main.rs | 4 +- 17 files changed, 858 insertions(+), 34 deletions(-) create mode 100644 fud2/fud-core/src/script/error.rs create mode 100644 fud2/fud-core/src/script/exec_scripts.rs create mode 100644 fud2/fud-core/src/script/mod.rs create mode 100644 fud2/fud-core/src/script/plugin.rs create mode 100644 fud2/fud-core/src/script/report.rs create mode 100644 fud2/plugins/axi.rhai create mode 100644 fud2/plugins/calyx.rhai create mode 100644 fud2/plugins/dahlia-to-calyx.rhai create mode 100644 fud2/plugins/mrxl.rhai create mode 100644 fud2/plugins/sim.rhai diff --git a/Cargo.lock b/Cargo.lock index 271275bfea..1ede71d979 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,6 +35,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b79b82693f705137f8fb9b37871d99e4f9a7df12b917eed79c3d3954830a60b" dependencies = [ "cfg-if", + "const-random", "getrandom", "once_cell", "version_check 0.9.4", @@ -150,6 +151,16 @@ dependencies = [ "serde", ] +[[package]] +name = "ariadne" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44055e597c674aef7cb903b2b9f6e4cba1277ed0d2d61dae7cd52d7ffa81f8e2" +dependencies = [ + "unicode-width", + "yansi", +] + [[package]] name = "arrayvec" version = "0.5.2" @@ -676,6 +687,26 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + [[package]] name = "copy_dir" version = "0.1.3" @@ -776,6 +807,12 @@ version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -1118,12 +1155,15 @@ version = "0.0.2" dependencies = [ "anyhow", "argh", + "ariadne", "camino", "cranelift-entity", "env_logger", "figment", "log", + "once_cell", "pathdiff", + "rhai", "serde", ] @@ -1421,6 +1461,15 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + [[package]] name = "interp" version = "0.1.1" @@ -2200,6 +2249,34 @@ dependencies = [ "dirs", ] +[[package]] +name = "rhai" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a7d88770120601ba1e548bb6bc2a05019e54ff01b51479e38e64ec3b59d4759" +dependencies = [ + "ahash 0.8.10", + "bitflags 2.4.2", + "instant", + "num-traits", + "once_cell", + "rhai_codegen", + "smallvec", + "smartstring", + "thin-vec", +] + +[[package]] +name = "rhai_codegen" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59aecf17969c04b9c0c5d21f6bc9da9fec9dd4980e64d1871443a476589d8c86" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -2491,6 +2568,17 @@ dependencies = [ "serde", ] +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "static_assertions", + "version_check 0.9.4", +] + [[package]] name = "socket2" version = "0.5.6" @@ -2635,6 +2723,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "thin-vec" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a38c90d48152c236a3ab59271da4f4ae63d678c5d7ad6b7714d7cb9760be5e4b" + [[package]] name = "thiserror" version = "1.0.59" @@ -2696,6 +2790,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -3338,6 +3441,12 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "zerocopy" version = "0.7.32" diff --git a/fud2/fud-core/Cargo.toml b/fud2/fud-core/Cargo.toml index 271ea0ffcb..52eae17012 100644 --- a/fud2/fud-core/Cargo.toml +++ b/fud2/fud-core/Cargo.toml @@ -19,3 +19,6 @@ camino = "1.1.6" anyhow.workspace = true log.workspace = true env_logger.workspace = true +rhai = { version = "1.18.0" } +once_cell = "1.19.0" +ariadne = "0.4.1" diff --git a/fud2/fud-core/src/exec/driver.rs b/fud2/fud-core/src/exec/driver.rs index 566b86550b..e5bed7ed60 100644 --- a/fud2/fud-core/src/exec/driver.rs +++ b/fud2/fud-core/src/exec/driver.rs @@ -2,7 +2,7 @@ use super::{OpRef, Operation, Request, Setup, SetupRef, State, StateRef}; use crate::{run, utils}; use camino::{Utf8Path, Utf8PathBuf}; use cranelift_entity::{PrimaryMap, SecondaryMap}; -use std::collections::HashMap; +use std::{collections::HashMap, error::Error, fmt::Display}; #[derive(PartialEq)] enum Destination { @@ -225,7 +225,7 @@ impl Driver { } pub struct DriverBuilder { - name: String, + pub name: String, setups: PrimaryMap, states: PrimaryMap, ops: PrimaryMap, @@ -233,6 +233,27 @@ pub struct DriverBuilder { rsrc_files: Option, } +#[derive(Debug)] +pub enum DriverError { + UnknownState(String), + UnknownSetup(String), +} + +impl Display for DriverError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DriverError::UnknownState(state) => { + write!(f, "Unknown state: {state}") + } + DriverError::UnknownSetup(setup) => { + write!(f, "Unknown state: {setup}") + } + } + } +} + +impl Error for DriverError {} + impl DriverBuilder { pub fn new(name: &str) -> Self { Self { @@ -252,21 +273,12 @@ impl DriverBuilder { }) } - fn add_op( - &mut self, - name: &str, - setups: &[SetupRef], - input: StateRef, - output: StateRef, - emit: T, - ) -> OpRef { - self.ops.push(Operation { - name: name.into(), - setups: setups.into(), - input, - output, - emit: Box::new(emit), - }) + pub fn find_state(&self, needle: &str) -> Result { + self.states + .iter() + .find(|(_, State { name, .. })| needle == name) + .map(|(state_ref, _)| state_ref) + .ok_or_else(|| DriverError::UnknownState(needle.to_string())) } pub fn add_setup( @@ -284,6 +296,31 @@ impl DriverBuilder { self.add_setup(name, func) } + pub fn find_setup(&self, needle: &str) -> Result { + self.setups + .iter() + .find(|(_, Setup { name, .. })| needle == name) + .map(|(setup_ref, _)| setup_ref) + .ok_or_else(|| DriverError::UnknownSetup(needle.to_string())) + } + + pub fn add_op( + &mut self, + name: &str, + setups: &[SetupRef], + input: StateRef, + output: StateRef, + emit: T, + ) -> OpRef { + self.ops.push(Operation { + name: name.into(), + setups: setups.into(), + input, + output, + emit: Box::new(emit), + }) + } + pub fn op( &mut self, name: &str, diff --git a/fud2/fud-core/src/lib.rs b/fud2/fud-core/src/lib.rs index 489530a392..632bfa3f7f 100644 --- a/fud2/fud-core/src/lib.rs +++ b/fud2/fud-core/src/lib.rs @@ -2,6 +2,8 @@ pub mod cli; pub mod config; pub mod exec; pub mod run; +pub mod script; pub mod utils; pub use exec::{Driver, DriverBuilder}; +pub use script::LoadPlugins; diff --git a/fud2/fud-core/src/run.rs b/fud2/fud-core/src/run.rs index ab99da1566..705776ec0f 100644 --- a/fud2/fud-core/src/run.rs +++ b/fud2/fud-core/src/run.rs @@ -47,18 +47,18 @@ pub type EmitResult = std::result::Result<(), RunError>; pub trait EmitBuild { fn build( &self, - emitter: &mut Emitter, + emitter: &mut StreamEmitter, input: &str, output: &str, ) -> EmitResult; } -pub type EmitBuildFn = fn(&mut Emitter, &str, &str) -> EmitResult; +pub type EmitBuildFn = fn(&mut StreamEmitter, &str, &str) -> EmitResult; impl EmitBuild for EmitBuildFn { fn build( &self, - emitter: &mut Emitter, + emitter: &mut StreamEmitter, input: &str, output: &str, ) -> EmitResult { @@ -75,7 +75,7 @@ pub struct EmitRuleBuild { impl EmitBuild for EmitRuleBuild { fn build( &self, - emitter: &mut Emitter, + emitter: &mut StreamEmitter, input: &str, output: &str, ) -> EmitResult { @@ -86,13 +86,13 @@ impl EmitBuild for EmitRuleBuild { /// Code to emit Ninja code at the setup stage. pub trait EmitSetup { - fn setup(&self, emitter: &mut Emitter) -> EmitResult; + fn setup(&self, emitter: &mut StreamEmitter) -> EmitResult; } -pub type EmitSetupFn = fn(&mut Emitter) -> EmitResult; +pub type EmitSetupFn = fn(&mut StreamEmitter) -> EmitResult; impl EmitSetup for EmitSetupFn { - fn setup(&self, emitter: &mut Emitter) -> EmitResult { + fn setup(&self, emitter: &mut StreamEmitter) -> EmitResult { self(emitter) } } @@ -248,7 +248,7 @@ impl<'a> Run<'a> { } pub fn emit(&self, out: T) -> EmitResult { - let mut emitter = Emitter::new( + let mut emitter = StreamEmitter::new( out, self.config_data.clone(), self.plan.workdir.clone(), @@ -293,13 +293,23 @@ impl<'a> Run<'a> { } } -pub struct Emitter<'a> { - pub out: Box, +/// A context for generating Ninja code. +/// +/// Callbacks to build functionality that generate Ninja code (setups and ops) use this +/// to access all the relevant configuration and to write out lines of Ninja code. +pub struct Emitter { + pub out: W, pub config_data: figment::Figment, pub workdir: Utf8PathBuf, } -impl<'a> Emitter<'a> { +/// A generic emitter that outputs to any `Write` stream. +pub type StreamEmitter<'a> = Emitter>; + +/// An emitter that buffers the Ninja code in memory. +pub type BufEmitter = Emitter>; + +impl<'a> StreamEmitter<'a> { fn new( out: T, config_data: figment::Figment, @@ -312,6 +322,23 @@ impl<'a> Emitter<'a> { } } + /// Create a new bufferred emitter with the same configuration. + pub fn buffer(&self) -> BufEmitter { + Emitter { + out: Vec::new(), + config_data: self.config_data.clone(), + workdir: self.workdir.clone(), + } + } + + /// Flush the output from a bufferred emitter to this emitter. + pub fn unbuffer(&mut self, buf: BufEmitter) -> EmitResult { + self.out.write_all(&buf.out)?; + Ok(()) + } +} + +impl Emitter { /// Fetch a configuration value, or panic if it's missing. pub fn config_val(&self, key: &str) -> Result { self.config_data diff --git a/fud2/fud-core/src/script/error.rs b/fud2/fud-core/src/script/error.rs new file mode 100644 index 0000000000..bf2374c0b5 --- /dev/null +++ b/fud2/fud-core/src/script/error.rs @@ -0,0 +1,46 @@ +use rhai::EvalAltResult; + +use std::fmt::Display; + +#[derive(Debug)] +pub(super) struct RhaiSystemError { + kind: RhaiSystemErrorKind, + pub position: rhai::Position, +} + +#[derive(Debug)] +pub(super) enum RhaiSystemErrorKind { + ErrorSetupRef(String), +} + +impl RhaiSystemError { + pub(super) fn setup_ref(v: rhai::Dynamic) -> Self { + Self { + kind: RhaiSystemErrorKind::ErrorSetupRef(v.to_string()), + position: rhai::Position::NONE, + } + } + + pub(super) fn with_pos(mut self, p: rhai::Position) -> Self { + self.position = p; + self + } +} + +impl Display for RhaiSystemError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.kind { + RhaiSystemErrorKind::ErrorSetupRef(v) => { + write!(f, "Unable to construct SetupRef: `{v:?}`") + } + } + } +} + +impl std::error::Error for RhaiSystemError {} + +impl From for Box { + fn from(value: RhaiSystemError) -> Self { + Box::new(EvalAltResult::ErrorSystem("".to_string(), Box::new(value))) + } +} diff --git a/fud2/fud-core/src/script/exec_scripts.rs b/fud2/fud-core/src/script/exec_scripts.rs new file mode 100644 index 0000000000..f35afa3dff --- /dev/null +++ b/fud2/fud-core/src/script/exec_scripts.rs @@ -0,0 +1,211 @@ +use crate::run::{BufEmitter, EmitBuild, EmitResult, EmitSetup, StreamEmitter}; +use camino::Utf8PathBuf; +use once_cell::unsync::Lazy; +use std::{cell::RefCell, path::PathBuf, rc::Rc}; + +use super::report::RhaiReport; + +pub(super) type RhaiResult = Result>; + +#[derive(Clone)] +pub(super) struct RhaiEmitter(Rc>); + +impl RhaiEmitter { + fn with(emitter: &mut StreamEmitter, f: F) -> EmitResult + where + F: Fn(Self), + { + let buf_emit = emitter.buffer(); + let rhai_emit = Self(Rc::new(RefCell::new(buf_emit))); + f(rhai_emit.clone()); + let buf_emit = Rc::into_inner(rhai_emit.0).unwrap().into_inner(); + emitter.unbuffer(buf_emit) + } +} + +pub(super) fn to_rhai_err( + e: E, +) -> Box { + Box::new(rhai::EvalAltResult::ErrorSystem( + "Emitter error".to_string(), + Box::new(e), + )) +} + +pub(super) fn to_str_slice(arr: &rhai::Array) -> Vec { + arr.iter() + .map(|x| x.clone().into_string().unwrap()) + .collect() +} + +impl RhaiEmitter { + fn config_val(&mut self, key: &str) -> RhaiResult { + self.0.borrow().config_val(key).map_err(to_rhai_err) + } + + fn config_or(&mut self, key: &str, default: &str) -> String { + self.0.borrow().config_or(key, default) + } + + fn config_var(&mut self, name: &str, key: &str) -> RhaiResult<()> { + self.0 + .borrow_mut() + .config_var(name, key) + .map_err(to_rhai_err) + } + + fn config_var_or( + &mut self, + name: &str, + key: &str, + default: &str, + ) -> RhaiResult<()> { + self.0 + .borrow_mut() + .config_var_or(name, key, default) + .map_err(to_rhai_err) + } + + fn var(&mut self, name: &str, value: &str) -> RhaiResult<()> { + self.0.borrow_mut().var(name, value).map_err(to_rhai_err) + } + + fn rule(&mut self, name: &str, command: &str) -> RhaiResult<()> { + self.0.borrow_mut().rule(name, command).map_err(to_rhai_err) + } + + fn build( + &mut self, + rule: &str, + input: &str, + output: &str, + ) -> RhaiResult<()> { + self.0 + .borrow_mut() + .build(rule, input, output) + .map_err(to_rhai_err) + } + + fn build_cmd( + &mut self, + targets: rhai::Array, + rule: &str, + deps: rhai::Array, + implicit_deps: rhai::Array, + ) -> RhaiResult<()> { + self.0 + .borrow_mut() + .build_cmd( + &to_str_slice(&targets) + .iter() + .map(|x| &**x) + .collect::>(), + rule, + &to_str_slice(&deps).iter().map(|x| &**x).collect::>(), + &to_str_slice(&implicit_deps) + .iter() + .map(|x| &**x) + .collect::>(), + ) + .map_err(to_rhai_err) + } + + fn comment(&mut self, text: &str) -> RhaiResult<()> { + self.0.borrow_mut().comment(text).map_err(to_rhai_err) + } + + #[allow(unused)] + fn add_file(&mut self, name: &str, contents: &[u8]) -> RhaiResult<()> { + todo!() + } + + fn external_path(&mut self, path: &str) -> Utf8PathBuf { + let utf8_path = Utf8PathBuf::from(path); + self.0.borrow().external_path(&utf8_path) + } + + fn arg(&mut self, name: &str, value: &str) -> RhaiResult<()> { + self.0.borrow_mut().arg(name, value).map_err(to_rhai_err) + } + + fn rsrc(&mut self, filename: &str) -> RhaiResult<()> { + self.0.borrow_mut().rsrc(filename).map_err(to_rhai_err) + } +} + +thread_local! { + /// Construct the engine we will use to evaluate setup functions + /// we do this with Lazy, so that we only create the engine once + /// this also needs to be thread_local so that we don't need + /// `rhai::Engine` to be `Sync`. + static EMIT_ENGINE: Lazy = + Lazy::new(|| { + let mut engine = rhai::Engine::new(); + + engine + .register_type_with_name::("RhaiEmitter") + .register_fn("config_val", RhaiEmitter::config_val) + .register_fn("config_or", RhaiEmitter::config_or) + .register_fn("config_var", RhaiEmitter::config_var) + .register_fn("config_var_or", RhaiEmitter::config_var_or) + .register_fn("var_", RhaiEmitter::var) + .register_fn("rule", RhaiEmitter::rule) + .register_fn("build", RhaiEmitter::build) + .register_fn("build_cmd", RhaiEmitter::build_cmd) + .register_fn("comment", RhaiEmitter::comment) + .register_fn("add_file", RhaiEmitter::add_file) + .register_fn("external_path", RhaiEmitter::external_path) + .register_fn("arg", RhaiEmitter::arg) + .register_fn("rsrc", RhaiEmitter::rsrc); + + engine + }); +} + +#[derive(Clone)] +pub(super) struct RhaiSetupCtx { + pub path: PathBuf, + pub ast: rhai::AST, + pub name: String, +} + +impl EmitSetup for RhaiSetupCtx { + fn setup(&self, emitter: &mut StreamEmitter) -> EmitResult { + RhaiEmitter::with(emitter, |rhai_emit| { + EMIT_ENGINE.with(|e| { + e.call_fn::<()>( + &mut rhai::Scope::new(), + &self.ast, + &self.name, + (rhai_emit.clone(),), + ) + .report(&self.path) + }); + })?; + + Ok(()) + } +} + +impl EmitBuild for RhaiSetupCtx { + fn build( + &self, + emitter: &mut StreamEmitter, + input: &str, + output: &str, + ) -> EmitResult { + RhaiEmitter::with(emitter, |rhai_emit| { + EMIT_ENGINE.with(|e| { + e.call_fn::<()>( + &mut rhai::Scope::new(), + &self.ast, + &self.name, + (rhai_emit.clone(), input.to_string(), output.to_string()), + ) + .report(&self.path) + }); + })?; + + Ok(()) + } +} diff --git a/fud2/fud-core/src/script/mod.rs b/fud2/fud-core/src/script/mod.rs new file mode 100644 index 0000000000..f2529c4304 --- /dev/null +++ b/fud2/fud-core/src/script/mod.rs @@ -0,0 +1,6 @@ +mod error; +mod exec_scripts; +mod plugin; +mod report; + +pub use plugin::LoadPlugins; diff --git a/fud2/fud-core/src/script/plugin.rs b/fud2/fud-core/src/script/plugin.rs new file mode 100644 index 0000000000..c42b4fa015 --- /dev/null +++ b/fud2/fud-core/src/script/plugin.rs @@ -0,0 +1,150 @@ +use crate::{ + config, + exec::{OpRef, SetupRef, StateRef}, + DriverBuilder, +}; +use std::{cell::RefCell, path::PathBuf, rc::Rc}; + +use super::{ + error::RhaiSystemError, + exec_scripts::{to_rhai_err, to_str_slice, RhaiResult, RhaiSetupCtx}, + report::RhaiReport, +}; + +fn to_setup_refs( + ctx: &rhai::NativeCallContext, + setups: rhai::Array, + path: PathBuf, + ast: rhai::AST, + this: Rc>, +) -> RhaiResult> { + setups + .into_iter() + .map(|s| match s.clone().try_cast::() { + Some(fnptr) => Ok(this.borrow_mut().add_setup( + &format!("{} (plugin)", fnptr.fn_name()), + RhaiSetupCtx { + path: path.clone(), + ast: ast.clone(), + name: fnptr.fn_name().to_string(), + }, + )), + // if we can't cast as a FnPtr, try casting as a SetupRef directly + None => s.clone().try_cast::().ok_or_else(move || { + RhaiSystemError::setup_ref(s) + .with_pos(ctx.position()) + .into() + }), + }) + .collect::>>() +} + +pub trait LoadPlugins { + fn load_plugins(self) -> Self; +} + +impl LoadPlugins for DriverBuilder { + fn load_plugins(self) -> Self { + // get list of plugins + let config = config::load_config(&self.name); + let plugin_files = + config.extract_inner::>("plugins").unwrap(); + + // wrap driver in a ref cell, so that we can call it from a + // Rhai context + let this = Rc::new(RefCell::new(self)); + + // scope rhai engine code so that all references to `this` + // are dropped before the end of the function + { + let mut engine = rhai::Engine::new(); + + // register AST independent functions + let t = this.clone(); + engine.register_fn( + "state", + move |name: &str, extensions: rhai::Array| { + let v = to_str_slice(&extensions); + let v = v.iter().map(|x| &**x).collect::>(); + t.borrow_mut().state(name, &v) + }, + ); + + let t = Rc::clone(&this); + engine.register_fn("get_state", move |state_name: &str| { + t.borrow().find_state(state_name).map_err(to_rhai_err) + }); + + let t = Rc::clone(&this); + engine.register_fn("get_setup", move |setup_name: &str| { + t.borrow().find_setup(setup_name).map_err(to_rhai_err) + }); + + // go through each plugin file, and execute the script which adds a plugin + // we need to define the following two functions in the loop because they + // need the ast of the current file + for path in plugin_files { + // compile the file into an Ast + let ast = engine.compile_file(path.clone()).unwrap(); + + let t = Rc::clone(&this); + let rule_ast = ast.clone_functions_only(); + let path_copy = path.clone(); + engine.register_fn::<_, 4, true, OpRef, true, _>( + "rule", + move |ctx: rhai::NativeCallContext, + setups: rhai::Array, + input: StateRef, + output: StateRef, + rule_name: &str| { + let setups = to_setup_refs( + &ctx, + setups, + path_copy.clone(), + rule_ast.clone(), + Rc::clone(&t), + )?; + Ok(t.borrow_mut() + .rule(&setups, input, output, rule_name)) + }, + ); + + let t = Rc::clone(&this); + let rule_ast = ast.clone_functions_only(); + let path_copy = path.clone(); + engine.register_fn::<_, 5, true, OpRef, true, _>( + "op", + move |ctx: rhai::NativeCallContext, + name: &str, + setups: rhai::Array, + input: StateRef, + output: StateRef, + build: rhai::FnPtr| { + let setups = to_setup_refs( + &ctx, + setups, + path_copy.clone(), + rule_ast.clone(), + Rc::clone(&t), + )?; + Ok(t.borrow_mut().add_op( + name, + &setups, + input, + output, + RhaiSetupCtx { + path: path_copy.clone(), + ast: rule_ast.clone(), + name: build.fn_name().to_string(), + }, + )) + }, + ); + + engine.run_ast(&ast).report(&path); + } + } + + Rc::into_inner(this).expect("Back into inner").into_inner() + } +} diff --git a/fud2/fud-core/src/script/report.rs b/fud2/fud-core/src/script/report.rs new file mode 100644 index 0000000000..a44b5278dc --- /dev/null +++ b/fud2/fud-core/src/script/report.rs @@ -0,0 +1,100 @@ +use ariadne::{Color, Fmt, Label, Report, ReportKind, Source}; +use rhai::EvalAltResult; +use std::{fs, path::Path}; + +use super::{error::RhaiSystemError, exec_scripts::RhaiResult}; + +pub(super) trait RhaiReport { + fn report_raw, S: AsRef>( + &self, + path: P, + len: usize, + msg: S, + ); + + fn report>(&self, path: P) { + self.report_raw(path, 0, "") + } +} + +impl RhaiReport for rhai::Position { + fn report_raw, S: AsRef>( + &self, + path: P, + len: usize, + msg: S, + ) { + let source = + fs::read_to_string(path.as_ref()).expect("Failed to open file"); + let name = path.as_ref().to_str().unwrap(); + + if let Some(line) = self.line() { + let position = self.position().unwrap_or(1); + // translate a line offset into a char offset + let line_offset = source + .lines() + // take all the lines up to pos.line() + .take(line - 1) + // add one to all the line lengths because `\n` chars are rmeoved with `.lines()` + .map(|line| line.len() + 1) + .sum::(); + + // add the column offset to get the beginning of the error + // we subtract 1, because the positions are 1 indexed + let err_offset = line_offset + (position - 1); + + Report::build(ReportKind::Error, name, err_offset) + .with_message("Failed to load plugin") + .with_label( + Label::new((name, err_offset..err_offset + len)) + .with_message(msg.as_ref().fg(Color::Red)), + ) + .finish() + .eprint((name, Source::from(source))) + .unwrap() + } else { + eprintln!("Failed to load plugin {name}"); + eprintln!(" {}", msg.as_ref()); + } + } +} + +impl RhaiReport for RhaiResult { + fn report_raw, S: AsRef>( + &self, + path: P, + _len: usize, + _msg: S, + ) { + if let Err(e) = self { + match &**e { + EvalAltResult::ErrorVariableNotFound(variable, pos) => { + pos.report_raw(&path, variable.len(), "Undefined variable") + } + EvalAltResult::ErrorFunctionNotFound(msg, pos) => { + let (fn_name, args) = + msg.split_once(' ').unwrap_or((msg, "")); + pos.report_raw( + &path, + fn_name.len(), + format!("{fn_name} {args}"), + ) + } + EvalAltResult::ErrorSystem(msg, err) + if err.is::() => + { + let e = err.downcast_ref::().unwrap(); + let msg = if msg.is_empty() { + format!("{err}") + } else { + format!("{msg}: {err}") + }; + e.position.report_raw(&path, 0, msg) + } + // for errors that we don't have custom processing, just point + // to the beginning of the error, and use the error Display as message + e => e.position().report_raw(&path, 0, format!("{e}")), + } + } + } +} diff --git a/fud2/plugins/axi.rhai b/fud2/plugins/axi.rhai new file mode 100644 index 0000000000..d37f6e6601 --- /dev/null +++ b/fud2/plugins/axi.rhai @@ -0,0 +1,49 @@ +let calyx = get_state("calyx"); + +fn wrapper_setup(e) { + e.config_var_or("axi-generator", "axi.generator", "$calyx-base/yxi/axi-calyx/axi-generator.py"); + e.config_var_or("python", "python", "python3"); + e.rule("gen-axi", "$python $axi-generator $in > $out"); + + // Define a simple `combine` rule that just concats any number of files. + e.rule("combine", "cat $in > $out"); + e.rule("remove-imports", "sed '1,/component main/{/component main/!d; }' $in > $out"); +} + +/// Replace the extension in `path` with `new_ext` +fn replace_ext(path, new_ext) { + if "." in path { + let no_ext = path.split_rev(".", 2)[-1]; + return `${no_ext}.${new_ext}`; + } else { + return `${path}.${new_ext}`; + } +} + +fn axi_wrapped_op(e, input, output) { + let file_name = input.split("/")[-1]; + let tmp_yxi = replace_ext(file_name, "yxi"); + + e.build_cmd([tmp_yxi], "calyx", [input], []); + e.arg("backend", "yxi"); + + let refified_calyx = `refified_${file_name}.futil`; + e.build_cmd([refified_calyx], "calyx-pass", [input], []); + e.arg("pass", "external-to-ref"); + + let axi_wrapper = "axi_wrapper.futil"; + e.build_cmd([axi_wrapper], "gen-axi", [tmp_yxi], []); + + let no_imports_calyx = `no_imports_${refified_calyx}`; + e.build_cmd([no_imports_calyx], "remove-imports", [refified_calyx], []); + + e.build_cmd([output], "combine", [axi_wrapper, no_imports_calyx], []); +} + +op( + "axi-wrapped2", + [get_setup("Calyx compiler"), wrapper_setup], + calyx, + calyx, + axi_wrapped_op +); diff --git a/fud2/plugins/calyx.rhai b/fud2/plugins/calyx.rhai new file mode 100644 index 0000000000..1ac3147581 --- /dev/null +++ b/fud2/plugins/calyx.rhai @@ -0,0 +1,18 @@ +fn calyx_setup(e) { + e.config_var("calyx-base", "calyx.base"); + e.config_var_or("calyx-exe", "calyx.exe", "$calyx-base/target/debug/calyx"); + e.config_var_or("args", "calyx.args", ""); + e.rule("calyx", "$calyx-exe -l $calyx-base -b $backend $args $in > $out"); + e.rule("calyx-pass", "$calyx-exe -l $calyx-base -p $pass $args $in > $out"); +} + +op( + "calyx2-to-verilog", + [calyx_setup], + get_state("calyx"), + get_state("verilog"), + |e, input, output| { + e.build_cmd([output], "calyx", [input], []) ; + e.arg("backend", "verilog"); + } +) diff --git a/fud2/plugins/dahlia-to-calyx.rhai b/fud2/plugins/dahlia-to-calyx.rhai new file mode 100644 index 0000000000..0db0247ac7 --- /dev/null +++ b/fud2/plugins/dahlia-to-calyx.rhai @@ -0,0 +1,8 @@ +let dahlia = state("dahlia", ["fuse"]); + +fn dahlia_setup(e) { + e.config_var("dahlia-exe", "dahlia"); + e.rule("dahlia-to-calyx", "$dahlia-exe -b calyx --lower -l error $in -o $out"); +} + +rule([dahlia_setup], dahlia, get_state("calyx"), "dahlia-to-calyx"); diff --git a/fud2/plugins/mrxl.rhai b/fud2/plugins/mrxl.rhai new file mode 100644 index 0000000000..54487f4884 --- /dev/null +++ b/fud2/plugins/mrxl.rhai @@ -0,0 +1,8 @@ +let mrxl = state("mrxl", ["mrxl"]); + +fn mrxl_setup(e) { + e.var_("mrxl-exe", "mrxl"); + e.rule("mrxl-to-calyx", "$mrxl-exe $in > $out"); +} + +rule([mrxl_setup], mrxl, get_state("calyx"), "mrxl-to-calyx"); diff --git a/fud2/plugins/sim.rhai b/fud2/plugins/sim.rhai new file mode 100644 index 0000000000..effcc7dc11 --- /dev/null +++ b/fud2/plugins/sim.rhai @@ -0,0 +1,50 @@ +// This file provides fud2 operations for simulating Calyx + +fn sim_setup(e) { + 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 input data file. `sim.data` is a required option. + let data_name = e.config_val("sim.data"); + let data_path = e.external_path(data_name); + e.var_("sim_data", data_path); + + 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"); + + e.config_var_or("cycle-limit", "sim.cycle_limit", "500000000"); +} + +let simulator = get_state("sim"); +let dat = get_state("dat"); +let vcd = get_state("vcd"); + +op( + "simulate2", + [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"]); + } +); + +op( + "trace2", + [sim_setup], + simulator, + vcd, + |e, input, output| { + e.build_cmd(["sim.log", output], "sim-run", [input, "$datadir"], []); + e.arg("bin", input); + e.arg("args", `+NOTRACE=0 +OUT=${output}`); + } +); diff --git a/fud2/src/lib.rs b/fud2/src/lib.rs index 06c578f3e1..6fdbb01655 100644 --- a/fud2/src/lib.rs +++ b/fud2/src/lib.rs @@ -1,6 +1,6 @@ use fud_core::{ exec::{SetupRef, StateRef}, - run::{EmitResult, Emitter}, + run::{EmitResult, StreamEmitter}, DriverBuilder, }; @@ -261,7 +261,7 @@ pub fn build_driver(bld: &mut DriverBuilder) { }); fn calyx_to_firrtl_helper( - e: &mut Emitter, + e: &mut StreamEmitter, input: &str, output: &str, firrtl_primitives: bool, // Use FIRRTL primitive implementations? @@ -372,7 +372,7 @@ pub fn build_driver(bld: &mut DriverBuilder) { }); fn firrtl_compile_helper( - e: &mut Emitter, + e: &mut StreamEmitter, input: &str, output: &str, firrtl_primitives: bool, @@ -456,7 +456,7 @@ pub fn build_driver(bld: &mut DriverBuilder) { Ok(()) }); fn verilator_build( - e: &mut Emitter, + e: &mut StreamEmitter, input: &str, output: &str, standalone_testbench: bool, diff --git a/fud2/src/main.rs b/fud2/src/main.rs index d84f687d29..68a1f49243 100644 --- a/fud2/src/main.rs +++ b/fud2/src/main.rs @@ -1,5 +1,5 @@ use fud2::build_driver; -use fud_core::{cli, DriverBuilder}; +use fud_core::{cli, DriverBuilder, LoadPlugins}; fn main() -> anyhow::Result<()> { let mut bld = DriverBuilder::new("fud2"); @@ -19,6 +19,6 @@ fn main() -> anyhow::Result<()> { .collect() }); - let driver = bld.build(); + let driver = bld.load_plugins().build(); cli::cli(&driver) }