Compare commits
106 Commits
Author | SHA1 | Date | |
---|---|---|---|
bc5c64afd1 | |||
d5d10a4818 | |||
632fa4aa73 | |||
a8378d4c01 | |||
d558d10d4e | |||
ff3541ef68 | |||
62c484e590 | |||
c5346bc15f | |||
eacdf85629 | |||
50060a5f9d | |||
175d429240 | |||
8a057356d9 | |||
c53dd56d0f | |||
ea05eb2aa7 | |||
ed3d576c04 | |||
bb7fa84e26 | |||
717c661c75 | |||
5460b73230 | |||
e879168188 | |||
07172d9975 | |||
3c76daed1e | |||
86bd8a39ee | |||
e2abb6873f | |||
471887c885 | |||
7e06d3060c | |||
2115ba49c9 | |||
6cf9d85516 | |||
76f6244814 | |||
864b171197 | |||
593ed74eb7 | |||
6f6e1453f7 | |||
1629a7e30c | |||
0be6307a41 | |||
b865e97b29 | |||
22cbe75dc2 | |||
49feefaedc | |||
009e9cfcb1 | |||
9a13efbd35 | |||
1e772a97e2 | |||
de9291cd50 | |||
4601874c64 | |||
f0ef67f9a0 | |||
3adf6031a4 | |||
64332a137b | |||
f5c7c14d2e | |||
494f2ad367 | |||
1279a1755d | |||
55b91944b2 | |||
657f90c39c | |||
a9a6b85c5f | |||
a0c4c06de7 | |||
701350a269 | |||
876d28fac6 | |||
0b8717aa2b | |||
ec0746011e | |||
5d795c3084 | |||
60858bde24 | |||
1253999961 | |||
f467025569 | |||
2c1b9c620e | |||
640c0ab620 | |||
f44f525c2c | |||
6bb51638cc | |||
9ff658f719 | |||
20653c2da5 | |||
d92ebf11b2 | |||
9cb6a8c6ce | |||
8f0fe08ecc | |||
b8ad929d0b | |||
e69014d98d | |||
712b528ca8 | |||
dd1143aa9b | |||
04bd5cf8c4 | |||
08a4bf87dc | |||
36bcbe3c9c | |||
6d1e505590 | |||
8c761fe0d4 | |||
b79edeef0a | |||
365d2db571 | |||
a556b14188 | |||
88dfef75fd | |||
c1c594d4f7 | |||
b73d205456 | |||
02b5226a90 | |||
05348a5dbc | |||
c18c1ced59 | |||
1d1673e5ee | |||
de025dc138 | |||
a6e94340ee | |||
6de1999b8d | |||
7dbcd4963f | |||
1ac8f4ead4 | |||
5998bbe116 | |||
e034f30455 | |||
ca0b77349a | |||
d8f2a393ba | |||
7c554f731a | |||
1530933464 | |||
81cd986f77 | |||
67fb9db461 | |||
140c6a67fd | |||
bd2cdba5bc | |||
29838e2113 | |||
67ddf2f254 | |||
b7d0271f4e | |||
3b943cb828 |
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[submodule "crates/pyftsubset/fonttools"]
|
||||||
|
path = crates/pyftsubset/fonttools
|
||||||
|
url = https://git.shadowfacts.net/shadowfacts/fonttools.git
|
2694
Cargo.lock
generated
2694
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
122
Cargo.toml
122
Cargo.toml
@ -1,72 +1,74 @@
|
|||||||
|
workspace = { members = [
|
||||||
|
"crates/compute_graph",
|
||||||
|
"crates/compute_graph_macros",
|
||||||
|
"crates/derive_test",
|
||||||
|
"crates/pyftsubset",
|
||||||
|
] }
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "v6"
|
name = "v7"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["serve"]
|
||||||
|
serve = ["dep:http", "dep:http-body", "dep:http-body-util", "dep:hyper", "dep:hyper-tungstenite", "dep:hyper-util", "dep:tower-http"]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
activitystreams = "0.7.0-alpha.22"
|
ahash = "0.8.11"
|
||||||
activitystreams-ext = "0.1.0-alpha.3"
|
anyhow = "1.0.95"
|
||||||
ammonia = "3.2"
|
base64 = "0.22.1"
|
||||||
anyhow = "1.0"
|
bit-vec = "0.8.0"
|
||||||
askama = "0.11.1"
|
bitflags = "2.7.0"
|
||||||
axum = "0.5.6"
|
chrono = { version = "0.4.39", features = ["serde"] }
|
||||||
base64 = "0.13"
|
clap = { version = "4.5.23", features = ["cargo"] }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
compute_graph = { path = "crates/compute_graph" }
|
||||||
clap = { version = "3.1", features = ["cargo"] }
|
debounced = "0.2.0"
|
||||||
env_logger = "0.9"
|
env_logger = "0.11.6"
|
||||||
futures = "0.3"
|
futures = "0.3.31"
|
||||||
html5ever = "0.26"
|
grass = { version = "0.13.4", default-features = false, git = "https://git.shadowfacts.net/shadowfacts/grass.git", branch = "custom-global-variables" }
|
||||||
http-signature-normalization = "0.6"
|
grass_compiler = { version = "0.13.4", features = [
|
||||||
hyper = "0.14"
|
"custom-builtin-fns",
|
||||||
log = "0.4"
|
], git = "https://git.shadowfacts.net/shadowfacts/grass.git", branch = "custom-global-variables" }
|
||||||
markup5ever_rcdom = "0.2"
|
html5ever = "0.27.0"
|
||||||
mime = "0.3"
|
http = { version = "1.2.0", optional = true }
|
||||||
notify = "5.0.0-pre.16"
|
http-body = { version = "1.0.1", optional = true }
|
||||||
notify-debouncer-mini = { version = "*", default-features = false }
|
http-body-util = { version = "0.1.2", optional = true }
|
||||||
once_cell = "1.13"
|
hyper = { version = "1.5.2", features = ["server", "http1"], optional = true }
|
||||||
# NOTE: openssl version also needs to be updated in ios target config below
|
hyper-tungstenite = { version = "0.17.0", optional = true }
|
||||||
openssl = "0.10"
|
hyper-util = { version = "0.1.10", features = ["tokio", "service"], optional = true }
|
||||||
pulldown-cmark = "0.9"
|
log = "0.4.22"
|
||||||
regex = "1.5"
|
markup5ever_rcdom = "0.3.0"
|
||||||
reqwest = { version = "0.11", features = ["json"] }
|
notify = "7.0.0"
|
||||||
rsass = "0.25"
|
once_cell = "1.20.2"
|
||||||
rss = { version = "2.0", features = ["atom"] }
|
pulldown-cmark = "0.12.2"
|
||||||
|
pulldown-cmark-escape = "0.11.0"
|
||||||
|
pyftsubset = { path = "crates/pyftsubset" }
|
||||||
|
regex = "1.11.1"
|
||||||
|
rss = { version = "2.0.11", features = ["atom"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
|
||||||
splash-rs = { version = "0.3.1", git = "https://git.shadowfacts.net/shadowfacts/splash-rs.git" }
|
splash-rs = { version = "0.3.1", git = "https://git.shadowfacts.net/shadowfacts/splash-rs.git" }
|
||||||
sqlx = { version = "0.5", features = ["runtime-tokio-native-tls", "sqlite"] }
|
tera = "1.20.0"
|
||||||
thiserror = "1.0"
|
tokio = { version = "1.42.0", features = ["full"] }
|
||||||
tokio = { version = "1.18", features = ["full"] }
|
tokio-stream = "0.1.17"
|
||||||
tokio-cron-scheduler = "0.8"
|
toml = "0.8.19"
|
||||||
tokio-stream = { version = "0.1.8", features = ["fs"] }
|
tower = { version = "0.5.2", features = ["steer", "util"] }
|
||||||
toml = "0.5"
|
tower-http = { version = "0.6.2", features = ["fs"], optional = true }
|
||||||
tower = "0.4"
|
tree-sitter-bash = "0.23.3"
|
||||||
tower-http = { version = "0.3", features = ["fs"] }
|
tree-sitter-c = "0.23.4"
|
||||||
# should be from crates.io
|
tree-sitter-css = "0.23.2"
|
||||||
tree-sitter-bash = { version = "0.20", git = "https://git.shadowfacts.net/shadowfacts/tree-sitter-bash.git" }
|
tree-sitter-elixir = "0.3.3"
|
||||||
tree-sitter-c = "0.20"
|
tree-sitter-highlight = "0.24.6"
|
||||||
# should be https://github.com/tree-sitter/tree-sitter-css.git
|
tree-sitter-html = "0.23.2"
|
||||||
tree-sitter-css = { version = "0.20", git = "https://git.shadowfacts.net/shadowfacts/tree-sitter-css.git" }
|
tree-sitter-java = "0.23.5"
|
||||||
tree-sitter-elixir = { version = "0.19", git = "https://github.com/elixir-lang/tree-sitter-elixir.git" }
|
tree-sitter-javascript = "0.23.1"
|
||||||
tree-sitter-highlight = "0.20"
|
tree-sitter-json = "0.24.8"
|
||||||
# should be from crates.io
|
tree-sitter-objc = "3.0.2"
|
||||||
tree-sitter-html = { version = "0.20", git = "https://git.shadowfacts.net/shadowfacts/tree-sitter-html.git" }
|
tree-sitter-rust = "0.23.2"
|
||||||
# should be from crates.io
|
unicode-normalization = "0.1.24"
|
||||||
tree-sitter-java = { version = "0.20", git = "https://github.com/tree-sitter/tree-sitter-java.git" }
|
|
||||||
tree-sitter-javascript = "0.20"
|
|
||||||
tree-sitter-json = { version = "0.20", git = "https://github.com/tree-sitter/tree-sitter-json.git" }
|
|
||||||
# should be https://github.com/jiyee/tree-sitter-objc.git
|
|
||||||
tree-sitter-objc = { version = "1.0.0", git = "https://git.shadowfacts.net/shadowfacts/tree-sitter-objc.git" }
|
|
||||||
tree-sitter-rust = "0.20"
|
|
||||||
unicode-normalization = "0.1.19"
|
|
||||||
url = "2.2"
|
|
||||||
uuid = { version = "1.1", features = ["v4" ] }
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "ios")'.dependencies]
|
|
||||||
openssl = { version = "0.10", features = ["vendored"] }
|
|
||||||
|
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
|||||||
The two parts of this work are covered by separate licenses.
|
The two parts of this work are covered by separate licenses.
|
||||||
|
|
||||||
The 'src/' directory (static site generator and backend code) is licensed under the Creative Commons BY-NC-SA 4.0 license.
|
The 'src/' and `crates/` directories (static site generator and backend code) is licensed under the Creative Commons BY-NC-SA 4.0 license.
|
||||||
A copy of the license is available in the 'src/LICENSE' file.
|
A copy of the license is available in the 'src/LICENSE' file.
|
||||||
|
|
||||||
The 'site/' directory (the contents of the website and the frontend code) is not publicly licensed and all rights are reserved.
|
The 'site/' directory (the contents of the website and the frontend code) is not publicly licensed and all rights are reserved.
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
[general]
|
|
||||||
dirs = ["site"]
|
|
3
build.rs
3
build.rs
@ -2,9 +2,6 @@ use serde::Deserialize;
|
|||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// trigger recompilation when a new migration is added
|
|
||||||
println!("cargo:rerun-if-changed=migrations");
|
|
||||||
|
|
||||||
let target = get_swift_target_info();
|
let target = get_swift_target_info();
|
||||||
if target.target.unversioned_triple.contains("linux") {
|
if target.target.unversioned_triple.contains("linux") {
|
||||||
target.paths.runtime_library_paths.iter().for_each(|path| {
|
target.paths.runtime_library_paths.iter().for_each(|path| {
|
||||||
|
15
crates/compute_graph/Cargo.toml
Normal file
15
crates/compute_graph/Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "compute_graph"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
compute_graph_macros = { path = "../compute_graph_macros" }
|
||||||
|
petgraph = "0.6.5"
|
||||||
|
syn = "2"
|
||||||
|
quote = "1"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio = { version = "1.41.0", features = ["rt", "macros"] }
|
462
crates/compute_graph/src/builder.rs
Normal file
462
crates/compute_graph/src/builder.rs
Normal file
@ -0,0 +1,462 @@
|
|||||||
|
use crate::input::{DynamicInput, Input};
|
||||||
|
use crate::node::{
|
||||||
|
AsyncConstNode, AsyncDynamicRuleNode, AsyncRuleNode, ConstNode, DynamicRuleNode, ErasedNode,
|
||||||
|
InvalidatableConstNode, Node, NodeValue, RuleNode,
|
||||||
|
};
|
||||||
|
use crate::rule::{AsyncDynamicRule, AsyncRule, DynamicRule, Rule};
|
||||||
|
use crate::synchronicity::{Asynchronous, Synchronicity, Synchronous};
|
||||||
|
use crate::util;
|
||||||
|
use crate::{Graph, InvalidationSignal, NodeGraph, NodeId, ValueInvalidationSignal};
|
||||||
|
use petgraph::visit::{EdgeRef, IntoEdgesDirected};
|
||||||
|
use std::cell::{Cell, RefCell};
|
||||||
|
use std::future::Future;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
/// Builds a [`Graph`].
|
||||||
|
///
|
||||||
|
/// The builder is generic over the type of the output value and the synchronicity of the graph.
|
||||||
|
/// Use [`GraphBuilder::new`] or [`GraphBuilder::new_async`] to create a builder.
|
||||||
|
/// Use [`GraphBuilder::set_output`] (or [`GraphBuilder::set_async_output`]) to set the rule for
|
||||||
|
/// the output node.
|
||||||
|
pub struct GraphBuilder<Output, Synch: Synchronicity> {
|
||||||
|
pub(crate) node_graph: Rc<RefCell<NodeGraph<Synch>>>,
|
||||||
|
pub(crate) output: Option<Input<Output>>,
|
||||||
|
pub(crate) output_type: std::marker::PhantomData<Output>,
|
||||||
|
pub(crate) is_valid: Rc<Cell<bool>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<O: 'static> GraphBuilder<O, Synchronous> {
|
||||||
|
/// Creates a builder for a synchronous graph.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
node_graph: Rc::new(RefCell::new(NodeGraph::new())),
|
||||||
|
output: None,
|
||||||
|
output_type: std::marker::PhantomData,
|
||||||
|
is_valid: Rc::new(Cell::new(false)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<O: 'static> GraphBuilder<O, Asynchronous> {
|
||||||
|
/// Creates a builder for an asynchronous graph.
|
||||||
|
pub fn new_async() -> Self {
|
||||||
|
Self {
|
||||||
|
node_graph: Rc::new(RefCell::new(NodeGraph::new())),
|
||||||
|
output: None,
|
||||||
|
output_type: std::marker::PhantomData,
|
||||||
|
is_valid: Rc::new(Cell::new(false)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<O: 'static, S: Synchronicity> GraphBuilder<O, S> {
|
||||||
|
/// Sets a synchronous rule for the output node.
|
||||||
|
///
|
||||||
|
/// The type of the output rule's value is the type of the output value of the overall graph.
|
||||||
|
pub fn set_output<R: Rule<Output = O>>(&mut self, rule: R) {
|
||||||
|
let input = self.add_rule(rule);
|
||||||
|
self.output = Some(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets an existing node represented by the given input to be the output node.
|
||||||
|
pub fn set_existing_output(&mut self, input: Input<O>) {
|
||||||
|
self.output = Some(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replaces the current output rule with a new one, changing the output value type.
|
||||||
|
pub fn with_output<R: Rule>(mut self, rule: R) -> GraphBuilder<R::Output, S> {
|
||||||
|
let input = self.add_rule(rule);
|
||||||
|
GraphBuilder {
|
||||||
|
node_graph: self.node_graph,
|
||||||
|
output: Some(input),
|
||||||
|
output_type: std::marker::PhantomData,
|
||||||
|
is_valid: self.is_valid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_node<V: NodeValue>(&mut self, node: impl Node<V, S> + 'static) -> Input<V> {
|
||||||
|
let value = Rc::clone(node.value_rc());
|
||||||
|
let erased = ErasedNode::new(node);
|
||||||
|
let idx = self.node_graph.borrow_mut().add_node(erased);
|
||||||
|
Input {
|
||||||
|
node_idx: Rc::new(Cell::new(Some(idx))),
|
||||||
|
value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a constant node with the given value to the graph.
|
||||||
|
///
|
||||||
|
/// Returns an [`Input`] representing the newly-added node, which can be used to construct rules.
|
||||||
|
pub fn add_value<V: NodeValue>(&mut self, value: V) -> Input<V> {
|
||||||
|
return self.add_node(ConstNode::new(value, None));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a constant node with the given value to the graph.
|
||||||
|
///
|
||||||
|
/// The node's label will be the given string.
|
||||||
|
///
|
||||||
|
/// Returns an [`Input`] representing the newly-added node, which can be used to construct rules.
|
||||||
|
pub fn add_named_value<V: NodeValue>(&mut self, value: V, label: String) -> Input<V> {
|
||||||
|
return self.add_node(ConstNode::new(value, Some(label)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds an invalidatable node with the given value to the graph.
|
||||||
|
///
|
||||||
|
/// Returns an [`Input`] and a [`ValueInvalidationSignal`] representing, respectively, the
|
||||||
|
/// newly-added node, which can be used be used to construct rules, and a signal through which
|
||||||
|
/// the value of the node can be replaced, invalidating the node in the process.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use compute_graph::{builder::GraphBuilder, rule::Rule, input::{Input, InputVisitable}};
|
||||||
|
/// let mut builder = GraphBuilder::new();
|
||||||
|
/// let (input, signal) = builder.add_invalidatable_value(0);
|
||||||
|
/// # #[derive(InputVisitable)]
|
||||||
|
/// # struct Double(Input<i32>);
|
||||||
|
/// # impl Rule for Double {
|
||||||
|
/// # type Output = i32;
|
||||||
|
/// # fn evaluate(&mut self) -> i32 {
|
||||||
|
/// # *self.input_0() * 2
|
||||||
|
/// # }
|
||||||
|
/// # }
|
||||||
|
/// builder.set_output(Double(input));
|
||||||
|
/// let mut graph = builder.build().unwrap();
|
||||||
|
/// assert_eq!(*graph.evaluate(), 0);
|
||||||
|
/// signal.set_value(1);
|
||||||
|
/// assert_eq!(*graph.evaluate(), 2);
|
||||||
|
/// ```
|
||||||
|
pub fn add_invalidatable_value<V: NodeValue>(
|
||||||
|
&mut self,
|
||||||
|
value: V,
|
||||||
|
) -> (Input<V>, ValueInvalidationSignal<V>) {
|
||||||
|
let input = self.add_node(InvalidatableConstNode::new(value));
|
||||||
|
let signal = ValueInvalidationSignal {
|
||||||
|
input: input.clone(),
|
||||||
|
signal: self.make_invalidation_signal(&input),
|
||||||
|
};
|
||||||
|
(input, signal)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a node whose value is produced using the given rule to the graph.
|
||||||
|
///
|
||||||
|
/// Returns an [`Input`] representing the newly-added node, which can be used to construct further rules.
|
||||||
|
pub fn add_rule<R>(&mut self, rule: R) -> Input<R::Output>
|
||||||
|
where
|
||||||
|
R: Rule,
|
||||||
|
{
|
||||||
|
return self.add_node(RuleNode::new(rule));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds an externally-invalidatable node whose value is produced using the given rule to the graph.
|
||||||
|
///
|
||||||
|
/// Returns an [`Input`] representing the newly-added node, which can be used to construct further rules,
|
||||||
|
/// as well as an [`InvalidationSignal`] which can be used to indicate that the node has been invalidated.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use compute_graph::{builder::GraphBuilder, rule::Rule, input::{Input, InputVisitable}};
|
||||||
|
/// let mut builder = GraphBuilder::new();
|
||||||
|
/// # #[derive(InputVisitable)]
|
||||||
|
/// # struct IncrementAfterEvaluate(#[skip_visit] i32);
|
||||||
|
/// # impl Rule for IncrementAfterEvaluate {
|
||||||
|
/// # type Output = i32;
|
||||||
|
/// # fn evaluate(&mut self) -> i32 {
|
||||||
|
/// # let result = self.0;
|
||||||
|
/// # self.0 += 1;
|
||||||
|
/// # result
|
||||||
|
/// # }
|
||||||
|
/// # }
|
||||||
|
/// # #[derive(InputVisitable)]
|
||||||
|
/// # struct Double(Input<i32>);
|
||||||
|
/// # impl Rule for Double {
|
||||||
|
/// # type Output = i32;
|
||||||
|
/// # fn evaluate(&mut self) -> i32 {
|
||||||
|
/// # *self.input_0() * 2
|
||||||
|
/// # }
|
||||||
|
/// # }
|
||||||
|
/// let (input, signal) = builder.add_invalidatable_rule(IncrementAfterEvaluate(1));
|
||||||
|
/// builder.set_output(Double(input));
|
||||||
|
/// let mut graph = builder.build().unwrap();
|
||||||
|
/// assert_eq!(*graph.evaluate(), 2);
|
||||||
|
/// signal.invalidate();
|
||||||
|
/// assert_eq!(*graph.evaluate(), 4);
|
||||||
|
/// ```
|
||||||
|
pub fn add_invalidatable_rule<R: Rule>(
|
||||||
|
&mut self,
|
||||||
|
rule: R,
|
||||||
|
) -> (Input<R::Output>, InvalidationSignal) {
|
||||||
|
let input = self.add_rule(rule);
|
||||||
|
let signal = self.make_invalidation_signal(&input);
|
||||||
|
(input, signal)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_invalidation_signal<V>(&self, input: &Input<V>) -> InvalidationSignal {
|
||||||
|
let graph = Rc::clone(&self.node_graph);
|
||||||
|
let graph_is_valid = Rc::clone(&self.is_valid);
|
||||||
|
InvalidationSignal::new(input, graph, graph_is_valid)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a node to the graph whose output is additional nodes produced by the given rule.
|
||||||
|
pub fn add_dynamic_rule<R>(&mut self, rule: R) -> DynamicInput<R::ChildOutput>
|
||||||
|
where
|
||||||
|
R: DynamicRule,
|
||||||
|
{
|
||||||
|
let input = self.add_node(DynamicRuleNode::<R, R::ChildOutput, S>::new(rule));
|
||||||
|
DynamicInput { input }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds an externally-invalidatable node to the graph whose output is additional
|
||||||
|
/// nodes produced by the given rule.
|
||||||
|
pub fn add_invalidatable_dynamic_rule<R>(
|
||||||
|
&mut self,
|
||||||
|
rule: R,
|
||||||
|
) -> (DynamicInput<R::ChildOutput>, InvalidationSignal)
|
||||||
|
where
|
||||||
|
R: DynamicRule,
|
||||||
|
{
|
||||||
|
let input = self.add_dynamic_rule(rule);
|
||||||
|
let signal = self.make_invalidation_signal(&input.input);
|
||||||
|
(input, signal)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a graph from this builder, consuming the builder.
|
||||||
|
///
|
||||||
|
/// To successfully build a graph, there must be an output node set (using either
|
||||||
|
/// [`GraphBuilder::set_output`] or [`GraphBuilder::set_async_output`]) and there canont be any
|
||||||
|
/// cycles in the graph.
|
||||||
|
///
|
||||||
|
/// Any nodes present in the builder not connected to the output node are removed from the graph.
|
||||||
|
pub fn build(self) -> Result<Graph<O, S>, BuildGraphError> {
|
||||||
|
let output: &Input<O> = match &self.output {
|
||||||
|
None => return Err(BuildGraphError::NoOutput),
|
||||||
|
Some(output) => output,
|
||||||
|
};
|
||||||
|
|
||||||
|
let graph = self.node_graph.borrow();
|
||||||
|
let indices = graph.node_indices().collect::<Vec<_>>();
|
||||||
|
drop(graph);
|
||||||
|
let mut edges = vec![];
|
||||||
|
for idx in indices {
|
||||||
|
let node = &mut self.node_graph.borrow_mut()[idx];
|
||||||
|
node.visit_inputs(&mut |input_idx| {
|
||||||
|
edges.push((input_idx, idx));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut graph = self.node_graph.borrow_mut();
|
||||||
|
|
||||||
|
for (source, dest) in edges {
|
||||||
|
// The graph may not contain the source node in the case of a removed child
|
||||||
|
// of a dynamic node.
|
||||||
|
if graph.contains_node(source) {
|
||||||
|
graph.add_edge(source, dest, ());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
util::remove_nodes_not_connected_to(&mut *graph, output.node_idx.get().unwrap());
|
||||||
|
|
||||||
|
drop(graph);
|
||||||
|
|
||||||
|
let sorted_nodes =
|
||||||
|
petgraph::algo::toposort(&**self.node_graph.borrow(), None).map_err(|cycle| {
|
||||||
|
let start_node = cycle.node_id();
|
||||||
|
// Do a depth-first search of the dependencies of start_node until we get back to start_node.
|
||||||
|
let mut cycle =
|
||||||
|
find_cycle(&self.node_graph.borrow().0, start_node, start_node).unwrap();
|
||||||
|
cycle.push(start_node);
|
||||||
|
BuildGraphError::Cycle(cycle)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let graph = Graph {
|
||||||
|
node_graph: self.node_graph,
|
||||||
|
output: self.output.unwrap(),
|
||||||
|
output_type: std::marker::PhantomData,
|
||||||
|
sorted_nodes,
|
||||||
|
is_valid: self.is_valid,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(graph)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_cycle<G: IntoEdgesDirected>(
|
||||||
|
graph: G,
|
||||||
|
current_node: G::NodeId,
|
||||||
|
target_node: G::NodeId,
|
||||||
|
) -> Option<Vec<G::NodeId>> {
|
||||||
|
for dep in graph.edges_directed(current_node, petgraph::Direction::Incoming) {
|
||||||
|
let node = dep.source();
|
||||||
|
if node == target_node {
|
||||||
|
return Some(vec![node]);
|
||||||
|
} else if let Some(mut path) = find_cycle(graph, node, target_node) {
|
||||||
|
path.push(node);
|
||||||
|
return Some(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<O: 'static> GraphBuilder<O, Asynchronous> {
|
||||||
|
/// Sets an asynchronous rule for the output node.
|
||||||
|
///
|
||||||
|
/// The type of the output rule's value is the type of the output value of the overall graph.
|
||||||
|
pub fn set_async_output<R: AsyncRule<Output = O>>(&mut self, rule: R) {
|
||||||
|
let input = self.add_async_rule(rule);
|
||||||
|
self.output = Some(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replaces the current output rule with a new one, changing the output value type.
|
||||||
|
pub fn with_async_output<R: AsyncRule>(
|
||||||
|
mut self,
|
||||||
|
rule: R,
|
||||||
|
) -> GraphBuilder<R::Output, Asynchronous> {
|
||||||
|
let input = self.add_async_rule(rule);
|
||||||
|
GraphBuilder {
|
||||||
|
node_graph: self.node_graph,
|
||||||
|
output: Some(input),
|
||||||
|
output_type: std::marker::PhantomData,
|
||||||
|
is_valid: self.is_valid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a constant node whose value is computed by the given function to the graph.
|
||||||
|
///
|
||||||
|
/// The function is not called until the node is evaluated by the graph.
|
||||||
|
///
|
||||||
|
/// Returns an [`Input`] representing the newly-added node, which can be used to construct rules.
|
||||||
|
pub fn add_async_value<V, P, F>(&mut self, value_provider: P) -> Input<V>
|
||||||
|
where
|
||||||
|
V: NodeValue,
|
||||||
|
P: FnOnce() -> F + 'static,
|
||||||
|
F: Future<Output = V> + 'static,
|
||||||
|
{
|
||||||
|
self.add_node(AsyncConstNode::new(value_provider))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a node whose value is produced using the given rule to the graph.
|
||||||
|
///
|
||||||
|
/// Returns an [`Input`] representing the newly-added node, which can be used to construct further rules.
|
||||||
|
pub fn add_async_rule<R>(&mut self, rule: R) -> Input<R::Output>
|
||||||
|
where
|
||||||
|
R: AsyncRule,
|
||||||
|
{
|
||||||
|
self.add_node(AsyncRuleNode::new(rule))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds an externally-invalidatable node whose value is produced using the given async rule to the graph.
|
||||||
|
///
|
||||||
|
/// Returns an [`Input`] representing the newly-added node, which can be used to construct further rules,
|
||||||
|
/// as well as an [`InvalidationSignal`] which can be used to indicate that the node has been invalidated.
|
||||||
|
pub fn add_invalidatable_async_rule<R: AsyncRule>(
|
||||||
|
&mut self,
|
||||||
|
rule: R,
|
||||||
|
) -> (Input<R::Output>, InvalidationSignal) {
|
||||||
|
let input = self.add_async_rule(rule);
|
||||||
|
let signal = self.make_invalidation_signal(&input);
|
||||||
|
(input, signal)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a node to the graph whose output is additional nodes produced asynchronously by the given rule.
|
||||||
|
pub fn add_async_dynamic_rule<R>(&mut self, rule: R) -> DynamicInput<R::ChildOutput>
|
||||||
|
where
|
||||||
|
R: AsyncDynamicRule,
|
||||||
|
{
|
||||||
|
let input = self.add_node(AsyncDynamicRuleNode::<R, R::ChildOutput>::new(rule));
|
||||||
|
DynamicInput { input }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds an externally-invalidatable node to the graph whose output is additional nodes produced
|
||||||
|
/// asynchronously by the given rule.
|
||||||
|
pub fn add_invalidatable_async_dynamic_rule<R>(
|
||||||
|
&mut self,
|
||||||
|
rule: R,
|
||||||
|
) -> (DynamicInput<R::ChildOutput>, InvalidationSignal)
|
||||||
|
where
|
||||||
|
R: AsyncDynamicRule,
|
||||||
|
{
|
||||||
|
let input = self.add_async_dynamic_rule(rule);
|
||||||
|
let signal = self.make_invalidation_signal(&input.input);
|
||||||
|
(input, signal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A reason why a [`GraphBuilder`] can fail to build a graph.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum BuildGraphError {
|
||||||
|
/// No output rule has been specified with [`GraphBuilder::set_output`].
|
||||||
|
NoOutput,
|
||||||
|
/// There is a cycle in the graph between the given nodes.
|
||||||
|
///
|
||||||
|
/// The first and last element of the `Vec` are the same, with the elements in between representing
|
||||||
|
/// the path on which the cycle was found.
|
||||||
|
Cycle(Vec<NodeId>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for BuildGraphError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{self:?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for BuildGraphError {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::tests::{DeferredInput, Double};
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn find_cycle() {
|
||||||
|
let mut graph = petgraph::graph::DiGraph::new();
|
||||||
|
let a = graph.add_node(());
|
||||||
|
let b = graph.add_node(());
|
||||||
|
let c = graph.add_node(());
|
||||||
|
let d = graph.add_node(());
|
||||||
|
graph.add_edge(a, b, ());
|
||||||
|
graph.add_edge(a, c, ());
|
||||||
|
graph.add_edge(b, d, ());
|
||||||
|
graph.add_edge(d, a, ());
|
||||||
|
assert!(petgraph::algo::is_cyclic_directed(&graph));
|
||||||
|
assert_eq!(super::find_cycle(&graph, a, a), Some(vec![a, b, d]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cant_build_no_output() {
|
||||||
|
let builder = super::GraphBuilder::<i32, crate::synchronicity::Synchronous>::new();
|
||||||
|
match builder.build() {
|
||||||
|
Err(super::BuildGraphError::NoOutput) => (),
|
||||||
|
Err(e) => assert!(false, "unexpected error {:?}", e),
|
||||||
|
Ok(_) => assert!(false, "shouldn't have built graph"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cant_build_cycle() {
|
||||||
|
let mut builder = super::GraphBuilder::new();
|
||||||
|
let a_input = Rc::new(RefCell::new(None));
|
||||||
|
let a = builder.add_rule(DeferredInput::new(Rc::clone(&a_input)));
|
||||||
|
let b_input = Rc::new(RefCell::new(Some(a.clone())));
|
||||||
|
let b = builder.add_rule(DeferredInput::new(b_input));
|
||||||
|
*a_input.borrow_mut() = Some(b.clone());
|
||||||
|
builder.set_output(Double::new(b.clone()));
|
||||||
|
match builder.build() {
|
||||||
|
Err(super::BuildGraphError::Cycle(cycle)) => {
|
||||||
|
let a_start = cycle
|
||||||
|
== vec![
|
||||||
|
a.node_idx.get().unwrap(),
|
||||||
|
b.node_idx.get().unwrap(),
|
||||||
|
a.node_idx.get().unwrap(),
|
||||||
|
];
|
||||||
|
let b_start = cycle
|
||||||
|
== vec![
|
||||||
|
b.node_idx.get().unwrap(),
|
||||||
|
a.node_idx.get().unwrap(),
|
||||||
|
b.node_idx.get().unwrap(),
|
||||||
|
];
|
||||||
|
// either is a permisisble way of describing the cycle
|
||||||
|
assert!(a_start || b_start);
|
||||||
|
}
|
||||||
|
Err(e) => assert!(false, "unexpected error {:?}", e),
|
||||||
|
Ok(_) => assert!(false, "shouldn't have built graph"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
165
crates/compute_graph/src/input.rs
Normal file
165
crates/compute_graph/src/input.rs
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
use std::{
|
||||||
|
cell::{Cell, Ref, RefCell},
|
||||||
|
ops::Deref,
|
||||||
|
rc::Rc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{node::DynamicRuleOutput, rule::DynamicNodeFactory, NodeId};
|
||||||
|
|
||||||
|
pub use compute_graph_macros::InputVisitable;
|
||||||
|
|
||||||
|
/// A placeholder for the output of one node, to be used as an input for another.
|
||||||
|
///
|
||||||
|
/// To obtain an input, add a value or rule to a [`GraphBuilder`](`crate::builder::GraphBuilder`).
|
||||||
|
///
|
||||||
|
/// Note that this type implements `Clone`, so can be cloned and used as an input for multiple nodes.
|
||||||
|
pub struct Input<T> {
|
||||||
|
pub(crate) node_idx: Rc<Cell<Option<NodeId>>>,
|
||||||
|
pub(crate) value: Rc<RefCell<Option<T>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Input<T> {
|
||||||
|
/// Retrieves a reference to the current value of the node the input represents.
|
||||||
|
///
|
||||||
|
/// Calling this method before the node it represents has been evaluated will panic.
|
||||||
|
pub fn value(&self) -> impl Deref<Target = T> + '_ {
|
||||||
|
Ref::map(self.value.borrow(), |opt| {
|
||||||
|
opt.as_ref()
|
||||||
|
.expect("node must be evaluated before reading value")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the ID of the node that this input refers to.
|
||||||
|
pub fn node_id(&self) -> NodeId {
|
||||||
|
self.node_idx.get().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts this input to a type-erased input.
|
||||||
|
pub fn as_any_input(&self) -> AnyInput {
|
||||||
|
AnyInput {
|
||||||
|
node_idx: Rc::clone(&self.node_idx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// can't derive this impl because it incorectly adds the bound T: Clone
|
||||||
|
impl<T> Clone for Input<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
node_idx: Rc::clone(&self.node_idx),
|
||||||
|
value: Rc::clone(&self.value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> std::fmt::Debug for Input<T> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Input<{}>({:?})",
|
||||||
|
std::any::type_name::<T>(),
|
||||||
|
self.node_idx
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A placeholder for the output of a dynamic rule node, to be used as an input for another.
|
||||||
|
///
|
||||||
|
/// See [`GraphBuilder::add_dynamic_rule`](`crate::builder::GraphBuilder::add_dynamic_rule`).
|
||||||
|
///
|
||||||
|
/// A dependency on a dynamic input represents both a dependency on the dynamic node itself,
|
||||||
|
/// as well as dependencies on each of the nodes that are the output of the dynamic node.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct DynamicInput<T> {
|
||||||
|
pub(crate) input: Input<DynamicRuleOutput<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> DynamicInput<T> {
|
||||||
|
/// Retrieves a reference to the current value of the dynamic node (i.e., the set of inputs
|
||||||
|
/// representing the nodes that are the outputs of the dynamic node).
|
||||||
|
pub fn value(&self) -> impl Deref<Target = DynamicRuleOutput<T>> + '_ {
|
||||||
|
self.input.value()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_input(&self) -> &Input<DynamicRuleOutput<T>> {
|
||||||
|
&self.input
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts this input to a type-erased input.
|
||||||
|
pub fn as_any_input(&self) -> AnyInput {
|
||||||
|
self.input.as_any_input()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A type erased [`Input`].
|
||||||
|
pub struct AnyInput {
|
||||||
|
node_idx: Rc<Cell<Option<NodeId>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnyInput {
|
||||||
|
/// Get the ID of the node that this input represents.
|
||||||
|
pub fn node_id(&self) -> NodeId {
|
||||||
|
self.node_idx.get().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: i really want Input to be able to implement Deref somehow
|
||||||
|
|
||||||
|
/// A type that can visit arbitrary [`Input`]s.
|
||||||
|
///
|
||||||
|
/// You generally do not implement this trait yourself. An implementation is provided to
|
||||||
|
/// [`InputVisitable::visit_inputs`].
|
||||||
|
pub trait InputVisitor {
|
||||||
|
/// Visit an input whose value is of type `T`.
|
||||||
|
fn visit<T>(&mut self, input: &Input<T>);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Common supertrait of [`Rule`](`crate::rule::Rule`) and [`AsyncRule`](`crate::rule::AsyncRule`)
|
||||||
|
/// that defines how rule inputs are visited.
|
||||||
|
///
|
||||||
|
/// The implementation of this trait can generally be derived using [`derive@InputVisitable`].
|
||||||
|
pub trait InputVisitable {
|
||||||
|
/// Visits all the [`Input`]s of this rule.
|
||||||
|
///
|
||||||
|
/// This method is called when the graph is built/modified in order to establish edges of the graph,
|
||||||
|
/// representing the dependencies. Any input that the [`InputVisitor::visit`] is called with is
|
||||||
|
/// considered a dependency of the rule's node.
|
||||||
|
///
|
||||||
|
/// While it is permitted for the dependencies of a rule to change after it has been added to the graph,
|
||||||
|
/// doing so only permitted before the graph has been built or during the callback of
|
||||||
|
/// [`Graph::modify`](`crate::Graph::modify`). Changes to the rule's dependencies outside of that will
|
||||||
|
/// not be detected and will not be represented in the graph.
|
||||||
|
fn visit_inputs(&self, visitor: &mut impl InputVisitor);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> InputVisitable for Input<T> {
|
||||||
|
fn visit_inputs(&self, visitor: &mut impl InputVisitor) {
|
||||||
|
visitor.visit(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> InputVisitable for DynamicInput<T> {
|
||||||
|
fn visit_inputs(&self, visitor: &mut impl InputVisitor) {
|
||||||
|
// Visit the dynamic node itself
|
||||||
|
visitor.visit(&self.input);
|
||||||
|
|
||||||
|
// And visit all the nodes it produces
|
||||||
|
let maybe_dynamic_output = self.input.value.borrow();
|
||||||
|
if let Some(dynamic_output) = maybe_dynamic_output.as_ref() {
|
||||||
|
// This might be slightly overzealous: it is possible for a node to only depend on the
|
||||||
|
// dynamic node itself, and not directly depend on any of the nodes the dynamic node produces.
|
||||||
|
for input in dynamic_output.inputs.iter() {
|
||||||
|
visitor.visit(input);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Haven't evaluated the dynamic node for the first time yet.
|
||||||
|
// Upon doing so, if the nodes it produces change, we'll modify the graph
|
||||||
|
// and end up back here in the other branch.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// no-op impl, here for convenience so you don't have to have to have as many `#[skip_input]`s
|
||||||
|
impl<ID, T> InputVisitable for DynamicNodeFactory<ID, T> {
|
||||||
|
fn visit_inputs(&self, _visitor: &mut impl InputVisitor) {}
|
||||||
|
}
|
988
crates/compute_graph/src/lib.rs
Normal file
988
crates/compute_graph/src/lib.rs
Normal file
@ -0,0 +1,988 @@
|
|||||||
|
//! Facilities for using a directed, acyclic graph to perform computation.
|
||||||
|
//!
|
||||||
|
//! A directed, acyclic graph (DAG) can be used to carry out computations by considering
|
||||||
|
//! each node to have a value and each edge to represent a dependency on the value of one
|
||||||
|
//! node to compute the value of another node. A node's value can either be constant or be
|
||||||
|
//! produced by a rule, which is a piece of code for generating the value of a node given its
|
||||||
|
//! dependencies. For example, an arithmetic operation can be implemented like so:
|
||||||
|
//!
|
||||||
|
//! ```rust
|
||||||
|
//! # use compute_graph::{builder::GraphBuilder, rule::Rule, input::{Input, InputVisitable}};
|
||||||
|
//! let mut builder = GraphBuilder::new();
|
||||||
|
//! let a = builder.add_value(1);
|
||||||
|
//! let b = builder.add_value(2);
|
||||||
|
//! # #[derive(InputVisitable)]
|
||||||
|
//! # struct Add(Input<i32>, Input<i32>);
|
||||||
|
//! # impl Rule for Add {
|
||||||
|
//! # type Output = i32;
|
||||||
|
//! # fn evaluate(&mut self) -> i32 {
|
||||||
|
//! # *self.input_0() + *self.input_1()
|
||||||
|
//! # }
|
||||||
|
//! # }
|
||||||
|
//! builder.set_output(Add(a, b));
|
||||||
|
//!
|
||||||
|
//! let mut graph = builder.build().unwrap();
|
||||||
|
//! assert_eq!(*graph.evaluate(), 3);
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Here, `a` and `b` are placeholders representing the values of the two constant nodes in the graph.
|
||||||
|
//! The `Add` struct implements the [`Rule`](`crate::rule::Rule`) trait and defines how to combine
|
||||||
|
//! those two values by addition.
|
||||||
|
//! The `Add` rule is implemented as follows:
|
||||||
|
//!
|
||||||
|
//! ```rust
|
||||||
|
//! # use compute_graph::{builder::GraphBuilder, rule::Rule, input::{Input, InputVisitable}};
|
||||||
|
//! #[derive(InputVisitable)]
|
||||||
|
//! struct Add(Input<i32>, Input<i32>);
|
||||||
|
//!
|
||||||
|
//! impl Rule for Add {
|
||||||
|
//! type Output = i32;
|
||||||
|
//! fn evaluate(&mut self) -> i32 {
|
||||||
|
//! *self.input_0() + *self.input_1()
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
pub mod builder;
|
||||||
|
pub mod input;
|
||||||
|
pub mod node;
|
||||||
|
pub mod rule;
|
||||||
|
pub mod synchronicity;
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
use builder::{BuildGraphError, GraphBuilder};
|
||||||
|
use input::{Input, InputVisitor};
|
||||||
|
use node::{ErasedNode, NodeUpdateContext, NodeValue};
|
||||||
|
use petgraph::visit::{IntoEdgeReferences, IntoNodeReferences, NodeIndexable, NodeRef};
|
||||||
|
use petgraph::{stable_graph::StableDiGraph, visit::EdgeRef};
|
||||||
|
use std::cell::{Cell, RefCell};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::ops::{Deref, DerefMut};
|
||||||
|
use std::rc::Rc;
|
||||||
|
use synchronicity::*;
|
||||||
|
|
||||||
|
// use a struct for this, not a type alias, because generic bounds of type aliases aren't enforced
|
||||||
|
struct NodeGraph<S: Synchronicity>(StableDiGraph<ErasedNode<S>, (), u32>);
|
||||||
|
pub type NodeId = petgraph::stable_graph::NodeIndex<u32>;
|
||||||
|
|
||||||
|
impl<S: Synchronicity> NodeGraph<S> {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self(StableDiGraph::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: Synchronicity> Deref for NodeGraph<S> {
|
||||||
|
type Target = StableDiGraph<ErasedNode<S>, (), u32>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: Synchronicity> DerefMut for NodeGraph<S> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A constructed graph that can evaluated.
|
||||||
|
///
|
||||||
|
/// Use [`GraphBuilder`] to construct a graph.
|
||||||
|
///
|
||||||
|
/// The graph is generic over the type of the output node's value and the [`Synchronicity`]
|
||||||
|
/// —that is, whether it can be evaluated synchronously or asynchronously.
|
||||||
|
pub struct Graph<Output, Synch: Synchronicity> {
|
||||||
|
node_graph: Rc<RefCell<NodeGraph<Synch>>>,
|
||||||
|
output: Input<Output>,
|
||||||
|
output_type: std::marker::PhantomData<Output>,
|
||||||
|
// The topological sort of nodes in the graph.
|
||||||
|
sorted_nodes: Vec<NodeId>,
|
||||||
|
is_valid: Rc<Cell<bool>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A synchronous graph, containing only sync nodes.
|
||||||
|
pub type SyncGraph<Output> = Graph<Output, Synchronous>;
|
||||||
|
|
||||||
|
/// An asynchronous graph, containing a mix of sync and async nodes.
|
||||||
|
pub type AsyncGraph<Output> = Graph<Output, Asynchronous>;
|
||||||
|
|
||||||
|
impl<O: 'static, S: Synchronicity> Graph<O, S> {
|
||||||
|
/// Whether the output value of the graph is currently valid.
|
||||||
|
///
|
||||||
|
/// The output is considered presumptively invalid if _any_ of the nodes in the graph are invalid,
|
||||||
|
/// even if, when evaluated, the invalid node's value is unchanged (in which case, downstream nodes
|
||||||
|
/// are not invalidated) and the output may be unchanged.
|
||||||
|
pub fn is_output_valid(&self) -> bool {
|
||||||
|
self.is_valid.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The number of nodes in the graph.
|
||||||
|
pub fn node_count(&self) -> usize {
|
||||||
|
self.node_graph.borrow().node_count()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Modify the graph using the given function.
|
||||||
|
///
|
||||||
|
/// The function receives as its parameter a [`GraphBuilder`] representing the current graph.
|
||||||
|
///
|
||||||
|
/// Because building a graph can fail and this method mutates the underlying graph, it takes
|
||||||
|
/// ownership of the current graph to prevent the graph being left in an invalid state.
|
||||||
|
/// It returns either the new, modified graph or an error.
|
||||||
|
pub fn modify<F>(mut self, f: F) -> Result<Self, BuildGraphError>
|
||||||
|
where
|
||||||
|
F: FnMut(&mut GraphBuilder<O, S>) -> (),
|
||||||
|
{
|
||||||
|
self._modify(f)?;
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _modify<F>(&mut self, mut f: F) -> Result<(), BuildGraphError>
|
||||||
|
where
|
||||||
|
F: FnMut(&mut GraphBuilder<O, S>) -> (),
|
||||||
|
{
|
||||||
|
// Copy all the current edges so we can check if any change.
|
||||||
|
let graph = self.node_graph.borrow();
|
||||||
|
let mut old_edges = HashMap::new();
|
||||||
|
for edge in graph.edge_references() {
|
||||||
|
old_edges
|
||||||
|
.entry(graph.to_index(edge.source()))
|
||||||
|
.or_insert(vec![])
|
||||||
|
.push(graph.to_index(edge.target()));
|
||||||
|
}
|
||||||
|
drop(graph);
|
||||||
|
|
||||||
|
let old_output = self.output.node_idx.get();
|
||||||
|
|
||||||
|
// Modify
|
||||||
|
let mut builder = self.to_builder();
|
||||||
|
f(&mut builder);
|
||||||
|
*self = builder.build()?;
|
||||||
|
|
||||||
|
// Any new inboud edges invalidate their target nodes.
|
||||||
|
let mut graph = self.node_graph.borrow_mut();
|
||||||
|
let mut to_invalidate = VecDeque::new();
|
||||||
|
for edge in graph.edge_references() {
|
||||||
|
let source = graph.to_index(edge.source());
|
||||||
|
let target = graph.to_index(edge.target());
|
||||||
|
if !old_edges
|
||||||
|
.get(&source)
|
||||||
|
.map_or(false, |old| old.contains(&target))
|
||||||
|
{
|
||||||
|
to_invalidate.push_back(edge.target());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Edge case: if the only node in the graph is the output node, and it's replaced in the modify block,
|
||||||
|
// there are no edges but we still need to invalidate.
|
||||||
|
if !to_invalidate.is_empty() || self.output.node_idx.get() != old_output {
|
||||||
|
self.is_valid.set(false);
|
||||||
|
for idx in to_invalidate {
|
||||||
|
let node = &mut graph[idx];
|
||||||
|
node.invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(graph);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert this graph back into a builder for further modifications.
|
||||||
|
///
|
||||||
|
/// Returns a builder with the same output and synchronicity types.
|
||||||
|
pub fn into_builder(self) -> GraphBuilder<O, S> {
|
||||||
|
self.to_builder()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_builder(&self) -> GraphBuilder<O, S> {
|
||||||
|
// Clear the edges before modifying so that rebuilding results in a graph with up-to-date edges.
|
||||||
|
let mut graph = self.node_graph.borrow_mut();
|
||||||
|
graph.clear_edges();
|
||||||
|
drop(graph);
|
||||||
|
|
||||||
|
GraphBuilder {
|
||||||
|
node_graph: Rc::clone(&self.node_graph),
|
||||||
|
output: Some(self.output.clone()),
|
||||||
|
output_type: std::marker::PhantomData,
|
||||||
|
is_valid: Rc::clone(&self.is_valid),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a graphviz representation of this graph.
|
||||||
|
///
|
||||||
|
/// The exact format is not guaranteed to be stable.
|
||||||
|
pub fn as_dot_string(&self) -> String {
|
||||||
|
// Escaper/Escaped are cribbed from petgraph (MIT licensed).
|
||||||
|
use std::fmt::Write;
|
||||||
|
struct Escaper<W>(W);
|
||||||
|
impl<W: std::fmt::Write> std::fmt::Write for Escaper<W> {
|
||||||
|
fn write_str(&mut self, s: &str) -> std::fmt::Result {
|
||||||
|
for c in s.chars() {
|
||||||
|
self.write_char(c)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn write_char(&mut self, c: char) -> std::fmt::Result {
|
||||||
|
match c {
|
||||||
|
'"' | '\\' => self.0.write_char('\\')?,
|
||||||
|
// \l is for left justified linebreak
|
||||||
|
'\n' => return self.0.write_str("\\l"),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
self.0.write_char(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct Escaped<T>(T);
|
||||||
|
impl<T: std::fmt::Debug> std::fmt::Debug for Escaped<T> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
write!(&mut Escaper(f), "{:?}", &self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Our own dot formatter, to include both node ids and labels.
|
||||||
|
struct Dot<'a, S: Synchronicity>(&'a NodeGraph<S>);
|
||||||
|
impl<'a, S: Synchronicity> std::fmt::Debug for Dot<'a, S> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
writeln!(f, "digraph {{")?;
|
||||||
|
for node in self.0.node_references() {
|
||||||
|
let id = self.0.to_index(node.id());
|
||||||
|
let label = Escaped(node.weight());
|
||||||
|
writeln!(f, "\t{id} [label=\"{label:?} (id={id})\"]")?;
|
||||||
|
}
|
||||||
|
for edge in self.0.edge_references() {
|
||||||
|
let source = self.0.to_index(edge.source());
|
||||||
|
let target = self.0.to_index(edge.target());
|
||||||
|
writeln!(f, "\t{source} -> {target} []")?;
|
||||||
|
}
|
||||||
|
writeln!(f, "}}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let graph = self.node_graph.borrow();
|
||||||
|
format!("{:?}", Dot(&*graph))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<O: 'static, S: Synchronicity> Graph<O, S> {
|
||||||
|
fn process_update_step<'a>(
|
||||||
|
&'a mut self,
|
||||||
|
current_idx: NodeId,
|
||||||
|
mut ctx: NodeUpdateContext<S>,
|
||||||
|
) -> UpdateStepResult {
|
||||||
|
let mut graph = self.node_graph.borrow_mut();
|
||||||
|
let mut nodes_changed = false;
|
||||||
|
while let Some(idx_to_remove) = ctx.removed_nodes.pop_front() {
|
||||||
|
assert!(
|
||||||
|
idx_to_remove != current_idx,
|
||||||
|
"cannot remove node curently being evaluated"
|
||||||
|
);
|
||||||
|
let node = &graph[idx_to_remove];
|
||||||
|
node.remove_children(&mut ctx);
|
||||||
|
let (index_to_remove_in_sorted, _) = self
|
||||||
|
.sorted_nodes
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find(|(_, idx)| **idx == idx_to_remove)
|
||||||
|
.expect("removed node must have been already added");
|
||||||
|
graph.remove_node(idx_to_remove);
|
||||||
|
self.sorted_nodes.remove(index_to_remove_in_sorted);
|
||||||
|
nodes_changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (added_node, id_cell) in ctx.added_nodes {
|
||||||
|
let id = graph.add_node(added_node);
|
||||||
|
id_cell.set(Some(id));
|
||||||
|
nodes_changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if nodes_changed {
|
||||||
|
// Update the graph before invalidating downstream nodes.
|
||||||
|
drop(graph);
|
||||||
|
self._modify(|_| {})
|
||||||
|
.expect("modifying graph during evaluation must produce valid graph");
|
||||||
|
graph = self.node_graph.borrow_mut();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.invalidate_dependent_nodes {
|
||||||
|
// Invalidate any downstream nodes (which we know we haven't visited yet, because
|
||||||
|
// we're iterating over a topological sort of the graph).
|
||||||
|
let dependents = graph
|
||||||
|
.edges_directed(current_idx, petgraph::Direction::Outgoing)
|
||||||
|
.map(|edge| edge.target())
|
||||||
|
// Need to collect because the edges_directed iterator borrows the graph, and
|
||||||
|
// we need to mutably borrow to invalidate.
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for dependent_idx in dependents {
|
||||||
|
let dependent = &mut graph[dependent_idx];
|
||||||
|
dependent.invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if nodes_changed {
|
||||||
|
UpdateStepResult::Restart
|
||||||
|
} else {
|
||||||
|
UpdateStepResult::Continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum UpdateStepResult {
|
||||||
|
Continue,
|
||||||
|
Restart,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<O: 'static> Graph<O, Synchronous> {
|
||||||
|
fn update_invalid_nodes(&mut self) {
|
||||||
|
let mut graph = self.node_graph.borrow_mut();
|
||||||
|
let mut i = 0;
|
||||||
|
while i < self.sorted_nodes.len() {
|
||||||
|
let idx = self.sorted_nodes[i];
|
||||||
|
let node = &mut graph[idx];
|
||||||
|
if !node.is_valid() {
|
||||||
|
// Update this node
|
||||||
|
let mut ctx = NodeUpdateContext::new(self);
|
||||||
|
node.update(&mut ctx);
|
||||||
|
|
||||||
|
drop(graph);
|
||||||
|
let result = self.process_update_step(idx, ctx);
|
||||||
|
graph = self.node_graph.borrow_mut();
|
||||||
|
|
||||||
|
if result == UpdateStepResult::Restart {
|
||||||
|
// If we added/removed nodes, the sorted order has changed, so start evaluating
|
||||||
|
// from the beginning, in case of changes before i.
|
||||||
|
i = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consistency check: after updating in the topological sort order, we should be left with
|
||||||
|
// no invalid nodes.
|
||||||
|
debug_assert!(self
|
||||||
|
.sorted_nodes
|
||||||
|
.iter()
|
||||||
|
.all(|&idx| { (&graph[idx]).is_valid() }));
|
||||||
|
|
||||||
|
self.is_valid.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synchronously evaluate the graph and return a reference to the value of the output node.
|
||||||
|
///
|
||||||
|
/// If the graph is valid (see [`Graph::is_output_valid`]), this is a constant-time operation.
|
||||||
|
/// Otherwise, any invalid nodes and their downstream dependents will be updated, which is an
|
||||||
|
/// O(n) operation.
|
||||||
|
///
|
||||||
|
/// This method is only available on synchronous graphs, which can only contain synchronous nodes.
|
||||||
|
pub fn evaluate(&mut self) -> impl Deref<Target = O> + '_ {
|
||||||
|
if !self.is_valid.get() {
|
||||||
|
self.update_invalid_nodes();
|
||||||
|
}
|
||||||
|
self.output.value()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<O: 'static> Graph<O, Asynchronous> {
|
||||||
|
async fn update_invalid_nodes(&mut self) {
|
||||||
|
// TODO: consider whether this can be done in parallel to any degree.
|
||||||
|
let mut graph = self.node_graph.borrow_mut();
|
||||||
|
let mut i = 0;
|
||||||
|
while i < self.sorted_nodes.len() {
|
||||||
|
let idx = self.sorted_nodes[i];
|
||||||
|
let node = &mut graph[idx];
|
||||||
|
if !node.is_valid() {
|
||||||
|
// Update this node
|
||||||
|
let mut ctx = NodeUpdateContext::new(self);
|
||||||
|
node.update(&mut ctx).await;
|
||||||
|
|
||||||
|
drop(graph);
|
||||||
|
let result = self.process_update_step(idx, ctx);
|
||||||
|
graph = self.node_graph.borrow_mut();
|
||||||
|
|
||||||
|
if result == UpdateStepResult::Restart {
|
||||||
|
// If we added/removed nodes, the sorted order has changed, so start evaluating
|
||||||
|
// from the beginning, in case of changes before i.
|
||||||
|
i = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consistency check: after updating in the topological sort order, we should be left with
|
||||||
|
// no invalid nodes.
|
||||||
|
debug_assert!(self
|
||||||
|
.sorted_nodes
|
||||||
|
.iter()
|
||||||
|
.all(|&idx| { (&graph[idx]).is_valid() }));
|
||||||
|
|
||||||
|
self.is_valid.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Asynchronously evaluate the graph and return a reference to the value of the output node.
|
||||||
|
///
|
||||||
|
/// If the graph is valid (see [`Graph::is_output_valid`]), this is a constant-time operation.
|
||||||
|
/// Otherwise, any invalid nodes and their downstream dependents will be updated, which is an
|
||||||
|
/// O(n) operation.
|
||||||
|
///
|
||||||
|
/// This method is only available on asynchronous graphs, which can contain a mix of asynchronous
|
||||||
|
/// and synchronous nodes.
|
||||||
|
pub async fn evaluate_async(&mut self) -> impl Deref<Target = O> + '_ {
|
||||||
|
if !self.is_valid.get() {
|
||||||
|
self.update_invalid_nodes().await;
|
||||||
|
}
|
||||||
|
self.output.value()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A type representing a node in a graph that can be invalidated due to external factors.
|
||||||
|
///
|
||||||
|
/// See [`GraphBuilder::add_invalidatable_rule`].
|
||||||
|
///
|
||||||
|
/// `InvalidationSignal` implements `Clone`, so the signal can be cloned and used from multiple places.
|
||||||
|
// TODO: there's a lot happening here, make sure this doesn't create a reference cycle
|
||||||
|
pub struct InvalidationSignal {
|
||||||
|
do_invalidate: Rc<Box<dyn Fn() -> ()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InvalidationSignal {
|
||||||
|
pub(crate) fn new<V, S: Synchronicity>(
|
||||||
|
input: &Input<V>,
|
||||||
|
graph: Rc<RefCell<NodeGraph<S>>>,
|
||||||
|
graph_is_valid: Rc<Cell<bool>>,
|
||||||
|
) -> Self {
|
||||||
|
let node_idx = Rc::clone(&input.node_idx);
|
||||||
|
InvalidationSignal {
|
||||||
|
do_invalidate: Rc::new(Box::new(move || {
|
||||||
|
graph_is_valid.set(false);
|
||||||
|
let mut graph = graph.borrow_mut();
|
||||||
|
let node = &mut graph[node_idx.get().unwrap()];
|
||||||
|
node.invalidate();
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tell the graph that the node corresponding to this signal is now invalid.
|
||||||
|
///
|
||||||
|
/// Note: Calling this method does not trigger a graph evaluation, it merely marks the corresponding
|
||||||
|
/// node as invalid. The graph will not be re-evaluated until [`Graph::evaluate`] or
|
||||||
|
/// [`Graph::evaluate_async`] is next called.
|
||||||
|
pub fn invalidate(&self) {
|
||||||
|
(self.do_invalidate)();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for InvalidationSignal {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
do_invalidate: Rc::clone(&self.do_invalidate),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A type representing a node with an externally injected value.
|
||||||
|
///
|
||||||
|
/// See [`GraphBuilder::add_invalidatable_value`].
|
||||||
|
pub struct ValueInvalidationSignal<V> {
|
||||||
|
input: Input<V>,
|
||||||
|
signal: InvalidationSignal,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V: NodeValue> ValueInvalidationSignal<V> {
|
||||||
|
/// Get a reference to current value for the node corresponding to this signal.
|
||||||
|
pub fn value(&self) -> impl Deref<Target = V> + '_ {
|
||||||
|
self.input.value()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a new value for the node corresponding to this signal.
|
||||||
|
///
|
||||||
|
/// Note: Calling this method does not trigger a graph evaluation, it merely sets a new value
|
||||||
|
/// for the corresponding node. The graph will not be re-evaluated until [`Graph::evaluate`] or
|
||||||
|
/// [`Graph::evaluate_async`] is next called.
|
||||||
|
pub fn set_value(&self, value: V) {
|
||||||
|
let mut current_value = self.input.value.borrow_mut();
|
||||||
|
if !current_value
|
||||||
|
.as_ref()
|
||||||
|
.expect("invalidatable value node must be initialized with value")
|
||||||
|
.node_value_eq(&value)
|
||||||
|
{
|
||||||
|
*current_value = Some(value);
|
||||||
|
self.signal.invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V> Clone for ValueInvalidationSignal<V> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
input: self.input.clone(),
|
||||||
|
signal: self.signal.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use rule::DynamicNodeFactory;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::input::{DynamicInput, InputVisitable};
|
||||||
|
use crate::rule::{AsyncDynamicRule, AsyncRule, ConstantRule, DynamicRule, Rule};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rule_output_with_no_inputs() {
|
||||||
|
let mut builder = GraphBuilder::new();
|
||||||
|
builder.set_output(ConstantRule::new(1234));
|
||||||
|
assert_eq!(*builder.build().unwrap().evaluate(), 1234);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_output_is_valid() {
|
||||||
|
let mut builder = GraphBuilder::new();
|
||||||
|
builder.set_output(ConstantRule::new(1));
|
||||||
|
let mut graph = builder.build().unwrap();
|
||||||
|
assert!(!graph.is_output_valid());
|
||||||
|
graph.evaluate();
|
||||||
|
assert!(graph.is_output_valid());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Double(Input<i32>);
|
||||||
|
impl Double {
|
||||||
|
pub(crate) fn new(input: Input<i32>) -> Self {
|
||||||
|
Self(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl InputVisitable for Double {
|
||||||
|
fn visit_inputs(&self, visitor: &mut impl InputVisitor) {
|
||||||
|
visitor.visit(&self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Rule for Double {
|
||||||
|
type Output = i32;
|
||||||
|
fn evaluate(&mut self) -> i32 {
|
||||||
|
*self.0.value() * 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rule_with_input() {
|
||||||
|
let mut builder = GraphBuilder::new();
|
||||||
|
let input = builder.add_value(42);
|
||||||
|
builder.set_output(Double(input));
|
||||||
|
assert_eq!(*builder.build().unwrap().evaluate(), 84);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rule_with_input_rule() {
|
||||||
|
let mut builder = GraphBuilder::new();
|
||||||
|
let input = builder.add_value(42);
|
||||||
|
let doubled = builder.add_rule(Double(input));
|
||||||
|
builder.set_output(Double(doubled));
|
||||||
|
assert_eq!(*builder.build().unwrap().evaluate(), 168);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Inc(i32);
|
||||||
|
impl InputVisitable for Inc {
|
||||||
|
fn visit_inputs(&self, _visitor: &mut impl InputVisitor) {}
|
||||||
|
}
|
||||||
|
impl Rule for Inc {
|
||||||
|
type Output = i32;
|
||||||
|
fn evaluate(&mut self) -> i32 {
|
||||||
|
self.0 += 1;
|
||||||
|
return self.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalidatable_rule() {
|
||||||
|
let mut builder = GraphBuilder::new();
|
||||||
|
let (input, invalidate) = builder.add_invalidatable_rule(Inc(0));
|
||||||
|
builder.set_output(Double(input));
|
||||||
|
let mut graph = builder.build().unwrap();
|
||||||
|
assert_eq!(*graph.evaluate(), 2);
|
||||||
|
invalidate.invalidate();
|
||||||
|
assert_eq!(*graph.evaluate(), 4);
|
||||||
|
assert_eq!(*graph.evaluate(), 4);
|
||||||
|
invalidate.invalidate();
|
||||||
|
assert_eq!(*graph.evaluate(), 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Add(Input<i32>, Input<i32>);
|
||||||
|
impl InputVisitable for Add {
|
||||||
|
fn visit_inputs(&self, visitor: &mut impl InputVisitor) {
|
||||||
|
visitor.visit(&self.0);
|
||||||
|
visitor.visit(&self.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Rule for Add {
|
||||||
|
type Output = i32;
|
||||||
|
fn evaluate(&mut self) -> i32 {
|
||||||
|
*self.0.value() + *self.1.value()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rule_with_multiple_inputs() {
|
||||||
|
let mut builder = GraphBuilder::new();
|
||||||
|
let a = builder.add_value(2);
|
||||||
|
let b = builder.add_value(3);
|
||||||
|
builder.set_output(Add(a, b));
|
||||||
|
assert_eq!(*builder.build().unwrap().evaluate(), 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rule_with_invalidatable_inputs() {
|
||||||
|
let mut builder = GraphBuilder::new();
|
||||||
|
let (a, invalidate) = builder.add_invalidatable_rule(Inc(0));
|
||||||
|
let b = builder.add_rule(Inc(0));
|
||||||
|
builder.set_output(Add(a, b));
|
||||||
|
let mut graph = builder.build().unwrap();
|
||||||
|
assert_eq!(*graph.evaluate(), 2);
|
||||||
|
invalidate.invalidate();
|
||||||
|
assert_eq!(*graph.evaluate(), 3);
|
||||||
|
assert_eq!(*graph.evaluate(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct DeferredInput(Rc<RefCell<Option<Input<i32>>>>);
|
||||||
|
impl DeferredInput {
|
||||||
|
pub(crate) fn new(value: Rc<RefCell<Option<Input<i32>>>>) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl InputVisitable for DeferredInput {
|
||||||
|
fn visit_inputs(&self, visitor: &mut impl InputVisitor) {
|
||||||
|
let borrowed = self.0.borrow();
|
||||||
|
let input = borrowed.as_ref().unwrap();
|
||||||
|
visitor.visit(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Rule for DeferredInput {
|
||||||
|
type Output = i32;
|
||||||
|
fn evaluate(&mut self) -> i32 {
|
||||||
|
*self.0.borrow().as_ref().unwrap().value()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn modify_graph() {
|
||||||
|
let mut builder = GraphBuilder::new();
|
||||||
|
builder.set_output(ConstantRule::new(1));
|
||||||
|
let mut graph = builder.build().unwrap();
|
||||||
|
assert_eq!(*graph.evaluate(), 1);
|
||||||
|
graph = graph
|
||||||
|
.modify(|g| {
|
||||||
|
g.set_output(ConstantRule::new(2));
|
||||||
|
})
|
||||||
|
.expect("modify");
|
||||||
|
assert_eq!(*graph.evaluate(), 2);
|
||||||
|
assert_eq!(graph.node_count(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn modify_with_dependencies() {
|
||||||
|
let mut builder = GraphBuilder::new();
|
||||||
|
let input = Rc::new(RefCell::new(None));
|
||||||
|
builder.set_output(DeferredInput(Rc::clone(&input)));
|
||||||
|
*input.borrow_mut() = Some(builder.add_value(1));
|
||||||
|
let mut graph = builder.build().unwrap();
|
||||||
|
assert_eq!(*graph.evaluate(), 1);
|
||||||
|
graph = graph
|
||||||
|
.modify(|g| {
|
||||||
|
*input.borrow_mut() = Some(g.add_value(2));
|
||||||
|
})
|
||||||
|
.expect("modify");
|
||||||
|
assert!(!graph.is_output_valid());
|
||||||
|
assert_eq!(*graph.evaluate(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn async_graph() {
|
||||||
|
let mut builder = GraphBuilder::new_async();
|
||||||
|
builder.set_output(ConstantRule::new(42));
|
||||||
|
let mut graph = builder.build().unwrap();
|
||||||
|
assert_eq!(*graph.evaluate_async().await, 42);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn async_rule() {
|
||||||
|
struct AsyncConst(i32);
|
||||||
|
impl InputVisitable for AsyncConst {
|
||||||
|
fn visit_inputs(&self, _visitor: &mut impl InputVisitor) {}
|
||||||
|
}
|
||||||
|
impl AsyncRule for AsyncConst {
|
||||||
|
type Output = i32;
|
||||||
|
async fn evaluate(&mut self) -> i32 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut builder = GraphBuilder::new_async();
|
||||||
|
builder.set_async_output(AsyncConst(42));
|
||||||
|
let mut graph = builder.build().unwrap();
|
||||||
|
assert_eq!(*graph.evaluate_async().await, 42);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_cloneable_output() {
|
||||||
|
#[derive(PartialEq, Debug)]
|
||||||
|
struct NonCloneable;
|
||||||
|
struct Output;
|
||||||
|
impl InputVisitable for Output {
|
||||||
|
fn visit_inputs(&self, _visitor: &mut impl InputVisitor) {}
|
||||||
|
}
|
||||||
|
impl Rule for Output {
|
||||||
|
type Output = NonCloneable;
|
||||||
|
fn evaluate(&mut self) -> Self::Output {
|
||||||
|
NonCloneable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut builder = GraphBuilder::new();
|
||||||
|
builder.set_output(Output);
|
||||||
|
let mut graph = builder.build().unwrap();
|
||||||
|
assert_eq!(*graph.evaluate(), NonCloneable);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct IncAdd(Input<i32>, i32);
|
||||||
|
impl InputVisitable for IncAdd {
|
||||||
|
fn visit_inputs(&self, visitor: &mut impl InputVisitor) {
|
||||||
|
visitor.visit(&self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Rule for IncAdd {
|
||||||
|
type Output = i32;
|
||||||
|
fn evaluate(&mut self) -> Self::Output {
|
||||||
|
self.1 += 1;
|
||||||
|
*self.0.value() + self.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_update_downstream_nodes_if_value_changes() {
|
||||||
|
let mut builder = GraphBuilder::new();
|
||||||
|
let (a, invalidate) = builder.add_invalidatable_rule(ConstantRule::new(0));
|
||||||
|
builder.set_output(IncAdd(a, 0));
|
||||||
|
let mut graph = builder.build().unwrap();
|
||||||
|
assert_eq!(*graph.evaluate(), 1);
|
||||||
|
|
||||||
|
// IncAdd should not be evaluated again, despite its input being invalidated, so the output should be unchanged
|
||||||
|
invalidate.invalidate();
|
||||||
|
assert!(!graph.is_output_valid());
|
||||||
|
assert_eq!(*graph.evaluate(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalidatable_value() {
|
||||||
|
let mut builder = GraphBuilder::new();
|
||||||
|
let (a, invalidate) = builder.add_invalidatable_value(0);
|
||||||
|
let b = builder.add_value(1);
|
||||||
|
builder.set_output(Add(a, b));
|
||||||
|
let mut graph = builder.build().unwrap();
|
||||||
|
assert_eq!(*graph.evaluate(), 1);
|
||||||
|
invalidate.set_value(42);
|
||||||
|
assert!(!graph.is_output_valid());
|
||||||
|
assert_eq!(*graph.evaluate(), 43);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn async_value() {
|
||||||
|
let mut builder = GraphBuilder::new_async();
|
||||||
|
let a = builder.add_async_value(|| async { 42 });
|
||||||
|
let b = builder.add_value(1);
|
||||||
|
builder.set_output(Add(a, b));
|
||||||
|
let mut graph = builder.build().unwrap();
|
||||||
|
assert_eq!(*graph.evaluate_async().await, 43);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn graphviz() {
|
||||||
|
let mut builder = GraphBuilder::new();
|
||||||
|
let a = builder.add_value(1);
|
||||||
|
let b = builder.add_value(2);
|
||||||
|
struct AddWithLabel(Input<i32>, Input<i32>);
|
||||||
|
impl InputVisitable for AddWithLabel {
|
||||||
|
fn visit_inputs(&self, visitor: &mut impl InputVisitor) {
|
||||||
|
visitor.visit(&self.0);
|
||||||
|
visitor.visit(&self.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Rule for AddWithLabel {
|
||||||
|
type Output = i32;
|
||||||
|
fn evaluate(&mut self) -> Self::Output {
|
||||||
|
*self.0.value() + *self.1.value()
|
||||||
|
}
|
||||||
|
fn node_label(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
write!(f, "test")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.set_output(AddWithLabel(a, b));
|
||||||
|
let graph = builder.build().unwrap();
|
||||||
|
println!("{}", graph.as_dot_string());
|
||||||
|
assert_eq!(
|
||||||
|
graph.as_dot_string(),
|
||||||
|
r#"digraph {
|
||||||
|
0 [label="ConstNode<i32> (id=0)"]
|
||||||
|
1 [label="ConstNode<i32> (id=1)"]
|
||||||
|
2 [label="RuleNode<AddWithLabel>(test) (id=2)"]
|
||||||
|
0 -> 2 []
|
||||||
|
1 -> 2 []
|
||||||
|
}
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DynamicSum(DynamicInput<i32>);
|
||||||
|
impl InputVisitable for DynamicSum {
|
||||||
|
fn visit_inputs(&self, visitor: &mut impl InputVisitor) {
|
||||||
|
self.0.visit_inputs(visitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Rule for DynamicSum {
|
||||||
|
type Output = i32;
|
||||||
|
fn evaluate(&mut self) -> Self::Output {
|
||||||
|
self.0.value().inputs.iter().map(|i| *i.value()).sum()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dynamic_rule() {
|
||||||
|
let mut builder = GraphBuilder::new();
|
||||||
|
let (count, set_count) = builder.add_invalidatable_value(1);
|
||||||
|
struct CountUpTo {
|
||||||
|
count: Input<i32>,
|
||||||
|
node_factory: DynamicNodeFactory<i32, i32>,
|
||||||
|
}
|
||||||
|
impl InputVisitable for CountUpTo {
|
||||||
|
fn visit_inputs(&self, visitor: &mut impl InputVisitor) {
|
||||||
|
visitor.visit(&self.count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl DynamicRule for CountUpTo {
|
||||||
|
type ChildOutput = i32;
|
||||||
|
fn evaluate(
|
||||||
|
&mut self,
|
||||||
|
ctx: &mut impl rule::DynamicRuleContext,
|
||||||
|
) -> Vec<Input<Self::ChildOutput>> {
|
||||||
|
let count = *self.count.value();
|
||||||
|
for i in 1..=count {
|
||||||
|
self.node_factory
|
||||||
|
.add_node(ctx, i, |ctx| ctx.add_rule(ConstantRule::new(i)));
|
||||||
|
}
|
||||||
|
self.node_factory.all_nodes(ctx)
|
||||||
|
}
|
||||||
|
fn remove_children(&self, ctx: &mut impl rule::DynamicRuleContext) {
|
||||||
|
self.node_factory.remove_all(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let all_inputs = builder.add_dynamic_rule(CountUpTo {
|
||||||
|
count,
|
||||||
|
node_factory: DynamicNodeFactory::new(),
|
||||||
|
});
|
||||||
|
builder.set_output(DynamicSum(all_inputs));
|
||||||
|
let mut graph = builder.build().unwrap();
|
||||||
|
assert_eq!(*graph.evaluate(), 1);
|
||||||
|
set_count.set_value(2);
|
||||||
|
assert_eq!(*graph.evaluate(), 3);
|
||||||
|
set_count.set_value(4);
|
||||||
|
assert_eq!(*graph.evaluate(), 10);
|
||||||
|
set_count.set_value(2);
|
||||||
|
assert_eq!(*graph.evaluate(), 3);
|
||||||
|
println!("{}", graph.as_dot_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn async_dynamic_rule() {
|
||||||
|
let mut builder = GraphBuilder::new_async();
|
||||||
|
let (count, set_count) = builder.add_invalidatable_value(1);
|
||||||
|
struct CountUpTo(Input<i32>, Vec<Input<i32>>);
|
||||||
|
impl InputVisitable for CountUpTo {
|
||||||
|
fn visit_inputs(&self, visitor: &mut impl InputVisitor) {
|
||||||
|
visitor.visit(&self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl AsyncDynamicRule for CountUpTo {
|
||||||
|
type ChildOutput = i32;
|
||||||
|
async fn evaluate<'a>(
|
||||||
|
&'a mut self,
|
||||||
|
ctx: &'a mut impl rule::AsyncDynamicRuleContext,
|
||||||
|
) -> Vec<Input<Self::ChildOutput>> {
|
||||||
|
let count = *self.0.value();
|
||||||
|
assert!(count >= self.1.len() as i32);
|
||||||
|
while (self.1.len() as i32) < count {
|
||||||
|
self.1
|
||||||
|
.push(ctx.add_rule(ConstantRule::new(self.1.len() as i32 + 1)));
|
||||||
|
}
|
||||||
|
self.1.clone()
|
||||||
|
}
|
||||||
|
fn remove_children(&self, ctx: &mut impl rule::DynamicRuleContext) {
|
||||||
|
for input in self.1.iter() {
|
||||||
|
ctx.remove_node(input.node_id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let all_inputs = builder.add_async_dynamic_rule(CountUpTo(count, vec![]));
|
||||||
|
builder.set_output(DynamicSum(all_inputs));
|
||||||
|
let mut graph = builder.build().unwrap();
|
||||||
|
assert_eq!(*graph.evaluate_async().await, 1);
|
||||||
|
set_count.set_value(2);
|
||||||
|
assert_eq!(*graph.evaluate_async().await, 3);
|
||||||
|
set_count.set_value(4);
|
||||||
|
assert_eq!(*graph.evaluate_async().await, 10);
|
||||||
|
println!("{}", graph.as_dot_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dynamic_invalidatable_rule() {
|
||||||
|
let mut builder = GraphBuilder::new();
|
||||||
|
let (count, set_count) = builder.add_invalidatable_value(1);
|
||||||
|
struct CountUpTo {
|
||||||
|
count: Input<i32>,
|
||||||
|
signals: Rc<RefCell<Vec<InvalidationSignal>>>,
|
||||||
|
node_factory: DynamicNodeFactory<i32, i32>,
|
||||||
|
}
|
||||||
|
impl InputVisitable for CountUpTo {
|
||||||
|
fn visit_inputs(&self, visitor: &mut impl InputVisitor) {
|
||||||
|
visitor.visit(&self.count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl DynamicRule for CountUpTo {
|
||||||
|
type ChildOutput = i32;
|
||||||
|
fn evaluate(
|
||||||
|
&mut self,
|
||||||
|
ctx: &mut impl rule::DynamicRuleContext,
|
||||||
|
) -> Vec<Input<Self::ChildOutput>> {
|
||||||
|
let count = *self.count.value();
|
||||||
|
for i in 1..=count {
|
||||||
|
self.node_factory.add_node(ctx, i, |ctx| {
|
||||||
|
let constant = ctx.add_rule(ConstantRule::new(i));
|
||||||
|
let (input, signal) = ctx.add_invalidatable_rule(IncAdd(constant, 0));
|
||||||
|
self.signals.borrow_mut().push(signal);
|
||||||
|
input
|
||||||
|
});
|
||||||
|
}
|
||||||
|
self.node_factory.all_nodes(ctx)
|
||||||
|
}
|
||||||
|
fn remove_children(&self, ctx: &mut impl rule::DynamicRuleContext) {
|
||||||
|
self.node_factory.remove_all(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let signals = Rc::new(RefCell::new(vec![]));
|
||||||
|
let all_inputs = builder.add_dynamic_rule(CountUpTo {
|
||||||
|
count,
|
||||||
|
signals: Rc::clone(&signals),
|
||||||
|
node_factory: DynamicNodeFactory::new(),
|
||||||
|
});
|
||||||
|
builder.set_output(DynamicSum(all_inputs));
|
||||||
|
let mut graph = builder.build().unwrap();
|
||||||
|
assert_eq!(*graph.evaluate(), 2);
|
||||||
|
for signal in signals.borrow().iter() {
|
||||||
|
signal.invalidate();
|
||||||
|
}
|
||||||
|
assert_eq!(*graph.evaluate(), 3);
|
||||||
|
set_count.set_value(2);
|
||||||
|
assert_eq!(*graph.evaluate(), 6); // new const node has value 2, IncAdd initially adds 1
|
||||||
|
for signal in signals.borrow().iter() {
|
||||||
|
signal.invalidate();
|
||||||
|
}
|
||||||
|
assert_eq!(*graph.evaluate(), 8);
|
||||||
|
}
|
||||||
|
}
|
815
crates/compute_graph/src/node.rs
Normal file
815
crates/compute_graph/src/node.rs
Normal file
@ -0,0 +1,815 @@
|
|||||||
|
use crate::input::InputVisitable;
|
||||||
|
use crate::rule::{
|
||||||
|
AsyncDynamicRule, AsyncDynamicRuleContext, AsyncRule, DynamicRule, DynamicRuleContext, Rule,
|
||||||
|
};
|
||||||
|
use crate::synchronicity::{Asynchronous, Synchronicity};
|
||||||
|
use crate::{Graph, Input, InputVisitor, InvalidationSignal, NodeGraph, NodeId, Synchronous};
|
||||||
|
use quote::ToTokens;
|
||||||
|
use std::any::Any;
|
||||||
|
use std::cell::{Cell, RefCell};
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::future::Future;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
pub(crate) struct ErasedNode<Synch: Synchronicity> {
|
||||||
|
any: Box<dyn Any>,
|
||||||
|
is_valid: Box<dyn Fn(&Box<dyn Any>) -> bool>,
|
||||||
|
invalidate: Box<dyn Fn(&mut Box<dyn Any>) -> ()>,
|
||||||
|
visit_inputs: Box<dyn Fn(&Box<dyn Any>, &mut dyn FnMut(NodeId) -> ()) -> ()>,
|
||||||
|
update: Box<
|
||||||
|
dyn for<'a> Fn(
|
||||||
|
&'a mut Box<dyn Any>,
|
||||||
|
&'a mut NodeUpdateContext<Synch>,
|
||||||
|
) -> Synch::UpdateResult<'a>,
|
||||||
|
>,
|
||||||
|
remove_children: Box<dyn for<'a> Fn(&Box<dyn Any>, &'a mut NodeUpdateContext<Synch>)>,
|
||||||
|
debug_fmt: Box<dyn Fn(&Box<dyn Any>, &mut std::fmt::Formatter<'_>) -> std::fmt::Result>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct NodeUpdateContext<Synch: Synchronicity> {
|
||||||
|
pub(crate) graph: Rc<RefCell<NodeGraph<Synch>>>,
|
||||||
|
pub(crate) graph_is_valid: Rc<Cell<bool>>,
|
||||||
|
pub(crate) invalidate_dependent_nodes: bool,
|
||||||
|
pub(crate) removed_nodes: VecDeque<NodeId>,
|
||||||
|
pub(crate) added_nodes: Vec<(ErasedNode<Synch>, Rc<Cell<Option<NodeId>>>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: Synchronicity> NodeUpdateContext<S> {
|
||||||
|
pub(crate) fn new<O>(graph: &Graph<O, S>) -> Self {
|
||||||
|
Self {
|
||||||
|
graph: Rc::clone(&graph.node_graph),
|
||||||
|
graph_is_valid: Rc::clone(&graph.is_valid),
|
||||||
|
invalidate_dependent_nodes: false,
|
||||||
|
removed_nodes: VecDeque::new(),
|
||||||
|
added_nodes: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invalidate_dependent_nodes(&mut self) {
|
||||||
|
self.invalidate_dependent_nodes = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: Synchronicity> ErasedNode<S> {
|
||||||
|
pub(crate) fn new<N: Node<V, S> + 'static, V: NodeValue>(base: N) -> Self {
|
||||||
|
// i don't love the double boxing, but i'm not sure how else to do this
|
||||||
|
let thing: Box<dyn Node<V, S>> = Box::new(base);
|
||||||
|
let any: Box<dyn Any> = Box::new(thing);
|
||||||
|
Self {
|
||||||
|
any,
|
||||||
|
is_valid: Box::new(|any| {
|
||||||
|
let x = any.downcast_ref::<Box<dyn Node<V, S>>>().unwrap();
|
||||||
|
x.is_valid()
|
||||||
|
}),
|
||||||
|
invalidate: Box::new(|any| {
|
||||||
|
let x = any.downcast_mut::<Box<dyn Node<V, S>>>().unwrap();
|
||||||
|
x.invalidate();
|
||||||
|
}),
|
||||||
|
visit_inputs: Box::new(|any, visitor| {
|
||||||
|
let x = any.downcast_ref::<Box<dyn Node<V, S>>>().unwrap();
|
||||||
|
x.visit_inputs(visitor);
|
||||||
|
}),
|
||||||
|
update: Box::new(|any, ctx| {
|
||||||
|
let x = any.downcast_mut::<Box<dyn Node<V, S>>>().unwrap();
|
||||||
|
x.update(ctx)
|
||||||
|
}),
|
||||||
|
remove_children: Box::new(|any, ctx| {
|
||||||
|
let x = any.downcast_ref::<Box<dyn Node<V, S>>>().unwrap();
|
||||||
|
x.remove_children(ctx);
|
||||||
|
}),
|
||||||
|
debug_fmt: Box::new(|any, f| {
|
||||||
|
let x = any.downcast_ref::<Box<dyn Node<V, S>>>().unwrap();
|
||||||
|
x.fmt(f)
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_valid(&self) -> bool {
|
||||||
|
(self.is_valid)(&self.any)
|
||||||
|
}
|
||||||
|
pub(crate) fn invalidate(&mut self) {
|
||||||
|
(self.invalidate)(&mut self.any);
|
||||||
|
}
|
||||||
|
pub(crate) fn visit_inputs(&self, f: &mut dyn FnMut(NodeId) -> ()) {
|
||||||
|
(self.visit_inputs)(&self.any, f);
|
||||||
|
}
|
||||||
|
pub(crate) fn remove_children(&self, ctx: &mut NodeUpdateContext<S>) {
|
||||||
|
(self.remove_children)(&self.any, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ErasedNode<Synchronous> {
|
||||||
|
pub(crate) fn update(&mut self, ctx: &mut NodeUpdateContext<Synchronous>) {
|
||||||
|
(self.update)(&mut self.any, ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ErasedNode<Asynchronous> {
|
||||||
|
pub(crate) async fn update(&mut self, ctx: &mut NodeUpdateContext<Asynchronous>) {
|
||||||
|
(self.update)(&mut self.any, ctx).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: Synchronicity> std::fmt::Debug for ErasedNode<S> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
(self.debug_fmt)(&self.any, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) trait Node<Value: NodeValue, Synch: Synchronicity>: std::fmt::Debug {
|
||||||
|
fn is_valid(&self) -> bool;
|
||||||
|
fn invalidate(&mut self);
|
||||||
|
fn visit_inputs(&self, visitor: &mut dyn FnMut(NodeId) -> ());
|
||||||
|
fn update<'a>(&'a mut self, ctx: &'a mut NodeUpdateContext<Synch>) -> Synch::UpdateResult<'a>;
|
||||||
|
fn remove_children<'a>(&'a self, ctx: &'a mut NodeUpdateContext<Synch>);
|
||||||
|
fn value_rc(&self) -> &Rc<RefCell<Option<Value>>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A value that can be used as the value of a node in the graph.
|
||||||
|
///
|
||||||
|
/// This trait is used to determine, when a node is invalidated, whether its value has truly changed
|
||||||
|
/// and thus whether downstream nodes need to be invalidated too.
|
||||||
|
///
|
||||||
|
/// A blanket implementation of this trait for all types implementing `PartialEq` is provided.
|
||||||
|
pub trait NodeValue: 'static {
|
||||||
|
/// Whether self is equal, for the purposes of graph invalidation, from other.
|
||||||
|
///
|
||||||
|
/// This method should be conservative. That is, if the equality of the two values cannot be affirmatively
|
||||||
|
/// determined, this method should return `false`.
|
||||||
|
///
|
||||||
|
/// The default implementation of this method always returns `false`, so any non-`PartialEq` type can
|
||||||
|
/// implement this trait simply:
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use compute_graph::node::NodeValue;
|
||||||
|
/// struct MyType;
|
||||||
|
/// impl NodeValue for MyType {}
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Note that always returning `false` may result in more node invalidations than strictly necessary.
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
fn node_value_eq(&self, other: &Self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: PartialEq + 'static> NodeValue for T {
|
||||||
|
fn node_value_eq(&self, other: &Self) -> bool {
|
||||||
|
self == other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct ConstNode<V, S> {
|
||||||
|
value: Rc<RefCell<Option<V>>>,
|
||||||
|
label: Option<String>,
|
||||||
|
synchronicity: std::marker::PhantomData<S>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V, S> ConstNode<V, S> {
|
||||||
|
pub(crate) fn new(value: V, label: Option<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
value: Rc::new(RefCell::new(Some(value))),
|
||||||
|
label,
|
||||||
|
synchronicity: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V: NodeValue, S: Synchronicity> Node<V, S> for ConstNode<V, S> {
|
||||||
|
fn is_valid(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invalidate(&mut self) {}
|
||||||
|
|
||||||
|
fn visit_inputs(&self, _visitor: &mut dyn FnMut(NodeId) -> ()) {}
|
||||||
|
|
||||||
|
fn update<'a>(&'a mut self, _ctx: &'a mut NodeUpdateContext<S>) -> S::UpdateResult<'a> {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_children<'a>(&'a self, _ctx: &'a mut NodeUpdateContext<S>) {}
|
||||||
|
|
||||||
|
fn value_rc(&self) -> &Rc<RefCell<Option<V>>> {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V, S> std::fmt::Debug for ConstNode<V, S> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if let Some(ref label) = self.label {
|
||||||
|
write!(f, "ConstNode<{}>({})", pretty_type_name::<V>(), label)
|
||||||
|
} else {
|
||||||
|
write!(f, "ConstNode<{}>", pretty_type_name::<V>())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct InvalidatableConstNode<V, S> {
|
||||||
|
value: Rc<RefCell<Option<V>>>,
|
||||||
|
valid: bool,
|
||||||
|
synchronicity: std::marker::PhantomData<S>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V, S> InvalidatableConstNode<V, S> {
|
||||||
|
pub(crate) fn new(value: V) -> Self {
|
||||||
|
Self {
|
||||||
|
value: Rc::new(RefCell::new(Some(value))),
|
||||||
|
valid: true,
|
||||||
|
synchronicity: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V: NodeValue, S: Synchronicity> Node<V, S> for InvalidatableConstNode<V, S> {
|
||||||
|
fn is_valid(&self) -> bool {
|
||||||
|
self.valid
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invalidate(&mut self) {
|
||||||
|
self.valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_inputs(&self, _visitor: &mut dyn FnMut(NodeId) -> ()) {}
|
||||||
|
|
||||||
|
fn update<'a>(&'a mut self, ctx: &'a mut NodeUpdateContext<S>) -> S::UpdateResult<'a> {
|
||||||
|
self.valid = true;
|
||||||
|
// This node is only invalidate when node_value_eq between the old/new value is false,
|
||||||
|
// so it is always the case that the update method has changed the value.
|
||||||
|
ctx.invalidate_dependent_nodes();
|
||||||
|
S::make_update_result(crate::synchronicity::private::Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_children<'a>(&'a self, _ctx: &'a mut NodeUpdateContext<S>) {}
|
||||||
|
|
||||||
|
fn value_rc(&self) -> &Rc<RefCell<Option<V>>> {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V, S> std::fmt::Debug for InvalidatableConstNode<V, S> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "InvalidatableConstNode<{}>", pretty_type_name::<V>())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct RuleNode<R, V, S> {
|
||||||
|
rule: R,
|
||||||
|
value: Rc<RefCell<Option<V>>>,
|
||||||
|
valid: bool,
|
||||||
|
synchronicity: std::marker::PhantomData<S>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Rule, S> RuleNode<R, R::Output, S> {
|
||||||
|
pub(crate) fn new(rule: R) -> Self {
|
||||||
|
Self {
|
||||||
|
rule,
|
||||||
|
value: Rc::new(RefCell::new(None)),
|
||||||
|
valid: false,
|
||||||
|
synchronicity: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_inputs<V: InputVisitable>(visitable: &V, visitor: &mut dyn FnMut(NodeId) -> ()) {
|
||||||
|
struct InputIndexVisitor<'a>(&'a mut dyn FnMut(NodeId) -> ());
|
||||||
|
impl<'a> InputVisitor for InputIndexVisitor<'a> {
|
||||||
|
fn visit<T>(&mut self, input: &Input<T>) {
|
||||||
|
self.0(input.node_idx.get().unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visitable.visit_inputs(&mut InputIndexVisitor(visitor));
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Rule, S: Synchronicity> Node<R::Output, S> for RuleNode<R, R::Output, S> {
|
||||||
|
fn is_valid(&self) -> bool {
|
||||||
|
self.valid
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invalidate(&mut self) {
|
||||||
|
self.valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_inputs(&self, visitor: &mut dyn FnMut(NodeId) -> ()) {
|
||||||
|
visit_inputs(&self.rule, visitor);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update<'a>(&'a mut self, ctx: &'a mut NodeUpdateContext<S>) -> S::UpdateResult<'a> {
|
||||||
|
self.valid = true;
|
||||||
|
|
||||||
|
let new_value = self.rule.evaluate();
|
||||||
|
let mut value = self.value.borrow_mut();
|
||||||
|
let value_changed = value
|
||||||
|
.as_ref()
|
||||||
|
.map_or(true, |v| !v.node_value_eq(&new_value));
|
||||||
|
|
||||||
|
if value_changed {
|
||||||
|
*value = Some(new_value);
|
||||||
|
ctx.invalidate_dependent_nodes();
|
||||||
|
}
|
||||||
|
|
||||||
|
S::make_update_result(crate::synchronicity::private::Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_children<'a>(&'a self, _ctx: &'a mut NodeUpdateContext<S>) {}
|
||||||
|
|
||||||
|
fn value_rc(&self) -> &Rc<RefCell<Option<R::Output>>> {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RuleLabel<'a, R: Rule>(&'a R);
|
||||||
|
impl<'a, R: Rule> std::fmt::Display for RuleLabel<'a, R> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
self.0.node_label(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Rule, V, S> std::fmt::Debug for RuleNode<R, V, S> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"RuleNode<{}>({})",
|
||||||
|
pretty_type_name::<R>(),
|
||||||
|
RuleLabel(&self.rule)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct AsyncConstNode<V, P: FnOnce() -> F, F: Future<Output = V>> {
|
||||||
|
provider: Option<P>,
|
||||||
|
value: Rc<RefCell<Option<V>>>,
|
||||||
|
valid: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V, P: FnOnce() -> F, F: Future<Output = V>> AsyncConstNode<V, P, F> {
|
||||||
|
pub(crate) fn new(provider: P) -> Self {
|
||||||
|
Self {
|
||||||
|
provider: Some(provider),
|
||||||
|
value: Rc::new(RefCell::new(None)),
|
||||||
|
valid: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn do_update(&mut self, ctx: &mut NodeUpdateContext<Asynchronous>) {
|
||||||
|
self.valid = true;
|
||||||
|
let mut provider = None;
|
||||||
|
std::mem::swap(&mut self.provider, &mut provider);
|
||||||
|
*self.value.borrow_mut() = Some(provider.unwrap()().await);
|
||||||
|
ctx.invalidate_dependent_nodes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V: NodeValue, P: FnOnce() -> F, F: Future<Output = V>> Node<V, Asynchronous>
|
||||||
|
for AsyncConstNode<V, P, F>
|
||||||
|
{
|
||||||
|
fn is_valid(&self) -> bool {
|
||||||
|
self.valid
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invalidate(&mut self) {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_inputs(&self, _visitor: &mut dyn FnMut(NodeId) -> ()) {}
|
||||||
|
|
||||||
|
fn update<'a>(
|
||||||
|
&'a mut self,
|
||||||
|
ctx: &'a mut NodeUpdateContext<Asynchronous>,
|
||||||
|
) -> <Asynchronous as Synchronicity>::UpdateResult<'a> {
|
||||||
|
Box::pin(self.do_update(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_children<'a>(&'a self, _ctx: &'a mut NodeUpdateContext<Asynchronous>) {}
|
||||||
|
|
||||||
|
fn value_rc(&self) -> &Rc<RefCell<Option<V>>> {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V, P: FnOnce() -> F, F: Future<Output = V>> std::fmt::Debug for AsyncConstNode<V, P, F> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "AsyncConstNode<{}>", pretty_type_name::<V>())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct AsyncRuleNode<R, V> {
|
||||||
|
rule: R,
|
||||||
|
value: Rc<RefCell<Option<V>>>,
|
||||||
|
valid: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: AsyncRule> AsyncRuleNode<R, R::Output> {
|
||||||
|
pub(crate) fn new(rule: R) -> Self {
|
||||||
|
Self {
|
||||||
|
rule,
|
||||||
|
value: Rc::new(RefCell::new(None)),
|
||||||
|
valid: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn do_update(&mut self, ctx: &mut NodeUpdateContext<Asynchronous>) {
|
||||||
|
self.valid = true;
|
||||||
|
|
||||||
|
let new_value = self.rule.evaluate().await;
|
||||||
|
let mut value = self.value.borrow_mut();
|
||||||
|
let value_changed = value
|
||||||
|
.as_ref()
|
||||||
|
.map_or(true, |v| !v.node_value_eq(&new_value));
|
||||||
|
|
||||||
|
if value_changed {
|
||||||
|
*value = Some(new_value);
|
||||||
|
ctx.invalidate_dependent_nodes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: AsyncRule> Node<R::Output, Asynchronous> for AsyncRuleNode<R, R::Output> {
|
||||||
|
fn is_valid(&self) -> bool {
|
||||||
|
self.valid
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invalidate(&mut self) {
|
||||||
|
self.valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_inputs(&self, visitor: &mut dyn FnMut(NodeId) -> ()) {
|
||||||
|
visit_inputs(&self.rule, visitor);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update<'a>(
|
||||||
|
&'a mut self,
|
||||||
|
ctx: &'a mut NodeUpdateContext<Asynchronous>,
|
||||||
|
) -> <Asynchronous as Synchronicity>::UpdateResult<'a> {
|
||||||
|
Box::pin(self.do_update(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_children<'a>(&'a self, _ctx: &'a mut NodeUpdateContext<Asynchronous>) {}
|
||||||
|
|
||||||
|
fn value_rc(&self) -> &Rc<RefCell<Option<R::Output>>> {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AsyncRuleLabel<'a, R: AsyncRule>(&'a R);
|
||||||
|
impl<'a, R: AsyncRule> std::fmt::Display for AsyncRuleLabel<'a, R> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
self.0.node_label(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: AsyncRule, V> std::fmt::Debug for AsyncRuleNode<R, V> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"AsyncRuleNode<{}>({})",
|
||||||
|
pretty_type_name::<R>(),
|
||||||
|
AsyncRuleLabel(&self.rule)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: better name for this
|
||||||
|
pub struct DynamicRuleOutput<O> {
|
||||||
|
pub inputs: Vec<Input<O>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<O: 'static> NodeValue for DynamicRuleOutput<O> {
|
||||||
|
fn node_value_eq(&self, other: &Self) -> bool {
|
||||||
|
if self.inputs.len() != other.inputs.len() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.inputs
|
||||||
|
.iter()
|
||||||
|
.zip(other.inputs.iter())
|
||||||
|
.all(|(s, o)| s.node_idx == o.node_idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<O> std::fmt::Debug for DynamicRuleOutput<O> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct(std::any::type_name::<Self>())
|
||||||
|
.field("inputs", &self.inputs)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct DynamicRuleNode<R, O, S> {
|
||||||
|
rule: R,
|
||||||
|
valid: bool,
|
||||||
|
value: Rc<RefCell<Option<DynamicRuleOutput<O>>>>,
|
||||||
|
synchronicity: std::marker::PhantomData<S>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R, O, S> DynamicRuleNode<R, O, S> {
|
||||||
|
pub(crate) fn new(rule: R) -> Self {
|
||||||
|
Self {
|
||||||
|
rule,
|
||||||
|
valid: false,
|
||||||
|
value: Rc::new(RefCell::new(None)),
|
||||||
|
synchronicity: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: DynamicRule, S: Synchronicity> Node<DynamicRuleOutput<R::ChildOutput>, S>
|
||||||
|
for DynamicRuleNode<R, R::ChildOutput, S>
|
||||||
|
{
|
||||||
|
fn is_valid(&self) -> bool {
|
||||||
|
self.valid
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invalidate(&mut self) {
|
||||||
|
self.valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_inputs(&self, visitor: &mut dyn FnMut(NodeId) -> ()) {
|
||||||
|
visit_inputs(&self.rule, visitor);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update<'a>(&'a mut self, ctx: &'a mut NodeUpdateContext<S>) -> S::UpdateResult<'a> {
|
||||||
|
self.valid = true;
|
||||||
|
|
||||||
|
let new_value = DynamicRuleOutput {
|
||||||
|
inputs: self.rule.evaluate(&mut DynamicRuleUpdateContext(ctx)),
|
||||||
|
};
|
||||||
|
let mut value = self.value.borrow_mut();
|
||||||
|
let value_changed = value
|
||||||
|
.as_ref()
|
||||||
|
.map_or(true, |v| !v.node_value_eq(&new_value));
|
||||||
|
|
||||||
|
if value_changed {
|
||||||
|
*value = Some(new_value);
|
||||||
|
ctx.invalidate_dependent_nodes();
|
||||||
|
}
|
||||||
|
|
||||||
|
S::make_update_result(crate::synchronicity::private::Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_children<'a>(&'a self, ctx: &'a mut NodeUpdateContext<S>) {
|
||||||
|
self.rule
|
||||||
|
.remove_children(&mut DynamicRuleRemoveChildrenContext(ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn value_rc(&self) -> &Rc<RefCell<Option<DynamicRuleOutput<R::ChildOutput>>>> {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DynamicRuleUpdateContext<'a, Synch: Synchronicity>(&'a mut NodeUpdateContext<Synch>);
|
||||||
|
|
||||||
|
impl<'a, S: Synchronicity> DynamicRuleUpdateContext<'a, S> {
|
||||||
|
fn add_node<V: NodeValue>(&mut self, node: impl Node<V, S> + 'static) -> Input<V> {
|
||||||
|
let node_idx = Rc::new(Cell::new(None));
|
||||||
|
let value = Rc::clone(node.value_rc());
|
||||||
|
let erased = ErasedNode::new(node);
|
||||||
|
self.0.added_nodes.push((erased, Rc::clone(&node_idx)));
|
||||||
|
Input { node_idx, value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, S: Synchronicity> DynamicRuleContext for DynamicRuleUpdateContext<'a, S> {
|
||||||
|
fn remove_node(&mut self, id: NodeId) {
|
||||||
|
self.0.removed_nodes.push_back(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_rule<R>(&mut self, rule: R) -> Input<R::Output>
|
||||||
|
where
|
||||||
|
R: Rule,
|
||||||
|
{
|
||||||
|
self.add_node(RuleNode::new(rule))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_dynamic_rule<R>(&mut self, rule: R) -> Input<DynamicRuleOutput<R::ChildOutput>>
|
||||||
|
where
|
||||||
|
R: DynamicRule,
|
||||||
|
{
|
||||||
|
self.add_node(DynamicRuleNode::new(rule))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_invalidatable_rule<R>(&mut self, rule: R) -> (Input<R::Output>, InvalidationSignal)
|
||||||
|
where
|
||||||
|
R: Rule,
|
||||||
|
{
|
||||||
|
let input = self.add_rule(rule);
|
||||||
|
let signal = InvalidationSignal::new(
|
||||||
|
&input,
|
||||||
|
Rc::clone(&self.0.graph),
|
||||||
|
Rc::clone(&self.0.graph_is_valid),
|
||||||
|
);
|
||||||
|
(input, signal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DynamicRuleRemoveChildrenContext<'a, Synch: Synchronicity>(&'a mut NodeUpdateContext<Synch>);
|
||||||
|
|
||||||
|
impl<'a, S: Synchronicity> DynamicRuleContext for DynamicRuleRemoveChildrenContext<'a, S> {
|
||||||
|
fn remove_node(&mut self, id: NodeId) {
|
||||||
|
self.0.removed_nodes.push_back(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_rule<R>(&mut self, _rule: R) -> Input<R::Output>
|
||||||
|
where
|
||||||
|
R: Rule,
|
||||||
|
{
|
||||||
|
panic!("cannot add node while removing children");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_dynamic_rule<R>(&mut self, _rule: R) -> Input<DynamicRuleOutput<R::ChildOutput>>
|
||||||
|
where
|
||||||
|
R: DynamicRule,
|
||||||
|
{
|
||||||
|
panic!("cannot add node while removing children");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_invalidatable_rule<R>(&mut self, _rule: R) -> (Input<R::Output>, InvalidationSignal)
|
||||||
|
where
|
||||||
|
R: Rule,
|
||||||
|
{
|
||||||
|
panic!("cannot add node while removing children");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DynamicRuleLabel<'a, R: DynamicRule>(&'a R);
|
||||||
|
impl<'a, R: DynamicRule> std::fmt::Display for DynamicRuleLabel<'a, R> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
self.0.node_label(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: DynamicRule, O, V> std::fmt::Debug for DynamicRuleNode<R, O, V> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"DynamicRuleNode<{}>({})",
|
||||||
|
pretty_type_name::<R>(),
|
||||||
|
DynamicRuleLabel(&self.rule)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct AsyncDynamicRuleNode<R, O> {
|
||||||
|
rule: R,
|
||||||
|
valid: bool,
|
||||||
|
value: Rc<RefCell<Option<DynamicRuleOutput<O>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: AsyncDynamicRule> AsyncDynamicRuleNode<R, R::ChildOutput> {
|
||||||
|
pub(crate) fn new(rule: R) -> Self {
|
||||||
|
Self {
|
||||||
|
rule,
|
||||||
|
valid: false,
|
||||||
|
value: Rc::new(RefCell::new(None)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn do_update(&mut self, ctx: &mut NodeUpdateContext<Asynchronous>) {
|
||||||
|
self.valid = true;
|
||||||
|
|
||||||
|
let new_value = DynamicRuleOutput {
|
||||||
|
inputs: self
|
||||||
|
.rule
|
||||||
|
.evaluate(&mut AsyncDynamicRuleUpdateContext(ctx))
|
||||||
|
.await,
|
||||||
|
};
|
||||||
|
let mut value = self.value.borrow_mut();
|
||||||
|
let value_changed = value
|
||||||
|
.as_ref()
|
||||||
|
.map_or(true, |v| !v.node_value_eq(&new_value));
|
||||||
|
|
||||||
|
if value_changed {
|
||||||
|
*value = Some(new_value);
|
||||||
|
ctx.invalidate_dependent_nodes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: AsyncDynamicRule> Node<DynamicRuleOutput<R::ChildOutput>, Asynchronous>
|
||||||
|
for AsyncDynamicRuleNode<R, R::ChildOutput>
|
||||||
|
{
|
||||||
|
fn is_valid(&self) -> bool {
|
||||||
|
self.valid
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invalidate(&mut self) {
|
||||||
|
self.valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_inputs(&self, visitor: &mut dyn FnMut(NodeId) -> ()) {
|
||||||
|
visit_inputs(&self.rule, visitor);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update<'a>(
|
||||||
|
&'a mut self,
|
||||||
|
ctx: &'a mut NodeUpdateContext<Asynchronous>,
|
||||||
|
) -> <Asynchronous as Synchronicity>::UpdateResult<'a> {
|
||||||
|
Box::pin(self.do_update(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_children<'a>(&'a self, ctx: &'a mut NodeUpdateContext<Asynchronous>) {
|
||||||
|
self.rule
|
||||||
|
.remove_children(&mut DynamicRuleRemoveChildrenContext(ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn value_rc(&self) -> &Rc<RefCell<Option<DynamicRuleOutput<R::ChildOutput>>>> {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AsyncDynamicRuleUpdateContext<'a>(&'a mut NodeUpdateContext<Asynchronous>);
|
||||||
|
|
||||||
|
impl<'a> DynamicRuleContext for AsyncDynamicRuleUpdateContext<'a> {
|
||||||
|
fn remove_node(&mut self, id: NodeId) {
|
||||||
|
DynamicRuleUpdateContext(self.0).remove_node(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_rule<R>(&mut self, rule: R) -> Input<R::Output>
|
||||||
|
where
|
||||||
|
R: Rule,
|
||||||
|
{
|
||||||
|
DynamicRuleUpdateContext(self.0).add_rule(rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_dynamic_rule<R>(&mut self, rule: R) -> Input<DynamicRuleOutput<R::ChildOutput>>
|
||||||
|
where
|
||||||
|
R: DynamicRule,
|
||||||
|
{
|
||||||
|
DynamicRuleUpdateContext(self.0).add_dynamic_rule(rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_invalidatable_rule<R>(&mut self, rule: R) -> (Input<R::Output>, InvalidationSignal)
|
||||||
|
where
|
||||||
|
R: Rule,
|
||||||
|
{
|
||||||
|
DynamicRuleUpdateContext(self.0).add_invalidatable_rule(rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> AsyncDynamicRuleContext for AsyncDynamicRuleUpdateContext<'a> {
|
||||||
|
fn add_async_rule<R>(&mut self, rule: R) -> Input<R::Output>
|
||||||
|
where
|
||||||
|
R: AsyncRule,
|
||||||
|
{
|
||||||
|
DynamicRuleUpdateContext(self.0).add_node(AsyncRuleNode::new(rule))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AsyncDynamicRuleLabel<'a, R: AsyncDynamicRule>(&'a R);
|
||||||
|
impl<'a, R: AsyncDynamicRule> std::fmt::Display for AsyncDynamicRuleLabel<'a, R> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
self.0.node_label(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: AsyncDynamicRule> std::fmt::Debug for AsyncDynamicRuleNode<R, R::ChildOutput> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"AsyncDynamicRuleNode<{}>({})",
|
||||||
|
pretty_type_name::<R>(),
|
||||||
|
AsyncDynamicRuleLabel(&self.rule)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pretty_type_name<T>() -> String {
|
||||||
|
// idk where the {{closure}} comes from in one of the tests, just do this to avoid panicking
|
||||||
|
let s = std::any::type_name::<T>().replace("{{closure}}", "__closure__");
|
||||||
|
let ty = syn::parse_str::<syn::Type>(&s).unwrap();
|
||||||
|
pretty_type_name_type(ty)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pretty_type_name_type(ty: syn::Type) -> String {
|
||||||
|
match ty {
|
||||||
|
syn::Type::Path(path) => pretty_type_name_path(path),
|
||||||
|
_ => format!("{}", ty.into_token_stream()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pretty_type_name_path(path: syn::TypePath) -> String {
|
||||||
|
let last_segment = path.path.segments.last().unwrap();
|
||||||
|
match &last_segment.arguments {
|
||||||
|
syn::PathArguments::None => {
|
||||||
|
format!("{}", last_segment.ident.to_token_stream())
|
||||||
|
}
|
||||||
|
syn::PathArguments::AngleBracketed(args) => {
|
||||||
|
let mut str = format!("{}", last_segment.ident.to_token_stream());
|
||||||
|
str.push('<');
|
||||||
|
for arg in &args.args {
|
||||||
|
match arg {
|
||||||
|
syn::GenericArgument::Type(ty) => {
|
||||||
|
str.push_str(&pretty_type_name_type(ty.clone()))
|
||||||
|
}
|
||||||
|
_ => str.push_str(&format!("{}", arg.into_token_stream())),
|
||||||
|
}
|
||||||
|
str.push_str(", ")
|
||||||
|
}
|
||||||
|
str.remove(str.len() - 1);
|
||||||
|
str.replace_range((str.len() - 1).., ">");
|
||||||
|
str
|
||||||
|
}
|
||||||
|
syn::PathArguments::Parenthesized(_) => {
|
||||||
|
format!("{}", last_segment.into_token_stream())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
295
crates/compute_graph/src/rule.rs
Normal file
295
crates/compute_graph/src/rule.rs
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
use crate::input::{Input, InputVisitable, InputVisitor};
|
||||||
|
use crate::node::{DynamicRuleOutput, NodeValue};
|
||||||
|
use crate::{InvalidationSignal, NodeId};
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::future::Future;
|
||||||
|
use std::hash::Hash;
|
||||||
|
|
||||||
|
/// A rule produces a value for a graph node using its [`Input`]s.
|
||||||
|
///
|
||||||
|
/// A rule for addition could be implemented like so:
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use compute_graph::{rule::Rule, input::{Input, InputVisitable}};
|
||||||
|
/// #[derive(InputVisitable)]
|
||||||
|
/// struct Add(Input<i32>, Input<i32>);
|
||||||
|
///
|
||||||
|
/// impl Rule for Add {
|
||||||
|
/// type Output = i32;
|
||||||
|
///
|
||||||
|
/// fn evaluate(&mut self) -> Self::Output {
|
||||||
|
/// *self.input_0() + *self.input_1()
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub trait Rule: InputVisitable + 'static {
|
||||||
|
/// The type of the output value of the rule.
|
||||||
|
type Output: NodeValue;
|
||||||
|
|
||||||
|
/// Produces the value of this rule using its inputs.
|
||||||
|
///
|
||||||
|
/// Note that the receiver of this method is a mutable reference to the rule itself. Rules are permitted
|
||||||
|
/// to have internal state that they modify during evaluation.
|
||||||
|
///
|
||||||
|
/// The following guarantees are made about rule evaluation:
|
||||||
|
/// 1. A rule will only be evaluated when one or more of its dependencies has changed. Note that "changed"
|
||||||
|
/// referes to returning `false` from [`NodeValue::node_value_eq`] for the dependency.
|
||||||
|
/// 2. A rule will never be evaluated before _all_ of its dependencies up-to-date. That is, it will never
|
||||||
|
/// be evaluated with mix of valid and invalid dependencies.
|
||||||
|
fn evaluate(&mut self) -> Self::Output;
|
||||||
|
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
fn node_label(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A rule produces a value for a graph node asynchronously.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use compute_graph::{rule::AsyncRule, input::{Input, InputVisitable}};
|
||||||
|
/// # async fn do_async_work(_: i32) -> i32 { 0 }
|
||||||
|
/// #[derive(InputVisitable)]
|
||||||
|
/// struct AsyncMath(Input<i32>);
|
||||||
|
///
|
||||||
|
/// impl AsyncRule for AsyncMath {
|
||||||
|
/// type Output = i32;
|
||||||
|
///
|
||||||
|
/// async fn evaluate(&mut self) -> Self::Output {
|
||||||
|
/// do_async_work(*self.input_0()).await
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub trait AsyncRule: InputVisitable + 'static {
|
||||||
|
/// The type of the output value of the rule.
|
||||||
|
type Output: NodeValue;
|
||||||
|
|
||||||
|
/// Asynchronously produces the value of this rule using its inputs.
|
||||||
|
///
|
||||||
|
/// See [`Rule::evaluate`] for additional details; the same considerations apply.
|
||||||
|
fn evaluate(&mut self) -> impl Future<Output = Self::Output> + '_;
|
||||||
|
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
fn node_label(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A rule whose output is further nodes in the graph.
|
||||||
|
///
|
||||||
|
/// Types implementing this rule should track which nodes they previously output and not
|
||||||
|
/// add additional equivalent nodes (for whatever domain-specific definition of equivalence)
|
||||||
|
/// on susbequent evaluations.
|
||||||
|
pub trait DynamicRule: InputVisitable + 'static {
|
||||||
|
/// The type of the output value of each of the child nodes that this rule produces.
|
||||||
|
type ChildOutput: NodeValue;
|
||||||
|
|
||||||
|
/// Evaluates this rule, producing additional nodes.
|
||||||
|
///
|
||||||
|
/// Use the methods on [`DynamicRuleContext`] to add or remove nodes from the graph.
|
||||||
|
/// Or, use [`DynamicNodeFactory`] to always register all nodes and allow that type to track
|
||||||
|
/// the specific additions/removals.
|
||||||
|
fn evaluate(&mut self, ctx: &mut impl DynamicRuleContext) -> Vec<Input<Self::ChildOutput>>;
|
||||||
|
|
||||||
|
/// Removes any children of this rule that have been added to the graph.
|
||||||
|
///
|
||||||
|
/// Called before this rule is removed, if this dynamic rule itself was the child of another
|
||||||
|
/// dynamic rule that is now removing it.
|
||||||
|
fn remove_children(&self, ctx: &mut impl DynamicRuleContext);
|
||||||
|
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
fn node_label(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Facilities for adding/removing nodes in the graph during the update of a [`DynamicRule`].
|
||||||
|
// todo: better abstracion for this
|
||||||
|
// something that handles diffing and does the add/remove automatically
|
||||||
|
pub trait DynamicRuleContext {
|
||||||
|
/// Removes the node with the given ID from the graph.
|
||||||
|
///
|
||||||
|
/// Be careful when removing nodes. Removing a node that is still depended-upon by another node
|
||||||
|
/// (i.e., is an input in some other node's [`InputVisitable::visit_inputs`]) is an error.
|
||||||
|
fn remove_node(&mut self, id: NodeId);
|
||||||
|
|
||||||
|
/// Adds a node whose value is produced using the given rule to the graph.
|
||||||
|
///
|
||||||
|
/// Returns an [`Input`] representing the newly-added node, which can be used to construct further rules.
|
||||||
|
fn add_rule<R>(&mut self, rule: R) -> Input<R::Output>
|
||||||
|
where
|
||||||
|
R: Rule;
|
||||||
|
|
||||||
|
/// Adds a node whose output is additional nodes produced by the given dynamic rule.
|
||||||
|
fn add_dynamic_rule<R>(&mut self, rule: R) -> Input<DynamicRuleOutput<R::ChildOutput>>
|
||||||
|
where
|
||||||
|
R: DynamicRule;
|
||||||
|
|
||||||
|
/// Adds an externally-invalidatable node whose value is produced using the given rule.
|
||||||
|
fn add_invalidatable_rule<R>(&mut self, rule: R) -> (Input<R::Output>, InvalidationSignal)
|
||||||
|
where
|
||||||
|
R: Rule;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper type for working with [`DynamicRule`]s.
|
||||||
|
///
|
||||||
|
/// When implementing [`DynamicRule::evaluate`], call the [`DynamicNodeFactory::add_node`] method for
|
||||||
|
/// each of the nodes that should be in the output. Then, call [`DynamicNodeFactory::all_nodes`] to produce
|
||||||
|
/// the output (i.e., `Vec` of [`Input`]s) of the dynamic node.
|
||||||
|
///
|
||||||
|
/// This type keeps track of which nodes need to be added and removed from the [`DynamicRuleContext`].
|
||||||
|
pub struct DynamicNodeFactory<ID, ChildOutput> {
|
||||||
|
existing_nodes: HashMap<ID, Input<ChildOutput>>,
|
||||||
|
ids_added_this_evaluation: HashSet<ID>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<ID: Hash + Eq + Clone, ChildOutput> DynamicNodeFactory<ID, ChildOutput> {
|
||||||
|
/// Constructs a new dynamic node factory.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
existing_nodes: HashMap::new(),
|
||||||
|
ids_added_this_evaluation: HashSet::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registers a node that is part of the output.
|
||||||
|
///
|
||||||
|
/// This method must be called for every node that is part of the output. The `build` function
|
||||||
|
/// will only be called for nodes that have not previously been built.
|
||||||
|
pub fn add_node<C, F>(&mut self, ctx: &mut C, id: ID, build: F) -> Input<ChildOutput>
|
||||||
|
where
|
||||||
|
C: DynamicRuleContext,
|
||||||
|
F: FnOnce(&mut C) -> Input<ChildOutput>,
|
||||||
|
{
|
||||||
|
let input = if let Some(input) = self.existing_nodes.get(&id) {
|
||||||
|
input.clone()
|
||||||
|
} else {
|
||||||
|
let input = build(ctx);
|
||||||
|
self.existing_nodes.insert(id.clone(), input.clone());
|
||||||
|
input
|
||||||
|
};
|
||||||
|
self.ids_added_this_evaluation.insert(id);
|
||||||
|
input
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registers a node that is part of the output.
|
||||||
|
///
|
||||||
|
/// See [`DynamicNodeFactory::add_node`].
|
||||||
|
pub fn add_async_node<C, F>(&mut self, ctx: &mut C, id: ID, build: F) -> Input<ChildOutput>
|
||||||
|
where
|
||||||
|
C: AsyncDynamicRuleContext,
|
||||||
|
F: FnOnce(&mut C) -> Input<ChildOutput>,
|
||||||
|
{
|
||||||
|
let input = if let Some(input) = self.existing_nodes.get(&id) {
|
||||||
|
input.clone()
|
||||||
|
} else {
|
||||||
|
let input = build(ctx);
|
||||||
|
self.existing_nodes.insert(id.clone(), input.clone());
|
||||||
|
input
|
||||||
|
};
|
||||||
|
self.ids_added_this_evaluation.insert(id);
|
||||||
|
input
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes any nodes that were previously output but which have not had [`DynamicNodeFactory::add_node`]
|
||||||
|
/// called during this evaluation.
|
||||||
|
///
|
||||||
|
/// Either this method or [`DynamicNodeFactory::all_nodes`] should only be called once per evaluation.
|
||||||
|
///
|
||||||
|
/// This method is useful when adding nodes that are not directly part of a dynamic node's output.
|
||||||
|
pub fn finalize_nodes(&mut self, ctx: &mut impl DynamicRuleContext) {
|
||||||
|
// collect everything up front so we can mutably borrow existing_nodes
|
||||||
|
let to_remove = self
|
||||||
|
.existing_nodes
|
||||||
|
.keys()
|
||||||
|
.filter(|k| !self.ids_added_this_evaluation.contains(*k))
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for key in to_remove {
|
||||||
|
let input = self.existing_nodes.remove(&key).unwrap();
|
||||||
|
ctx.remove_node(input.node_id());
|
||||||
|
}
|
||||||
|
self.ids_added_this_evaluation.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the final list of all nodes currently present in the output.
|
||||||
|
///
|
||||||
|
/// This method calls [`DynamicNodeFactory::finalize_nodes`], and this method or that one should only
|
||||||
|
/// be called once per evaluation.
|
||||||
|
pub fn all_nodes(&mut self, ctx: &mut impl DynamicRuleContext) -> Vec<Input<ChildOutput>> {
|
||||||
|
self.finalize_nodes(ctx);
|
||||||
|
self.existing_nodes.values().cloned().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes all added child nodes _without resetting the factory_.
|
||||||
|
///
|
||||||
|
/// This method should generally only be called from [`DynamicRule::remove_children`].
|
||||||
|
pub fn remove_all(&self, ctx: &mut impl DynamicRuleContext) {
|
||||||
|
for (_, input) in self.existing_nodes.iter() {
|
||||||
|
ctx.remove_node(input.node_id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An asynchronous rule whose output is further nodes in the graph.
|
||||||
|
///
|
||||||
|
/// See [`DynamicRule`].
|
||||||
|
pub trait AsyncDynamicRule: InputVisitable + 'static {
|
||||||
|
/// The type of the output value of each of the child nodes that this rule produces.
|
||||||
|
type ChildOutput: NodeValue;
|
||||||
|
|
||||||
|
/// Evaluates this rule asynchronously, producing additional nodes.
|
||||||
|
///
|
||||||
|
/// Use the methods on [`AsyncDynamicRuleContext`] to add or remove nodes from the graph.
|
||||||
|
/// Or, use [`DynamicNodeFactory`] to always register all nodes and allow that type to track
|
||||||
|
/// the specific additions/removals.
|
||||||
|
fn evaluate<'a>(
|
||||||
|
&'a mut self,
|
||||||
|
ctx: &'a mut impl AsyncDynamicRuleContext,
|
||||||
|
) -> impl Future<Output = Vec<Input<Self::ChildOutput>>> + 'a;
|
||||||
|
|
||||||
|
/// Removes any children of this rule that have been added to the graph.
|
||||||
|
///
|
||||||
|
/// Called before this rule is removed, if this dynamic rule itself was the child of another
|
||||||
|
/// dynamic rule that is now removing it.
|
||||||
|
fn remove_children(&self, ctx: &mut impl DynamicRuleContext);
|
||||||
|
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
fn node_label(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Facilities for adding/removing nodes in the graph during the update of an [`AsyncDynamicRule`].
|
||||||
|
pub trait AsyncDynamicRuleContext: DynamicRuleContext {
|
||||||
|
/// Adds a node whose value is produced using the given rule to the graph.
|
||||||
|
///
|
||||||
|
/// Returns an [`Input`] representing the newly-added node, which can be used to construct further rules.
|
||||||
|
fn add_async_rule<R>(&mut self, rule: R) -> Input<R::Output>
|
||||||
|
where
|
||||||
|
R: AsyncRule;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A simple rule that provides a constant value.
|
||||||
|
///
|
||||||
|
/// Note that, because [`Rule::evaluate`] returns an owned value, this rule's value type must implement `Clone`.
|
||||||
|
pub struct ConstantRule<T>(T);
|
||||||
|
|
||||||
|
impl<T> ConstantRule<T> {
|
||||||
|
/// Constructs a new constant rule with the given value.
|
||||||
|
pub fn new(value: T) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone + NodeValue> Rule for ConstantRule<T> {
|
||||||
|
type Output = T;
|
||||||
|
|
||||||
|
fn evaluate(&mut self) -> Self::Output {
|
||||||
|
self.0.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> InputVisitable for ConstantRule<T> {
|
||||||
|
fn visit_inputs(&self, _visitor: &mut impl InputVisitor) {}
|
||||||
|
}
|
42
crates/compute_graph/src/synchronicity.rs
Normal file
42
crates/compute_graph/src/synchronicity.rs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
//! Types used to make [`Graph`](`crate::Graph`) and [`GraphBuilder`](`crate::builder::GraphBuilder`) generic
|
||||||
|
//! over the synchronicity of the graph.
|
||||||
|
//!
|
||||||
|
//! The [`Synchronicity`] trait is sealed to outside implementors, and you generally do not need to refer
|
||||||
|
//! directly to the [`Synchronous`] or [`Asynchronous`] types.
|
||||||
|
|
||||||
|
use std::future::Future;
|
||||||
|
use std::pin::Pin;
|
||||||
|
|
||||||
|
pub(crate) mod private {
|
||||||
|
pub trait Sealed {}
|
||||||
|
impl Sealed for super::Synchronous {}
|
||||||
|
impl Sealed for super::Asynchronous {}
|
||||||
|
impl Sealed for () {}
|
||||||
|
impl<'a> Sealed for <super::Asynchronous as super::Synchronicity>::UpdateResult<'a> {}
|
||||||
|
pub struct Token;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Synchronicity: private::Sealed + 'static {
|
||||||
|
type UpdateResult<'a>: private::Sealed;
|
||||||
|
// Necessary for synchronous nodes that can be part of an async graph to return the
|
||||||
|
// appropriate result based on the type of graph they're in.
|
||||||
|
fn make_update_result<'a>(_: private::Token) -> Self::UpdateResult<'a>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Synchronous;
|
||||||
|
|
||||||
|
impl Synchronicity for Synchronous {
|
||||||
|
type UpdateResult<'a> = ();
|
||||||
|
|
||||||
|
fn make_update_result<'a>(_: private::Token) -> Self::UpdateResult<'a> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Asynchronous;
|
||||||
|
|
||||||
|
impl Synchronicity for Asynchronous {
|
||||||
|
type UpdateResult<'a> = Pin<Box<dyn Future<Output = ()> + 'a>>;
|
||||||
|
|
||||||
|
fn make_update_result<'a>(_: private::Token) -> Self::UpdateResult<'a> {
|
||||||
|
Box::pin(std::future::ready(()))
|
||||||
|
}
|
||||||
|
}
|
47
crates/compute_graph/src/util.rs
Normal file
47
crates/compute_graph/src/util.rs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
use petgraph::{
|
||||||
|
stable_graph::{IndexType, NodeIndex, StableGraph},
|
||||||
|
unionfind::UnionFind,
|
||||||
|
visit::{EdgeRef, IntoEdgeReferences, NodeIndexable},
|
||||||
|
EdgeType,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn remove_nodes_not_connected_to<N, E, Ty: EdgeType, Ix: IndexType>(
|
||||||
|
g: &mut StableGraph<N, E, Ty, Ix>,
|
||||||
|
node: NodeIndex<Ix>,
|
||||||
|
) {
|
||||||
|
// based on petgraph's connected_components
|
||||||
|
let mut vertex_sets = UnionFind::new(g.node_bound());
|
||||||
|
for edge in g.edge_references() {
|
||||||
|
vertex_sets.union(g.to_index(edge.source()), g.to_index(edge.target()));
|
||||||
|
}
|
||||||
|
let to_remove = g
|
||||||
|
.node_indices()
|
||||||
|
.filter(|other_idx| !vertex_sets.equiv(g.to_index(node), g.to_index(*other_idx)))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for idx in to_remove {
|
||||||
|
g.remove_node(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use petgraph::stable_graph::StableGraph;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn remove_nodes_not_connected_to() {
|
||||||
|
let mut graph: StableGraph<(), ()> = Default::default();
|
||||||
|
let a = graph.add_node(());
|
||||||
|
let b = graph.add_node(());
|
||||||
|
let c = graph.add_node(());
|
||||||
|
let d = graph.add_node(());
|
||||||
|
let e = graph.add_node(());
|
||||||
|
graph.extend_with_edges(&[(a, b), (a, c), (d, e)]);
|
||||||
|
super::remove_nodes_not_connected_to(&mut graph, a);
|
||||||
|
assert_eq!(graph.node_count(), 3);
|
||||||
|
assert!(graph.contains_node(a));
|
||||||
|
assert!(graph.contains_node(b));
|
||||||
|
assert!(graph.contains_node(c));
|
||||||
|
assert!(!graph.contains_node(d));
|
||||||
|
assert!(!graph.contains_node(e));
|
||||||
|
}
|
||||||
|
}
|
12
crates/compute_graph_macros/Cargo.toml
Normal file
12
crates/compute_graph_macros/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "compute_graph_macros"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
syn = "2"
|
||||||
|
quote = "1"
|
||||||
|
proc-macro2 = "1"
|
268
crates/compute_graph_macros/src/lib.rs
Normal file
268
crates/compute_graph_macros/src/lib.rs
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
use proc_macro::TokenStream;
|
||||||
|
use proc_macro2::{Literal, Span};
|
||||||
|
use quote::{format_ident, quote, ToTokens};
|
||||||
|
use syn::{
|
||||||
|
parse_macro_input, Attribute, Data, DataEnum, DataStruct, DeriveInput, Field, Fields,
|
||||||
|
GenericArgument, GenericParam, Index, Member, PathArguments, Type,
|
||||||
|
};
|
||||||
|
|
||||||
|
extern crate proc_macro;
|
||||||
|
|
||||||
|
/// Derive an implementation of the `InputVisitable` trait and helper methods.
|
||||||
|
///
|
||||||
|
/// This macro generates an implementation of the `InputVisitable` trait and the `visit_inputs` method that
|
||||||
|
/// calls `visit_inputs` on each field of the struct that is does not have the `#[skip_visit]` attribute.
|
||||||
|
///
|
||||||
|
/// For structs only, the macro also generates helper methods for accessing the value of each input less verbosely.
|
||||||
|
/// For unnamed struct fields, the methods generated have the form `input_0`, `input_1`, etc.
|
||||||
|
/// For named fields, the generated method name matches the field name. In both cases, the method
|
||||||
|
/// returns a reference to the input value. As with the `Input::value` method, calling the helper methods
|
||||||
|
/// before the referenced node has been evaluated is forbidden.
|
||||||
|
#[proc_macro_derive(InputVisitable, attributes(skip_visit))]
|
||||||
|
pub fn derive_input_visitable(input: TokenStream) -> TokenStream {
|
||||||
|
let input = parse_macro_input!(input as DeriveInput);
|
||||||
|
match input.data {
|
||||||
|
Data::Struct(ref data) => derive_input_visitable_struct(&input, data),
|
||||||
|
Data::Enum(ref data) => derive_input_visitable_enum(&input, data),
|
||||||
|
Data::Union(_) => TokenStream::from(
|
||||||
|
syn::Error::new(input.ident.span(), "Unions cannot derive `InputVisitable`")
|
||||||
|
.to_compile_error(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn derive_input_visitable_struct(input: &DeriveInput, data: &DataStruct) -> TokenStream {
|
||||||
|
let visit_inputs = derive_visit_inputs(
|
||||||
|
&data.fields,
|
||||||
|
|field| {
|
||||||
|
let ident = field.ident.clone().unwrap();
|
||||||
|
quote!(&self.#ident)
|
||||||
|
},
|
||||||
|
|i| {
|
||||||
|
let member = Member::Unnamed(Index {
|
||||||
|
index: i as u32,
|
||||||
|
span: Span::call_site(),
|
||||||
|
});
|
||||||
|
quote!(&self.#member)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let input_value_methods = match data.fields {
|
||||||
|
Fields::Named(ref named) => named
|
||||||
|
.named
|
||||||
|
.iter()
|
||||||
|
.filter_map(|field| {
|
||||||
|
input_value_type(field).map(|(ty, is_dynamic)| (field, ty, is_dynamic))
|
||||||
|
})
|
||||||
|
.map(|(field, ty, is_dynamic)| {
|
||||||
|
let ident = field.ident.as_ref().unwrap();
|
||||||
|
let target = if is_dynamic {
|
||||||
|
quote!(::compute_graph::node::DynamicRuleOutput<#ty>)
|
||||||
|
} else {
|
||||||
|
ty.to_token_stream()
|
||||||
|
};
|
||||||
|
quote!(
|
||||||
|
|
||||||
|
fn #ident(&self) -> impl ::std::ops::Deref<Target = #target> + '_ {
|
||||||
|
self.#ident.value()
|
||||||
|
}
|
||||||
|
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
Fields::Unnamed(ref unnamed) => unnamed
|
||||||
|
.unnamed
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(i, field)| {
|
||||||
|
input_value_type(field).map(|(ty, is_dynamic)| (i, ty, is_dynamic))
|
||||||
|
})
|
||||||
|
.map(|(i, ty, is_dynamic)| {
|
||||||
|
let idx_lit = Literal::usize_unsuffixed(i);
|
||||||
|
let ident = format_ident!("input_{i}");
|
||||||
|
let target = if is_dynamic {
|
||||||
|
quote!(::compute_graph::node::DynamicRuleOutput<#ty>)
|
||||||
|
} else {
|
||||||
|
ty.to_token_stream()
|
||||||
|
};
|
||||||
|
quote!(
|
||||||
|
|
||||||
|
fn #ident(&self) -> impl ::std::ops::Deref<Target = #target> + '_ {
|
||||||
|
self.#idx_lit.value()
|
||||||
|
}
|
||||||
|
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
Fields::Unit => vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let input_visitable_impl = make_derive_impl(
|
||||||
|
input,
|
||||||
|
quote!(::compute_graph::input::InputVisitable for),
|
||||||
|
quote!(
|
||||||
|
fn visit_inputs(&self, visitor: &mut impl ::compute_graph::input::InputVisitor) {
|
||||||
|
#visit_inputs
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let input_value_methods_impl = make_derive_impl(
|
||||||
|
input,
|
||||||
|
quote!(),
|
||||||
|
quote!(
|
||||||
|
#(#input_value_methods)*
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
TokenStream::from(quote!(
|
||||||
|
#input_visitable_impl
|
||||||
|
#input_value_methods_impl
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn derive_input_visitable_enum(input: &DeriveInput, data: &DataEnum) -> TokenStream {
|
||||||
|
let variants = data.variants.iter().map(|variant| {
|
||||||
|
let ident = &variant.ident;
|
||||||
|
let fields = match &variant.fields {
|
||||||
|
Fields::Named(fields) => {
|
||||||
|
let field_refs = fields.named.iter().filter(should_visit_field).map(|field| {
|
||||||
|
let ident = field.ident.as_ref().expect("named field must have ident");
|
||||||
|
quote!(ref #ident)
|
||||||
|
});
|
||||||
|
quote!({ #(#field_refs,)* .. })
|
||||||
|
}
|
||||||
|
Fields::Unnamed(fields) => {
|
||||||
|
let field_refs = fields.unnamed.iter().enumerate().map(|(i, field)| {
|
||||||
|
if should_visit_field(&field) {
|
||||||
|
let ident = format_ident!("field_{i}");
|
||||||
|
quote!(ref #ident)
|
||||||
|
} else {
|
||||||
|
quote!(_)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
quote!((#(#field_refs,)*))
|
||||||
|
}
|
||||||
|
Fields::Unit => quote!(),
|
||||||
|
};
|
||||||
|
let variant_visit_inputs = derive_visit_inputs(
|
||||||
|
&variant.fields,
|
||||||
|
|field| field.ident.clone().unwrap(),
|
||||||
|
|i| format_ident!("field_{i}"),
|
||||||
|
);
|
||||||
|
quote!(Self::#ident #fields => {
|
||||||
|
#variant_visit_inputs
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
make_derive_impl(
|
||||||
|
input,
|
||||||
|
quote!(::compute_graph::input::InputVisitable for),
|
||||||
|
quote!(
|
||||||
|
fn visit_inputs(&self, visitor: &mut impl ::compute_graph::input::InputVisitor) {
|
||||||
|
match self {
|
||||||
|
#(#variants)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn derive_visit_inputs<NamedAccessTokens: ToTokens, UnnamedAccessTokens: ToTokens>(
|
||||||
|
fields: &Fields,
|
||||||
|
named_field_access: impl Fn(&Field) -> NamedAccessTokens,
|
||||||
|
unnamed_field_access: impl Fn(usize) -> UnnamedAccessTokens,
|
||||||
|
) -> proc_macro2::TokenStream {
|
||||||
|
match fields {
|
||||||
|
Fields::Named(ref named) => {
|
||||||
|
let fields = named.named.iter().filter(should_visit_field).map(|field| {
|
||||||
|
let field_access = named_field_access(field);
|
||||||
|
quote!(::compute_graph::input::InputVisitable::visit_inputs(#field_access, visitor);)
|
||||||
|
});
|
||||||
|
quote!(#(#fields)*)
|
||||||
|
}
|
||||||
|
Fields::Unnamed(ref unnamed) => {
|
||||||
|
let fields = unnamed
|
||||||
|
.unnamed
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_i, field)| should_visit_field(field))
|
||||||
|
.map(|(i, _field)| {
|
||||||
|
let field_access = unnamed_field_access(i);
|
||||||
|
quote!(::compute_graph::input::InputVisitable::visit_inputs(#field_access, visitor);)
|
||||||
|
});
|
||||||
|
quote!(#(#fields)*)
|
||||||
|
}
|
||||||
|
Fields::Unit => proc_macro2::TokenStream::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_derive_impl(
|
||||||
|
input: &DeriveInput,
|
||||||
|
trait_for: proc_macro2::TokenStream,
|
||||||
|
body: proc_macro2::TokenStream,
|
||||||
|
) -> proc_macro2::TokenStream {
|
||||||
|
let name = &input.ident;
|
||||||
|
let lt = &input.generics.lt_token;
|
||||||
|
let params = &input.generics.params;
|
||||||
|
let gt = &input.generics.gt_token;
|
||||||
|
let generics = quote!(#lt #params #gt);
|
||||||
|
let where_clause = &input.generics.where_clause;
|
||||||
|
let params_only_names = params.iter().map(|p| match p {
|
||||||
|
GenericParam::Lifetime(_) => {
|
||||||
|
panic!("Lifetime generics aren't supported when deriving `InputVisitable`")
|
||||||
|
}
|
||||||
|
GenericParam::Type(ty) => &ty.ident,
|
||||||
|
GenericParam::Const(_) => {
|
||||||
|
panic!("Const generics aren't supported when deriving `InputVisitable`")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let generics_only_names = quote!(#lt #(#params_only_names),* #gt);
|
||||||
|
|
||||||
|
quote!(
|
||||||
|
impl #generics #trait_for #name #generics_only_names #where_clause {
|
||||||
|
#body
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn input_value_type(field: &Field) -> Option<(&Type, bool)> {
|
||||||
|
if field.attrs.iter().any(is_skip_attr) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if let Type::Path(ref path) = field.ty {
|
||||||
|
let last_segment = path.path.segments.last().unwrap();
|
||||||
|
if last_segment.ident == "Input" || last_segment.ident == "DynamicInput" {
|
||||||
|
let is_dynamic = last_segment.ident == "DynamicInput";
|
||||||
|
if let PathArguments::AngleBracketed(ref args) = last_segment.arguments {
|
||||||
|
if args.args.len() == 1 {
|
||||||
|
if let GenericArgument::Type(ref ty) = args.args.first().unwrap() {
|
||||||
|
Some((ty, is_dynamic))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_visit_field(field: &&Field) -> bool {
|
||||||
|
!field.attrs.iter().any(is_skip_attr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_skip_attr(attr: &Attribute) -> bool {
|
||||||
|
match attr.meta.require_path_only() {
|
||||||
|
Ok(path) => path.is_ident("skip_visit"),
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
|
}
|
7
crates/derive_test/Cargo.toml
Normal file
7
crates/derive_test/Cargo.toml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[package]
|
||||||
|
name = "derive_test"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
compute_graph = { path = "../compute_graph" }
|
171
crates/derive_test/src/lib.rs
Normal file
171
crates/derive_test/src/lib.rs
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
use compute_graph::input::{DynamicInput, Input, InputVisitable};
|
||||||
|
use compute_graph::node::NodeValue;
|
||||||
|
use compute_graph::rule::Rule;
|
||||||
|
|
||||||
|
#[derive(InputVisitable)]
|
||||||
|
struct Add(Input<i32>, Input<i32>, #[skip_visit] i32);
|
||||||
|
|
||||||
|
impl Rule for Add {
|
||||||
|
type Output = i32;
|
||||||
|
fn evaluate(&mut self) -> Self::Output {
|
||||||
|
*self.input_0() + *self.input_1() + self.2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(InputVisitable)]
|
||||||
|
struct Add2 {
|
||||||
|
a: Input<i32>,
|
||||||
|
b: Input<i32>,
|
||||||
|
#[skip_visit]
|
||||||
|
c: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rule for Add2 {
|
||||||
|
type Output = i32;
|
||||||
|
fn evaluate(&mut self) -> Self::Output {
|
||||||
|
*self.a() + *self.b() + self.c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(InputVisitable)]
|
||||||
|
struct Passthrough<T: NodeValue + Clone>(Input<T>);
|
||||||
|
impl<T: NodeValue + Clone> Rule for Passthrough<T> {
|
||||||
|
type Output = T;
|
||||||
|
fn evaluate(&mut self) -> Self::Output {
|
||||||
|
self.input_0().clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(InputVisitable)]
|
||||||
|
struct Sum(DynamicInput<i32>);
|
||||||
|
impl Rule for Sum {
|
||||||
|
type Output = i32;
|
||||||
|
fn evaluate(&mut self) -> Self::Output {
|
||||||
|
self.input_0()
|
||||||
|
.inputs
|
||||||
|
.iter()
|
||||||
|
.map(|input| *input.value())
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(InputVisitable)]
|
||||||
|
enum E {
|
||||||
|
A(#[skip_visit] i32, Input<i32>),
|
||||||
|
B {
|
||||||
|
#[skip_visit]
|
||||||
|
x: i32,
|
||||||
|
y: Input<i32>,
|
||||||
|
},
|
||||||
|
C {
|
||||||
|
x: Input<i32>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use compute_graph::{
|
||||||
|
builder::GraphBuilder,
|
||||||
|
input::InputVisitor,
|
||||||
|
rule::{ConstantRule, DynamicRule},
|
||||||
|
synchronicity::Synchronous,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add() {
|
||||||
|
let mut builder = GraphBuilder::new();
|
||||||
|
let a = builder.add_value(1);
|
||||||
|
let b = builder.add_value(2);
|
||||||
|
builder.set_output(Add(a, b, 3));
|
||||||
|
let mut graph = builder.build().unwrap();
|
||||||
|
assert_eq!(*graph.evaluate(), 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add2() {
|
||||||
|
let mut builder = GraphBuilder::new();
|
||||||
|
let a = builder.add_value(1);
|
||||||
|
let b = builder.add_value(2);
|
||||||
|
builder.set_output(Add2 { a, b, c: 3 });
|
||||||
|
let mut graph = builder.build().unwrap();
|
||||||
|
assert_eq!(*graph.evaluate(), 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sum() {
|
||||||
|
#[derive(InputVisitable)]
|
||||||
|
struct Dynamic;
|
||||||
|
impl DynamicRule for Dynamic {
|
||||||
|
type ChildOutput = i32;
|
||||||
|
fn evaluate(
|
||||||
|
&mut self,
|
||||||
|
ctx: &mut impl compute_graph::rule::DynamicRuleContext,
|
||||||
|
) -> Vec<Input<Self::ChildOutput>> {
|
||||||
|
vec![
|
||||||
|
ctx.add_rule(ConstantRule::new(1)),
|
||||||
|
ctx.add_rule(ConstantRule::new(2)),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut builder = GraphBuilder::new();
|
||||||
|
let dynamic_input = builder.add_dynamic_rule(Dynamic);
|
||||||
|
builder.set_output(Sum(dynamic_input));
|
||||||
|
let mut graph = builder.build().unwrap();
|
||||||
|
assert_eq!(*graph.evaluate(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ignore() {
|
||||||
|
#[allow(unused)]
|
||||||
|
#[derive(InputVisitable)]
|
||||||
|
struct Ignore {
|
||||||
|
#[skip_visit]
|
||||||
|
input: Input<i32>,
|
||||||
|
}
|
||||||
|
let mut builder = GraphBuilder::<i32, Synchronous>::new();
|
||||||
|
struct Visitor;
|
||||||
|
impl InputVisitor for Visitor {
|
||||||
|
fn visit<T>(&mut self, _input: &Input<T>) {
|
||||||
|
assert!(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ignore {
|
||||||
|
input: builder.add_value(0),
|
||||||
|
}
|
||||||
|
.visit_inputs(&mut Visitor);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_enum() {
|
||||||
|
let mut builder = GraphBuilder::<i32, Synchronous>::new();
|
||||||
|
let input = builder.add_value(1);
|
||||||
|
struct Visitor(bool, Input<i32>);
|
||||||
|
impl InputVisitor for Visitor {
|
||||||
|
fn visit<T>(&mut self, input: &Input<T>) {
|
||||||
|
assert_eq!(input.node_id(), self.1.node_id());
|
||||||
|
assert!(!self.0);
|
||||||
|
self.0 = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let a = E::A(0, input.clone());
|
||||||
|
let mut visitor = Visitor(false, input.clone());
|
||||||
|
InputVisitable::visit_inputs(&a, &mut visitor);
|
||||||
|
assert!(visitor.0);
|
||||||
|
|
||||||
|
let b = E::B {
|
||||||
|
x: 0,
|
||||||
|
y: input.clone(),
|
||||||
|
};
|
||||||
|
let mut visitor = Visitor(false, input.clone());
|
||||||
|
InputVisitable::visit_inputs(&b, &mut visitor);
|
||||||
|
assert!(visitor.0);
|
||||||
|
|
||||||
|
let c = E::C { x: input.clone() };
|
||||||
|
let mut visitor = Visitor(false, input);
|
||||||
|
InputVisitable::visit_inputs(&c, &mut visitor);
|
||||||
|
assert!(visitor.0);
|
||||||
|
}
|
||||||
|
}
|
9
crates/pyftsubset/Cargo.toml
Normal file
9
crates/pyftsubset/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "pyftsubset"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
log = "0.4.25"
|
||||||
|
pyo3 = "0.23.4"
|
||||||
|
tempfile = "3.16.0"
|
1
crates/pyftsubset/fonttools
Submodule
1
crates/pyftsubset/fonttools
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit d6f40c2e453f3c07807fef7926a31717f180b660
|
65
crates/pyftsubset/src/lib.rs
Normal file
65
crates/pyftsubset/src/lib.rs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
use std::{
|
||||||
|
ffi::CString,
|
||||||
|
fs::read_to_string,
|
||||||
|
io::{Read, Write},
|
||||||
|
path::Path,
|
||||||
|
};
|
||||||
|
|
||||||
|
use log::debug;
|
||||||
|
use pyo3::{
|
||||||
|
ffi::c_str,
|
||||||
|
prelude::*,
|
||||||
|
types::{PyList, PyTuple},
|
||||||
|
};
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
|
pub fn subset(data: &[u8], unicodes: &[u32]) -> Vec<u8> {
|
||||||
|
pyo3::prepare_freethreaded_python();
|
||||||
|
|
||||||
|
let mut input = NamedTempFile::new().expect("input file");
|
||||||
|
input.write_all(data).expect("writing input");
|
||||||
|
let mut output = NamedTempFile::new().expect("output file");
|
||||||
|
|
||||||
|
let path = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/fonttools/Lib"));
|
||||||
|
let init_path = path.join("fontTools/subset/__init__.py");
|
||||||
|
let py_init = CString::new(read_to_string(init_path).unwrap()).unwrap();
|
||||||
|
let result = Python::with_gil(|py| -> PyResult<Py<PyAny>> {
|
||||||
|
let syspath = py
|
||||||
|
.import("sys")?
|
||||||
|
.getattr("path")?
|
||||||
|
.downcast_into::<PyList>()?;
|
||||||
|
syspath.insert(0, path)?;
|
||||||
|
let app: Py<PyAny> = PyModule::from_code(py, py_init.as_c_str(), c_str!(""), c_str!(""))?
|
||||||
|
.getattr("main")?
|
||||||
|
.into();
|
||||||
|
let args = PyList::new(py, vec![
|
||||||
|
input.path().to_str().unwrap(),
|
||||||
|
&format_unicodes(unicodes),
|
||||||
|
&format!("--output-file={}", output.path().to_str().unwrap()),
|
||||||
|
])?;
|
||||||
|
debug!("Running pyftsubset with {:?}", args);
|
||||||
|
let args_tuple = PyTuple::new(py, &[args])?;
|
||||||
|
app.call1(py, args_tuple)
|
||||||
|
});
|
||||||
|
result.expect("subsetting");
|
||||||
|
|
||||||
|
let mut buf = vec![];
|
||||||
|
output.read_to_end(&mut buf).expect("reading output");
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_unicodes(unicodes: &[u32]) -> String {
|
||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
let mut s = "--unicodes=".to_owned();
|
||||||
|
let mut first = true;
|
||||||
|
for u in unicodes {
|
||||||
|
if first {
|
||||||
|
write!(s, "{:x}", u).expect("append unicode");
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
write!(s, ",{:x}", u).expect("append unicode");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
@ -1,28 +0,0 @@
|
|||||||
create table if not exists articles (
|
|
||||||
id text primary key not null,
|
|
||||||
conversation text not null,
|
|
||||||
has_federated boolean not null,
|
|
||||||
article_object text not null
|
|
||||||
);
|
|
||||||
|
|
||||||
create table if not exists actors (
|
|
||||||
id text primary key not null,
|
|
||||||
actor_object text not null,
|
|
||||||
is_follower boolean not null default 0,
|
|
||||||
display_name text,
|
|
||||||
inbox text not null,
|
|
||||||
shared_inbox text,
|
|
||||||
icon_url text,
|
|
||||||
public_key_pem text
|
|
||||||
);
|
|
||||||
|
|
||||||
create table if not exists notes (
|
|
||||||
id text primary key not null,
|
|
||||||
content text not null,
|
|
||||||
in_reply_to text not null,
|
|
||||||
conversation text not null,
|
|
||||||
published text not null, -- RFC 3339 formatted date (e.g., 1985-04-12T23:20:50.52Z)
|
|
||||||
actor_id text not null,
|
|
||||||
digested boolean not null default 0,
|
|
||||||
foreign key (actor_id) references actors (id) on delete cascade
|
|
||||||
);
|
|
11
site/404.html
Normal file
11
site/404.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{% extends "default" %}
|
||||||
|
|
||||||
|
{% block titlevariable %}
|
||||||
|
{% set title = "Not Found" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content -%}
|
||||||
|
|
||||||
|
<h1 class="headline">404 Not Found</h1>
|
||||||
|
|
||||||
|
{%- endblock %}
|
@ -1,20 +1,31 @@
|
|||||||
{% extends "layout/default.html" %}
|
{% extends "default" %}
|
||||||
|
|
||||||
{% block title %}Archive{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
{% for (year, posts) in years %}
|
|
||||||
<h2>{{ year }}</h2>
|
|
||||||
<ul>
|
|
||||||
{% for post in posts %}
|
|
||||||
<li>
|
|
||||||
<a href="{{ post.permalink() }}">
|
|
||||||
{{ post.metadata.title }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
|
{% block titlevariable %}
|
||||||
|
{% set title = "Archive" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content -%}
|
||||||
|
|
||||||
|
<h1 class="headline">Archive</h1>
|
||||||
|
|
||||||
|
{% for year in years %}
|
||||||
|
<div class="archive-list">
|
||||||
|
{% for entry in posts_by_year[year] %}
|
||||||
|
<code>
|
||||||
|
<time datetime="{{ entry.date | iso_datetime }}" title="{{ entry.date | pretty_datetime}}">
|
||||||
|
{{ entry.date | iso_date }}
|
||||||
|
</time>
|
||||||
|
</code>
|
||||||
|
<div>
|
||||||
|
<a href="{{ entry.permalink }}">
|
||||||
|
{{ entry.title }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% if not loop.last %}
|
||||||
|
<hr>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{%- endblock %}
|
||||||
|
52
site/colophon.html
Normal file
52
site/colophon.html
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
{% extends "default" %}
|
||||||
|
|
||||||
|
{% block titlevariable %}
|
||||||
|
{% set title = "Colophon" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content -%}
|
||||||
|
|
||||||
|
<h1 class="headline">Colophon</h1>
|
||||||
|
|
||||||
|
<div class="body-content">
|
||||||
|
<p>
|
||||||
|
This website is produced using a custom static site generator called v7, which is <a href="https://git.shadowfacts.net/shadowfacts/v7/">open source</a>.
|
||||||
|
It comprises some 7,000 lines of Rust code, and took too much time to develop.
|
||||||
|
<a href="/2025/version-7/">Read more</a> about the architecture.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Body text is typeset in Matthew Butterick’s <a href="https://mbtype.com/fonts/valkyrie/">Valkyrie</a>.
|
||||||
|
Butterick’s <a href="https://practicaltypography.com/"><em>Practical Typography</em></a> also informed many of the typographic choices of this website.
|
||||||
|
Code is set in <a href="https://mass-driver.com/typefaces/md-io/"><code>MD IO</code></a> by Mass Driver.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
For analytics, I use <a href="https://www.goatcounter.com/">GoatCounter</a> (<a href="https://www.goatcounter.com/help/privacy">privacy policy</a>) to get a rough sense of what people are reading.
|
||||||
|
No personally identifiable information is stored, and no cookies are used.
|
||||||
|
The <a href="https://shadowfacts.goatcounter.com/" rel="nofollow">statistics</a> gathered are public.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
All of the posts are written by yours truly.
|
||||||
|
No generative artificial intelligence tools were used to create this website.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
All content on this website is under copyright and may not be republished without permission.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<blockquote>
|
||||||
|
<p>
|
||||||
|
Now, there’s this about cynicism, Sergeant.
|
||||||
|
It’s the universe’s most supine moral position.
|
||||||
|
Real comfortable.
|
||||||
|
If nothing can be done, then you’re not some kind of shit for not doing it, and you can lie there and stink to yourself in perfect peace.
|
||||||
|
</p>
|
||||||
|
<p>— Lois McMaster Bujold, <em>Borders of Infinity</em></p>
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{%- endblock %}
|
@ -1,5 +0,0 @@
|
|||||||
@import "light.scss";
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
@import "dark.scss";
|
|
||||||
}
|
|
@ -1,59 +0,0 @@
|
|||||||
:root {
|
|
||||||
--accent-color: var(--dark-accent-color);
|
|
||||||
--content-background-color: var(--dark-content-background-color);
|
|
||||||
--shadow-color: var(--dark-shadow-color);
|
|
||||||
--ui-background-color: var(--dark-ui-background-color);
|
|
||||||
--ui-text-color: var(--dark-ui-text-color);
|
|
||||||
--secondary-ui-text-color: var(--dark-secondary-ui-text-color);
|
|
||||||
--content-text-color: var(--dark-content-text-color);
|
|
||||||
|
|
||||||
--aside-background: var(--dark-aside-background);
|
|
||||||
--aside-border: var(--dark-aside-border);
|
|
||||||
--aside-warning-background: var(--dark-aside-warning-background);
|
|
||||||
--aside-warning-border: var(--dark-aside-warning-border);
|
|
||||||
|
|
||||||
--webring-background: var(--dark-webring-background);
|
|
||||||
|
|
||||||
// Syntax highdarking
|
|
||||||
--atom-base: var(--dark-atom-base);
|
|
||||||
--atom-mono-1: var(--dark-atom-mono-1);
|
|
||||||
--atom-mono-2: var(--dark-atom-mono-2);
|
|
||||||
--atom-mono-3: var(--dark-atom-mono-3);
|
|
||||||
--atom-hue-1: var(--dark-atom-hue-1);
|
|
||||||
--atom-hue-2: var(--dark-atom-hue-2);
|
|
||||||
--atom-hue-3: var(--dark-atom-hue-3);
|
|
||||||
--atom-hue-4: var(--dark-atom-hue-4);
|
|
||||||
--atom-hue-5: var(--dark-atom-hue-5);
|
|
||||||
--atom-hue-5-2: var(--dark-atom-hue-5-2);
|
|
||||||
--atom-hue-6: var(--dark-atom-hue-6);
|
|
||||||
--atom-hue-6-2: var(--dark-atom-hue-6-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-inverted {
|
|
||||||
--accent-color: var(--light-accent-color);
|
|
||||||
--content-background-color: var(--light-content-background-color);
|
|
||||||
--shadow-color: var(--light-shadow-color);
|
|
||||||
--ui-background-color: var(--light-ui-background-color);
|
|
||||||
--ui-text-color: var(--light-ui-text-color);
|
|
||||||
--secondary-ui-text-color: var(--light-secondary-ui-text-color);
|
|
||||||
--content-text-color: var(--light-content-text-color);
|
|
||||||
|
|
||||||
--aside-background: var(--light-aside-background);
|
|
||||||
--aside-border: var(--light-aside-border);
|
|
||||||
--aside-warning-background: var(--light-aside-warning-background);
|
|
||||||
--aside-warning-border: var(--light-aside-warning-border);
|
|
||||||
|
|
||||||
// Syntax highlighting
|
|
||||||
--atom-base: var(--light-atom-base);
|
|
||||||
--atom-mono-1: var(--light-atom-mono-1);
|
|
||||||
--atom-mono-2: var(--light-atom-mono-2);
|
|
||||||
--atom-mono-3: var(--light-atom-mono-3);
|
|
||||||
--atom-hue-1: var(--light-atom-hue-1);
|
|
||||||
--atom-hue-2: var(--light-atom-hue-2);
|
|
||||||
--atom-hue-3: var(--light-atom-hue-3);
|
|
||||||
--atom-hue-4: var(--light-atom-hue-4);
|
|
||||||
--atom-hue-5: var(--light-atom-hue-5);
|
|
||||||
--atom-hue-5-2: var(--light-atom-hue-5-2);
|
|
||||||
--atom-hue-6: var(--light-atom-hue-6);
|
|
||||||
--atom-hue-6-2: var(--light-atom-hue-6-2);
|
|
||||||
}
|
|
26
site/css/external-link.svg
Normal file
26
site/css/external-link.svg
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<svg
|
||||||
|
width="13.333401"
|
||||||
|
height="13.3334"
|
||||||
|
viewBox="0 0 13.333401 13.3334"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M 10.66672,7.3333701 V 11.33339 A 1.33334,1.33334 0 0 1 9.33338,12.66673 H 2.00001 A 1.33334,1.33334 0 0 1 0.66667002,11.33339 V 4.0000201 A 1.33334,1.33334 0 0 1 2.00001,2.6666801 h 4.00002"
|
||||||
|
id="path1"
|
||||||
|
style="stroke-width:1.33334" />
|
||||||
|
<polyline
|
||||||
|
points="15 3 21 3 21 9"
|
||||||
|
id="polyline1"
|
||||||
|
transform="matrix(0.66667,0,0,0.66667,-1.33334,-1.3333399)" />
|
||||||
|
<line
|
||||||
|
x1="5.3333602"
|
||||||
|
y1="8.0000401"
|
||||||
|
x2="12.666731"
|
||||||
|
y2="0.66667002"
|
||||||
|
id="line1"
|
||||||
|
style="stroke-width:1.33334" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 772 B |
File diff suppressed because one or more lines are too long
BIN
site/css/fonts/MDIO-Bold.woff2
Normal file
BIN
site/css/fonts/MDIO-Bold.woff2
Normal file
Binary file not shown.
BIN
site/css/fonts/MDIO-Italic.woff2
Normal file
BIN
site/css/fonts/MDIO-Italic.woff2
Normal file
Binary file not shown.
BIN
site/css/fonts/MDIO-Regular.woff2
Normal file
BIN
site/css/fonts/MDIO-Regular.woff2
Normal file
Binary file not shown.
BIN
site/css/fonts/valkyrie_a_bold.woff2
Normal file
BIN
site/css/fonts/valkyrie_a_bold.woff2
Normal file
Binary file not shown.
BIN
site/css/fonts/valkyrie_a_bold_italic.woff2
Normal file
BIN
site/css/fonts/valkyrie_a_bold_italic.woff2
Normal file
Binary file not shown.
BIN
site/css/fonts/valkyrie_a_italic.woff2
Normal file
BIN
site/css/fonts/valkyrie_a_italic.woff2
Normal file
Binary file not shown.
BIN
site/css/fonts/valkyrie_a_regular.woff2
Normal file
BIN
site/css/fonts/valkyrie_a_regular.woff2
Normal file
Binary file not shown.
@ -1,59 +0,0 @@
|
|||||||
:root {
|
|
||||||
--accent-color: var(--light-accent-color);
|
|
||||||
--content-background-color: var(--light-content-background-color);
|
|
||||||
--shadow-color: var(--light-shadow-color);
|
|
||||||
--ui-background-color: var(--light-ui-background-color);
|
|
||||||
--ui-text-color: var(--light-ui-text-color);
|
|
||||||
--secondary-ui-text-color: var(--light-secondary-ui-text-color);
|
|
||||||
--content-text-color: var(--light-content-text-color);
|
|
||||||
|
|
||||||
--aside-background: var(--light-aside-background);
|
|
||||||
--aside-border: var(--light-aside-border);
|
|
||||||
--aside-warning-background: var(--light-aside-warning-background);
|
|
||||||
--aside-warning-border: var(--light-aside-warning-border);
|
|
||||||
|
|
||||||
--webring-background: var(--light-webring-background);
|
|
||||||
|
|
||||||
// Syntax highlighting
|
|
||||||
--atom-base: var(--light-atom-base);
|
|
||||||
--atom-mono-1: var(--light-atom-mono-1);
|
|
||||||
--atom-mono-2: var(--light-atom-mono-2);
|
|
||||||
--atom-mono-3: var(--light-atom-mono-3);
|
|
||||||
--atom-hue-1: var(--light-atom-hue-1);
|
|
||||||
--atom-hue-2: var(--light-atom-hue-2);
|
|
||||||
--atom-hue-3: var(--light-atom-hue-3);
|
|
||||||
--atom-hue-4: var(--light-atom-hue-4);
|
|
||||||
--atom-hue-5: var(--light-atom-hue-5);
|
|
||||||
--atom-hue-5-2: var(--light-atom-hue-5-2);
|
|
||||||
--atom-hue-6: var(--light-atom-hue-6);
|
|
||||||
--atom-hue-6-2: var(--light-atom-hue-6-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-inverted {
|
|
||||||
--accent-color: var(--dark-accent-color);
|
|
||||||
--content-background-color: var(--dark-content-background-color);
|
|
||||||
--shadow-color: var(--dark-shadow-color);
|
|
||||||
--ui-background-color: var(--dark-ui-background-color);
|
|
||||||
--ui-text-color: var(--dark-ui-text-color);
|
|
||||||
--secondary-ui-text-color: var(--dark-secondary-ui-text-color);
|
|
||||||
--content-text-color: var(--dark-content-text-color);
|
|
||||||
|
|
||||||
--aside-background: var(--dark-aside-background);
|
|
||||||
--aside-border: var(--dark-aside-border);
|
|
||||||
--aside-warning-background: var(--dark-aside-warning-background);
|
|
||||||
--aside-warning-border: var(--dark-aside-warning-border);
|
|
||||||
|
|
||||||
// Syntax highdarking
|
|
||||||
--atom-base: var(--dark-atom-base);
|
|
||||||
--atom-mono-1: var(--dark-atom-mono-1);
|
|
||||||
--atom-mono-2: var(--dark-atom-mono-2);
|
|
||||||
--atom-mono-3: var(--dark-atom-mono-3);
|
|
||||||
--atom-hue-1: var(--dark-atom-hue-1);
|
|
||||||
--atom-hue-2: var(--dark-atom-hue-2);
|
|
||||||
--atom-hue-3: var(--dark-atom-hue-3);
|
|
||||||
--atom-hue-4: var(--dark-atom-hue-4);
|
|
||||||
--atom-hue-5: var(--dark-atom-hue-5);
|
|
||||||
--atom-hue-5-2: var(--dark-atom-hue-5-2);
|
|
||||||
--atom-hue-6: var(--dark-atom-hue-6);
|
|
||||||
--atom-hue-6-2: var(--dark-atom-hue-6-2);
|
|
||||||
}
|
|
1344
site/css/main.scss
1344
site/css/main.scss
File diff suppressed because it is too large
Load Diff
120
site/css/normalize.scss
vendored
120
site/css/normalize.scss
vendored
@ -9,8 +9,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
html {
|
html {
|
||||||
line-height: 1.15; /* 1 */
|
line-height: 1.15; /* 1 */
|
||||||
-webkit-text-size-adjust: 100%; /* 2 */
|
-webkit-text-size-adjust: 100%; /* 2 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sections
|
/* Sections
|
||||||
@ -21,7 +21,7 @@ html {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -29,7 +29,7 @@ body {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
main {
|
main {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -38,8 +38,8 @@ main {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
margin: 0.67em 0;
|
margin: 0.67em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Grouping content
|
/* Grouping content
|
||||||
@ -51,9 +51,9 @@ h1 {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
box-sizing: content-box; /* 1 */
|
box-sizing: content-box; /* 1 */
|
||||||
height: 0; /* 1 */
|
height: 0; /* 1 */
|
||||||
overflow: visible; /* 2 */
|
overflow: visible; /* 2 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -62,8 +62,8 @@ hr {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
font-family: monospace, monospace; /* 1 */
|
font-family: monospace, monospace; /* 1 */
|
||||||
font-size: 1em; /* 2 */
|
font-size: 1em; /* 2 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Text-level semantics
|
/* Text-level semantics
|
||||||
@ -74,7 +74,7 @@ pre {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
a {
|
a {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -83,9 +83,9 @@ a {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
abbr[title] {
|
abbr[title] {
|
||||||
border-bottom: none; /* 1 */
|
border-bottom: none; /* 1 */
|
||||||
text-decoration: underline; /* 2 */
|
text-decoration: underline; /* 2 */
|
||||||
text-decoration: underline dotted; /* 2 */
|
text-decoration: underline dotted; /* 2 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -94,7 +94,7 @@ abbr[title] {
|
|||||||
|
|
||||||
b,
|
b,
|
||||||
strong {
|
strong {
|
||||||
font-weight: bolder;
|
font-weight: bolder;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -105,8 +105,8 @@ strong {
|
|||||||
code,
|
code,
|
||||||
kbd,
|
kbd,
|
||||||
samp {
|
samp {
|
||||||
font-family: monospace, monospace; /* 1 */
|
font-family: monospace, monospace; /* 1 */
|
||||||
font-size: 1em; /* 2 */
|
font-size: 1em; /* 2 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -114,7 +114,7 @@ samp {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
small {
|
small {
|
||||||
font-size: 80%;
|
font-size: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -124,18 +124,18 @@ small {
|
|||||||
|
|
||||||
sub,
|
sub,
|
||||||
sup {
|
sup {
|
||||||
font-size: 75%;
|
font-size: 75%;
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
vertical-align: baseline;
|
vertical-align: baseline;
|
||||||
}
|
}
|
||||||
|
|
||||||
sub {
|
sub {
|
||||||
bottom: -0.25em;
|
bottom: -0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
sup {
|
sup {
|
||||||
top: -0.5em;
|
top: -0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Embedded content
|
/* Embedded content
|
||||||
@ -146,7 +146,7 @@ sup {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
img {
|
img {
|
||||||
border-style: none;
|
border-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Forms
|
/* Forms
|
||||||
@ -162,10 +162,10 @@ input,
|
|||||||
optgroup,
|
optgroup,
|
||||||
select,
|
select,
|
||||||
textarea {
|
textarea {
|
||||||
font-family: inherit; /* 1 */
|
font-family: inherit; /* 1 */
|
||||||
font-size: 100%; /* 1 */
|
font-size: 100%; /* 1 */
|
||||||
line-height: 1.15; /* 1 */
|
line-height: 1.15; /* 1 */
|
||||||
margin: 0; /* 2 */
|
margin: 0; /* 2 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -174,8 +174,9 @@ textarea {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
button,
|
button,
|
||||||
input { /* 1 */
|
input {
|
||||||
overflow: visible;
|
/* 1 */
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -184,8 +185,9 @@ input { /* 1 */
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
button,
|
button,
|
||||||
select { /* 1 */
|
select {
|
||||||
text-transform: none;
|
/* 1 */
|
||||||
|
text-transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -196,7 +198,7 @@ button,
|
|||||||
[type="button"],
|
[type="button"],
|
||||||
[type="reset"],
|
[type="reset"],
|
||||||
[type="submit"] {
|
[type="submit"] {
|
||||||
-webkit-appearance: button;
|
-webkit-appearance: button;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -207,8 +209,8 @@ button::-moz-focus-inner,
|
|||||||
[type="button"]::-moz-focus-inner,
|
[type="button"]::-moz-focus-inner,
|
||||||
[type="reset"]::-moz-focus-inner,
|
[type="reset"]::-moz-focus-inner,
|
||||||
[type="submit"]::-moz-focus-inner {
|
[type="submit"]::-moz-focus-inner {
|
||||||
border-style: none;
|
border-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -219,7 +221,7 @@ button:-moz-focusring,
|
|||||||
[type="button"]:-moz-focusring,
|
[type="button"]:-moz-focusring,
|
||||||
[type="reset"]:-moz-focusring,
|
[type="reset"]:-moz-focusring,
|
||||||
[type="submit"]:-moz-focusring {
|
[type="submit"]:-moz-focusring {
|
||||||
outline: 1px dotted ButtonText;
|
outline: 1px dotted ButtonText;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -227,7 +229,7 @@ button:-moz-focusring,
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
fieldset {
|
fieldset {
|
||||||
padding: 0.35em 0.75em 0.625em;
|
padding: 0.35em 0.75em 0.625em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -238,12 +240,12 @@ fieldset {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
legend {
|
legend {
|
||||||
box-sizing: border-box; /* 1 */
|
box-sizing: border-box; /* 1 */
|
||||||
color: inherit; /* 2 */
|
color: inherit; /* 2 */
|
||||||
display: table; /* 1 */
|
display: table; /* 1 */
|
||||||
max-width: 100%; /* 1 */
|
max-width: 100%; /* 1 */
|
||||||
padding: 0; /* 3 */
|
padding: 0; /* 3 */
|
||||||
white-space: normal; /* 1 */
|
white-space: normal; /* 1 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -251,7 +253,7 @@ legend {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
progress {
|
progress {
|
||||||
vertical-align: baseline;
|
vertical-align: baseline;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -259,7 +261,7 @@ progress {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -269,8 +271,8 @@ textarea {
|
|||||||
|
|
||||||
[type="checkbox"],
|
[type="checkbox"],
|
||||||
[type="radio"] {
|
[type="radio"] {
|
||||||
box-sizing: border-box; /* 1 */
|
box-sizing: border-box; /* 1 */
|
||||||
padding: 0; /* 2 */
|
padding: 0; /* 2 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -279,7 +281,7 @@ textarea {
|
|||||||
|
|
||||||
[type="number"]::-webkit-inner-spin-button,
|
[type="number"]::-webkit-inner-spin-button,
|
||||||
[type="number"]::-webkit-outer-spin-button {
|
[type="number"]::-webkit-outer-spin-button {
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -288,8 +290,8 @@ textarea {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
[type="search"] {
|
[type="search"] {
|
||||||
-webkit-appearance: textfield; /* 1 */
|
-webkit-appearance: textfield; /* 1 */
|
||||||
outline-offset: -2px; /* 2 */
|
outline-offset: -2px; /* 2 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -297,7 +299,7 @@ textarea {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
[type="search"]::-webkit-search-decoration {
|
[type="search"]::-webkit-search-decoration {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -306,8 +308,8 @@ textarea {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
::-webkit-file-upload-button {
|
::-webkit-file-upload-button {
|
||||||
-webkit-appearance: button; /* 1 */
|
-webkit-appearance: button; /* 1 */
|
||||||
font: inherit; /* 2 */
|
font: inherit; /* 2 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Interactive
|
/* Interactive
|
||||||
@ -318,7 +320,7 @@ textarea {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
details {
|
details {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -326,7 +328,7 @@ details {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
summary {
|
summary {
|
||||||
display: list-item;
|
display: list-item;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Misc
|
/* Misc
|
||||||
@ -337,7 +339,7 @@ summary {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
template {
|
template {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -345,5 +347,5 @@ template {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
[hidden] {
|
[hidden] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -1,97 +1,67 @@
|
|||||||
/*
|
:root {
|
||||||
Atom One color scheme by Daniel Gamage
|
--solarized-base01: #586e75;
|
||||||
Modified to use colors from CSS vars, defined in theme.scss
|
--solarized-base00: #657b83;
|
||||||
*/
|
/* --solarized-base0: #839496; */
|
||||||
|
--solarized-base1: #93a1a1;
|
||||||
|
--solarized-base2: #eee8d5;
|
||||||
|
--solarized-base3: #fdf6e3;
|
||||||
|
--solarized-yellow: #b58900;
|
||||||
|
--solarized-orange: #cb4b16;
|
||||||
|
--solarized-red: #dc322f;
|
||||||
|
--solarized-blue: #268bd2;
|
||||||
|
--solarized-cyan: #2aa198;
|
||||||
|
--solarized-green: #859900;
|
||||||
|
}
|
||||||
|
|
||||||
.highlight {
|
.highlight {
|
||||||
display: block;
|
display: block;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
color: var(--atom-mono-1);
|
color: var(--solarized-base01);
|
||||||
background: var(--atom-base);
|
// darkened base2
|
||||||
|
background: darken(#eee8d5, 5%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlight > code {
|
.highlight > code {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hl-cmt,
|
.hl-cmt {
|
||||||
.hljs-quote {
|
color: var(--solarized-base00);
|
||||||
color: var(--atom-mono-3);
|
font-style: italic;
|
||||||
font-style: italic;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hljs-doctag,
|
|
||||||
.hl-kw,
|
.hl-kw,
|
||||||
.hljs-formula,
|
|
||||||
.hl-const {
|
.hl-const {
|
||||||
color: var(--atom-hue-3);
|
color: var(--solarized-green);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hljs-section,
|
|
||||||
.hljs-name,
|
|
||||||
.hljs-selector-tag,
|
|
||||||
.hljs-deletion,
|
|
||||||
.hljs-subst,
|
|
||||||
.hl-punct-sp,
|
.hl-punct-sp,
|
||||||
.hl-tag {
|
.hl-tag {
|
||||||
color: var(--atom-hue-5);
|
color: var(--solarized-blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hl-emb {
|
.hl-emb {
|
||||||
color: var(--atom-mono-1);
|
color: var(--solarized-base01);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hljs-literal,
|
|
||||||
.hl-attr,
|
.hl-attr,
|
||||||
.hl-mod,
|
.hl-mod,
|
||||||
.hl-key {
|
.hl-key,
|
||||||
color: var(--atom-hue-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hl-str,
|
|
||||||
.hljs-regexp,
|
|
||||||
.hljs-addition,
|
|
||||||
.hljs-meta-string {
|
|
||||||
color: var(--atom-hue-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hl-prop {
|
.hl-prop {
|
||||||
color: var(--atom-hue-1);
|
color: var(--solarized-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hl-str {
|
||||||
|
color: var(--solarized-cyan);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hl-builtin,
|
.hl-builtin,
|
||||||
.hljs-class .hljs-title {
|
|
||||||
color: var(--atom-hue-6-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-attr,
|
|
||||||
.hljs-template-variable,
|
|
||||||
.hl-type,
|
.hl-type,
|
||||||
.hljs-selector-class,
|
|
||||||
.hljs-selector-attr,
|
|
||||||
.hljs-selector-pseudo,
|
|
||||||
.hl-num {
|
.hl-num {
|
||||||
color: var(--atom-hue-6);
|
color: var(--solarized-yellow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hljs-symbol,
|
|
||||||
.hljs-bullet,
|
|
||||||
.hljs-link,
|
|
||||||
.hljs-meta,
|
|
||||||
.hljs-selector-id,
|
|
||||||
.hl-fn {
|
.hl-fn {
|
||||||
color: var(--atom-hue-2);
|
color: var(--solarized-orange);
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-emphasis {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-strong {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-link {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
}
|
||||||
|
37
site/elsewhere.html
Normal file
37
site/elsewhere.html
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{% extends "default" %}
|
||||||
|
|
||||||
|
{% block titlevariable %}
|
||||||
|
{% set title = "Elsewhere" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content -%}
|
||||||
|
|
||||||
|
<h1 class="headline">Elsewhere</h1>
|
||||||
|
|
||||||
|
<div class="body-content">
|
||||||
|
<p>A non-exhaustive list of other places I can be found on the internet.</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://bsky.app/profile/shadowfacts.net">Bluesky</a> (read-only)</li>
|
||||||
|
<li><a href="https://legacy.curseforge.com/members/shadowfactsdev/projects">CurseForge</a> (inactive)</li>
|
||||||
|
<li><a href="mailto:me@shadowfacts.net">Email</a></li>
|
||||||
|
<li><a href="https://github.com/shadowfacts">GitHub</a></li>
|
||||||
|
<li><a href="https://git.shadowfacts.net/shadowfacts">Gitea</a></li>
|
||||||
|
<li><a href="https://news.ycombinator.com/user?id=shadowfacts">“Hacker” “News”</a></li>
|
||||||
|
<li>
|
||||||
|
<a href="https://letterboxd.com/shadowfacts/">Letterboxd</a>
|
||||||
|
(pronounced like it's a <a href="https://en.wikipedia.org/wiki/Daemon_(computing)">daemon</a>)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://social.shadowfacts.net/users/shadowfacts" rel="me">Mastodon</a>
|
||||||
|
(technically the fediverse)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://social.shadowfacts.net/users/tusker" rel="me">Mastodon</a>
|
||||||
|
(for <a href="https://vaccor.space/tusker/">Tusker</a> announcements)
|
||||||
|
</li>
|
||||||
|
<li><a href="https://www.reddit.com/user/shadowfactsdev">Reddit</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -1,26 +0,0 @@
|
|||||||
<article itemscope itemtype="https://schema.org/BlogPosting">
|
|
||||||
<h2 class="article-title" itemprop="headline">
|
|
||||||
<a href="{{ post.permalink() }}" itemprop="url mainEntityOfPage">
|
|
||||||
{% match post.metadata.html_title %}
|
|
||||||
{% when Some with (html) %}
|
|
||||||
{{ html|safe }}
|
|
||||||
{% when None %}
|
|
||||||
{{ post.metadata.title }}
|
|
||||||
{% endmatch %}
|
|
||||||
</a>
|
|
||||||
</h2>
|
|
||||||
{% include "includes/article-meta.html" %}
|
|
||||||
<div class="article-content" itemprop="description">
|
|
||||||
{% match post.excerpt %}
|
|
||||||
{% when Some with (excerpt) %}
|
|
||||||
{{ excerpt|safe }}
|
|
||||||
{% when None %}
|
|
||||||
{{ post.content.html()|safe }}
|
|
||||||
{% endmatch %}
|
|
||||||
</div>
|
|
||||||
{% if post.excerpt.is_some() %}
|
|
||||||
<p class="read-more-link">
|
|
||||||
<a href="{{ post.permalink() }}">Read more...</a>
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
</article>
|
|
@ -1,27 +0,0 @@
|
|||||||
<p class="article-meta">
|
|
||||||
<span itemprop="author" itemscope="" itemtype="https://schema.org/Person">
|
|
||||||
<meta itemprop="name" content="Shadowfacts">
|
|
||||||
</span>
|
|
||||||
on
|
|
||||||
<span>
|
|
||||||
<time itemprop="datePublished" datetime="{{ post.metadata.date|iso_datetime }}" title="{{ post.metadata.date|pretty_datetime }}">
|
|
||||||
{{ post.metadata.date|pretty_date }}
|
|
||||||
</time>
|
|
||||||
</span>
|
|
||||||
{% match post.metadata.tags %}
|
|
||||||
{% when Some with (tags) %}
|
|
||||||
in
|
|
||||||
{% for tag in tags %}
|
|
||||||
<span itemprop="articleSection">
|
|
||||||
<a href="/{{ tag.slug }}/">{{ tag.name }}</a>{% if !loop.last %},{% endif %}
|
|
||||||
</span>
|
|
||||||
{% endfor %}
|
|
||||||
{% when None %}
|
|
||||||
{% endmatch %}
|
|
||||||
{% match post.word_count %}
|
|
||||||
{% when Some with (wc) %}
|
|
||||||
•
|
|
||||||
<span title="{{ wc }} words">{{ wc|reading_time }} min read</span>
|
|
||||||
{% when None %}
|
|
||||||
{% endmatch %}
|
|
||||||
</p>
|
|
@ -1,29 +0,0 @@
|
|||||||
<div class="pagination" role="navigation">
|
|
||||||
<p>
|
|
||||||
<span class="pagination-link">
|
|
||||||
{% match pagination_info.prev_href %}
|
|
||||||
{% when Some with (href) %}
|
|
||||||
<a href="{{ href }}">
|
|
||||||
<span class="arrow arrow-left" aria-hidden="true"></span>
|
|
||||||
<span>Previous</span>
|
|
||||||
</a>
|
|
||||||
{% when None %}
|
|
||||||
<span class="arrow arrow-left" aria-hidden="true"></span>
|
|
||||||
<span>Previous</span>
|
|
||||||
{% endmatch %}
|
|
||||||
</span>
|
|
||||||
Page {{ pagination_info.page }} of {{ pagination_info.total_pages }}
|
|
||||||
<span class="pagination-link">
|
|
||||||
{% match pagination_info.next_href %}
|
|
||||||
{% when Some with (href) %}
|
|
||||||
<a href="{{ href }}">
|
|
||||||
<span>Next</span>
|
|
||||||
<span class="arrow arrow-right" aria-hidden="true"></span>
|
|
||||||
</a>
|
|
||||||
{% when None %}
|
|
||||||
<span>Next</span>
|
|
||||||
<span class="arrow arrow-right" aria-hidden="true"></span>
|
|
||||||
{% endmatch %}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
@ -1,13 +1,71 @@
|
|||||||
{% extends "layout/default.html" %}
|
{% extends "default" %}
|
||||||
|
|
||||||
{% block title %}Shadowfacts{% endblock %}
|
{% block footer_vars %}
|
||||||
|
{% set footer_links = false %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content -%}
|
{% block content -%}
|
||||||
|
|
||||||
{% for post in posts %}
|
<h2 class="headline">About Me</h2>
|
||||||
{% include "includes/article-listing.html" %}
|
<div class="body-content">
|
||||||
{% endfor %}
|
<p class="about">
|
||||||
|
Hi.
|
||||||
|
My day job is building software for people who pay me to build software.
|
||||||
|
In the evenings, I build software for people who don’t pay me to build software (myself included).
|
||||||
|
I mostly write about building software on here.
|
||||||
|
That probably doesn’t come as a shock.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% include "includes/pagination.html" %}
|
<h2 class="headline">
|
||||||
|
Latest Post:
|
||||||
|
<a href="{{ latest_post_permalink }}">
|
||||||
|
{{ latest_post.metadata.title }}
|
||||||
|
</a>
|
||||||
|
</h2>
|
||||||
|
<p class="article-meta">
|
||||||
|
Published on
|
||||||
|
<time itemprop="datePublished" datetime="{{ latest_post.metadata.date | iso_datetime }}" title="{{ latest_post.metadata.date | pretty_datetime }}">
|
||||||
|
{{ latest_post.metadata.date | pretty_date }},
|
||||||
|
</time>
|
||||||
|
in
|
||||||
|
{% for tag in latest_post.metadata.tags %}
|
||||||
|
<span itemprop="articleSection">
|
||||||
|
<a href="/{{ tag.slug }}/">{{ tag.name }}</a>{% if loop.last %}.{% else %},{% endif %}
|
||||||
|
</span>
|
||||||
|
{% endfor %}
|
||||||
|
<span title="{{ latest_post.word_count }} word{% if latest_post.word_count != 1 %}s{% endif %}">
|
||||||
|
{{ latest_post.word_count | reading_time }} minute read.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<div class="body-content">
|
||||||
|
{% if latest_post.excerpt %}
|
||||||
|
{{ latest_post.excerpt }}
|
||||||
|
<p>
|
||||||
|
<a href="{{ latest_post_permalink }}" class="italic">Read more…</a>
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
{{ latest_post_content }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="headline">Other Things</h2>
|
||||||
|
|
||||||
{%- endblock %}
|
{%- endblock %}
|
||||||
|
|
||||||
|
{% block footer_links %}
|
||||||
|
{% set additional_links = [
|
||||||
|
"TV Commentary", "/tv/",
|
||||||
|
"Modding Tutorials", "/tutorials/",
|
||||||
|
] %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block after_footer_links %}
|
||||||
|
<p>
|
||||||
|
<span class="webring">
|
||||||
|
<a href="https://metro.bieszczady.pl" class="no-external-link-decoration">Metro Bieszczady Webring</a>
|
||||||
|
<a title="previous page in webring" href="https://metro.bieszczady.pl/cgi-bin/webring?action=previous&from=shadowfacts" class="no-external-link-decoration">←</a>
|
||||||
|
<a title="next page in webring" href="https://metro.bieszczady.pl/cgi-bin/webring?action=next&from=shadowfacts" class="no-external-link-decoration">→</a>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
||||||
|
@ -1,77 +1,82 @@
|
|||||||
{% extends "layout/default.html" %}
|
{% extends "default" %}
|
||||||
|
|
||||||
|
{% block titlevariable %}
|
||||||
|
{% set title = metadata.title %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block head -%}
|
{% block head -%}
|
||||||
|
|
||||||
{% match post.metadata.short_desc %}
|
<meta property="og:type" content="article">
|
||||||
{% when Some with (val) %}
|
{% if metadata.short_desc %}
|
||||||
<meta property="og:type" content="article">
|
<meta property="og:description" content="{{ metadata.short_desc }}">
|
||||||
<meta property="og:description" content="{{ val }}">
|
{% else %}
|
||||||
{% when None %}
|
<meta property="og:description" content="The outer part of a shadow is called the penumbra.">
|
||||||
<meta property="og:type" content="website">
|
{% endif %}
|
||||||
<meta property="og:description" content="The outer part of a shadow is called the penumbra.">
|
|
||||||
{% endmatch %}
|
|
||||||
|
|
||||||
{%- endblock %}
|
{%- endblock %}
|
||||||
|
|
||||||
{% block image %}
|
{% block image %}
|
||||||
{% match post.metadata.card_image_path %}
|
{% if metadata.card_image_path %}
|
||||||
{% when Some with (path) %}
|
<meta property="twitter:image" content="https://{{ _domain }}{{ metadata.card_image_path }}">
|
||||||
<meta property="twitter:image" content="https://{{ Self::domain() }}{{ path }}">
|
<meta property="og:image" content="https://{{ _domain }}{{ metadata.card_image_path }}">
|
||||||
<meta property="og:image" content="https://{{ Self::domain() }}{{ path }}">
|
{% else %}
|
||||||
{% when None %}
|
<meta property="twitter:image" content="https://{{ _domain }}/shadowfacts.png">
|
||||||
<meta property="twitter:image" content="https://{{ Self::domain() }}/shadowfacts.png">
|
<meta property="og:image" content="https://{{ _domain }}/shadowfacts.png">
|
||||||
<meta property="og:image" content="https://{{ Self::domain() }}/shadowfacts.png">
|
{% endif %}
|
||||||
{% endmatch %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block title %}{{ post.metadata.title }}{% endblock %}
|
|
||||||
|
|
||||||
{% block content -%}
|
{% block content -%}
|
||||||
|
|
||||||
<article itemprop="blogPost" itemscope itematype="https://schema.org/BlogPosting">
|
<article itemprop="blogPost" itemscope itemtype="https://schema.org/BlogPosting">
|
||||||
<meta itemprop="mainEntityOfPage" content="https://{{ Self::domain() }}{{ self.permalink() }}">
|
<meta itemprop="mainEntityOfPage" content="https://{{ _domain }}{{ _permalink }}">
|
||||||
<h1 class="article-title" itemprop="name headline">
|
<h1 class="headline" itemprop="name headline">
|
||||||
{% match post.metadata.html_title %}
|
{% if metadata.html_title %}
|
||||||
{% when Some with (html) %}
|
{{ metadata.html_title }}
|
||||||
{{ html|safe }}
|
{% else %}
|
||||||
{% when None %}
|
{{ metadata.title }}
|
||||||
{{ post.metadata.title }}
|
{% endif %}
|
||||||
{% endmatch %}
|
</h1>
|
||||||
</h1>
|
<p class="article-meta">
|
||||||
{% include "includes/article-meta.html" %}
|
Published on
|
||||||
<div class="article-content" itemprop="articleBody">
|
<time itemprop="datePublished" datetime="{{ metadata.date | iso_datetime }}" title="{{ metadata.date | pretty_datetime }}">
|
||||||
{% match post.metadata.preamble %}
|
{{ metadata.date | pretty_date }},
|
||||||
{% when Some with (html) %}
|
</time>
|
||||||
{{ html|safe }}
|
in
|
||||||
{% when None %}
|
{% for tag in metadata.tags %}
|
||||||
{% endmatch %}
|
<span itemprop="articleSection">
|
||||||
{{ post.content.html()|safe }}
|
<a href="/{{ tag.slug }}/">{{ tag.name }}</a>{% if loop.last %}.{% else %},{% endif %}
|
||||||
</div>
|
</span>
|
||||||
<details id="comments-container">
|
{% endfor %}
|
||||||
<summary>
|
<span title="{{ word_count }} word{% if word_count != 1 %}s{% endif %}">
|
||||||
<h2 id="comments-container-title">Comments</h2>
|
{{ word_count | reading_time }} minute read.
|
||||||
</summary>
|
</span>
|
||||||
<p class="comments-info">
|
</p>
|
||||||
Comments powered by ActivityPub. To respond to this post, enter your username and instance below, or copy its URL into the search interface for client for Mastodon, Pleroma, or other compatible software.
|
<div class="body-content" itemprop="articleBody">
|
||||||
<a href="/2019/reincarnation/#activity-pub">Learn more</a>.
|
{% if metadata.preamble %}
|
||||||
</p>
|
{{ metadata.preamble }}
|
||||||
<form id="remote-interact" action="/interact" method="POST">
|
{% endif %}
|
||||||
<span>Reply from your instance:</span>
|
{{ content }}
|
||||||
<input type="hidden" name="permalink" value="{{ post.comments_permalink() }}">
|
</div>
|
||||||
<!-- name needs to be exactly this to get the browser to use the same completions as mastodon -->
|
|
||||||
<input type="text" placeholder="Enter your user@domain" required id="acct" name="remote_follow[acct]">
|
|
||||||
<input type="submit" value="Interact">
|
|
||||||
</form>
|
|
||||||
<noscript>
|
|
||||||
<p id="comments-js-warning">JavaScript is required to display comments.</p>
|
|
||||||
</noscript>
|
|
||||||
</details>
|
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
{% if metadata.comments_post_id %}
|
||||||
|
<hr>
|
||||||
<script>
|
<script>
|
||||||
const articleDomain = "{{ Self::domain() }}";
|
const commentsPostID = "{{ metadata.comments_post_id }}";
|
||||||
const articlePermalink = "{{ post.comments_permalink() }}";
|
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/comments.js?{{ Self::stylesheet_cache_buster() }}" async></script>
|
<details id="comments-container">
|
||||||
|
<summary><h2>Comments</h2></summary>
|
||||||
|
<p class="italic">Reply to this post <a href="https://social.shadowfacts.net/notice/{{ metadata.comments_post_id }}" target="_blank">via the Fediverse</a>.</p>
|
||||||
|
<div id="comments-list"></div>
|
||||||
|
<noscript>
|
||||||
|
<aside class="inline">
|
||||||
|
<p>
|
||||||
|
Comments cannot be shown inline since you have JavaScript disabled.
|
||||||
|
</p>
|
||||||
|
</aside>
|
||||||
|
</noscript>
|
||||||
|
</details>
|
||||||
|
<script src="/js/comments.js?{{ _stylesheet_cache_buster }}" async></script>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{%- endblock %}
|
{%- endblock %}
|
||||||
|
@ -1,128 +1,90 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width">
|
<meta name="viewport" content="width=device-width">
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
|
||||||
<title>{% block title %}Shadowfacts{% endblock %}</title>
|
{% block titlevariable %}
|
||||||
|
{% set title = "Shadowfacts" %}
|
||||||
|
{% endblock %}
|
||||||
|
<title>{{ title }}</title>
|
||||||
|
|
||||||
<link rel="cannonical" href="https://{{ Self::domain() }}{{ self.permalink() }}">
|
<link rel="cannonical" href="https://{{ _domain }}{{ _permalink }}">
|
||||||
<link rel="alternate" type="application/rss+xml" title="Shadowfacts" href="https://{{ Self::domain() }}/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="Shadowfacts" href="https://{{ _domain }}/feed.xml">
|
||||||
|
|
||||||
<link rel="icon" href="/favicon.ico">
|
<link rel="icon" href="/favicon.ico">
|
||||||
<link rel="apple-touch-icon-precomposed" href="/favicon-152.png">
|
<link rel="apple-touch-icon-precomposed" href="/favicon-152.png">
|
||||||
<meta name="msapplication-TileColor" content="#F9C72F">
|
<meta name="msapplication-TileColor" content="#F9C72F">
|
||||||
<meta name="msapplication-TileImage" content="/favicon-152.png">
|
<meta name="msapplication-TileImage" content="/favicon-152.png">
|
||||||
|
|
||||||
<meta name="twitter:card" content="summary">
|
<meta name="twitter:card" content="summary">
|
||||||
<meta name="twitter:creator" content="@ShadowfactsDev">
|
<meta property="og:title" content="{{ title }}">
|
||||||
<meta property="og:title" content="{% block title %}{% endblock %}">
|
{% block image %}
|
||||||
{% block image %}
|
<meta property="twitter:image" content="https://{{ _domain }}/shadowfacts.png">
|
||||||
<meta property="twitter:image" content="https://{{ Self::domain() }}shadowfacts.png">
|
<meta property="og:image" content="https://{{ _domain }}/shadowfacts.png">
|
||||||
<meta property="og:image" content="https://{{ Self::domain() }}shadowfacts.png">
|
{% endblock %}
|
||||||
{% endblock %}
|
<meta property="og:url" content="https://{{ _domain }}{{ _permalink }}">
|
||||||
<meta property="og:url" content="https://{{ Self::domain() }}{{ self.permalink() }}">
|
<meta property="og:site_name" content="Shadowfacts">
|
||||||
<meta property="og:site_name" content="Shadowfacts">
|
<meta name="fediverse:creator" content="@shadowfacts@social.shadowfacts.net">
|
||||||
|
|
||||||
{% block head %}{% endblock %}
|
{% block head %}{% endblock %}
|
||||||
|
|
||||||
<script>
|
<link rel="stylesheet" href="/css/main.css?{{ _stylesheet_cache_buster }}">
|
||||||
(() => {
|
|
||||||
const theme = localStorage.getItem("theme") || "auto";
|
|
||||||
document.write(`<link rel="stylesheet" href="/css/${theme}.css?{{ Self::stylesheet_cache_buster() }}">`);
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
<noscript>
|
|
||||||
<link rel="stylesheet" href="/css/auto.css?{{ Self::stylesheet_cache_buster() }}">
|
|
||||||
</noscript>
|
|
||||||
</head>
|
</head>
|
||||||
<body itemscope itemtype="https://schema.org/Blog">
|
<body itemscope itemtype="https://schema.org/Blog">
|
||||||
<header class="site-header container">
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<h1 class="site-title">{{ Self::fancy_link("Shadowfacts", "/", None)|safe }}</h1>
|
|
||||||
<p class="site-description">The outer part of a shadow is called the penumbra.</p>
|
|
||||||
</div>
|
|
||||||
<nav class="site-nav" role="navigation">
|
|
||||||
<ul>
|
|
||||||
<li>{{ Self::fancy_link("Archive", "/archive", None)|safe }}</li>
|
|
||||||
<li>{{ Self::fancy_link("Tutorials", "/tutorials", None)|safe }}</li>
|
|
||||||
<li>{{ Self::fancy_link("TV", "/tv", None)|safe }}</li>
|
|
||||||
<li>
|
|
||||||
<a href="#" class="dropdown-link" aria-haspopup="true">Other <span class="arrow arrow-down" aria-hidden="true"></span></a>
|
|
||||||
<ul aria-label="Other links">
|
|
||||||
<li>{{ Self::fancy_link("Gitea", "https://git.shadowfacts.net", None)|safe }}</li>
|
|
||||||
<li>{{ Self::fancy_link("RTFM", "https://rtfm.shadowfacts.net", None)|safe }}</li>
|
|
||||||
<li>{{ Self::fancy_link("Maven", "https://maven.shadowfacts.net", None)|safe }}</li>
|
|
||||||
<li>{{ Self::fancy_link("Type", "https://type.shadowfacts.net", None)|safe }}</li>
|
|
||||||
<li>{{ Self::fancy_link("Meme Machine", "https://mememachine.shadowfacts.net", None)|safe }}</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="container main" role="main">
|
<header>
|
||||||
{% block content %}{% endblock %}
|
<div class="container">
|
||||||
</div>
|
<h1><a href="/">Shadowfacts</a></h1>
|
||||||
|
<p id="epigraph">The outer part of a shadow is called the penumbra.</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<footer class="site-footer container">
|
<main>
|
||||||
<!-- <h2 class="site-title">Shadowfacts</h2> -->
|
<div class="container">
|
||||||
<span class="ui-controls">
|
{% block content %}{% endblock %}
|
||||||
Theme:
|
|
||||||
<input type="radio" name="theme" id="auto" value="auto">
|
|
||||||
<label for="auto">auto</label>
|
|
||||||
<input type="radio" name="theme" id="light" value="light">
|
|
||||||
<label for="light">light</label>
|
|
||||||
<input type="radio" name="theme" id="dark" value="dark">
|
|
||||||
<label for="dark">dark</label>
|
|
||||||
</span>
|
|
||||||
<span class="site-colophon">
|
|
||||||
Generated on {{ Self::generated_at() | pretty_date }} by <a href="https://git.shadowfacts.net/shadowfacts/v6">v6</a>.
|
|
||||||
<br>
|
|
||||||
<img src="/buttons/vi.png" alt="Made with vi" />
|
|
||||||
<img src="/buttons/rust.png" alt="Powered by Rust" />
|
|
||||||
<img src="/buttons/html.png" alt="HTML: Learn it today!" />
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<nav class="links">
|
{% if _permalink != "/" %}
|
||||||
<ul>
|
<hr>
|
||||||
<li>{{ Self::fancy_link("Email", "mailto:me@shadowfacts.net", Some("rel=me"))|safe }}</li>
|
{% endif %}
|
||||||
<li>{{ Self::fancy_link("RSS", "/feed.xml", None)|safe }}</li>
|
</div>
|
||||||
<li>{{ Self::fancy_link("GitHub", "https://github.com/shadowfacts", Some("rel=me"))|safe }}</li>
|
</main>
|
||||||
<li>{{ Self::fancy_link("Mastodon", "https://social.shadowfacts.net/users/shadowfacts", Some("rel=me"))|safe }}</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
<aside class="webring">
|
|
||||||
<a href="https://metro.bieszczady.pl">Metro Bieszczady webring</a>
|
|
||||||
<a title="previous page in webring" href="https://metro.bieszczady.pl/cgi-bin/webring?action=previous&from=shadowfacts">←</a>
|
|
||||||
<a title="next page in webring" href="https://metro.bieszczady.pl/cgi-bin/webring?action=next&from=shadowfacts">→</a>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<script>
|
<footer>
|
||||||
(() => {
|
<div class="container">
|
||||||
const theme = localStorage.getItem("theme") || "auto";
|
<ul>
|
||||||
document.getElementsByName("theme").forEach((el) => {
|
{% set footer_links = [
|
||||||
el.checked = theme === el.value;
|
"Archive", "/archive/",
|
||||||
el.onchange = () => {
|
"Colophon", "/colophon/",
|
||||||
localStorage.setItem("theme", el.value);
|
"Contact", "/elsewhere/"
|
||||||
window.location.reload();
|
] %}
|
||||||
};
|
{% block footer_links %}
|
||||||
});
|
{% set additional_links = [] %}
|
||||||
})();
|
{% endblock %}
|
||||||
</script>
|
{% set sorted_links = footer_links | concat(with=additional_links) | zip | sort(attribute="0") %}
|
||||||
<noscript>
|
{% for link in sorted_links %}
|
||||||
<style>
|
<li><a href="{{ link.1 }}">{{ link.0 }}</a></li>
|
||||||
.ui-controls {
|
{% endfor %}
|
||||||
display: none;
|
</ul>
|
||||||
}
|
{% block after_footer_links %}
|
||||||
</style>
|
{% endblock %}
|
||||||
</noscript>
|
<p id="generated-on">Generated on {{ _generated_at | pretty_date }}, by <a href="https://git.shadowfacts.net/shadowfacts/v7">v7</a>.</p>
|
||||||
</footer>
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
<script data-goatcounter="https://shadowfacts.goatcounter.com/count" async src="//gc.zgo.at/count.v3.js" crossorigin="anonymous"></script>
|
<script data-goatcounter="https://shadowfacts.goatcounter.com/count" defer src="//gc.zgo.at/count.v3.js" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
{% if _development %}
|
||||||
|
<script>
|
||||||
|
let ws = new WebSocket("/_dev/live_reload");
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
if (event.data == "regenerated") {
|
||||||
|
ws.close();
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
41
site/layout/show.html
Normal file
41
site/layout/show.html
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{% extends "default" %}
|
||||||
|
|
||||||
|
{% block titlevariable %}
|
||||||
|
{% set title = show.metadata.title %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content -%}
|
||||||
|
|
||||||
|
<h1 class="headline">{{ show.metadata.title }}</h1>
|
||||||
|
<p class="article-meta">
|
||||||
|
{{ show.episodes | length }}
|
||||||
|
entr{% if show.episodes | length == 1 %}y{% else %}ies{% endif %}.
|
||||||
|
Last updated on
|
||||||
|
<time datetime="{{ show.last_updated | iso_datetime }}">
|
||||||
|
{{ show.last_updated | pretty_date }}.
|
||||||
|
</time>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button id="expand-all" onclick="document.querySelectorAll('summary').forEach(e => e.click())">Expand All</button>
|
||||||
|
<noscript>
|
||||||
|
<style>#expand-all { display: none; }</style>
|
||||||
|
</noscript>
|
||||||
|
|
||||||
|
{% for episode in show.episodes %}
|
||||||
|
<details class="tv-show-entry">
|
||||||
|
<summary>
|
||||||
|
<h2 class="headline">{{ episode.title }}</h2>
|
||||||
|
<span class="article-meta">
|
||||||
|
Watched:
|
||||||
|
<time datetime="{{ episode.date | iso_datetime }}">
|
||||||
|
{{ episode.date | pretty_date }}
|
||||||
|
</time>
|
||||||
|
</span>
|
||||||
|
</summary>
|
||||||
|
<div class="body-content">
|
||||||
|
{{ episode.content }}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{%- endblock %}
|
31
site/layout/tag.html
Normal file
31
site/layout/tag.html
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{% extends "default" %}
|
||||||
|
|
||||||
|
{% block titlevariable %}
|
||||||
|
{% set title = tag_name ~ " posts" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content -%}
|
||||||
|
|
||||||
|
<h1>Posts tagged ‘{{ tag_name }}’</h1>
|
||||||
|
|
||||||
|
{% for year in years %}
|
||||||
|
<div class="archive-list">
|
||||||
|
{% for entry in posts_by_year[year] %}
|
||||||
|
<code>
|
||||||
|
<time datetime="{{ entry.date | iso_datetime }}" title="{{ entry.date | pretty_datetime }}">
|
||||||
|
{{ entry.date | iso_date }}
|
||||||
|
</time>
|
||||||
|
</code>
|
||||||
|
<div>
|
||||||
|
<a href="{{ entry.permalink }}">
|
||||||
|
{{ entry.title }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% if not loop.last %}
|
||||||
|
<hr>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{%- endblock %}
|
@ -1,21 +0,0 @@
|
|||||||
{% extends "layout/default.html" %}
|
|
||||||
|
|
||||||
{% block title%}{{ series.name }}{% endblock %}
|
|
||||||
|
|
||||||
{% block content -%}
|
|
||||||
|
|
||||||
<h1 class="page-heading">{{ series.name }}</h1>
|
|
||||||
|
|
||||||
{% for post in series.posts %}
|
|
||||||
<article>
|
|
||||||
<h2 class="article-title">
|
|
||||||
<a href="/tutorials/{{ series.slug }}/{{ post.slug }}/">
|
|
||||||
{{ post.metadata.title }}
|
|
||||||
</a>
|
|
||||||
</h2>
|
|
||||||
{% include "includes/article-meta.html" %}
|
|
||||||
</article>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{%- endblock %}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
|||||||
{% extends "layout/default.html" %}
|
|
||||||
|
|
||||||
{% block title%}{{ post.metadata.title }}{% endblock %}
|
|
||||||
|
|
||||||
{% block content -%}
|
|
||||||
|
|
||||||
<article itemprop="blogPost" itemscope="" itemtype="https://schema.org/BlogPosting">
|
|
||||||
<meta itemprop="mainEntityOfPage" content="https://{{ Self::domain() }}{{ self.permalink() }}">
|
|
||||||
<h1 class="article-title" itemprop="name headline">
|
|
||||||
{{ post.metadata.title }}
|
|
||||||
</h1>
|
|
||||||
{% include "includes/article-meta.html" %}
|
|
||||||
<div class="article-content" itemprop="articleBody">
|
|
||||||
{{ post.content|safe }}
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
{%- endblock %}
|
|
||||||
|
|
||||||
|
|
27
site/layout/tutorial_post.html
Normal file
27
site/layout/tutorial_post.html
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{% extends "default" %}
|
||||||
|
|
||||||
|
{% block titlevariable %}
|
||||||
|
{% set title = post.metadata.title ~ " | " ~ post.series_name %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content -%}
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<h1 class="headline">{{ post.metadata.title }}</h1>
|
||||||
|
<p class="article-meta">
|
||||||
|
Published on
|
||||||
|
<time datetime="{{ post.metadata.date | iso_datetime }}">
|
||||||
|
{{ post.metadata.date | pretty_date }},
|
||||||
|
</time>
|
||||||
|
in
|
||||||
|
<a href="/tutorials/{{ post.series_slug }}/">{{ post.series_name }}</a>.
|
||||||
|
<span title="{{ post.word_count }} word{% if post.word_count != 1 %}s{% endif %}">
|
||||||
|
{{ post.word_count | reading_time }} minute read.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<div class="body-content">
|
||||||
|
{{ post.content }}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{%- endblock %}
|
30
site/layout/tutorial_series.html
Normal file
30
site/layout/tutorial_series.html
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{% extends "default" %}
|
||||||
|
|
||||||
|
{% block titlevariable %}
|
||||||
|
{% set title = series_name %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content -%}
|
||||||
|
|
||||||
|
<h1 class="headline">{{ series_name }}</h1>
|
||||||
|
|
||||||
|
{% for entry in entries %}
|
||||||
|
|
||||||
|
<h2 class="headline">
|
||||||
|
<a href="/tutorials/{{ series_slug }}/{{ entry.slug }}/">
|
||||||
|
{{ entry.title }}
|
||||||
|
</a>
|
||||||
|
</h2>
|
||||||
|
<p class="article-meta">
|
||||||
|
Published on
|
||||||
|
<time datetime="{{ entry.date | iso_datetime }}">
|
||||||
|
{{ entry.date | pretty_date }}.
|
||||||
|
</time>
|
||||||
|
<span title="{{ entry.word_count }} word{% if entry.word_count != 1 %}s{% endif %}">
|
||||||
|
{{ entry.word_count | reading_time }} minute read.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{%- endblock %}
|
@ -4,7 +4,7 @@ tags = ["meta", "activitypub"]
|
|||||||
date = "2019-09-18 10:34:42 -0400"
|
date = "2019-09-18 10:34:42 -0400"
|
||||||
short_desc = "Stand by for reincarnation."
|
short_desc = "Stand by for reincarnation."
|
||||||
old_permalink = "/meta/2019/reincarnation/"
|
old_permalink = "/meta/2019/reincarnation/"
|
||||||
use_old_permalink_for_comments = true
|
comments_post_id = "9n3ZJWQsxI74wRaMzI"
|
||||||
```
|
```
|
||||||
|
|
||||||
<figure>
|
<figure>
|
||||||
@ -42,12 +42,10 @@ The site remains almost entirely JavaScript free. There are two places where cli
|
|||||||
|
|
||||||
## The Backend
|
## The Backend
|
||||||
|
|
||||||
The previous version of my website used [Jekyll](https://jekyllrb.com/) (and WordPress before that, and Jekyll again before that). In what may become a pattern, I've once more switched away from Jekyll. Version Five uses something completely custom. It has been a work-in-progress in one form or another for about a year now. It started out as a Node.js project that was going to be a general-purpose static site generator. Then, around the time I was learning Elixir (which I love, and will be the subject of another blog post), I attempted to rewrite it in that[^3]. Then we finally arrive at the current iteration of the current iteration of my website. In spite of my distaste for the ecosystem[^4], I returned to Node.js. This time, however, the project took a bit of a different direction than the previous two attempts at a rewrite. It has two main parts: the static site generator and the ActivityPub integration.
|
The previous version of my website used [Jekyll](https://jekyllrb.com/) (and WordPress before that, and Jekyll again before that). In what may become a pattern, I've once more switched away from Jekyll. Version Five uses something completely custom. It has been a work-in-progress in one form or another for about a year now. It started out as a Node.js project that was going to be a general-purpose static site generator. Then, around the time I was learning Elixir (which I love, and will be the subject of another blog post), I attempted to rewrite it in that[^3]. Then we finally arrive at the current iteration of the current iteration of my website. In spite of my distaste for the ecosystem (the `package.json` lists 30 dependencies, 13 of which are TypeScript type definitions, yet there are 311 packages in my `node_modules` folder), I returned to Node.js. This time, however, the project took a bit of a different direction than the previous two attempts at a rewrite. It has two main parts: the static site generator and the ActivityPub integration.
|
||||||
|
|
||||||
[^3]: Unfortunately, this attempt ran into some issues fairly quickly. Elixir itself is wonderful, but the package ecosystem for web-related things such as Sass, Markdown rendering, and syntax highlighting, is lackluster.
|
[^3]: Unfortunately, this attempt ran into some issues fairly quickly. Elixir itself is wonderful, but the package ecosystem for web-related things such as Sass, Markdown rendering, and syntax highlighting, is lackluster.
|
||||||
|
|
||||||
[^4]: The `package.json` for the project explicitly lists 30 dependencies, 13 of which are TypeScript type definitions. There are 311 packages in my `node_modules` folder. Enough said.
|
|
||||||
|
|
||||||
### Static Site Generator
|
### Static Site Generator
|
||||||
|
|
||||||
The static site generator is by far the most important piece. Without it, there would be no website. I once again went with an SSG for a couple reasons, starting and ending with performance. When it comes down to it, nothing is generated at request time. Everything exists as static files on disk that are generated when the service starts up. The basic architecture isn't all that special: there are posts written in Markdown, gathered into various collections, rendered to HTML using various page layouts, and then gathered together in various indexes (the main index, category-specific ones, and RSS feeds).
|
The static site generator is by far the most important piece. Without it, there would be no website. I once again went with an SSG for a couple reasons, starting and ending with performance. When it comes down to it, nothing is generated at request time. Everything exists as static files on disk that are generated when the service starts up. The basic architecture isn't all that special: there are posts written in Markdown, gathered into various collections, rendered to HTML using various page layouts, and then gathered together in various indexes (the main index, category-specific ones, and RSS feeds).
|
||||||
@ -76,7 +74,7 @@ If you're interested, here are some of the technical details about how the back
|
|||||||
|
|
||||||
All of the backend stuff is written in Node.js and TypeScript. The SSG piece uses Markdown (with some extensions) for rendering posts, Sass (specifically SCSS) for the styles, and EJS for the templates. The ActivityPub integration uses Postgres with TypeORM to store remote actors and comments. And the web server itself is Express.js.
|
All of the backend stuff is written in Node.js and TypeScript. The SSG piece uses Markdown (with some extensions) for rendering posts, Sass (specifically SCSS) for the styles, and EJS for the templates. The ActivityPub integration uses Postgres with TypeORM to store remote actors and comments. And the web server itself is Express.js.
|
||||||
|
|
||||||
When the program starts, the static site generation is performed before the web server is started to ensure that broken or outdated files aren't served. First, some files are copied over verbatim (such as favicons and the client-side JS for loading comments), the CSS files for the individual themes are compiled from the Sass files, and the error pages are generated. Then, tutorials and blog posts are generated by scanning through the respective directories and rendering any Markdown files. Finally, the home page, category index pages, and RSS feeds are generated.
|
When the program starts, the static site generation is performed before the web server is started to ensure that broken or outdated files aren't served. First, some files are copied over verbatim (such as favicons and the client-side JS for loading comments), the CSS files for the individual themes are compiled from the Sass files, and the error pages are generated. Then, tutorials and blog posts are generated by scanning through the respective directories and rendering any Markdown files. Finally, the home page, category index pages, and RSS feeds are generated.
|
||||||
|
|
||||||
That's it for the generation, but before the web server starts, the ActivityPub module takes all of the posts, checks if there are any new ones, and, if so, adds them to the database and federates them out to all the remote actors following the blog. Then, all the web server routes get set up and the server finally starts.
|
That's it for the generation, but before the web server starts, the ActivityPub module takes all of the posts, checks if there are any new ones, and, if so, adds them to the database and federates them out to all the remote actors following the blog. Then, all the web server routes get set up and the server finally starts.
|
||||||
|
|
||||||
@ -85,5 +83,3 @@ By the way, the source code for the generator, ActivityPub integration, and cont
|
|||||||
## Conclusion
|
## Conclusion
|
||||||
|
|
||||||
A lot of stuff has technical, under the hood stuff changes, and I'd like to think that I haven't wasted my time and that I'll actually use this new version of my blog to publish posts. But, I don't know what will happen and I can't make any promises. I have some drafts of posts that I'd like to finish and finally publish, so stay tuned (you can subscribe on [RSS](/feed.xml) or by following `@blog@shadowfacts.net` on your favorite ActivityPub platform). As for the Minecraft modding tutorial series, those have been discontinued. They remain available for posterity, but they haven't been updated (merely transplanted into the new blog), and I don't currently have any plans to write new ones.
|
A lot of stuff has technical, under the hood stuff changes, and I'd like to think that I haven't wasted my time and that I'll actually use this new version of my blog to publish posts. But, I don't know what will happen and I can't make any promises. I have some drafts of posts that I'd like to finish and finally publish, so stay tuned (you can subscribe on [RSS](/feed.xml) or by following `@blog@shadowfacts.net` on your favorite ActivityPub platform). As for the Minecraft modding tutorial series, those have been discontinued. They remain available for posterity, but they haven't been updated (merely transplanted into the new blog), and I don't currently have any plans to write new ones.
|
||||||
|
|
||||||
|
|
@ -4,7 +4,6 @@ tags = ["activitypub"]
|
|||||||
date = "2019-09-22 17:50:42 -0400"
|
date = "2019-09-22 17:50:42 -0400"
|
||||||
short_desc = "A compilation of resources I found useful in learning/implementing ActivityPub."
|
short_desc = "A compilation of resources I found useful in learning/implementing ActivityPub."
|
||||||
old_permalink = "/activitypub/2019/activity-pub-resources/"
|
old_permalink = "/activitypub/2019/activity-pub-resources/"
|
||||||
use_old_permalink_for_comments = true
|
|
||||||
slug = "activity-pub-resources"
|
slug = "activity-pub-resources"
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -43,4 +42,3 @@ This post was last updated on Mar 7, 2023.
|
|||||||
- Testing against other implementations running locally (be it on your machine or inside a VM/container) lets you access debug logs and see what the other server is actually receiving, which can be quite useful.
|
- Testing against other implementations running locally (be it on your machine or inside a VM/container) lets you access debug logs and see what the other server is actually receiving, which can be quite useful.
|
||||||
- Darius Kazemi also wrote [an application](https://tinysubversions.com/notes/activitypub-tool/) that lets you send ActivityPub objects directly to other servers, which is useful for testing your application against outside data without polluting other people's instances.
|
- Darius Kazemi also wrote [an application](https://tinysubversions.com/notes/activitypub-tool/) that lets you send ActivityPub objects directly to other servers, which is useful for testing your application against outside data without polluting other people's instances.
|
||||||
- Ted Unangst also has his own [compilation of AP-related links](https://flak.tedunangst.com/post/ActivityPub-as-it-has-been-understood).
|
- Ted Unangst also has his own [compilation of AP-related links](https://flak.tedunangst.com/post/ActivityPub-as-it-has-been-understood).
|
||||||
|
|
@ -4,7 +4,6 @@ tags = ["elixir"]
|
|||||||
date = "2019-10-10 12:29:42 -0400"
|
date = "2019-10-10 12:29:42 -0400"
|
||||||
short_desc = "How I learned Elixir and why I love it."
|
short_desc = "How I learned Elixir and why I love it."
|
||||||
old_permalink = "/elixir/2019/learning-elixir/"
|
old_permalink = "/elixir/2019/learning-elixir/"
|
||||||
use_old_permalink_for_comments = true
|
|
||||||
```
|
```
|
||||||
|
|
||||||
About a year ago, I set out to learn the [Elixir](https://elixir-lang.org) programming language. At the time, it was mainly so I could contribute to [Pleroma](https://pleroma.social), but I've since fallen in love with the language.
|
About a year ago, I set out to learn the [Elixir](https://elixir-lang.org) programming language. At the time, it was mainly so I could contribute to [Pleroma](https://pleroma.social), but I've since fallen in love with the language.
|
||||||
@ -17,9 +16,9 @@ I strongly believe that the best way to learn a programming language (especially
|
|||||||
|
|
||||||
By this point, it was almost December, so I decided I was going to try to do the [Advent of Code](https://adventofcode.com) problems only using Elixir. These challenges were more difficult than the Exercism ones, but they provided the same benefit of letting me get experience actually writing Elixir and solving problems with it an isolated context, without a whole bunch of moving parts.
|
By this point, it was almost December, so I decided I was going to try to do the [Advent of Code](https://adventofcode.com) problems only using Elixir. These challenges were more difficult than the Exercism ones, but they provided the same benefit of letting me get experience actually writing Elixir and solving problems with it an isolated context, without a whole bunch of moving parts.
|
||||||
|
|
||||||
I knew what I ultimately wanted to do with Elixir was build web apps, so after that I went through the official [Phoenix Guide](https://hexdocs.pm/phoenix/overview.html) which explains the overall architecture of the Phoenix framework and shows you how a bunch of common patterns and techniques for building webapps with it.
|
I knew what I ultimately wanted to do with Elixir was build web apps, so after that I went through the official [Phoenix Guide](https://hexdocs.pm/phoenix/overview.html) which explains the overall architecture of the Phoenix framework and shows you how a bunch of common patterns and techniques for building webapps with it.
|
||||||
|
|
||||||
Lastly, and most importantly, I actually started building projects using Elixir. The first one I started was [frenzy](https://git.shadowfacts.net/shadowfacts/frenzy), an RSS aggregator I built using Phoenix and Ecto. Originally, the project was a couple hundred lines of shoddily written JS. I wrote it even before I started learning Elixir, inteding it to be a stopgap. As I was learning Elixir, I knew this project was what it was building up to, so as read things and did programming exercises, I noticed things that I thought would become useful once I got around to rewriting frenzy in Elixir.
|
Lastly, and most importantly, I actually started building projects using Elixir. The first one I started was [frenzy](https://git.shadowfacts.net/shadowfacts/frenzy), an RSS aggregator I built using Phoenix and Ecto. Originally, the project was a couple hundred lines of shoddily written JS. I wrote it even before I started learning Elixir, inteding it to be a stopgap. As I was learning Elixir, I knew this project was what it was building up to, so as read things and did programming exercises, I noticed things that I thought would become useful once I got around to rewriting frenzy in Elixir.
|
||||||
|
|
||||||
When learning a language, there's no substitute for actually learning, and this step was by far the most important for me. In addition to all of the algorithmic experience, and general knowledge of how to write Elixir, actually doing this project gave me the _pratical_ knowledge of what it's like to actually work with this language and these tools. If you're interested in learning Elixir (or any programming language, really), my biggest piece of advice is to keep in the back of your head something concrete that you want to build with it.
|
When learning a language, there's no substitute for actually learning, and this step was by far the most important for me. In addition to all of the algorithmic experience, and general knowledge of how to write Elixir, actually doing this project gave me the _pratical_ knowledge of what it's like to actually work with this language and these tools. If you're interested in learning Elixir (or any programming language, really), my biggest piece of advice is to keep in the back of your head something concrete that you want to build with it.
|
||||||
|
|
||||||
@ -36,4 +35,3 @@ The reason I find this makes such a big difference to the way I code is that let
|
|||||||
Compared to something like iOS app development, this is a godsend. Even in small projects where incremental compiles only take a few seconds, the iteration loop is much slower. My usual development cycle goes something like this: 1) make a change, 2) hit build and run, 3) switch to my browser to glance at social media, 4) 30 seconds later switch to the Simulator and hope it's finished launching. With Elixir projects, I'm generally just switching back and forth between my editor and the terminal and/or web browser to test whatever I'm working on. There are no intermediate steps. When I make a change, there's no waiting for an app to launch, or for a database connection to be established, or for a network request to be made, or for config files to be read, or for anything else. Generally, it takes me more time to switch windows and type `recompile` than it does for the recompilation to actually take place and the change to take effect.
|
Compared to something like iOS app development, this is a godsend. Even in small projects where incremental compiles only take a few seconds, the iteration loop is much slower. My usual development cycle goes something like this: 1) make a change, 2) hit build and run, 3) switch to my browser to glance at social media, 4) 30 seconds later switch to the Simulator and hope it's finished launching. With Elixir projects, I'm generally just switching back and forth between my editor and the terminal and/or web browser to test whatever I'm working on. There are no intermediate steps. When I make a change, there's no waiting for an app to launch, or for a database connection to be established, or for a network request to be made, or for config files to be read, or for anything else. Generally, it takes me more time to switch windows and type `recompile` than it does for the recompilation to actually take place and the change to take effect.
|
||||||
|
|
||||||
Elixir is a language that I've come to find incredibly valuable. It's very powerful and in the areas where it excels, it's unique characteristics make it an extremely valuable tool. If you're thinking about dipping your toes into functional programming, or want to try something new, or even just spend a lot of time doing back end web development,I encourage you to try Elixir.
|
Elixir is a language that I've come to find incredibly valuable. It's very powerful and in the areas where it excels, it's unique characteristics make it an extremely valuable tool. If you're thinking about dipping your toes into functional programming, or want to try something new, or even just spend a lot of time doing back end web development,I encourage you to try Elixir.
|
||||||
|
|
@ -5,7 +5,6 @@ date = "2019-11-11 21:08:42 -0400"
|
|||||||
short_desc = "Building a slide-over hamburger menu without using JavaScript."
|
short_desc = "Building a slide-over hamburger menu without using JavaScript."
|
||||||
old_permalink = "/web/2019/js-free-hamburger-menu/"
|
old_permalink = "/web/2019/js-free-hamburger-menu/"
|
||||||
slug = "js-free-hamburger-menu"
|
slug = "js-free-hamburger-menu"
|
||||||
use_old_permalink_for_comments = true
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Slide-over menus on the web are a pretty common design pattern, especially on mobile. Unfortunately, they seem to generally be accompanied by massive, bloated web apps pulling in megabytes of JavaScript for the simplest of functionality. But fear not, even if you're building a JavaScript-free web app, or simply prefer to fail gracefully in the event the user has disabled JavaScript, it's still possible to use this technique by (ab)using HTML form and label elements.
|
Slide-over menus on the web are a pretty common design pattern, especially on mobile. Unfortunately, they seem to generally be accompanied by massive, bloated web apps pulling in megabytes of JavaScript for the simplest of functionality. But fear not, even if you're building a JavaScript-free web app, or simply prefer to fail gracefully in the event the user has disabled JavaScript, it's still possible to use this technique by (ab)using HTML form and label elements.
|
||||||
@ -83,7 +82,7 @@ Now, all we need to start toggling our sidebar is just a few CSS rules:
|
|||||||
|
|
||||||
The user never needs to see the checkbox itself, since they'll always interact with it through the label elements, so we can always hide it. For a good measure, we'll have our labels use the pointer cursor when they're hovered over, to hint to the user that they can be clicked on. Then we'll hide the sidebar content element by default, since we want it to start out hidden.
|
The user never needs to see the checkbox itself, since they'll always interact with it through the label elements, so we can always hide it. For a good measure, we'll have our labels use the pointer cursor when they're hovered over, to hint to the user that they can be clicked on. Then we'll hide the sidebar content element by default, since we want it to start out hidden.
|
||||||
|
|
||||||
The most important rule, and what this whole thing hinges on, is that last selector. We're looking for an element with the ID `sidebar-visible` that matches the `:checked` pseudo-selector (which only applies to checked checkboxes or radio inputs) that _has a sibling_ whose ID is `sidebar-content`. The key is that the element we're actually selecting here is the `#sidebar-content`, not the checkbox itself. We're essentially using the `:checked` pseudo-selector as a predicate, telling the browser that we only want to select the sidebar content element _when our checkbox is checked_.
|
The most important rule, and what this whole thing hinges on, is that last selector. We're looking for an element with the ID `sidebar-visible` that matches the `:checked` pseudo-selector (which only applies to checked checkboxes or radio inputs) that _has a sibling_ whose ID is `sidebar-content`. The key is that the element we're actually selecting here is the `#sidebar-content`, not the checkbox itself. We're essentially using the `:checked` pseudo-selector as a predicate, telling the browser that we only want to select the sidebar content element _when our checkbox is checked_.
|
||||||
|
|
||||||
If we take a look at <a href="/2019/js-free-hamburger-menu/toggling.html" data-link="toggling.html">our web page now</a>, we can see we've got the building blocks in place for our slide-over menu. The page starts off not showing our sidebar content, but we can click the Open Sidebar label to show it, and then click the Close label to hide it once more.
|
If we take a look at <a href="/2019/js-free-hamburger-menu/toggling.html" data-link="toggling.html">our web page now</a>, we can see we've got the building blocks in place for our slide-over menu. The page starts off not showing our sidebar content, but we can click the Open Sidebar label to show it, and then click the Close label to hide it once more.
|
||||||
|
|
||||||
@ -239,4 +238,3 @@ Lastly, we'll tell the sidebar content element to transition its left property w
|
|||||||
Now <a href="/2019/js-free-hamburger-menu/transition.html" data-link="transition.html">our menu</a> has a nice transition so it's not quite so jarring when it's shown/hidden.
|
Now <a href="/2019/js-free-hamburger-menu/transition.html" data-link="transition.html">our menu</a> has a nice transition so it's not quite so jarring when it's shown/hidden.
|
||||||
|
|
||||||
I've polished it up a little bit more for the <a href="/2019/js-free-hamburger-menu/final.html" data-link="final.html">final version</a>, but the core of the menu is done! And all without a single line of JavaScript.
|
I've polished it up a little bit more for the <a href="/2019/js-free-hamburger-menu/final.html" data-link="final.html">final version</a>, but the core of the menu is done! And all without a single line of JavaScript.
|
||||||
|
|
@ -5,7 +5,6 @@ date = "2019-12-22 19:12:42 -0400"
|
|||||||
short_desc = "Integrating a tiny web server into your Xcode UI test target to mock HTTP requests."
|
short_desc = "Integrating a tiny web server into your Xcode UI test target to mock HTTP requests."
|
||||||
old_permalink = "/ios/2019/mock-http-ios-ui-testing/"
|
old_permalink = "/ios/2019/mock-http-ios-ui-testing/"
|
||||||
slug = "mock-http-ios-ui-testing"
|
slug = "mock-http-ios-ui-testing"
|
||||||
use_old_permalink_for_comments = true
|
|
||||||
```
|
```
|
||||||
|
|
||||||
I recently decided to start writing User Interface tests for [Tusker](https://git.shadowfacts.net/shadowfacts/Tusker), my iOS app for Mastodon and Pleroma. But I couldn't just write tests that interacted with an account on any real instance, as that would be far too unpredictable and mean my tests could have an impact on other people. The solution to this problem is, of course, mocking. The core idea is that instead of interacting with external things, your program interacts with mock versions of them, which appear to be their real counterparts, but don't actually perform any of the operations they claim to. This allows for very tight control over what data the application receives, making it much more amenable to testing.
|
I recently decided to start writing User Interface tests for [Tusker](https://git.shadowfacts.net/shadowfacts/Tusker), my iOS app for Mastodon and Pleroma. But I couldn't just write tests that interacted with an account on any real instance, as that would be far too unpredictable and mean my tests could have an impact on other people. The solution to this problem is, of course, mocking. The core idea is that instead of interacting with external things, your program interacts with mock versions of them, which appear to be their real counterparts, but don't actually perform any of the operations they claim to. This allows for very tight control over what data the application receives, making it much more amenable to testing.
|
||||||
@ -48,7 +47,7 @@ if [ "${CONFIGURATION}" == "Debug" ]; then
|
|||||||
echo "Embedding ${SCRIPT_INPUT_FILE_0}"
|
echo "Embedding ${SCRIPT_INPUT_FILE_0}"
|
||||||
cp -R $SCRIPT_INPUT_FILE_0 $SCRIPT_OUTPUT_FILE_0
|
cp -R $SCRIPT_INPUT_FILE_0 $SCRIPT_OUTPUT_FILE_0
|
||||||
codesign --force --verbose --sign $EXPANDED_CODE_SIGN_IDENTITY $SCRIPT_OUTPUT_FILE_0
|
codesign --force --verbose --sign $EXPANDED_CODE_SIGN_IDENTITY $SCRIPT_OUTPUT_FILE_0
|
||||||
|
|
||||||
echo "Embedding ${SCRIPT_INPUT_FILE_1}"
|
echo "Embedding ${SCRIPT_INPUT_FILE_1}"
|
||||||
cp -R $SCRIPT_INPUT_FILE_1 $SCRIPT_OUTPUT_FILE_1
|
cp -R $SCRIPT_INPUT_FILE_1 $SCRIPT_OUTPUT_FILE_1
|
||||||
codesign --force --verbose --sign $EXPANDED_CODE_SIGN_IDENTITY $SCRIPT_OUTPUT_FILE_1
|
codesign --force --verbose --sign $EXPANDED_CODE_SIGN_IDENTITY $SCRIPT_OUTPUT_FILE_1
|
||||||
@ -128,4 +127,3 @@ func testWebServer() {
|
|||||||
Once we run the test and the simulator's up and runing, we can visit `http://localhost:8080/hello` in a web browser and see the JSON response we defined. Now, actually using the mock web server from the app is a simple matter of adding an environment variable the override the default API host.
|
Once we run the test and the simulator's up and runing, we can visit `http://localhost:8080/hello` in a web browser and see the JSON response we defined. Now, actually using the mock web server from the app is a simple matter of adding an environment variable the override the default API host.
|
||||||
|
|
||||||
One caveat to note with this set up is that, because the web server is running in the same process as the test code (just in a different thread), when the debugger pauses in a test (_not_ in the app itself), any web requests we make to the mock server won't complete until the process is resumed.
|
One caveat to note with this set up is that, because the web server is running in the same process as the test code (just in a different thread), when the debugger pauses in a test (_not_ in the app itself), any web requests we make to the mock server won't complete until the process is resumed.
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
```
|
```
|
||||||
title = "Algorithmic Bias"
|
title = "Algorithmic Bias"
|
||||||
tags = ["misc", "social media"]
|
tags = ["politics", "social media"]
|
||||||
date = "2020-06-05 09:55:42 -0400"
|
date = "2020-06-05 09:55:42 -0400"
|
||||||
slug = "algorithmic-bias"
|
slug = "algorithmic-bias"
|
||||||
```
|
```
|
||||||
@ -15,4 +15,3 @@ This is what algorithmic bias looks like. **Algorithms are not neutral.**[^1]
|
|||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
[^1]: "Algorithm" is a word here used not in the purely computer science sense, but to mean a element of software which operates in a black box, often with a machine learning component, with little or no human supervision, input, or control.
|
[^1]: "Algorithm" is a word here used not in the purely computer science sense, but to mean a element of software which operates in a black box, often with a machine learning component, with little or no human supervision, input, or control.
|
||||||
|
|
@ -10,9 +10,9 @@ On and off for the past year and a half or so, I've been working on a small side
|
|||||||
|
|
||||||
I knew that MP3 files had some embedded metadata, only for the reason that looking at most tracks in Finder shows album artwork and information about the track. Cursory googling led me to the [ID3 spec](https://id3.org/).
|
I knew that MP3 files had some embedded metadata, only for the reason that looking at most tracks in Finder shows album artwork and information about the track. Cursory googling led me to the [ID3 spec](https://id3.org/).
|
||||||
|
|
||||||
[^1]: Actual, DRM-free files because music streaming services by and large don't pay artists fairly[^2]. MP3s specifically because they Just Work everywhere, and I cannot for the life of me hear the difference between a 320kbps MP3 and an \<insert audiophile format of choice> file.
|
[^1]: Actual, DRM-free files because music streaming services by and large don't pay artists fairly. MP3s specifically because they Just Work everywhere, and I cannot for the life of me hear the difference between a 320kbps MP3 and an \<insert audiophile format of choice> file.
|
||||||
|
<br><br>
|
||||||
[^2]: Spotify pays artists 0.38¢ per play and Apple Music pays 0.783¢ per play ([source](https://help.songtrust.com/knowledge/what-is-the-pay-rate-for-spotify-streams)). For an album of 12 songs that costs $10 (assuming wherever you buy it from takes a 30% cut), you would have to listen all the way through it between 75 and 150 times for the artist to receive as much money as if you had just purchased the album outright. That's hardly fair and is not sustainable for all but the largest of musicians.
|
Spotify pays artists 0.38¢ per play and Apple Music pays 0.783¢ per play ([source](https://help.songtrust.com/knowledge/what-is-the-pay-rate-for-spotify-streams)). For an album of 12 songs that costs $10 (assuming wherever you buy it from takes a 30% cut), you would have to listen all the way through it between 75 and 150 times for the artist to receive as much money as if you had just purchased the album outright. That's hardly fair and is not sustainable for all but the largest of musicians.
|
||||||
|
|
||||||
<!-- excerpt-end -->
|
<!-- excerpt-end -->
|
||||||
|
|
||||||
@ -136,7 +136,7 @@ reversed: 00011010 01010010 00000001
|
|||||||
|
|
||||||
You may have noticed the `unsynchronized` flag in the tag header. The ID3 unschronization scheme is another way of preventing false syncs in longer blocks of data within the tag (such as image data in the frame used for album artwork). I elected not to handle this flag for now, since none of the tracks in my library have the flag set. The ID3v2.4 spec says the unsynchronization scheme is primarily intended to prevent old software which isn't aware of ID3 tags from incorrectly trying to sync onto data in the ID3 tag. Since the ID3v2 spec is over 20 years old, pieces of software which aren't aware of it are few and far between, so I guess the unsynchronization scheme has fallen out of favor.
|
You may have noticed the `unsynchronized` flag in the tag header. The ID3 unschronization scheme is another way of preventing false syncs in longer blocks of data within the tag (such as image data in the frame used for album artwork). I elected not to handle this flag for now, since none of the tracks in my library have the flag set. The ID3v2.4 spec says the unsynchronization scheme is primarily intended to prevent old software which isn't aware of ID3 tags from incorrectly trying to sync onto data in the ID3 tag. Since the ID3v2 spec is over 20 years old, pieces of software which aren't aware of it are few and far between, so I guess the unsynchronization scheme has fallen out of favor.
|
||||||
|
|
||||||
So, since we've gotten the 4-byte binary that contains the tag size out of the header, we can use the `decode_synchsafe_integer` function to decode it.
|
So, since we've gotten the 4-byte binary that contains the tag size out of the header, we can use the `decode_synchsafe_integer` function to decode it.
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
def parse_tag(...) do
|
def parse_tag(...) do
|
||||||
@ -207,7 +207,7 @@ def parse_frames(major_version, data, tag_length_remaining, frames \\ [])
|
|||||||
The first case of the function is for if it's reached the total length of the tag, in which case it will just convert the accumulated tags into a map, and return the data that's left (we want to return whatever data's left after the end of the ID3 tag so that it can be used by other parts of the code, say, an MP3 parser...). We can just directly convert the list of frames into a map because, as you'll see shortly, each frame is a tuple of the name of the frame and its data, in whatever form that may be.
|
The first case of the function is for if it's reached the total length of the tag, in which case it will just convert the accumulated tags into a map, and return the data that's left (we want to return whatever data's left after the end of the ID3 tag so that it can be used by other parts of the code, say, an MP3 parser...). We can just directly convert the list of frames into a map because, as you'll see shortly, each frame is a tuple of the name of the frame and its data, in whatever form that may be.
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
def parse_frames(_, data, tag_length_remaining, frames)
|
def parse_frames(_, data, tag_length_remaining, frames)
|
||||||
when tag_length_remaining <= 0 do
|
when tag_length_remaining <= 0 do
|
||||||
{Map.new(frames), data}
|
{Map.new(frames), data}
|
||||||
end
|
end
|
||||||
@ -489,4 +489,3 @@ iex> ID3.parse_tag(data)
|
|||||||
```
|
```
|
||||||
|
|
||||||
One of the pieces of information I was hoping I could get from the ID3 tags was the durations of the MP3s in my library. But alas, none of the tracks I have use the TLEN frame, so it looks like I'll have to try and pull that data out of the MP3 myself. But that's a post for another time...
|
One of the pieces of information I was hoping I could get from the ID3 tags was the durations of the MP3s in my library. But alas, none of the tracks I have use the TLEN frame, so it looks like I'll have to try and pull that data out of the MP3 myself. But that's a post for another time...
|
||||||
|
|
@ -4,6 +4,7 @@ tags = ["computers"]
|
|||||||
date = "2021-01-13 21:43:42 -0400"
|
date = "2021-01-13 21:43:42 -0400"
|
||||||
short_desc = "The M1 Mac mini is my favorite computer. Let me tell you why."
|
short_desc = "The M1 Mac mini is my favorite computer. Let me tell you why."
|
||||||
slug = "m1"
|
slug = "m1"
|
||||||
|
comments_post_id = "A3DOePzX2ze5ZaHBoW"
|
||||||
```
|
```
|
||||||
|
|
||||||
I've had an M1 Mac mini for about a month now, so I thought I'd write up my experiences with it. The configuration I got has 16GB of RAM and 256GB of storage (the base amount). The reasoning for the bare minimum storage is that Apple charges an arm and a leg for extra space, and I intend this to primarily be a stopgap machine until there are higher-power Apple Silicon equipped laptops. If I really desperately need extra space down the line, I can buy a cheap external SSD (that will continue to be useful after this computer is gone). The 16GB of RAM, however, is necessary to do any iOS development (Xcode and all the various associated services can by themselves easily consume almost 8 gigs). So far, I've moved just about all of my non-work desktop computing over to it, and it's been absolutely fantastic.
|
I've had an M1 Mac mini for about a month now, so I thought I'd write up my experiences with it. The configuration I got has 16GB of RAM and 256GB of storage (the base amount). The reasoning for the bare minimum storage is that Apple charges an arm and a leg for extra space, and I intend this to primarily be a stopgap machine until there are higher-power Apple Silicon equipped laptops. If I really desperately need extra space down the line, I can buy a cheap external SSD (that will continue to be useful after this computer is gone). The 16GB of RAM, however, is necessary to do any iOS development (Xcode and all the various associated services can by themselves easily consume almost 8 gigs). So far, I've moved just about all of my non-work desktop computing over to it, and it's been absolutely fantastic.
|
||||||
@ -58,4 +59,3 @@ Unlike on the laptop variants of the M1 machine, where the port selection is a m
|
|||||||
I've had only a few issues I've experienced that _may_ be attributable to hardware. The first is that when I'm playing back audio to my Bluetooth headphones, they periodically cut out for a fraction of a second before resuming audio playback. I regularly use these same headphones with my Intel Mac on which I haven't experienced this issue while running either Catalina or Big Sur. The other issue is that when the Mac mini is connected to my 1440p/144Hz display, it loses the 144Hz setting almost every time I wake it from sleep. Odly, just changing the refresh rate while the resolution is set to "Default for display" does nothing. I have to first change the resolution to be scaled down, and then back to the default before changing the refresh rate actually takes effect. The final issue, which has only happened once in the past month, is that when I woke the computer up from sleep, it had stopped outputting video over the HDMI port. It was still sending video over a USB-C to DisplayPort adapter, but not HDMI. Unplugging and reconnecting the HDMI cable didn't fix the issue, nor did power cycling the monitor. I had to fully restart the Mac mini to resolve the issue.
|
I've had only a few issues I've experienced that _may_ be attributable to hardware. The first is that when I'm playing back audio to my Bluetooth headphones, they periodically cut out for a fraction of a second before resuming audio playback. I regularly use these same headphones with my Intel Mac on which I haven't experienced this issue while running either Catalina or Big Sur. The other issue is that when the Mac mini is connected to my 1440p/144Hz display, it loses the 144Hz setting almost every time I wake it from sleep. Odly, just changing the refresh rate while the resolution is set to "Default for display" does nothing. I have to first change the resolution to be scaled down, and then back to the default before changing the refresh rate actually takes effect. The final issue, which has only happened once in the past month, is that when I woke the computer up from sleep, it had stopped outputting video over the HDMI port. It was still sending video over a USB-C to DisplayPort adapter, but not HDMI. Unplugging and reconnecting the HDMI cable didn't fix the issue, nor did power cycling the monitor. I had to fully restart the Mac mini to resolve the issue.
|
||||||
|
|
||||||
But, all in all, for a product that's the first generation of both a new (to macOS) CPU and GPU architecture, this has been an phenomenally good experience. I am _incredibly_ eager to see both what a higher-performance variant of the M1 looks like and future generations of this architecture.
|
But, all in all, for a product that's the first generation of both a new (to macOS) CPU and GPU architecture, this has been an phenomenally good experience. I am _incredibly_ eager to see both what a higher-performance variant of the M1 looks like and future generations of this architecture.
|
||||||
|
|
@ -4,6 +4,7 @@ tags = ["social media"]
|
|||||||
date = "2021-02-25 22:46:42 -0400"
|
date = "2021-02-25 22:46:42 -0400"
|
||||||
short_desc = "The technique Twitter borrows from game design to keep you engaged."
|
short_desc = "The technique Twitter borrows from game design to keep you engaged."
|
||||||
slug = "twitter-game-design"
|
slug = "twitter-game-design"
|
||||||
|
comments_post_id = "A4ebygwwBL6frT8YHg"
|
||||||
```
|
```
|
||||||
|
|
||||||
A few weeks ago, I read [a thread](https://twitter.com/kchironis/status/1355585411943260162) on Twitter by a game designer at Riot Games. The thread is about the concept of "celebration" in the world of game design. It refers to the techniques used to reinforce "good" behavior in video games (good not in any objective sense, but simply whatever is desired by the game designer). It was an interesting thread, but I moved on shortly after reading it and didn't give it much more thought. Until last week when I was reading [an article](https://craigwritescode.medium.com/user-engagement-is-code-for-addiction-a2f50d36d7ac) about how social media is designed to be addictive. With the thread still floating around in the back of my mind, what I realized while thinking about the design of social media platforms was that Twitter uses the exact same techniques as video games.
|
A few weeks ago, I read [a thread](https://twitter.com/kchironis/status/1355585411943260162) on Twitter by a game designer at Riot Games. The thread is about the concept of "celebration" in the world of game design. It refers to the techniques used to reinforce "good" behavior in video games (good not in any objective sense, but simply whatever is desired by the game designer). It was an interesting thread, but I moved on shortly after reading it and didn't give it much more thought. Until last week when I was reading [an article](https://craigwritescode.medium.com/user-engagement-is-code-for-addiction-a2f50d36d7ac) about how social media is designed to be addictive. With the thread still floating around in the back of my mind, what I realized while thinking about the design of social media platforms was that Twitter uses the exact same techniques as video games.
|
||||||
@ -21,4 +22,3 @@ As part of the design of a video game, you need to get the player to do certain
|
|||||||
When you click the Like button on a tweet, a few things happen: the heart button itself turns solid red, a small particle effect plays and the number of likes rolls up. The same techniques game designers use. The animation is eye-catching without being distracting and the like count increasing lets you subconsciously connect the action you just took to the effect it had.
|
When you click the Like button on a tweet, a few things happen: the heart button itself turns solid red, a small particle effect plays and the number of likes rolls up. The same techniques game designers use. The animation is eye-catching without being distracting and the like count increasing lets you subconsciously connect the action you just took to the effect it had.
|
||||||
|
|
||||||
I can't know if Twitter does it with the deliberate intent of making users form habits, but I can't help but feel like that is a consequence, even if a small one.
|
I can't know if Twitter does it with the deliberate intent of making users form habits, but I can't help but feel like that is a consequence, even if a small one.
|
||||||
|
|
14833
site/posts/2021/2021-03-17-minecraft-mod-statistics.md
Normal file
14833
site/posts/2021/2021-03-17-minecraft-mod-statistics.md
Normal file
File diff suppressed because one or more lines are too long
@ -4,6 +4,7 @@ tags = ["swift"]
|
|||||||
date = "2021-04-08 18:25:42 -0400"
|
date = "2021-04-08 18:25:42 -0400"
|
||||||
short_desc = "The three hardest problems in computer science are naming, caching, and off-by-one errors."
|
short_desc = "The three hardest problems in computer science are naming, caching, and off-by-one errors."
|
||||||
slug = "image-caching"
|
slug = "image-caching"
|
||||||
|
comments_post_id = "A63BbRVFkfDpEqxFzM"
|
||||||
```
|
```
|
||||||
|
|
||||||
A fairly important part of Tusker (my iOS Mastodon app) is displaying images. And a bunch of varieties of images: user avatars, post attachments, custom emojis, user profile headers, as well as a few other types of rarely-shown images that get lumped in with attachments. And displaying lots of images in a performant way means caching. Lots of caching.
|
A fairly important part of Tusker (my iOS Mastodon app) is displaying images. And a bunch of varieties of images: user avatars, post attachments, custom emojis, user profile headers, as well as a few other types of rarely-shown images that get lumped in with attachments. And displaying lots of images in a performant way means caching. Lots of caching.
|
||||||
@ -83,4 +84,3 @@ Before we reach the end, there's one final bit of image caching Tusker does. Som
|
|||||||
[^5]: In an ideal world, this could be done with something like a fragment shader at render-time, but I couldn't find any reasonable way of doing that. Oh well.
|
[^5]: In an ideal world, this could be done with something like a fragment shader at render-time, but I couldn't find any reasonable way of doing that. Oh well.
|
||||||
|
|
||||||
And that finally brings us to how image caching in Tusker works today. It started out very simple, and the underlying concepts largely haven't changed, there's just been a steady series of improvements. As with most things related to caching, what seemed initially to be a simple problem got progressively more and more complex. And, though there are a lot of moving parts, the system overall works quite well. Images are no longer the bottleneck in scrolling performance, except in the rarest of cases (like using grayscale images on the oldest supported devices. Either of those individually are fine, but together they're just too much). And, memory usage overall is substantially reduced making the app a better platform citizen.
|
And that finally brings us to how image caching in Tusker works today. It started out very simple, and the underlying concepts largely haven't changed, there's just been a steady series of improvements. As with most things related to caching, what seemed initially to be a simple problem got progressively more and more complex. And, though there are a lot of moving parts, the system overall works quite well. Images are no longer the bottleneck in scrolling performance, except in the rarest of cases (like using grayscale images on the oldest supported devices. Either of those individually are fine, but together they're just too much). And, memory usage overall is substantially reduced making the app a better platform citizen.
|
||||||
|
|
@ -4,7 +4,7 @@ tags = ["build a programming language", "rust"]
|
|||||||
date = "2021-04-13 17:00:42 -0400"
|
date = "2021-04-13 17:00:42 -0400"
|
||||||
short_desc = "Turning a string into a sequence of tokens."
|
short_desc = "Turning a string into a sequence of tokens."
|
||||||
slug = "lexing"
|
slug = "lexing"
|
||||||
preamble = '<p style="font-style: italic;">This post is part of a <a href="/build-a-programming-language/" data-link="/build-a-programming-language/">series</a> about learning Rust and building a small programming language.</p><hr>'
|
preamble = '<p class="italic">This post is part of a <a href="/build-a-programming-language/">series</a> about learning Rust and building a small programming language.</p><hr>'
|
||||||
```
|
```
|
||||||
|
|
||||||
The first part of the language I've built is the lexer. It takes the program text as input and produces a vector of tokens. Tokens are the individual units that the parser will work with, rather than it having to work directly with characters. A token could be a bunch of different things. It could be a literal value (like a number or string), or it could be an identifier, or a specific symbol (like a plus sign).
|
The first part of the language I've built is the lexer. It takes the program text as input and produces a vector of tokens. Tokens are the individual units that the parser will work with, rather than it having to work directly with characters. A token could be a bunch of different things. It could be a literal value (like a number or string), or it could be an identifier, or a specific symbol (like a plus sign).
|
||||||
@ -96,5 +96,3 @@ fn main() {
|
|||||||
$ cargo run
|
$ cargo run
|
||||||
tokens: [Integer(12), Plus, Integer(34)]
|
tokens: [Integer(12), Plus, Integer(34)]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -4,7 +4,7 @@ tags = ["build a programming language", "rust"]
|
|||||||
date = "2021-04-14 17:00:42 -0400"
|
date = "2021-04-14 17:00:42 -0400"
|
||||||
short_desc = "Building a small AST from the stream of tokens."
|
short_desc = "Building a small AST from the stream of tokens."
|
||||||
slug = "parsing"
|
slug = "parsing"
|
||||||
preamble = '<p style="font-style: italic;">This post is part of a <a href="/build-a-programming-language/" data-link="/build-a-programming-language/">series</a> about learning Rust and building a small programming language.</p><hr>'
|
preamble = '<p class="italic">This post is part of a <a href="/build-a-programming-language/">series</a> about learning Rust and building a small programming language.</p><hr>'
|
||||||
```
|
```
|
||||||
|
|
||||||
Now that the lexer is actually lexing, we can start parsing. This is where the Tree in Abstract Syntax Tree really comes in. What the parser is going to do is take a flat sequence of tokens and transform it into a shape that represents the actual structure of the code.
|
Now that the lexer is actually lexing, we can start parsing. This is where the Tree in Abstract Syntax Tree really comes in. What the parser is going to do is take a flat sequence of tokens and transform it into a shape that represents the actual structure of the code.
|
||||||
@ -97,4 +97,3 @@ node: Some(
|
|||||||
```
|
```
|
||||||
|
|
||||||
The eagle-eyed may notice that while we have parsed the expression, we have not parsed it correctly. What's missing is operator precedence and associativity, but that will have to wait for next time.
|
The eagle-eyed may notice that while we have parsed the expression, we have not parsed it correctly. What's missing is operator precedence and associativity, but that will have to wait for next time.
|
||||||
|
|
@ -4,7 +4,7 @@ tags = ["build a programming language", "rust"]
|
|||||||
date = "2021-04-15 17:00:42 -0400"
|
date = "2021-04-15 17:00:42 -0400"
|
||||||
short_desc = "A bad calculator."
|
short_desc = "A bad calculator."
|
||||||
slug = "evaluation"
|
slug = "evaluation"
|
||||||
preamble = '<p style="font-style: italic;">This post is part of a <a href="/build-a-programming-language/" data-link="/build-a-programming-language/">series</a> about learning Rust and building a small programming language.</p><hr>'
|
preamble = '<p class="italic">This post is part of a <a href="/build-a-programming-language/">series</a> about learning Rust and building a small programming language.</p><hr>'
|
||||||
```
|
```
|
||||||
|
|
||||||
Last time I said operator precedence was going to be next. Well, if you've read the title, you know that's not the case. I decided I really wanted to see this actually run[^1] some code[^2], so let's do that.
|
Last time I said operator precedence was going to be next. Well, if you've read the title, you know that's not the case. I decided I really wanted to see this actually run[^1] some code[^2], so let's do that.
|
||||||
@ -61,7 +61,7 @@ fn eval_binary_op(left: &Node, right: &Node) -> Value {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
And with that surpisingly small amount of code, I've got a very dumb calculator that can perform arbitrary additions:
|
And with that surpisingly small amount of code, I've got a very dumb calculator that can perform arbitrary additions:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
fn main() {
|
fn main() {
|
||||||
@ -78,4 +78,3 @@ result: Integer(6)
|
|||||||
```
|
```
|
||||||
|
|
||||||
Next time, I'll add some more operators and actually get around to operator precedence.
|
Next time, I'll add some more operators and actually get around to operator precedence.
|
||||||
|
|
@ -3,7 +3,7 @@ title = "Part 4: Operator Precedence"
|
|||||||
tags = ["build a programming language", "rust"]
|
tags = ["build a programming language", "rust"]
|
||||||
date = "2021-04-16 17:00:42 -0400"
|
date = "2021-04-16 17:00:42 -0400"
|
||||||
slug = "operator-precedence"
|
slug = "operator-precedence"
|
||||||
preamble = '<p style="font-style: italic;">This post is part of a <a href="/build-a-programming-language/" data-link="/build-a-programming-language/">series</a> about learning Rust and building a small programming language.</p><hr>'
|
preamble = '<p class="italic">This post is part of a <a href="/build-a-programming-language/">series</a> about learning Rust and building a small programming language.</p><hr>'
|
||||||
```
|
```
|
||||||
|
|
||||||
I've gone through the lexer, parser, and evaluator and added subtraction, multiplication, and division in addition to, uh... addition. And they kind of work, but there's one glaring issue that I mentioned back in part 2. It's that the parser has no understanding of operator precedence. That is to say, it doesn't know which operators have a higher priority in the order of operations when implicit grouping is taking place.
|
I've gone through the lexer, parser, and evaluator and added subtraction, multiplication, and division in addition to, uh... addition. And they kind of work, but there's one glaring issue that I mentioned back in part 2. It's that the parser has no understanding of operator precedence. That is to say, it doesn't know which operators have a higher priority in the order of operations when implicit grouping is taking place.
|
||||||
@ -189,4 +189,3 @@ fn main() {
|
|||||||
$ cargo run
|
$ cargo run
|
||||||
result: Integer(10)
|
result: Integer(10)
|
||||||
```
|
```
|
||||||
|
|
@ -4,7 +4,7 @@ tags = ["build a programming language", "rust"]
|
|||||||
date = "2021-04-17 17:00:42 -0400"
|
date = "2021-04-17 17:00:42 -0400"
|
||||||
short_desc = "A small gotcha in Rust's TakeWhile iterator."
|
short_desc = "A small gotcha in Rust's TakeWhile iterator."
|
||||||
slug = "fixing-floats"
|
slug = "fixing-floats"
|
||||||
preamble = '<p style="font-style: italic;">This post is part of a <a href="/build-a-programming-language/" data-link="/build-a-programming-language/">series</a> about learning Rust and building a small programming language.</p><hr>'
|
preamble = '<p class="italic">This post is part of a <a href="/build-a-programming-language/">series</a> about learning Rust and building a small programming language.</p><hr>'
|
||||||
```
|
```
|
||||||
|
|
||||||
In the process of adding floating point numbers, I ran into something a little bit unexpected. The issue turned out to be pretty simple, but I thought it was worth mentioning.
|
In the process of adding floating point numbers, I ran into something a little bit unexpected. The issue turned out to be pretty simple, but I thought it was worth mentioning.
|
||||||
@ -56,7 +56,7 @@ I inquired about this behavior on the fediverse, and learned that I missed a key
|
|||||||
I would have expected it to use the `peek()` method on peekable iterators to avoid this, but I guess not. No matter, a peeking version is easy to implement:
|
I would have expected it to use the `peek()` method on peekable iterators to avoid this, but I guess not. No matter, a peeking version is easy to implement:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
fn take_while_peek<I, P>(peekable: &mut Peekable<I>, mut predicate: P) -> Vec<I::Item>
|
fn take_while_peek<I, P>(peekable: &mut Peekable<I>, mut predicate: P) -> Vec<I::Item>
|
||||||
where
|
where
|
||||||
I: Iterator,
|
I: Iterator,
|
||||||
P: FnMut(&I::Item) -> bool,
|
P: FnMut(&I::Item) -> bool,
|
||||||
@ -84,5 +84,3 @@ fn parse_number<T: Iterator<Item = char>>(it: &mut T) -> Option<Token> {
|
|||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -3,7 +3,7 @@ title = "Part 6: Grouping"
|
|||||||
tags = ["build a programming language", "rust"]
|
tags = ["build a programming language", "rust"]
|
||||||
date = "2021-04-18 14:42:42 -0400"
|
date = "2021-04-18 14:42:42 -0400"
|
||||||
slug = "grouping"
|
slug = "grouping"
|
||||||
preamble = '<p style="font-style: italic;">This post is part of a <a href="/build-a-programming-language/" data-link="/build-a-programming-language/">series</a> about learning Rust and building a small programming language.</p><hr>'
|
preamble = '<p class="italic">This post is part of a <a href="/build-a-programming-language/">series</a> about learning Rust and building a small programming language.</p><hr>'
|
||||||
```
|
```
|
||||||
|
|
||||||
Parsing groups is pretty straightforward, with only one minor pain point to keep in mind. I'll gloss over adding left and right parentheses because it's super easy—just another single character token.
|
Parsing groups is pretty straightforward, with only one minor pain point to keep in mind. I'll gloss over adding left and right parentheses because it's super easy—just another single character token.
|
||||||
@ -97,4 +97,3 @@ node: Group {
|
|||||||
```
|
```
|
||||||
|
|
||||||
(I won't bother discussing evaluating groups because it's trivial.)
|
(I won't bother discussing evaluating groups because it's trivial.)
|
||||||
|
|
@ -4,7 +4,7 @@ tags = ["build a programming language", "rust"]
|
|||||||
date = "2021-04-19 17:00:42 -0400"
|
date = "2021-04-19 17:00:42 -0400"
|
||||||
short_desc = "A minor fight with the Rust borrow checker."
|
short_desc = "A minor fight with the Rust borrow checker."
|
||||||
slug = "cleaning-up-binary-operators"
|
slug = "cleaning-up-binary-operators"
|
||||||
preamble = '<p style="font-style: italic;">This post is part of a <a href="/build-a-programming-language/" data-link="/build-a-programming-language/">series</a> about learning Rust and building a small programming language.</p><hr>'
|
preamble = '<p class="italic">This post is part of a <a href="/build-a-programming-language/">series</a> about learning Rust and building a small programming language.</p><hr>'
|
||||||
```
|
```
|
||||||
|
|
||||||
The code from [part 4](/2021/operator-precedence/) that checks whether a pair of binary operators should be grouped to the left or right works, but I'm not particularly happy with it. The issue is that it needs to pattern match on the right node twice: first in the `should_group_left` function, and then again in `combine_with_binary_operator` if `should_group_left` returned true.
|
The code from [part 4](/2021/operator-precedence/) that checks whether a pair of binary operators should be grouped to the left or right works, but I'm not particularly happy with it. The issue is that it needs to pattern match on the right node twice: first in the `should_group_left` function, and then again in `combine_with_binary_operator` if `should_group_left` returned true.
|
||||||
@ -140,4 +140,3 @@ fn combine_with_binary_operator(left: Node, token: &Token, right: Node) -> Node
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -3,7 +3,7 @@ title = "Part 8: Variable Lookups and Function Calls"
|
|||||||
tags = ["build a programming language", "rust"]
|
tags = ["build a programming language", "rust"]
|
||||||
date = "2021-04-25 11:15:42 -0400"
|
date = "2021-04-25 11:15:42 -0400"
|
||||||
slug = "variable-lookups-and-function-calls"
|
slug = "variable-lookups-and-function-calls"
|
||||||
preamble = '<p style="font-style: italic;">This post is part of a <a href="/build-a-programming-language/" data-link="/build-a-programming-language/">series</a> about learning Rust and building a small programming language.</p><hr>'
|
preamble = '<p class="italic">This post is part of a <a href="/build-a-programming-language/">series</a> about learning Rust and building a small programming language.</p><hr>'
|
||||||
```
|
```
|
||||||
|
|
||||||
Arithmetic expressions are all well and good, but they don't really feel much like a programming language. To fix that, let's start working on variables and function calls.
|
Arithmetic expressions are all well and good, but they don't really feel much like a programming language. To fix that, let's start working on variables and function calls.
|
||||||
@ -127,4 +127,3 @@ Call {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -3,7 +3,7 @@ title = "Part 9: Statements"
|
|||||||
tags = ["build a programming language", "rust"]
|
tags = ["build a programming language", "rust"]
|
||||||
date = "2021-05-03 17:46:42 -0400"
|
date = "2021-05-03 17:46:42 -0400"
|
||||||
slug = "statements"
|
slug = "statements"
|
||||||
preamble = '<p style="font-style: italic;">This post is part of a <a href="/build-a-programming-language/" data-link="/build-a-programming-language/">series</a> about learning Rust and building a small programming language.</p><hr>'
|
preamble = '<p class="italic">This post is part of a <a href="/build-a-programming-language/">series</a> about learning Rust and building a small programming language.</p><hr>'
|
||||||
```
|
```
|
||||||
|
|
||||||
So the parser can handle a single expression, but since we're not building a Lisp, that's not enough. It needs to handle multiple statements. For context, an expression is a piece of code that represents a value whereas a statement is a piece of code that can be executed but does not result in a value.
|
So the parser can handle a single expression, but since we're not building a Lisp, that's not enough. It needs to handle multiple statements. For context, an expression is a piece of code that represents a value whereas a statement is a piece of code that can be executed but does not result in a value.
|
||||||
@ -61,7 +61,7 @@ fn parse_statement<'a, I: Iterator<Item = &'a Token>>(it: &mut Peekable<'a, I>)
|
|||||||
}
|
}
|
||||||
None => (),
|
None => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
node
|
node
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -93,5 +93,3 @@ statements: [
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -3,7 +3,7 @@ title = "Part 10: Variable Declarations"
|
|||||||
tags = ["build a programming language", "rust"]
|
tags = ["build a programming language", "rust"]
|
||||||
date = "2021-05-09 19:14:42 -0400"
|
date = "2021-05-09 19:14:42 -0400"
|
||||||
slug = "variable-declarations"
|
slug = "variable-declarations"
|
||||||
preamble = '<p style="font-style: italic;">This post is part of a <a href="/build-a-programming-language/" data-link="/build-a-programming-language/">series</a> about learning Rust and building a small programming language.</p><hr>'
|
preamble = '<p class="italic">This post is part of a <a href="/build-a-programming-language/">series</a> about learning Rust and building a small programming language.</p><hr>'
|
||||||
```
|
```
|
||||||
|
|
||||||
Now that the parser can handle multiple statements and the usage of variables, let's add the ability to actually declare variables.
|
Now that the parser can handle multiple statements and the usage of variables, let's add the ability to actually declare variables.
|
||||||
@ -64,7 +64,7 @@ There are also a few methods for `Context`, one to construct a new context and o
|
|||||||
|
|
||||||
```rust
|
```rust
|
||||||
impl Context {
|
impl Context {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
variables: HashMap::new(),
|
variables: HashMap::new(),
|
||||||
}
|
}
|
||||||
@ -110,4 +110,3 @@ Integer(1)
|
|||||||
```
|
```
|
||||||
|
|
||||||
[^2]: The `dbg` function is a builtin I added that prints out the Rust version of the `Value` it's passed.
|
[^2]: The `dbg` function is a builtin I added that prints out the Rust version of the `Value` it's passed.
|
||||||
|
|
@ -4,7 +4,7 @@ tags = ["build a programming language", "rust"]
|
|||||||
date = "2021-06-29 19:14:42 -0400"
|
date = "2021-06-29 19:14:42 -0400"
|
||||||
short_desc = "Evaluating if statements and dealing with nested scopes."
|
short_desc = "Evaluating if statements and dealing with nested scopes."
|
||||||
slug = "lexical-scope"
|
slug = "lexical-scope"
|
||||||
preamble = '<p style="font-style: italic;">This post is part of a <a href="/build-a-programming-language/" data-link="/build-a-programming-language/">series</a> about learning Rust and building a small programming language.</p><hr>'
|
preamble = '<p class="italic">This post is part of a <a href="/build-a-programming-language/">series</a> about learning Rust and building a small programming language.</p><hr>'
|
||||||
```
|
```
|
||||||
|
|
||||||
After adding variables, I added boolean values and comparison operators, because why not. With that in place, I figured it would be a good time to add if statements. Parsing them is straightforward—you just look for the `if` keyword, followed by a bunch of stuff—so I won't go into the details. But actually evaluating them was a bit more complicated.
|
After adding variables, I added boolean values and comparison operators, because why not. With that in place, I figured it would be a good time to add if statements. Parsing them is straightforward—you just look for the `if` keyword, followed by a bunch of stuff—so I won't go into the details. But actually evaluating them was a bit more complicated.
|
||||||
@ -98,4 +98,3 @@ fn main() {
|
|||||||
$ cargo run
|
$ cargo run
|
||||||
Integer(1)
|
Integer(1)
|
||||||
```
|
```
|
||||||
|
|
@ -3,6 +3,7 @@ title = "Debugging My Gemini NWProtocolFramer Implementation"
|
|||||||
tags = ["swift", "gemini"]
|
tags = ["swift", "gemini"]
|
||||||
date = "2021-07-07 23:32:42 -0400"
|
date = "2021-07-07 23:32:42 -0400"
|
||||||
slug = "gemini-client-debugging"
|
slug = "gemini-client-debugging"
|
||||||
|
comments_post_id = "A94C2CRwCLrfHpkPPk"
|
||||||
```
|
```
|
||||||
|
|
||||||
I recently ran into an issue with the Network.framework Gemini client I'd [previously implemented](/2020/gemini-network-framework/) that turned out to be somewhat perplexing. So, I thought I'd write a brief post about it in case anyone finds it interesting or helpful.
|
I recently ran into an issue with the Network.framework Gemini client I'd [previously implemented](/2020/gemini-network-framework/) that turned out to be somewhat perplexing. So, I thought I'd write a brief post about it in case anyone finds it interesting or helpful.
|
||||||
@ -81,11 +82,10 @@ There's a note in the Network.framework header comments[^2] for `nw_framer_parse
|
|||||||
|
|
||||||
The possibility of a copy being needed to form a contiguous buffer implies that there could be discontiguous data, which lines up with my "chunks" hypothesis and would explain the behavior I observed.
|
The possibility of a copy being needed to form a contiguous buffer implies that there could be discontiguous data, which lines up with my "chunks" hypothesis and would explain the behavior I observed.
|
||||||
|
|
||||||
<aside>
|
<aside class="inline">
|
||||||
|
|
||||||
Fun fact, the C function corresponding to this Swift API, `nw_framer_parse_input`, takes a maximum length, but it also lets to pass in your own temporary buffer, in the form of a `uint8_t*`. It's therefore up to the caller to ensure that the buffer that's pointed to is at least as long as the maximum length. This seems like a place ripe for buffer overruns in sloppily written protocol framer implementations.
|
Fun fact, the C function corresponding to this Swift API, `nw_framer_parse_input`, takes a maximum length, but it also lets to pass in your own temporary buffer, in the form of a `uint8_t*`. It's therefore up to the caller to ensure that the buffer that's pointed to is at least as long as the maximum length. This seems like a place ripe for buffer overruns in sloppily written protocol framer implementations.
|
||||||
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
Anyhow, if you're interested, you can find the current version of my Gemini client implementation (as of this post) [here](https://git.shadowfacts.net/shadowfacts/Gemini/src/commit/3055cc339fccad99ab064f2daccdb65efa8024c0/GeminiProtocol/GeminiProtocol.swift).
|
Anyhow, if you're interested, you can find the current version of my Gemini client implementation (as of this post) [here](https://git.shadowfacts.net/shadowfacts/Gemini/src/commit/3055cc339fccad99ab064f2daccdb65efa8024c0/GeminiProtocol/GeminiProtocol.swift).
|
||||||
|
|
@ -3,6 +3,7 @@ title = "On SwiftUI"
|
|||||||
tags = ["swift"]
|
tags = ["swift"]
|
||||||
date = "2021-08-25 15:34:42 -0400"
|
date = "2021-08-25 15:34:42 -0400"
|
||||||
slug = "swiftui"
|
slug = "swiftui"
|
||||||
|
comments_post_id = "AAh41HcWJSC0MjhaFM"
|
||||||
```
|
```
|
||||||
|
|
||||||
Over the past several days, I built a complete, functioning app in SwiftUI, and, well, I have some thoughts.
|
Over the past several days, I built a complete, functioning app in SwiftUI, and, well, I have some thoughts.
|
||||||
@ -21,7 +22,7 @@ This was also the first project where I actually managed to use the Xcode Previe
|
|||||||
|
|
||||||
Building almost all of the UI was easy. The design is simple enough that it doesn't have to use any fancy tricks or workarounds—not a `GeometryReader` in sight. Plumbing up the model was similarly painless, both for editing and displaying data (aside from some oddities involving timing and refreshing codes).
|
Building almost all of the UI was easy. The design is simple enough that it doesn't have to use any fancy tricks or workarounds—not a `GeometryReader` in sight. Plumbing up the model was similarly painless, both for editing and displaying data (aside from some oddities involving timing and refreshing codes).
|
||||||
|
|
||||||
But, here's where the complaints start. Although the layout and design were uncomplicated, building some of the interactions were not. While working on it, SwiftUI felt incredibly powerful and uncomfortably restrictive simultaneously. For example:
|
But, here's where the complaints start. Although the layout and design were uncomplicated, building some of the interactions were not. While working on it, SwiftUI felt incredibly powerful and uncomfortably restrictive simultaneously. For example:
|
||||||
|
|
||||||
Want to make give a context menu action the destructive style? Ok. Want to make a nested menu destructive? Nope.
|
Want to make give a context menu action the destructive style? Ok. Want to make a nested menu destructive? Nope.
|
||||||
|
|
||||||
@ -38,4 +39,3 @@ I don't think this is just because I have more experience with UIKit. It's not t
|
|||||||
The impression I get from Apple is that SwiftUI wants to make hard things easy. That's a great goal, but currently it comes at the expense of making easy things complicated. And every time I do anything substantial with SwiftUI, I constantly feel this tension. SwiftUI both empowers me and hinders me, every step of the way.
|
The impression I get from Apple is that SwiftUI wants to make hard things easy. That's a great goal, but currently it comes at the expense of making easy things complicated. And every time I do anything substantial with SwiftUI, I constantly feel this tension. SwiftUI both empowers me and hinders me, every step of the way.
|
||||||
|
|
||||||
I don't know what the solution is, if there is one (part of me thinks these sorts of issues are intrinsic to declarative tools), but I really hope there is one. When it works, I really enjoy SwiftUI and I want to be able to enjoy it more without running into unresolvable framework issues quite so frequently.
|
I don't know what the solution is, if there is one (part of me thinks these sorts of issues are intrinsic to declarative tools), but I really hope there is one. When it works, I really enjoy SwiftUI and I want to be able to enjoy it more without running into unresolvable framework issues quite so frequently.
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user