From de9ca81b652dad9799547be319890286681cb084 Mon Sep 17 00:00:00 2001 From: MultisampledNight Date: Sat, 20 Jan 2024 21:47:33 +0100 Subject: [PATCH 01/10] chore: appease clippy --- crates/app/src/config/config_file.rs | 7 +++++-- crates/app/src/error_reporting.rs | 8 ++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/app/src/config/config_file.rs b/crates/app/src/config/config_file.rs index a0c4db1..a8ba835 100644 --- a/crates/app/src/config/config_file.rs +++ b/crates/app/src/config/config_file.rs @@ -40,10 +40,13 @@ pub(super) fn find_config_file() -> Result { impl Configs { pub fn read(p: PathBuf) -> Result { - match p.extension().map(|v| v.to_str().unwrap()) { + 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(|v| v.to_owned()))), + e => Err(ConfigError::UnknownExtension(e.map(str::to_string))), } } } diff --git a/crates/app/src/error_reporting.rs b/crates/app/src/error_reporting.rs index 33ffff6..bc4c44b 100644 --- a/crates/app/src/error_reporting.rs +++ b/crates/app/src/error_reporting.rs @@ -3,12 +3,12 @@ use std::process; use ron::error::Position; /// Report an `Error` from the `serde_json` crate -pub fn report_serde_json_err(src: &str, err: serde_json::Error) -> ! { +pub fn report_serde_json_err(src: &str, err: &serde_json::Error) -> ! { report_serde_err(src, err.line(), err.column(), err.to_string()) } /// Report a `SpannedError` from the `ron` crate -pub fn report_serde_ron_err(src: &str, err: ron::error::SpannedError) -> ! { +pub fn report_serde_ron_err(src: &str, err: &ron::error::SpannedError) -> ! { let Position { line, col } = err.position; report_serde_err(src, line, col, err.to_string()) } @@ -23,8 +23,8 @@ fn report_serde_err(src: &str, line: usize, col: usize, msg: String) -> ! { .with_message(msg) .with_note("We'd like to give better errors, but serde errors are horrible to work with...") .finish() - .print(("test", Source::from(src))) - .unwrap(); + .eprint(("test", Source::from(src))) + .expect("writing error to stderr failed"); process::exit(1); } From 3c529c3a1a8e5dd26dedf0052c0f4d8da8597b9e Mon Sep 17 00:00:00 2001 From: MultisampledNight Date: Sun, 21 Jan 2024 03:22:46 +0100 Subject: [PATCH 02/10] 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", From 3e208335c36b2b78d379dac4d2d271f11a29fecc Mon Sep 17 00:00:00 2001 From: MultisampledNight Date: Sun, 21 Jan 2024 04:21:33 +0100 Subject: [PATCH 03/10] feat: get full evaluation back online Very hacky, but this is enough to be finished with the graph IR for now. --- .gitignore | 2 + crates/eval/src/kind/debug/mod.rs | 75 +++++++++++++++++++++++++------ crates/eval/src/value/mod.rs | 9 +++- crates/ir/src/id.rs | 4 +- crates/ir/src/lib.rs | 23 ++++++++++ crates/ir/src/value/mod.rs | 3 -- testfiles/bare.ron | 8 ++-- testfiles/gen/.gitkeep | 1 + testfiles/invert.ron | 6 +-- 9 files changed, 105 insertions(+), 26 deletions(-) delete mode 100644 crates/ir/src/value/mod.rs create mode 100644 testfiles/gen/.gitkeep diff --git a/.gitignore b/.gitignore index 49067bd..bca6aa2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ .pre-commit-config.yaml *.pdf /docs/*.png +/testfiles/gen/* +!/testfiles/gen/.gitkeep diff --git a/crates/eval/src/kind/debug/mod.rs b/crates/eval/src/kind/debug/mod.rs index 55c4fbb..1f46c49 100644 --- a/crates/eval/src/kind/debug/mod.rs +++ b/crates/eval/src/kind/debug/mod.rs @@ -1,31 +1,39 @@ -use std::mem; - use ir::{ + id, instruction::{Filter, Kind}, - GraphIr, Instruction, InstructionRef, + GraphIr, Instruction, Map, }; -use crate::value::Dynamic; +use crate::value::Variant; mod instr; #[derive(Debug, Default)] pub struct Evaluator { ir: GraphIr, + + /// What the output of each individual streamer, and as result its output sockets, is. + /// Grows larger as evaluation continues, + /// as there's no mechanism for purging never-to-be-used-anymore instructions yet. + evaluated: Map, } 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; + self.evaluated.clear(); } fn eval_full(&mut self) { + // GraphIr::topological_sort returns InstructionRefs, which are mostly cool + // but we'd like to have them owned, so we can call Self::step without lifetime hassle let queue: Vec = self .ir .topological_sort() .into_iter() .map(Into::into) .collect(); + for instr in queue { self.step(instr); } @@ -35,23 +43,64 @@ impl crate::Evaluator for Evaluator { 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))), + // what inputs does this instr need? fetch them + let inputs: Vec<_> = instr + .input_sources() + .iter() + .map(|source| { + let source_socket = source + .as_ref() + .expect("all inputs to be connected when an instruction is ran"); + self.evaluated + .get(source_socket) + .expect("toposort to yield later instrs only after previous ones") + }) + .collect(); + + // then actually do whatever the instruction should do + // NOTE: makes heavy use of index slicing, + // on the basis that ir::instruction::Kind::socket_count is correct + // TODO: make this a more flexible dispatch-ish arch + let output = match instr.kind { + Kind::Read(details) => Some(Variant::Image(instr::read::read(details))), Kind::Write(details) => { - instr::write::write(details, todo!()); + #[allow(irrefutable_let_patterns)] // will necessarily change + let Variant::Image(input) = inputs[0] else { + panic!("cannot only write images, but received: `{:?}`", inputs[0]); + }; + instr::write::write(details, input); 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"), - }, - ))), + Filter::Invert => { + #[allow(irrefutable_let_patterns)] + let Variant::Image(input) = inputs[0] else { + panic!( + "cannot only filter invert images, but received: `{:?}`", + inputs[0] + ); + }; + + Some(Variant::Image(instr::filters::invert::invert( + input.clone(), + ))) + } }, }; + + if let Some(output) = output { + // TODO: very inaccurate, actually respect individual instructions. + // should be implied by a different arch + // TODO: all of those should not be public, offer some methods to get this on + // `Instruction` instead (can infer short-term based on Kind::socket_count) + let socket = id::Output(id::Socket { + belongs_to: instr.id, + idx: id::SocketIdx(0), + }); + self.evaluated.insert(socket, output); + } } } diff --git a/crates/eval/src/value/mod.rs b/crates/eval/src/value/mod.rs index f3595d0..b1d85d5 100644 --- a/crates/eval/src/value/mod.rs +++ b/crates/eval/src/value/mod.rs @@ -1,5 +1,12 @@ use image::DynamicImage; -pub enum Dynamic { +/// Any runtime value that an instruction can input or output. +/// +/// The name is taken from [Godot's `Variant` type], +/// which is very similar to this one. +/// +/// [Godot's `Variant` type]: https://docs.godotengine.org/en/stable/classes/class_variant.html +#[derive(Clone, Debug)] +pub enum Variant { Image(DynamicImage), } diff --git a/crates/ir/src/id.rs b/crates/ir/src/id.rs index 8f456a8..4834a4f 100644 --- a/crates/ir/src/id.rs +++ b/crates/ir/src/id.rs @@ -49,7 +49,7 @@ impl Input { /// /// In contrast to [`Input`]s, [`Output`]s may be used or unused. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -pub struct Output(pub(super) Socket); +pub struct Output(pub Socket); // TODO: Restrict publicness to super impl Output { #[must_use] @@ -75,7 +75,7 @@ pub struct Socket { /// This really only serves for denoting where a socket is, /// when it's already clear which instruction is referred to. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -pub struct SocketIdx(pub u16); +pub struct SocketIdx(pub u16); // TODO: Restrict publicness to super impl fmt::Debug for SocketIdx { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { diff --git a/crates/ir/src/lib.rs b/crates/ir/src/lib.rs index 68aca01..702244c 100644 --- a/crates/ir/src/lib.rs +++ b/crates/ir/src/lib.rs @@ -27,6 +27,8 @@ pub fn from_ron(source: &str) -> ron::error::SpannedResult { /// The toplevel representation of a whole pipeline. /// +/// # DAGs +/// /// Pipelines may not be fully linear. They may branch out and recombine later on. /// As such, the representation for them which is currently used is a /// [**D**irected **A**cyclic **G**raph](https://en.wikipedia.org/wiki/Directed_acyclic_graph). @@ -56,6 +58,27 @@ 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**. +/// +/// # Usage +/// +/// - If you want to build one from scratch, +/// add a few helper methods like +/// constructing an empty one, +/// adding instructions and +/// adding edges +/// - If you want to construct one from an existing repr, +/// maybe you want to use [`semi_human::GraphIr`]. +/// +/// # Storing additional data +/// +/// Chances are the graph IR seems somewhat fit to put metadata in it. +/// However, most likely you're interacting in context of some other system, +/// and also want to manage and index that data on your own. +/// +/// As such, consider using _secondary_ maps instead. +/// That is, store in a data structure _you_ own a mapping +/// from [`id`]s +/// to whatever data you need. #[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] pub struct GraphIr { /// "Backbone" storage of all **instruction** IDs to diff --git a/crates/ir/src/value/mod.rs b/crates/ir/src/value/mod.rs deleted file mode 100644 index 238b9c2..0000000 --- a/crates/ir/src/value/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub enum DynamicValue { - Image(DynamicImage), -} diff --git a/testfiles/bare.ron b/testfiles/bare.ron index f2a9621..de78944 100644 --- a/testfiles/bare.ron +++ b/testfiles/bare.ron @@ -1,12 +1,12 @@ ( instructions: { 0: Read(( - source: File("testfiles/juan.jpg"), - format: Jpeg, + source: File("testfiles/rails.png"), + format: Png, )), 1: Write(( - target: File("testfiles/out.png"), - format: Png, + target: File("testfiles/gen/out.jpg"), + format: Jpeg, )), }, edges: { diff --git a/testfiles/gen/.gitkeep b/testfiles/gen/.gitkeep new file mode 100644 index 0000000..9daccef --- /dev/null +++ b/testfiles/gen/.gitkeep @@ -0,0 +1 @@ +the testfile scripts will place generated images and media here. thank you for your understanding. diff --git a/testfiles/invert.ron b/testfiles/invert.ron index 3f1f48c..ce8e68d 100644 --- a/testfiles/invert.ron +++ b/testfiles/invert.ron @@ -1,12 +1,12 @@ ( instructions: { 0: Read(( - source: File("testfiles/juan.jpg"), - format: Jpeg, + source: File("testfiles/rails.png"), + format: Png, )), 1: Filter(Invert), 2: Write(( - target: File("testfiles/inverted.png"), + target: File("testfiles/gen/inverted.png"), format: Png, )), }, From 1df57eba6b78f29222abb229c101bc04f522c51f Mon Sep 17 00:00:00 2001 From: MultisampledNight Date: Sun, 21 Jan 2024 04:23:53 +0100 Subject: [PATCH 04/10] chore: remove accidental whitespace at end of line --- docs/design/type-notation.typ | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/design/type-notation.typ b/docs/design/type-notation.typ index ee33517..ad991ee 100644 --- a/docs/design/type-notation.typ +++ b/docs/design/type-notation.typ @@ -34,5 +34,5 @@ One dimensional list of 32 bit signed integers: Due to inference, you'll also be able to use that in some mathematical operations with integers: -`[i32] + i32` is a valid operation, for example (of course, you can't add types.) But that wouldn't add the second one to the list, but rather add the single i32 to all values in the left hand side list). That would also work with more dimensional arrays and dynamic streams like videos. +`[i32] + i32` is a valid operation, for example (of course, you can't add types.) But that wouldn't add the second one to the list, but rather add the single i32 to all values in the left hand side list). That would also work with more dimensional arrays and dynamic streams like videos. From 98f4a6cdeba19ae6ce0d2a20c16104d1dbfc38de Mon Sep 17 00:00:00 2001 From: MultisampledNight Date: Sun, 21 Jan 2024 21:00:36 +0100 Subject: [PATCH 05/10] chore: use to_owned instead of to_string --- crates/app/src/config/config_file.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/app/src/config/config_file.rs b/crates/app/src/config/config_file.rs index aaf92d1..15f660c 100644 --- a/crates/app/src/config/config_file.rs +++ b/crates/app/src/config/config_file.rs @@ -47,7 +47,7 @@ impl Configs { { 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(Config::UnknownExtension(e.map(str::to_string))), + e => Err(Config::UnknownExtension(e.map(str::to_owned))), } } } From bbde1c84ca5dc86f005680201f66256e07aae3b0 Mon Sep 17 00:00:00 2001 From: MultisampledNight Date: Sun, 21 Jan 2024 21:08:27 +0100 Subject: [PATCH 06/10] chore: move error handling todos from msgs into comments --- crates/app/src/main.rs | 10 ++++------ crates/eval/src/kind/debug/instr/mod.rs | 3 ++- crates/ir/src/semi_human.rs | 3 ++- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs index c523594..58103a5 100644 --- a/crates/app/src/main.rs +++ b/crates/app/src/main.rs @@ -10,18 +10,16 @@ mod error_reporting; mod welcome_msg; fn main() { - // TODO: proper error handling + // TODO: proper error handling across the whole function + // don't forget to also look inside `Config` let cfg = Config::read(); 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 source = fs::read_to_string(cfg.source).expect("can't find source file"); + let ir = ir::from_ron(&source).expect("failed to parse source to graph ir"); let mut machine = cfg.evaluator.pick(); machine.feed(ir); diff --git a/crates/eval/src/kind/debug/instr/mod.rs b/crates/eval/src/kind/debug/instr/mod.rs index b78c7b5..14afac8 100644 --- a/crates/eval/src/kind/debug/instr/mod.rs +++ b/crates/eval/src/kind/debug/instr/mod.rs @@ -18,6 +18,7 @@ pub mod write { use ir::instruction::write::{TargetFormat, TargetType, Write}; pub fn write(Write { target, format }: Write, input_data: &DynamicImage) { + // TODO: actual error handling input_data .save_with_format( match target { @@ -28,7 +29,7 @@ pub mod write { TargetFormat::Png => ImageFormat::Png, }, ) - .expect("couldn't save file — come back later and handle me properly please uwu"); + .expect("couldn't save file"); } } diff --git a/crates/ir/src/semi_human.rs b/crates/ir/src/semi_human.rs index 7d7ff83..e8d3bdd 100644 --- a/crates/ir/src/semi_human.rs +++ b/crates/ir/src/semi_human.rs @@ -77,7 +77,8 @@ fn reverse_and_type_edges(edges: Map>) -> Map Date: Sun, 21 Jan 2024 21:28:35 +0100 Subject: [PATCH 07/10] chore: remove confusing comment --- crates/eval/src/kind/debug/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/eval/src/kind/debug/mod.rs b/crates/eval/src/kind/debug/mod.rs index 1f46c49..9b53ebe 100644 --- a/crates/eval/src/kind/debug/mod.rs +++ b/crates/eval/src/kind/debug/mod.rs @@ -19,7 +19,6 @@ pub struct Evaluator { 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; self.evaluated.clear(); } From 10886be00ae79b3e4db31b6d66fa8ef29fb4a46c Mon Sep 17 00:00:00 2001 From: MultisampledNight Date: Sun, 21 Jan 2024 22:11:41 +0100 Subject: [PATCH 08/10] docs(design): add term list --- docs/design/iowo-design.typ | 80 ++++++++++++++++++++++++++++++++++--- docs/template.typ | 7 ++-- 2 files changed, 78 insertions(+), 9 deletions(-) diff --git a/docs/design/iowo-design.typ b/docs/design/iowo-design.typ index c05db3c..0645bee 100644 --- a/docs/design/iowo-design.typ +++ b/docs/design/iowo-design.typ @@ -7,6 +7,74 @@ subtitle: [don't worry, we're just dreaming], ) += Term overview + +/ Processing stages: + Whole workflow of iOwO, + from the source to evaluation. + +/ Source: + Nice textual representation of what iOwO is supposed to do. + Consists of instructions and pipelines. + +/ Graph IR: + Intermediate machine-readable representation of the source. + Can be modified by the optimizer + and is evaluated or "ran" by the runtime. + +/ Optimizer: + Simplifies the graph IR and makes it faster to run. + +/ Runtime: + All-encompassing term for what's done + after a graph IR is optimized and fully ready to go. + +/ Scheduler: + Looks at the graph IR + and decides which evaluator gets to run which part of it. + +/ Evaluator: + One specific implementation of how to run + through the whole graph IR to get its results. + +/ Function: + On the source level and before the graph IR, + anything that can be run with inputs, + possibly receiving outputs. + +/ Instruction: + Function, but in the graph IR and at runtime. + Ask schrottkatze on why the differentiation is important. + +/ Input: + Received by a function or instruction. + Different inputs may result in different behavior + and/or in different outputs. + +/ Argument: + On the source level, + an input which is given ad-hoc + instead of provided through a pipeline. + +/ Output: + Returned by a function or instruction, + and can be fed into other functions or instructions. + +/ Consumer: + Function or instruction that takes at least 1 input. + +/ Streamer: + Function or instruction that returns at least 1 output. + +/ Modifier: + Function or instruction that is _both_ a consumer and a streamer. + +/ Pipeline: + Any chain of streamers and consumers, + possibly with modifiers in-between, + that may branch out + and recombine arbitrarily. + = Processing stages #graphics.stages-overview @@ -152,17 +220,17 @@ Merges and simplifies functions in the graph IR. == Runtime Runs through all functions in the graph IR. -It does not have any significantly other representation, +It does not have any significantly different representation, and despite its name there's _no_ bytecode involved. -Different runtimes are called evaluators. -Evaluators operate on instructions instead of functions. - === Scheduler -Looks at the graph IR and decides when the VM should evaluate what. +Looks at the graph IR and decides when which evaluator gets to evaluate what. -=== VM +=== Evaluator + +Runs instructions given to it in a specific way, +such as for example on the GPU using OpenCL. = Open questions diff --git a/docs/template.typ b/docs/template.typ index 0402015..cedb5e6 100644 --- a/docs/template.typ +++ b/docs/template.typ @@ -33,22 +33,23 @@ // this'd require wrapping the whole document in a show rule // at which point `query` doesn't find anything anymore #let terms = ( + "processing stage", "source", "AST", "graph IR", "runtime", - "evaluator", "optimizer", "scheduler", - "VM", + "evaluator", + "evaluation", "function", "instruction", - "pipeline", "input", "argument", "consumer", "output", "streamer", "modifier", + "pipeline", ) // yes, the shadowing is intentional to avoid accidentally using the list #let terms = regex("\\b(" + terms.map(expand).join("|") + ")\\b") From d79383a7dff9c79754846e5713160acfb18fc7c1 Mon Sep 17 00:00:00 2001 From: Schrottkatze Date: Tue, 23 Jan 2024 12:40:25 +0100 Subject: [PATCH 09/10] cleanup: remove unneeded source format in read instruction The image crate is able to infer the format, so that data is redundant --- crates/eval/src/kind/debug/instr/mod.rs | 2 +- crates/ir/src/instruction/read.rs | 7 ------- testfiles/bare.ron | 1 - testfiles/invert.ron | 1 - 4 files changed, 1 insertion(+), 10 deletions(-) diff --git a/crates/eval/src/kind/debug/instr/mod.rs b/crates/eval/src/kind/debug/instr/mod.rs index 14afac8..77e4101 100644 --- a/crates/eval/src/kind/debug/instr/mod.rs +++ b/crates/eval/src/kind/debug/instr/mod.rs @@ -2,7 +2,7 @@ pub mod read { use image::{io::Reader as ImageReader, DynamicImage}; use ir::instruction::read::{Read, SourceType}; - pub fn read(Read { source, .. }: Read) -> DynamicImage { + pub fn read(Read { source }: Read) -> DynamicImage { // TODO: actual error handling let img = ImageReader::open(match source { SourceType::File(path) => path, diff --git a/crates/ir/src/instruction/read.rs b/crates/ir/src/instruction/read.rs index dfb2275..2e4ab60 100644 --- a/crates/ir/src/instruction/read.rs +++ b/crates/ir/src/instruction/read.rs @@ -4,16 +4,9 @@ use std::path::PathBuf; #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct Read { pub source: SourceType, - pub format: SourceFormat, } #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub enum SourceType { File(PathBuf), } - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -pub enum SourceFormat { - Jpeg, - Png, -} diff --git a/testfiles/bare.ron b/testfiles/bare.ron index de78944..66565bf 100644 --- a/testfiles/bare.ron +++ b/testfiles/bare.ron @@ -2,7 +2,6 @@ instructions: { 0: Read(( source: File("testfiles/rails.png"), - format: Png, )), 1: Write(( target: File("testfiles/gen/out.jpg"), diff --git a/testfiles/invert.ron b/testfiles/invert.ron index ce8e68d..575ad1d 100644 --- a/testfiles/invert.ron +++ b/testfiles/invert.ron @@ -2,7 +2,6 @@ instructions: { 0: Read(( source: File("testfiles/rails.png"), - format: Png, )), 1: Filter(Invert), 2: Write(( From 1772b70172010af94158df4a6e146357582bf234 Mon Sep 17 00:00:00 2001 From: Schrottkatze Date: Fri, 26 Jan 2024 10:55:45 +0100 Subject: [PATCH 10/10] chore: alias `docs` to `doc` in justfile --- justfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/justfile b/justfile index 468c9fb..4d57068 100644 --- a/justfile +++ b/justfile @@ -4,6 +4,9 @@ test: #!/usr/bin/env nu cargo nextest run + +alias doc := docs + # Compile all documentation as in proposals and design documents, placing them under `docs/compiled`. docs: #!/usr/bin/env nu