diff --git a/Cargo.lock b/Cargo.lock index 154e1ff3ca..a651a1bf49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -518,6 +518,9 @@ name = "camino" version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab" +dependencies = [ + "serde", +] [[package]] name = "cast" @@ -1314,6 +1317,7 @@ dependencies = [ "egg", "env_logger", "figment", + "insta", "itertools 0.11.0", "libc", "log", @@ -1323,6 +1327,7 @@ dependencies = [ "rand_chacha 0.3.1", "rhai", "serde", + "serde_json", "toml_edit", ] diff --git a/fud2/fud-core/Cargo.toml b/fud2/fud-core/Cargo.toml index 56a30a45ee..d2bafdacc4 100644 --- a/fud2/fud-core/Cargo.toml +++ b/fud2/fud-core/Cargo.toml @@ -14,9 +14,10 @@ description = "Library for building declarative build tools" argh.workspace = true cranelift-entity = "0.103.0" serde.workspace = true +serde_json.workspace = true figment = { version = "0.10.12", features = ["toml"] } pathdiff = { version = "0.2.1", features = ["camino"] } -camino = "1.1.6" +camino = { version = "1.1.6", features = ["serde1"] } anyhow.workspace = true log.workspace = true env_logger.workspace = true @@ -31,6 +32,7 @@ libc = "0.2.174" [dev-dependencies] rand_chacha = "0.3.1" +insta = "1.43.1" [features] egg_planner = ["dep:egg"] diff --git a/fud2/fud-core/src/cli.rs b/fud2/fud-core/src/cli.rs index 2e75f274b5..0d6501dce3 100644 --- a/fud2/fud-core/src/cli.rs +++ b/fud2/fud-core/src/cli.rs @@ -18,6 +18,7 @@ enum Mode { Generate, Run, Cmds, + JsonPlan, } impl FromStr for Mode { @@ -31,6 +32,7 @@ impl FromStr for Mode { "run" => Ok(Mode::Run), "dot" => Ok(Mode::ShowDot), "cmds" => Ok(Mode::Cmds), + "json-plan" => Ok(Mode::JsonPlan), _ => Err("unknown mode".to_string()), } } @@ -45,6 +47,7 @@ impl Display for Mode { Mode::Run => write!(f, "run"), Mode::ShowDot => write!(f, "dot"), Mode::Cmds => write!(f, "cmds"), + Mode::JsonPlan => write!(f, "json-plan"), } } } @@ -57,6 +60,7 @@ enum Planner { #[cfg(feature = "egg_planner")] Egg, Enumerate, + FromJson, } impl FromStr for Planner { @@ -68,6 +72,7 @@ impl FromStr for Planner { #[cfg(feature = "egg_planner")] "egg" => Ok(Planner::Egg), "enumerate" => Ok(Planner::Enumerate), + "json" => Ok(Planner::FromJson), _ => Err("unknown planner".to_string()), } } @@ -80,6 +85,7 @@ impl Display for Planner { #[cfg(feature = "egg_planner")] Planner::Egg => write!(f, "egg"), Planner::Enumerate => write!(f, "enumerate"), + Planner::FromJson => write!(f, "json"), } } } @@ -166,7 +172,7 @@ pub struct FudArgs { #[argh(option)] to: Vec, - /// execution mode (run, plan, emit, gen, dot) + /// execution mode (run, plan, emit, gen, dot, json-plan) #[argh(option, short = 'm', default = "Mode::Run")] mode: Mode, @@ -305,6 +311,7 @@ fn get_request( #[cfg(feature = "egg_planner")] Planner::Egg => Box::new(plan::EggPlanner {}), Planner::Enumerate => Box::new(plan::EnumeratePlanner {}), + Planner::FromJson => Box::new(plan::JsonPlanner {}), }, timing_csv: args.timing_csv.clone(), }) @@ -523,6 +530,7 @@ fn cli_ext( args.force_rebuild, csv_path, )?, + Mode::JsonPlan => run.show_ops_json(), } Ok(()) diff --git a/fud2/fud-core/src/exec/driver.rs b/fud2/fud-core/src/exec/driver.rs index 18f03e65c0..121c528a45 100644 --- a/fud2/fud-core/src/exec/driver.rs +++ b/fud2/fud-core/src/exec/driver.rs @@ -1,9 +1,18 @@ -use super::{OpRef, Operation, Request, Setup, SetupRef, State, StateRef}; -use crate::{run, script, utils}; +use super::{ + OpRef, Operation, Request, Setup, SetupRef, State, StateRef, + plan::PlannerType, +}; +use crate::{flang::ast_to_steps, run, script, utils}; use camino::{Utf8Path, Utf8PathBuf}; use cranelift_entity::PrimaryMap; use rand::distributions::{Alphanumeric, DistString}; -use std::{collections::HashMap, error::Error, ffi::OsStr, fmt::Display}; +use std::{ + collections::HashMap, + error::Error, + ffi::OsStr, + fmt::Display, + io::{self, Read}, +}; type FileData = HashMap<&'static str, &'static [u8]>; @@ -163,11 +172,60 @@ impl Driver { .collect() } + /// Concoct a plan to carry out the requested build. + /// + /// This has to be special cased if the planner is a `PlannerType::FromJson` due to + /// `FindPlan`'s api not letting planners choose filenames for states. + pub fn plan_json(&self, req: &Request) -> Option { + let mut stdin = io::stdin().lock(); + let mut input = String::new(); + let res = stdin.read_to_string(&mut input); + if let Err(e) = res { + eprintln!("error: {e}"); + return None; + } + + let ast = serde_json::from_str(&input); + match ast { + Err(e) => { + eprintln!("error: {e}"); + None + } + Ok(ast) => { + let steps = ast_to_steps(&ast, &self.ops); + let results = self.gen_names( + &req.end_states, + req, + false, + &req.end_states, + ); + let inputs = self.gen_names( + &req.start_states, + req, + true, + &req.start_states, + ); + Some(Plan { + steps, + inputs, + results, + workdir: req.workdir.clone(), + }) + } + } + } + /// Concoct a plan to carry out the requested build. /// /// This works by searching for a path through the available operations from the input state /// to the output state. If no such path exists in the operation graph, we return None. pub fn plan(&self, req: &Request) -> Option { + // Special case if the planner is the one which reads from stdin. + let planner_ty = req.planner.ty(); + if let PlannerType::FromJson = planner_ty { + return self.plan_json(req); + } + // Find a plan through the states. let path = req.planner.find_plan( &req.start_states, diff --git a/fud2/fud-core/src/exec/plan/egg_planner.rs b/fud2/fud-core/src/exec/plan/egg_planner.rs index a9948f3553..b5c0c4e21d 100644 --- a/fud2/fud-core/src/exec/plan/egg_planner.rs +++ b/fud2/fud-core/src/exec/plan/egg_planner.rs @@ -55,7 +55,7 @@ use crate::exec::State; use super::{ super::{OpRef, Operation, StateRef}, - FindPlan, Step, + FindPlan, PlannerType, Step, }; use cranelift_entity::PrimaryMap; use egg::{ @@ -301,4 +301,8 @@ impl FindPlan for EggPlanner { }) .filter(|steps| !steps.is_empty()) } + + fn ty(&self) -> PlannerType { + PlannerType::Egg + } } diff --git a/fud2/fud-core/src/exec/plan/enumerative_planner.rs b/fud2/fud-core/src/exec/plan/enumerative_planner.rs index ba1aff8bf6..20085ec4e3 100644 --- a/fud2/fud-core/src/exec/plan/enumerative_planner.rs +++ b/fud2/fud-core/src/exec/plan/enumerative_planner.rs @@ -2,7 +2,7 @@ use crate::exec::State; use super::{ super::{OpRef, Operation, StateRef}, - FindPlan, Step, + FindPlan, PlannerType, Step, }; use cranelift_entity::PrimaryMap; @@ -177,4 +177,8 @@ impl FindPlan for EnumeratePlanner { ) -> Option> { Self::find_plan(start, end, through, ops) } + + fn ty(&self) -> PlannerType { + PlannerType::Enumerative + } } diff --git a/fud2/fud-core/src/exec/plan/json_planner.rs b/fud2/fud-core/src/exec/plan/json_planner.rs new file mode 100644 index 0000000000..69513b4cdf --- /dev/null +++ b/fud2/fud-core/src/exec/plan/json_planner.rs @@ -0,0 +1,31 @@ +//! A planner which is predetermined by input from stdin. This always returns `None` for the plan +//! and must be special cased by the logic later. Currently planners only emit states with file +//! assignment done later. + +use super::{FindPlan, PlannerType}; + +#[derive(Debug)] +pub struct JsonPlanner {} + +impl FindPlan for JsonPlanner { + fn find_plan( + &self, + _start: &[crate::exec::StateRef], + _end: &[crate::exec::StateRef], + _through: &[crate::exec::OpRef], + _ops: &cranelift_entity::PrimaryMap< + crate::exec::OpRef, + crate::exec::Operation, + >, + _states: &cranelift_entity::PrimaryMap< + crate::exec::StateRef, + crate::exec::State, + >, + ) -> Option> { + None + } + + fn ty(&self) -> PlannerType { + PlannerType::FromJson + } +} diff --git a/fud2/fud-core/src/exec/plan/legacy_planner.rs b/fud2/fud-core/src/exec/plan/legacy_planner.rs index 88284996ea..8c65473dfc 100644 --- a/fud2/fud-core/src/exec/plan/legacy_planner.rs +++ b/fud2/fud-core/src/exec/plan/legacy_planner.rs @@ -2,7 +2,7 @@ use crate::exec::State; use super::{ super::{OpRef, Operation, StateRef}, - FindPlan, + FindPlan, PlannerType, planner::Step, }; use cranelift_entity::{PrimaryMap, SecondaryMap}; @@ -123,4 +123,8 @@ impl FindPlan for LegacyPlanner { assert!(start.len() == 1 && end.len() == 1); Self::find_plan(start[0], end[0], through, ops) } + + fn ty(&self) -> PlannerType { + PlannerType::Legacy + } } diff --git a/fud2/fud-core/src/exec/plan/mod.rs b/fud2/fud-core/src/exec/plan/mod.rs index 2031aab781..ee95ce4192 100644 --- a/fud2/fud-core/src/exec/plan/mod.rs +++ b/fud2/fud-core/src/exec/plan/mod.rs @@ -1,6 +1,7 @@ #[cfg(feature = "egg_planner")] mod egg_planner; mod enumerative_planner; +mod json_planner; mod legacy_planner; mod planner; @@ -8,5 +9,6 @@ mod planner; pub use egg_planner::EggPlanner; pub use enumerative_planner::EnumeratePlanner; +pub use json_planner::JsonPlanner; pub use legacy_planner::LegacyPlanner; -pub use planner::{FindPlan, Step}; +pub use planner::{FindPlan, PlannerType, Step}; diff --git a/fud2/fud-core/src/exec/plan/planner.rs b/fud2/fud-core/src/exec/plan/planner.rs index 030d8d4f91..7182f3f4ca 100644 --- a/fud2/fud-core/src/exec/plan/planner.rs +++ b/fud2/fud-core/src/exec/plan/planner.rs @@ -7,6 +7,14 @@ use super::super::{OpRef, Operation, StateRef}; /// `Step` is an op paired with its used outputs. pub type Step = (OpRef, Vec); +/// The type of a planner. +pub enum PlannerType { + Egg, + Enumerative, + Legacy, + FromJson, +} + /// A reified function for finding a sequence of operations taking a start set of states to an end /// set of states while guaranteing a set of "though" operations is used in the sequence. pub trait FindPlan: std::fmt::Debug { @@ -22,4 +30,7 @@ pub trait FindPlan: std::fmt::Debug { ops: &PrimaryMap, states: &PrimaryMap, ) -> Option>; + + /// Returns the type of the planner. + fn ty(&self) -> PlannerType; } diff --git a/fud2/fud-core/src/flang/ast.rs b/fud2/fud-core/src/flang/ast.rs new file mode 100644 index 0000000000..2fb0035931 --- /dev/null +++ b/fud2/fud-core/src/flang/ast.rs @@ -0,0 +1,127 @@ +//! The AST types used to represent plan files and ways to traverse them + +use camino::Utf8PathBuf; + +use serde::{Deserialize, Serialize}; +use std::ops::ControlFlow; + +/// The type returned by a visitor function. +pub trait VisitorResult { + /// Visitors may find and return data throughout their run using `from_residual`. This is the + /// type of that data. It is common this is set to `()`. + type Residual; + + /// Returns a result build from nothing. + fn output() -> Self; + + /// Returns a result built from a `Residual`. + fn from_residual(r: Self::Residual) -> Self; + + /// Returns signal for how the visitor should continue traversing the AST. + /// + /// `ControlFlow::Continue(())` signals the visitor should continue, traversing the node's + /// children. `ControlFlow::Break(r)` signals the visitor not traverse a node's children and + /// instead to immediately return a `VisitorResult` built from `Residual` `r`. + fn branch(self) -> ControlFlow; +} + +/// It's very common to use a `ControlFlow` as a `VisitorResult` so the implementation is provided +/// here. +impl VisitorResult for ControlFlow { + type Residual = T; + + fn output() -> Self { + ControlFlow::Continue(()) + } + + fn from_residual(r: Self::Residual) -> Self { + ControlFlow::Break(r) + } + + fn branch(self) -> ControlFlow { + self + } +} + +macro_rules! try_visit { + ($e:expr) => { + match $crate::flang::ast::VisitorResult::branch($e) { + core::ops::ControlFlow::Continue(()) => (), + core::ops::ControlFlow::Break(r) => { + return $crate::flang::ast::VisitorResult::from_residual(r); + } + } + }; +} + +/// Implemented by visitors of a flang AST. +pub trait Visitor { + /// This is generally set to `std::ops::ControlFlow`. It is not done so here as a default + /// because that is not yet a stable language feature in rust. + type Result: VisitorResult; + + fn visit_op(&mut self, _f: &Op) -> Self::Result { + Self::Result::output() + } + + fn visit_assignment(&mut self, _a: &Assignment) -> Self::Result { + Self::Result::output() + } + + fn visit_assignment_list(&mut self, _a: &AssignmentList) -> Self::Result { + Self::Result::output() + } +} + +pub trait Visitable { + fn visit(&self, visitor: &mut V) -> V::Result; +} + +pub(crate) type FunId = String; +pub(crate) type VarId = Utf8PathBuf; + +/// A call to an op. For example, `calyx-to-verilog(infile)` +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Op { + pub name: FunId, + pub args: Vec, +} + +impl Visitable for Op { + fn visit(&self, visitor: &mut V) -> V::Result { + try_visit!(visitor.visit_op(self)); + V::Result::output() + } +} + +/// A list of variables being assigned to the result of an op. For example, +/// ```text +/// x, y = op1(in1, in2); +/// ``` +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Assignment { + pub vars: Vec, + pub value: Op, +} + +impl Visitable for Assignment { + fn visit(&self, visitor: &mut V) -> V::Result { + try_visit!(self.value.visit(visitor)); + visitor.visit_assignment(self) + } +} + +/// A list of assignments making up a program. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AssignmentList { + pub assigns: Vec, +} + +impl Visitable for AssignmentList { + fn visit(&self, visitor: &mut V) -> V::Result { + for assign in &self.assigns { + try_visit!(assign.visit(visitor)); + } + V::Result::output() + } +} diff --git a/fud2/fud-core/src/flang/ast_converter.rs b/fud2/fud-core/src/flang/ast_converter.rs new file mode 100644 index 0000000000..b310d78898 --- /dev/null +++ b/fud2/fud-core/src/flang/ast_converter.rs @@ -0,0 +1,93 @@ +use cranelift_entity::PrimaryMap; +use std::{collections::HashMap, ops}; + +use crate::{ + exec::{IO, OpRef, Operation}, + flang::ast::{ + Assignment, AssignmentList, Op, Visitable, Visitor, VisitorResult, + }, +}; + +pub fn steps_to_ast( + plan: &Vec<(OpRef, Vec, Vec)>, + ops: &PrimaryMap, +) -> AssignmentList { + let mut ast = AssignmentList { assigns: vec![] }; + for step in plan { + let vars = step + .1 + .iter() + .map(|v| match v { + IO::StdIO(utf8_path_buf) => utf8_path_buf, + IO::File(utf8_path_buf) => utf8_path_buf, + }) + .cloned() + .collect(); + let args = step + .2 + .iter() + .map(|v| match v { + IO::StdIO(utf8_path_buf) => utf8_path_buf, + IO::File(utf8_path_buf) => utf8_path_buf, + }) + .cloned() + .collect(); + + let fun = Op { + name: ops[step.0].name.clone(), + args, + }; + + let assignment = Assignment { vars, value: fun }; + ast.assigns.push(assignment); + } + + ast +} + +/// A struct to convert a flang AST into the steps of a `Plan`. +struct ASTToStepList { + step_list: Vec<(OpRef, Vec, Vec)>, + name_to_op_ref: HashMap, +} + +impl ASTToStepList { + fn from_ops(ops: &PrimaryMap) -> Self { + let name_to_op_ref = + ops.iter().map(|(k, v)| (v.name.clone(), k)).collect(); + ASTToStepList { + step_list: vec![], + name_to_op_ref, + } + } + + fn step_list_from_ast( + mut self, + ast: &AssignmentList, + ) -> Vec<(OpRef, Vec, Vec)> { + let _ = ast.visit(&mut self); + self.step_list + } +} + +impl Visitor for ASTToStepList { + type Result = ops::ControlFlow<()>; + + fn visit_assignment(&mut self, a: &Assignment) -> Self::Result { + let vars = a.vars.iter().map(|s| IO::File(s.clone())).collect(); + let args = a.value.args.iter().map(|s| IO::File(s.clone())).collect(); + let op_ref = self.name_to_op_ref[&a.value.name]; + + self.step_list.push((op_ref, vars, args)); + Self::Result::output() + } +} + +/// Given a flang AST and a set of ops, returns the steps of a `Plan` which the flang AST +/// represents. +pub fn ast_to_steps( + ast: &AssignmentList, + ops: &PrimaryMap, +) -> Vec<(OpRef, Vec, Vec)> { + ASTToStepList::from_ops(ops).step_list_from_ast(ast) +} diff --git a/fud2/fud-core/src/flang/mod.rs b/fud2/fud-core/src/flang/mod.rs new file mode 100644 index 0000000000..220e6f90b8 --- /dev/null +++ b/fud2/fud-core/src/flang/mod.rs @@ -0,0 +1,4 @@ +mod ast_converter; + +pub mod ast; +pub use ast_converter::{ast_to_steps, steps_to_ast}; diff --git a/fud2/fud-core/src/lib.rs b/fud2/fud-core/src/lib.rs index 143e1fc000..c672c53572 100644 --- a/fud2/fud-core/src/lib.rs +++ b/fud2/fud-core/src/lib.rs @@ -2,6 +2,7 @@ pub mod cli; mod cli_ext; pub mod config; pub mod exec; +pub mod flang; pub mod log_parser; pub mod run; pub mod script; diff --git a/fud2/fud-core/src/run.rs b/fud2/fud-core/src/run.rs index eecd5f565f..a7dcc016b0 100644 --- a/fud2/fud-core/src/run.rs +++ b/fud2/fud-core/src/run.rs @@ -1,3 +1,4 @@ +use crate::flang::steps_to_ast; use crate::uninterrupt::Uninterrupt; use crate::utils::relative_path; use crate::{config, log_parser}; @@ -354,6 +355,13 @@ impl<'a> Run<'a> { println!("}}"); } + /// Emit the sequence of ops used to create a plan as json text + pub fn show_ops_json(&self) { + let ast = steps_to_ast(&self.plan.steps, &self.driver.ops); + let s = serde_json::to_string_pretty(&ast).unwrap(); + println!("{s}"); + } + /// Print the `build.ninja` file to stdout. pub fn emit_to_stdout(&self) -> EmitResult { self.emit(std::io::stdout())