Compare commits

..

10 commits

28 changed files with 408 additions and 150 deletions

2
.gitignore vendored
View file

@ -4,3 +4,5 @@
.pre-commit-config.yaml .pre-commit-config.yaml
*.pdf *.pdf
/docs/*.png /docs/*.png
/testfiles/gen/*
!/testfiles/gen/.gitkeep

3
Cargo.lock generated
View file

@ -63,6 +63,8 @@ dependencies = [
"ariadne", "ariadne",
"clap", "clap",
"dirs", "dirs",
"eval",
"ir",
"owo-colors", "owo-colors",
"ron", "ron",
"serde", "serde",
@ -272,6 +274,7 @@ dependencies = [
"clap", "clap",
"image", "image",
"ir", "ir",
"serde",
] ]
[[package]] [[package]]

View file

@ -6,14 +6,16 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
clap = { workspace = true, features = [ "derive", "env" ] }
serde = { workspace = true, features = [ "derive" ] }
ron = "0.8"
serde_json = "1.0"
ariadne = "0.4" ariadne = "0.4"
time = { version = "0.3", features = [ "local-offset" ] } clap = { workspace = true, features = [ "derive", "env" ] }
dirs = "5" dirs = "5"
eval = { path = "../eval" }
ir = { path = "../ir" }
owo-colors = "4" owo-colors = "4"
ron = "0.8"
serde = { workspace = true, features = [ "derive" ] }
serde_json = "1.0"
time = { version = "0.3", features = [ "local-offset" ] }
[lints] [lints]
workspace = true workspace = true

View file

@ -1,3 +1,5 @@
use std::path::PathBuf;
use clap::Parser; use clap::Parser;
use self::{ use self::{
@ -10,6 +12,9 @@ mod config_file;
/// this struct may hold all configuration /// this struct may hold all configuration
pub struct Config { pub struct Config {
pub source: PathBuf,
pub evaluator: eval::Available,
pub startup_msg: bool, pub startup_msg: bool,
} }
@ -17,37 +22,37 @@ impl Config {
/// Get the configs from all possible places (args, file, env...) /// Get the configs from all possible places (args, file, env...)
pub fn read() -> Self { pub fn read() -> Self {
let args = Args::parse(); let args = Args::parse();
let config_path = if let Some(config_path) = args.config_path { let config = if let Some(config) = args.config_path {
Ok(config_path) Ok(config)
} else { } else {
find_config_file() find_config_file()
}; };
// try to read a maybe existing config file // try to read a maybe existing config file
let file_config = if let Ok(config_path) = config_path { let config = config.ok().and_then(|path| {
let file_config = Configs::read(config_path); Configs::read(path).map_or_else(
|e| {
match file_config {
Ok(c) => Some(c),
Err(e) => {
eprintln!("Config error: {e:?}"); eprintln!("Config error: {e:?}");
eprintln!("Proceeding with defaults or cli args..."); eprintln!("Proceeding with defaults or cli args...");
None None
} },
} Some,
} else { )
None });
};
if let Some(file_config) = file_config { if let Some(file) = config {
Self { 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, // this is negated because to an outward api, the negative is more intuitive,
// while in the source the other way around 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 { } else {
Self { Self {
source: args.source,
startup_msg: !args.no_startup_message, startup_msg: !args.no_startup_message,
evaluator: args.evaluator.unwrap_or_default(),
} }
} }
} }
@ -56,7 +61,7 @@ impl Config {
pub mod error { pub mod error {
/// Errors that can occur when reading configs /// Errors that can occur when reading configs
#[derive(Debug)] #[derive(Debug)]
pub enum ConfigError { pub enum Config {
/// The config dir doesn't exist /// The config dir doesn't exist
NoConfigDir, NoConfigDir,
/// We didn't find a config file in the config dir /// We didn't find a config file in the config dir
@ -73,19 +78,19 @@ pub mod error {
SerdeRonError(ron::error::SpannedError), SerdeRonError(ron::error::SpannedError),
} }
impl From<std::io::Error> for ConfigError { impl From<std::io::Error> for Config {
fn from(value: std::io::Error) -> Self { fn from(value: std::io::Error) -> Self {
Self::IoError(value) Self::IoError(value)
} }
} }
impl From<serde_json::Error> for ConfigError { impl From<serde_json::Error> for Config {
fn from(value: serde_json::Error) -> Self { fn from(value: serde_json::Error) -> Self {
Self::SerdeJsonError(value) Self::SerdeJsonError(value)
} }
} }
impl From<ron::error::SpannedError> for ConfigError { impl From<ron::error::SpannedError> for Config {
fn from(value: ron::error::SpannedError) -> Self { fn from(value: ron::error::SpannedError) -> Self {
Self::SerdeRonError(value) Self::SerdeRonError(value)
} }

View file

@ -4,6 +4,14 @@ use clap::{builder::BoolishValueParser, ArgAction, Parser};
#[derive(Parser)] #[derive(Parser)]
pub(crate) struct Args { 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<eval::Available>,
/// Read this config file. /// Read this config file.
#[arg(short, long)] #[arg(short, long)]
pub config_path: Option<PathBuf>, pub config_path: Option<PathBuf>,

View file

@ -5,7 +5,7 @@ use std::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::error::ConfigError; use super::error::Config;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct Configs { pub struct Configs {
@ -13,6 +13,7 @@ pub struct Configs {
pub example_value: i32, pub example_value: i32,
#[serde(default)] #[serde(default)]
pub no_startup_message: bool, pub no_startup_message: bool,
pub evaluator: Option<eval::Available>,
} }
/// what the fuck serde why do i need this /// 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 /// Find the location of a config file and check if there is, in fact, a file
pub(super) fn find_config_file() -> Result<PathBuf, ConfigError> { pub(super) fn find_config_file() -> Result<PathBuf, Config> {
let Some(config_path) = dirs::config_dir() else { 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"); let ron_path = config_path.with_file_name("config.ron");
@ -34,16 +35,19 @@ pub(super) fn find_config_file() -> Result<PathBuf, ConfigError> {
} else if Path::new(&json_path).exists() { } else if Path::new(&json_path).exists() {
Ok(json_path) Ok(json_path)
} else { } else {
Err(ConfigError::NoConfigFileFound) Err(Config::NoConfigFileFound)
} }
} }
impl Configs { impl Configs {
pub fn read(p: PathBuf) -> Result<Self, ConfigError> { pub fn read(p: PathBuf) -> Result<Self, Config> {
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("ron") => Ok(serde_json::from_str(&fs::read_to_string(p)?)?),
Some("json") => Ok(ron::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))),
} }
} }
} }

View file

@ -3,12 +3,12 @@ use std::process;
use ron::error::Position; use ron::error::Position;
/// Report an `Error` from the `serde_json` crate /// 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_serde_err(src, err.line(), err.column(), err.to_string())
} }
/// Report a `SpannedError` from the `ron` crate /// 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; let Position { line, col } = err.position;
report_serde_err(src, line, col, err.to_string()) 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_message(msg)
.with_note("We'd like to give better errors, but serde errors are horrible to work with...") .with_note("We'd like to give better errors, but serde errors are horrible to work with...")
.finish() .finish()
.print(("test", Source::from(src))) .eprint(("test", Source::from(src)))
.unwrap(); .expect("writing error to stderr failed");
process::exit(1); process::exit(1);
} }

View file

@ -1,3 +1,5 @@
use std::fs;
use config::Config; use config::Config;
use welcome_msg::print_startup_msg; use welcome_msg::print_startup_msg;
@ -8,10 +10,18 @@ mod error_reporting;
mod welcome_msg; mod welcome_msg;
fn main() { 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(); let cfg = Config::read();
if cfg.startup_msg { if cfg.startup_msg {
print_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();
} }

View file

@ -9,6 +9,7 @@ edition = "2021"
clap = { workspace = true, features = [ "derive" ] } clap = { workspace = true, features = [ "derive" ] }
image = "0.24" image = "0.24"
ir = { path = "../ir" } ir = { path = "../ir" }
serde = { workspace = true }
[lints] [lints]
workspace = true workspace = true

View file

@ -1 +0,0 @@
pub(crate) struct CpuExecutor;

View file

@ -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<Dynamic>) -> Option<Dynamic> {
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"),
},
))),
},
}
}
}

View file

@ -2,7 +2,7 @@ pub mod read {
use image::{io::Reader as ImageReader, DynamicImage}; use image::{io::Reader as ImageReader, DynamicImage};
use ir::instruction::read::{Read, SourceType}; use ir::instruction::read::{Read, SourceType};
pub fn read(Read { source, .. }: Read) -> DynamicImage { pub fn read(Read { source }: Read) -> DynamicImage {
// TODO: actual error handling // TODO: actual error handling
let img = ImageReader::open(match source { let img = ImageReader::open(match source {
SourceType::File(path) => path, SourceType::File(path) => path,
@ -18,6 +18,7 @@ pub mod write {
use ir::instruction::write::{TargetFormat, TargetType, Write}; use ir::instruction::write::{TargetFormat, TargetType, Write};
pub fn write(Write { target, format }: Write, input_data: &DynamicImage) { pub fn write(Write { target, format }: Write, input_data: &DynamicImage) {
// TODO: actual error handling
input_data input_data
.save_with_format( .save_with_format(
match target { match target {
@ -28,7 +29,7 @@ pub mod write {
TargetFormat::Png => ImageFormat::Png, TargetFormat::Png => ImageFormat::Png,
}, },
) )
.expect("couldn't save file — come back later and handle me properly please uwu"); .expect("couldn't save file");
} }
} }

View file

@ -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<id::Output, Variant>,
}
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<Instruction> = 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);
}
}
}

View file

@ -0,0 +1 @@
pub mod debug;

View file

@ -1,34 +1,43 @@
use ir::instruction::Kind; use ir::GraphIr;
use value::Dynamic;
mod debug; mod kind;
mod value; mod value;
/// The available executors /// Can collapse a [`GraphIr`] in meaningful ways and do interesting work on it.
/// unused in early dev. ///
#[derive(Debug, Clone, Copy, clap::ValueEnum)] /// It's surprisingly difficult to find a fitting description for this.
pub enum RegisteredExecutor { pub trait Evaluator {
/// the debug executor is single threaded and really, *really* slow. And unstable. Don't use. Unless you're a dev working on this. /// 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, Debug,
} }
trait Executor { impl Available {
fn execute(instruction: Kind, input: Option<Dynamic>) -> Option<Dynamic>; /// Selects the [`Evaluator`] corresponding to this label.
} #[must_use]
pub fn pick(&self) -> Box<dyn Evaluator> {
pub fn execute_all(instructions: Vec<Kind>) { match self {
let mut tmp = None; Self::Debug => Box::new(kind::debug::Evaluator::default()),
}
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

View file

@ -1,5 +1,12 @@
use image::DynamicImage; 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), Image(DynamicImage),
} }

View file

@ -49,7 +49,7 @@ impl Input {
/// ///
/// In contrast to [`Input`]s, [`Output`]s may be used or unused. /// In contrast to [`Input`]s, [`Output`]s may be used or unused.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[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 { impl Output {
#[must_use] #[must_use]
@ -75,7 +75,7 @@ pub struct Socket {
/// This really only serves for denoting where a socket is, /// This really only serves for denoting where a socket is,
/// when it's already clear which instruction is referred to. /// when it's already clear which instruction is referred to.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[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 { impl fmt::Debug for SocketIdx {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {

View file

@ -4,16 +4,9 @@ use std::path::PathBuf;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Read { pub struct Read {
pub source: SourceType, pub source: SourceType,
pub format: SourceFormat,
} }
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum SourceType { pub enum SourceType {
File(PathBuf), File(PathBuf),
} }
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum SourceFormat {
Jpeg,
Png,
}

View file

@ -27,6 +27,8 @@ pub fn from_ron(source: &str) -> ron::error::SpannedResult<GraphIr> {
/// The toplevel representation of a whole pipeline. /// The toplevel representation of a whole pipeline.
/// ///
/// # DAGs
///
/// Pipelines may not be fully linear. They may branch out and recombine later on. /// 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 /// 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). /// [**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<GraphIr> {
/// So the vertices of the DAG are the **sockets** /// So the vertices of the DAG are the **sockets**
/// (which are either [`id::Input`] or [`id::Output`] depending on the direction), /// (which are either [`id::Input`] or [`id::Output`] depending on the direction),
/// and each **socket** in turn belongs to an **instruction**. /// 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 { pub struct GraphIr {
/// "Backbone" storage of all **instruction** IDs to /// "Backbone" storage of all **instruction** IDs to
/// what **kind of instruction** they are. /// what **kind of instruction** they are.
@ -123,13 +146,13 @@ impl GraphIr {
/// to actually have multiple [`GraphIr`]s at one point in time. /// to actually have multiple [`GraphIr`]s at one point in time.
/// Open an issue if that poses a problem for you. /// Open an issue if that poses a problem for you.
#[must_use] #[must_use]
pub fn resolve<'ir>(&'ir self, subject: &id::Instruction) -> Option<Instruction<'ir>> { pub fn resolve<'ir>(&'ir self, subject: &id::Instruction) -> Option<InstructionRef<'ir>> {
let (id, kind) = self.instructions.get_key_value(subject)?; let (id, kind) = self.instructions.get_key_value(subject)?;
let input_sources = self.input_sources(subject)?.collect(); let input_sources = self.input_sources(subject)?.collect();
let output_targets = self.output_targets(subject)?.collect(); let output_targets = self.output_targets(subject)?.collect();
Some(Instruction { Some(InstructionRef {
id, id,
kind, kind,
input_sources, input_sources,
@ -141,7 +164,7 @@ impl GraphIr {
/// ///
/// The same caveats as for [`GraphIr::resolve`] apply. /// The same caveats as for [`GraphIr::resolve`] apply.
#[must_use] #[must_use]
pub fn owner_of_input<'ir>(&'ir self, input: &id::Input) -> Option<Instruction<'ir>> { pub fn owner_of_input<'ir>(&'ir self, input: &id::Input) -> Option<InstructionRef<'ir>> {
self.resolve(&input.socket().belongs_to) self.resolve(&input.socket().belongs_to)
} }
@ -149,7 +172,7 @@ impl GraphIr {
/// ///
/// The same caveats as for [`GraphIr::resolve`] apply. /// The same caveats as for [`GraphIr::resolve`] apply.
#[must_use] #[must_use]
pub fn owner_of_output<'ir>(&'ir self, output: &id::Output) -> Option<Instruction<'ir>> { pub fn owner_of_output<'ir>(&'ir self, output: &id::Output) -> Option<InstructionRef<'ir>> {
self.resolve(&output.socket().belongs_to) self.resolve(&output.socket().belongs_to)
} }
@ -163,7 +186,7 @@ impl GraphIr {
#[must_use] #[must_use]
// yes, this function could probably return an iterator and be lazy // yes, this function could probably return an iterator and be lazy
// no, not today // no, not today
pub fn topological_sort(&self) -> Vec<Instruction> { pub fn topological_sort(&self) -> Vec<InstructionRef> {
// count how many incoming edges each vertex has // count how many incoming edges each vertex has
let mut nonzero_input_counts: Map<_, NonZeroUsize> = let mut nonzero_input_counts: Map<_, NonZeroUsize> =
self.rev_edges 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)] #[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<Option<id::Output>>,
output_targets: Vec<Set<id::Input>>,
}
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<id::Output>] {
&self.input_sources
}
/// To whom outputs are sent.
#[must_use]
pub fn output_targets(&self) -> &[Set<id::Input>] {
&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 id: &'ir id::Instruction,
pub kind: &'ir instruction::Kind, pub kind: &'ir instruction::Kind,
// can't have these two public since then a user might corrupt their length
input_sources: Vec<Option<&'ir id::Output>>, input_sources: Vec<Option<&'ir id::Output>>,
output_targets: Vec<Option<&'ir Set<id::Input>>>, output_targets: Vec<Option<&'ir Set<id::Input>>>,
} }
impl<'ir> Instruction<'ir> { impl<'ir> InstructionRef<'ir> {
/// Where this instruction gets its inputs from. /// Where this instruction gets its inputs from.
/// ///
/// [`None`] means that this input is unfilled, /// [`None`] means that this input is unfilled,
@ -268,10 +331,23 @@ impl<'ir> Instruction<'ir> {
} }
} }
/// Constructs an [`id::Socket`] a bit more tersely. // would love to use ToOwned but Rust has no specialization yet
fn socket(id: &id::Instruction, idx: u16) -> id::Socket { // and it'd hurt a blanket impl of ToOwned otherwise
id::Socket { impl From<InstructionRef<'_>> for Instruction {
belongs_to: id.clone(), fn from(source: InstructionRef<'_>) -> Self {
idx: id::SocketIdx(idx), 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(),
}
} }
} }

View file

@ -77,7 +77,8 @@ fn reverse_and_type_edges(edges: Map<Socket, Set<Socket>>) -> Map<id::Input, id:
let input = id::Input(input.into()); let input = id::Input(input.into());
let previous = rev_edges.insert(input, output.clone()); let previous = rev_edges.insert(input, output.clone());
if let Some(previous) = previous { if let Some(previous) = previous {
panic!("two or more outputs referred to the same input {previous:#?} — handle me better later with a TryFrom impl"); // TODO: handle this with a TryFrom impl
panic!("two or more outputs referred to the same input {previous:#?}");
} }
} }

View file

@ -1,3 +0,0 @@
pub enum DynamicValue {
Image(DynamicImage),
}

View file

@ -7,7 +7,75 @@
subtitle: [don't worry, we're just dreaming], subtitle: [don't worry, we're just dreaming],
) )
= Evaluation stages = 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 #graphics.stages-overview
@ -18,7 +86,7 @@ This has a number of benefits and implications:
- Bugs are easier to trace down to one stage. - Bugs are easier to trace down to one stage.
- Stages are also replacable, pluggable and usable somewhere else. - Stages are also replacable, pluggable and usable somewhere else.
- For example, - 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. 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]! 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]!
@ -152,17 +220,17 @@ Merges and simplifies functions in the graph IR.
== Runtime <runtime> == Runtime <runtime>
Runs through all functions in the graph IR. 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. and despite its name there's _no_ bytecode involved.
Different runtimes are called executors.
Executors operate on instructions instead of functions.
=== Scheduler === 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 <vm> === Evaluator
Runs instructions given to it in a specific way,
such as for example on the GPU using OpenCL.
= Open questions = Open questions

View file

@ -33,22 +33,23 @@
// this'd require wrapping the whole document in a show rule // this'd require wrapping the whole document in a show rule
// at which point `query` doesn't find anything anymore // at which point `query` doesn't find anything anymore
#let terms = ( #let terms = (
"processing stage",
"source", "source",
"AST", "AST",
"graph IR", "graph IR",
"runtime", "runtime",
"executor",
"optimizer", "optimizer",
"scheduler", "scheduler",
"VM", "evaluator",
"evaluation",
"function", "function",
"instruction", "instruction",
"pipeline",
"input", "argument", "consumer", "input", "argument", "consumer",
"output", "streamer", "output", "streamer",
"modifier", "modifier",
"pipeline",
) )
// yes, the shadowing is intentional to avoid accidentally using the list // yes, the shadowing is intentional to avoid accidentally using the list
#let terms = regex("\\b(" + terms.map(expand).join("|") + ")\\b") #let terms = regex("\\b(" + terms.map(expand).join("|") + ")\\b")

View file

@ -4,6 +4,9 @@ test:
#!/usr/bin/env nu #!/usr/bin/env nu
cargo nextest run cargo nextest run
alias doc := docs
# Compile all documentation as in proposals and design documents, placing them under `docs/compiled`. # Compile all documentation as in proposals and design documents, placing them under `docs/compiled`.
docs: docs:
#!/usr/bin/env nu #!/usr/bin/env nu

View file

@ -1,12 +1,11 @@
( (
instructions: { instructions: {
0: Read(( 0: Read((
source: File("testfiles/juan.jpg"), source: File("testfiles/rails.png"),
format: Jpeg,
)), )),
1: Write(( 1: Write((
target: File("testfiles/out.png"), target: File("testfiles/gen/out.jpg"),
format: Png, format: Jpeg,
)), )),
}, },
edges: { edges: {

1
testfiles/gen/.gitkeep Normal file
View file

@ -0,0 +1 @@
the testfile scripts will place generated images and media here. thank you for your understanding.

View file

@ -1,12 +1,11 @@
( (
instructions: { instructions: {
0: Read(( 0: Read((
source: File("testfiles/juan.jpg"), source: File("testfiles/rails.png"),
format: Jpeg,
)), )),
1: Filter(Invert), 1: Filter(Invert),
2: Write(( 2: Write((
target: File("testfiles/inverted.png"), target: File("testfiles/gen/inverted.png"),
format: Png, format: Png,
)), )),
}, },