From 3c529c3a1a8e5dd26dedf0052c0f4d8da8597b9e Mon Sep 17 00:00:00 2001 From: MultisampledNight Date: Sun, 21 Jan 2024 03:22:46 +0100 Subject: [PATCH] chore: rename executor -> evaluator and integrate into app --- Cargo.lock | 3 + crates/app/Cargo.toml | 12 +-- crates/app/src/config.rs | 43 +++++----- crates/app/src/config/cli.rs | 8 ++ crates/app/src/config/config_file.rs | 13 +-- crates/app/src/main.rs | 12 +++ crates/eval/Cargo.toml | 1 + crates/eval/src/cpu/mod.rs | 1 - crates/eval/src/debug/mod.rs | 37 --------- .../instructions => kind/debug/instr}/mod.rs | 0 crates/eval/src/kind/debug/mod.rs | 57 +++++++++++++ crates/eval/src/kind/mod.rs | 1 + crates/eval/src/lib.rs | 61 ++++++++------ crates/ir/src/lib.rs | 83 +++++++++++++++---- docs/design/iowo-design.typ | 10 +-- docs/template.typ | 2 +- 16 files changed, 229 insertions(+), 115 deletions(-) delete mode 100644 crates/eval/src/cpu/mod.rs delete mode 100644 crates/eval/src/debug/mod.rs rename crates/eval/src/{debug/instructions => kind/debug/instr}/mod.rs (100%) create mode 100644 crates/eval/src/kind/debug/mod.rs create mode 100644 crates/eval/src/kind/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 43e560a..7447d0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,6 +63,8 @@ dependencies = [ "ariadne", "clap", "dirs", + "eval", + "ir", "owo-colors", "ron", "serde", @@ -272,6 +274,7 @@ dependencies = [ "clap", "image", "ir", + "serde", ] [[package]] diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index 92315cf..2caaaf4 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -6,14 +6,16 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -clap = { workspace = true, features = [ "derive", "env" ] } -serde = { workspace = true, features = [ "derive" ] } -ron = "0.8" -serde_json = "1.0" ariadne = "0.4" -time = { version = "0.3", features = [ "local-offset" ] } +clap = { workspace = true, features = [ "derive", "env" ] } dirs = "5" +eval = { path = "../eval" } +ir = { path = "../ir" } owo-colors = "4" +ron = "0.8" +serde = { workspace = true, features = [ "derive" ] } +serde_json = "1.0" +time = { version = "0.3", features = [ "local-offset" ] } [lints] workspace = true diff --git a/crates/app/src/config.rs b/crates/app/src/config.rs index 9f2b15a..b4d1ed1 100644 --- a/crates/app/src/config.rs +++ b/crates/app/src/config.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use clap::Parser; use self::{ @@ -10,6 +12,9 @@ mod config_file; /// this struct may hold all configuration pub struct Config { + pub source: PathBuf, + pub evaluator: eval::Available, + pub startup_msg: bool, } @@ -17,37 +22,37 @@ impl Config { /// Get the configs from all possible places (args, file, env...) pub fn read() -> Self { let args = Args::parse(); - let config_path = if let Some(config_path) = args.config_path { - Ok(config_path) + let config = if let Some(config) = args.config_path { + Ok(config) } else { find_config_file() }; // try to read a maybe existing config file - let file_config = if let Ok(config_path) = config_path { - let file_config = Configs::read(config_path); - - match file_config { - Ok(c) => Some(c), - Err(e) => { + let config = config.ok().and_then(|path| { + Configs::read(path).map_or_else( + |e| { eprintln!("Config error: {e:?}"); eprintln!("Proceeding with defaults or cli args..."); None - } - } - } else { - None - }; + }, + Some, + ) + }); - if let Some(file_config) = file_config { + if let Some(file) = config { Self { + source: args.source, + evaluator: args.evaluator.and(file.evaluator).unwrap_or_default(), // this is negated because to an outward api, the negative is more intuitive, // while in the source the other way around is more intuitive - startup_msg: !(args.no_startup_message || file_config.no_startup_message), + startup_msg: !(args.no_startup_message || file.no_startup_message), } } else { Self { + source: args.source, startup_msg: !args.no_startup_message, + evaluator: args.evaluator.unwrap_or_default(), } } } @@ -56,7 +61,7 @@ impl Config { pub mod error { /// Errors that can occur when reading configs #[derive(Debug)] - pub enum ConfigError { + pub enum Config { /// The config dir doesn't exist NoConfigDir, /// We didn't find a config file in the config dir @@ -73,19 +78,19 @@ pub mod error { SerdeRonError(ron::error::SpannedError), } - impl From for ConfigError { + impl From for Config { fn from(value: std::io::Error) -> Self { Self::IoError(value) } } - impl From for ConfigError { + impl From for Config { fn from(value: serde_json::Error) -> Self { Self::SerdeJsonError(value) } } - impl From for ConfigError { + impl From for Config { fn from(value: ron::error::SpannedError) -> Self { Self::SerdeRonError(value) } diff --git a/crates/app/src/config/cli.rs b/crates/app/src/config/cli.rs index bdf769e..1d9c57a 100644 --- a/crates/app/src/config/cli.rs +++ b/crates/app/src/config/cli.rs @@ -4,6 +4,14 @@ use clap::{builder::BoolishValueParser, ArgAction, Parser}; #[derive(Parser)] pub(crate) struct Args { + /// What file contains the pipeline to evaluate. + pub source: PathBuf, + + /// How to actually run the pipeline. + /// Overrides the config file. Defaults to the debug evaluator. + #[arg(short, long)] + pub evaluator: Option, + /// Read this config file. #[arg(short, long)] pub config_path: Option, diff --git a/crates/app/src/config/config_file.rs b/crates/app/src/config/config_file.rs index a8ba835..aaf92d1 100644 --- a/crates/app/src/config/config_file.rs +++ b/crates/app/src/config/config_file.rs @@ -5,7 +5,7 @@ use std::{ use serde::{Deserialize, Serialize}; -use super::error::ConfigError; +use super::error::Config; #[derive(Debug, Serialize, Deserialize)] pub struct Configs { @@ -13,6 +13,7 @@ pub struct Configs { pub example_value: i32, #[serde(default)] pub no_startup_message: bool, + pub evaluator: Option, } /// what the fuck serde why do i need this @@ -21,9 +22,9 @@ fn default_example_value() -> i32 { } /// Find the location of a config file and check if there is, in fact, a file -pub(super) fn find_config_file() -> Result { +pub(super) fn find_config_file() -> Result { let Some(config_path) = dirs::config_dir() else { - return Err(ConfigError::NoConfigDir); + return Err(Config::NoConfigDir); }; let ron_path = config_path.with_file_name("config.ron"); @@ -34,19 +35,19 @@ pub(super) fn find_config_file() -> Result { } else if Path::new(&json_path).exists() { Ok(json_path) } else { - Err(ConfigError::NoConfigFileFound) + Err(Config::NoConfigFileFound) } } impl Configs { - pub fn read(p: PathBuf) -> Result { + pub fn read(p: PathBuf) -> Result { match p .extension() .map(|v| v.to_str().expect("config path to be UTF-8")) { Some("ron") => Ok(serde_json::from_str(&fs::read_to_string(p)?)?), Some("json") => Ok(ron::from_str(&fs::read_to_string(p)?)?), - e => Err(ConfigError::UnknownExtension(e.map(str::to_string))), + e => Err(Config::UnknownExtension(e.map(str::to_string))), } } } diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs index 1347572..c523594 100644 --- a/crates/app/src/main.rs +++ b/crates/app/src/main.rs @@ -1,3 +1,5 @@ +use std::fs; + use config::Config; use welcome_msg::print_startup_msg; @@ -14,4 +16,14 @@ fn main() { if cfg.startup_msg { print_startup_msg(); } + + let source = + fs::read_to_string(cfg.source).expect("can't find source file lol handle me better please"); + + let ir = + ir::from_ron(&source).expect("aww failed to parse source to graph ir handle me better"); + + let mut machine = cfg.evaluator.pick(); + machine.feed(ir); + machine.eval_full(); } diff --git a/crates/eval/Cargo.toml b/crates/eval/Cargo.toml index 1889f32..b542d2f 100644 --- a/crates/eval/Cargo.toml +++ b/crates/eval/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" clap = { workspace = true, features = [ "derive" ] } image = "0.24" ir = { path = "../ir" } +serde = { workspace = true } [lints] workspace = true diff --git a/crates/eval/src/cpu/mod.rs b/crates/eval/src/cpu/mod.rs deleted file mode 100644 index e82d20a..0000000 --- a/crates/eval/src/cpu/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub(crate) struct CpuExecutor; diff --git a/crates/eval/src/debug/mod.rs b/crates/eval/src/debug/mod.rs deleted file mode 100644 index b9cd743..0000000 --- a/crates/eval/src/debug/mod.rs +++ /dev/null @@ -1,37 +0,0 @@ -use ir::instruction::{Filter, Kind}; - -use crate::value::Dynamic; -mod instructions; - -pub struct Executor; - -impl crate::Executor for Executor { - fn execute(instruction: Kind, input: Option) -> Option { - match instruction { - Kind::Read(read_instruction) => { - Some(Dynamic::Image(instructions::read::read(read_instruction))) - } - Kind::Write(write_instruction) => { - instructions::write::write( - write_instruction, - match input { - Some(Dynamic::Image(ref img)) => img, - _ => panic!("awawwawwa"), - }, - ); - None - } - Kind::Math(_) => todo!(), - Kind::Blend(_) => todo!(), - Kind::Noise(_) => todo!(), - Kind::Filter(filter_instruction) => match filter_instruction { - Filter::Invert => Some(Dynamic::Image(instructions::filters::invert::invert( - match input { - Some(Dynamic::Image(img)) => img, - _ => panic!("invalid value type for invert"), - }, - ))), - }, - } - } -} diff --git a/crates/eval/src/debug/instructions/mod.rs b/crates/eval/src/kind/debug/instr/mod.rs similarity index 100% rename from crates/eval/src/debug/instructions/mod.rs rename to crates/eval/src/kind/debug/instr/mod.rs diff --git a/crates/eval/src/kind/debug/mod.rs b/crates/eval/src/kind/debug/mod.rs new file mode 100644 index 0000000..55c4fbb --- /dev/null +++ b/crates/eval/src/kind/debug/mod.rs @@ -0,0 +1,57 @@ +use std::mem; + +use ir::{ + instruction::{Filter, Kind}, + GraphIr, Instruction, InstructionRef, +}; + +use crate::value::Dynamic; +mod instr; + +#[derive(Debug, Default)] +pub struct Evaluator { + ir: GraphIr, +} + +impl crate::Evaluator for Evaluator { + fn feed(&mut self, ir: GraphIr) { + // TODO: should add instead of replace, see note in Evaluator trait above this method + self.ir = ir; + } + + fn eval_full(&mut self) { + let queue: Vec = self + .ir + .topological_sort() + .into_iter() + .map(Into::into) + .collect(); + for instr in queue { + self.step(instr); + } + } +} + +impl Evaluator { + #[allow(clippy::needless_pass_by_value)] + fn step(&mut self, instr: Instruction) { + let _ = match instr.kind { + Kind::Read(details) => Some(Dynamic::Image(instr::read::read(details))), + Kind::Write(details) => { + instr::write::write(details, todo!()); + None + } + Kind::Math(_) => todo!(), + Kind::Blend(_) => todo!(), + Kind::Noise(_) => todo!(), + Kind::Filter(filter_instruction) => match filter_instruction { + Filter::Invert => Some(Dynamic::Image(instr::filters::invert::invert( + match todo!() { + Some(Dynamic::Image(img)) => img, + _ => panic!("invalid value type for invert"), + }, + ))), + }, + }; + } +} diff --git a/crates/eval/src/kind/mod.rs b/crates/eval/src/kind/mod.rs new file mode 100644 index 0000000..2f36523 --- /dev/null +++ b/crates/eval/src/kind/mod.rs @@ -0,0 +1 @@ +pub mod debug; diff --git a/crates/eval/src/lib.rs b/crates/eval/src/lib.rs index 9732bb0..c3a6c38 100644 --- a/crates/eval/src/lib.rs +++ b/crates/eval/src/lib.rs @@ -1,34 +1,43 @@ -use ir::instruction::Kind; -use value::Dynamic; +use ir::GraphIr; -mod debug; +mod kind; mod value; -/// The available executors -/// unused in early dev. -#[derive(Debug, Clone, Copy, clap::ValueEnum)] -pub enum RegisteredExecutor { - /// the debug executor is single threaded and really, *really* slow. And unstable. Don't use. Unless you're a dev working on this. +/// Can collapse a [`GraphIr`] in meaningful ways and do interesting work on it. +/// +/// It's surprisingly difficult to find a fitting description for this. +pub trait Evaluator { + /// Take some [`GraphIr`] which will then be processed later. + /// May be called multiple times, in which the [`GraphIr`]s should add up. + // TODO: atm they definitely don't add up -- add some functionality to GraphIr to + // make it combine two graphs into one + fn feed(&mut self, ir: GraphIr); + + /// Walk through the _whole_ [`GraphIr`] and run through each instruction. + fn eval_full(&mut self); + + // TODO: for an LSP or the like, eval_single which starts at a given instr +} + +/// The available [`Evaluator`]s. +/// +/// Checklist for adding new ones: +/// +/// 1. Create a new module under the [`kind`] module. +/// 2. Add a struct and implement [`Evaluator`] for it. +#[derive(Clone, Copy, Debug, Default, clap::ValueEnum, serde::Deserialize, serde::Serialize)] +pub enum Available { + /// Runs fully on the CPU. Single-threaded, debug-friendly and quick to implement. + #[default] Debug, } -trait Executor { - fn execute(instruction: Kind, input: Option) -> Option; -} - -pub fn execute_all(instructions: Vec) { - let mut tmp = None; - - for instruction in instructions { - tmp = debug::Executor::execute(instruction, tmp); +impl Available { + /// Selects the [`Evaluator`] corresponding to this label. + #[must_use] + pub fn pick(&self) -> Box { + match self { + Self::Debug => Box::new(kind::debug::Evaluator::default()), + } } } - -// scratchpad lol: -// execution structure: -// 1. take in rpl -// 2. analyse/validate structure against allowed executors -// 3. assign executors to instructions -// 4. optimize -// 5. prepare memory management patterns -// 6. run diff --git a/crates/ir/src/lib.rs b/crates/ir/src/lib.rs index ef7a23d..68aca01 100644 --- a/crates/ir/src/lib.rs +++ b/crates/ir/src/lib.rs @@ -56,7 +56,7 @@ pub fn from_ron(source: &str) -> ron::error::SpannedResult { /// So the vertices of the DAG are the **sockets** /// (which are either [`id::Input`] or [`id::Output`] depending on the direction), /// and each **socket** in turn belongs to an **instruction**. -#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] pub struct GraphIr { /// "Backbone" storage of all **instruction** IDs to /// what **kind of instruction** they are. @@ -123,13 +123,13 @@ impl GraphIr { /// to actually have multiple [`GraphIr`]s at one point in time. /// Open an issue if that poses a problem for you. #[must_use] - pub fn resolve<'ir>(&'ir self, subject: &id::Instruction) -> Option> { + pub fn resolve<'ir>(&'ir self, subject: &id::Instruction) -> Option> { let (id, kind) = self.instructions.get_key_value(subject)?; let input_sources = self.input_sources(subject)?.collect(); let output_targets = self.output_targets(subject)?.collect(); - Some(Instruction { + Some(InstructionRef { id, kind, input_sources, @@ -141,7 +141,7 @@ impl GraphIr { /// /// The same caveats as for [`GraphIr::resolve`] apply. #[must_use] - pub fn owner_of_input<'ir>(&'ir self, input: &id::Input) -> Option> { + pub fn owner_of_input<'ir>(&'ir self, input: &id::Input) -> Option> { self.resolve(&input.socket().belongs_to) } @@ -149,7 +149,7 @@ impl GraphIr { /// /// The same caveats as for [`GraphIr::resolve`] apply. #[must_use] - pub fn owner_of_output<'ir>(&'ir self, output: &id::Output) -> Option> { + pub fn owner_of_output<'ir>(&'ir self, output: &id::Output) -> Option> { self.resolve(&output.socket().belongs_to) } @@ -163,7 +163,7 @@ impl GraphIr { #[must_use] // yes, this function could probably return an iterator and be lazy // no, not today - pub fn topological_sort(&self) -> Vec { + pub fn topological_sort(&self) -> Vec { // count how many incoming edges each vertex has let mut nonzero_input_counts: Map<_, NonZeroUsize> = self.rev_edges @@ -240,18 +240,58 @@ impl GraphIr { } } -/// A full instruction in context, with its inputs and outputs. +/// Constructs an [`id::Socket`] a bit more tersely. +fn socket(id: &id::Instruction, idx: u16) -> id::Socket { + id::Socket { + belongs_to: id.clone(), + idx: id::SocketIdx(idx), + } +} + +/// A full instruction bundeled in context, with its inputs and outputs. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct Instruction<'ir> { +pub struct Instruction { + pub id: id::Instruction, + pub kind: instruction::Kind, + + // can't have these two public since then a user might corrupt their length + input_sources: Vec>, + output_targets: Vec>, +} + +impl Instruction { + /// Where this instruction gets its inputs from. + /// + /// [`None`] means that this input is unfilled, + /// and must be filled before the instruction can be ran. + #[must_use] + pub fn input_sources(&self) -> &[Option] { + &self.input_sources + } + + /// To whom outputs are sent. + #[must_use] + pub fn output_targets(&self) -> &[Set] { + &self.output_targets + } +} + +/// [`Instruction`], but every single field is borrowed instead. +/// See its docs. +/// +/// Use the [`From`] impl to handily convert into an [`Instruction`]. +/// The other way around is unlikely to be wanted — since you already have an [`Instruction`], +/// chances are you just want to take a reference (`&`) of it. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct InstructionRef<'ir> { pub id: &'ir id::Instruction, pub kind: &'ir instruction::Kind, - // can't have these two public since then a user might corrupt their length input_sources: Vec>, output_targets: Vec>>, } -impl<'ir> Instruction<'ir> { +impl<'ir> InstructionRef<'ir> { /// Where this instruction gets its inputs from. /// /// [`None`] means that this input is unfilled, @@ -268,10 +308,23 @@ impl<'ir> Instruction<'ir> { } } -/// Constructs an [`id::Socket`] a bit more tersely. -fn socket(id: &id::Instruction, idx: u16) -> id::Socket { - id::Socket { - belongs_to: id.clone(), - idx: id::SocketIdx(idx), +// would love to use ToOwned but Rust has no specialization yet +// and it'd hurt a blanket impl of ToOwned otherwise +impl From> for Instruction { + fn from(source: InstructionRef<'_>) -> Self { + Self { + id: source.id.clone(), + kind: source.kind.clone(), + input_sources: source + .input_sources + .into_iter() + .map(Option::<&_>::cloned) + .collect(), + output_targets: source + .output_targets + .into_iter() + .map(|outputs| outputs.map(Clone::clone).unwrap_or_default()) + .collect(), + } } } diff --git a/docs/design/iowo-design.typ b/docs/design/iowo-design.typ index aa9f9f2..c05db3c 100644 --- a/docs/design/iowo-design.typ +++ b/docs/design/iowo-design.typ @@ -7,7 +7,7 @@ subtitle: [don't worry, we're just dreaming], ) -= Evaluation stages += Processing stages #graphics.stages-overview @@ -18,7 +18,7 @@ This has a number of benefits and implications: - Bugs are easier to trace down to one stage. - Stages are also replacable, pluggable and usable somewhere else. - For example, - one could write a Just-In-Time compiler as a new executor to replace the runtime stage, + one could write a Just-In-Time compiler as a new evaluator to replace the runtime stage, while preserving the source #arrow() graph IR step. However, this also makes the architecture somewhat more complicated. So here we try our best to describe how each stage looks like. If you have any feedback, feel free to drop it on #link("https://forge.katzen.cafe/katzen-cafe/iowo/issues")[the issues in the repository]! @@ -155,12 +155,12 @@ Runs through all functions in the graph IR. It does not have any significantly other representation, and despite its name there's _no_ bytecode involved. -Different runtimes are called executors. -Executors operate on instructions instead of functions. +Different runtimes are called evaluators. +Evaluators operate on instructions instead of functions. === Scheduler -Looks at the graph IR and decides when the VM should execute what. +Looks at the graph IR and decides when the VM should evaluate what. === VM diff --git a/docs/template.typ b/docs/template.typ index a624cc7..0402015 100644 --- a/docs/template.typ +++ b/docs/template.typ @@ -37,7 +37,7 @@ "AST", "graph IR", "runtime", - "executor", + "evaluator", "optimizer", "scheduler",