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/Cargo.lock b/Cargo.lock index e191631..7447d0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,6 +63,8 @@ dependencies = [ "ariadne", "clap", "dirs", + "eval", + "ir", "owo-colors", "ron", "serde", @@ -171,15 +173,6 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" -[[package]] -name = "cli" -version = "0.1.0" -dependencies = [ - "clap", - "executor", - "ir", -] - [[package]] name = "color_quant" version = "1.1.0" @@ -275,12 +268,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] -name = "executor" +name = "eval" version = "0.1.0" dependencies = [ "clap", "image", "ir", + "serde", ] [[package]] @@ -327,6 +321,17 @@ dependencies = [ "spin", ] +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "gif" version = "0.12.0" @@ -380,6 +385,12 @@ dependencies = [ "serde", ] +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + [[package]] name = "jpeg-decoder" version = "0.3.0" @@ -395,6 +406,23 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" +[[package]] +name = "libc" +version = "0.2.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" + +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.1", + "libc", + "redox_syscall", +] + [[package]] name = "lock_api" version = "0.4.11" @@ -445,6 +473,27 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "owo-colors" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f" + [[package]] name = "png" version = "0.17.10" @@ -543,6 +592,12 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "ryu" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + [[package]] name = "scopeguard" version = "1.2.0" @@ -687,6 +742,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "weezl" version = "0.1.7" @@ -825,6 +886,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + [[package]] name = "zune-inflate" version = "0.2.54" diff --git a/Cargo.toml b/Cargo.toml index f4aa395..6d7f53d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,14 @@ [workspace] members = [ - "crates/executor", + "crates/app", + "crates/eval", "crates/ir", - "crates/cli", ] resolver = "2" [workspace.dependencies] clap = { version = "4", features = ["derive"] } +serde = { version = "1.0", features = ["derive"] } # to enable all the lints below, this must be present in a workspace member's Cargo.toml: # [lints] diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index a0043ee..2caaaf4 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -6,11 +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 a0c4db1..15f660c 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,16 +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 { - match p.extension().map(|v| v.to_str().unwrap()) { + 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(|v| v.to_owned()))), + e => Err(Config::UnknownExtension(e.map(str::to_owned))), } } } 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); } diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs index 1347572..58103a5 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; @@ -8,10 +10,18 @@ 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"); + let ir = ir::from_ron(&source).expect("failed to parse source to graph ir"); + + let mut machine = cfg.evaluator.pick(); + machine.feed(ir); + machine.eval_full(); } diff --git a/crates/executor/Cargo.toml b/crates/eval/Cargo.toml similarity index 86% rename from crates/executor/Cargo.toml rename to crates/eval/Cargo.toml index 711b9b9..b542d2f 100644 --- a/crates/executor/Cargo.toml +++ b/crates/eval/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "executor" +name = "eval" version = "0.1.0" edition = "2021" @@ -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/executor/src/debug/instructions/mod.rs b/crates/eval/src/kind/debug/instr/mod.rs similarity index 92% rename from crates/executor/src/debug/instructions/mod.rs rename to crates/eval/src/kind/debug/instr/mod.rs index b78c7b5..14afac8 100644 --- a/crates/executor/src/debug/instructions/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/eval/src/kind/debug/mod.rs b/crates/eval/src/kind/debug/mod.rs new file mode 100644 index 0000000..9b53ebe --- /dev/null +++ b/crates/eval/src/kind/debug/mod.rs @@ -0,0 +1,105 @@ +use ir::{ + id, + instruction::{Filter, Kind}, + GraphIr, Instruction, Map, +}; + +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) { + 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); + } + } +} + +impl Evaluator { + #[allow(clippy::needless_pass_by_value)] + fn step(&mut self, instr: Instruction) { + // 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) => { + #[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 => { + #[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/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 new file mode 100644 index 0000000..c3a6c38 --- /dev/null +++ b/crates/eval/src/lib.rs @@ -0,0 +1,43 @@ +use ir::GraphIr; + +mod kind; +mod value; + +/// 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, +} + +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()), + } + } +} diff --git a/crates/eval/src/value/mod.rs b/crates/eval/src/value/mod.rs new file mode 100644 index 0000000..b1d85d5 --- /dev/null +++ b/crates/eval/src/value/mod.rs @@ -0,0 +1,12 @@ +use image::DynamicImage; + +/// 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/executor/src/cpu/mod.rs b/crates/executor/src/cpu/mod.rs deleted file mode 100644 index e82d20a..0000000 --- a/crates/executor/src/cpu/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub(crate) struct CpuExecutor; diff --git a/crates/executor/src/debug/mod.rs b/crates/executor/src/debug/mod.rs deleted file mode 100644 index b9cd743..0000000 --- a/crates/executor/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/executor/src/lib.rs b/crates/executor/src/lib.rs deleted file mode 100644 index 9732bb0..0000000 --- a/crates/executor/src/lib.rs +++ /dev/null @@ -1,34 +0,0 @@ -use ir::instruction::Kind; -use value::Dynamic; - -mod debug; -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. - 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); - } -} - -// 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/executor/src/value/mod.rs b/crates/executor/src/value/mod.rs deleted file mode 100644 index f3595d0..0000000 --- a/crates/executor/src/value/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -use image::DynamicImage; - -pub enum Dynamic { - 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 ef7a23d..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,7 +58,28 @@ 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)] +/// +/// # 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 /// what **kind of instruction** they are. @@ -123,13 +146,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 +164,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 +172,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 +186,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 +263,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 +331,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/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 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 executors. -Executors 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 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/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. diff --git a/docs/template.typ b/docs/template.typ index a624cc7..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", - "executor", "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") 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, )), },