Compare commits

...

93 Commits
main ... v7

Author SHA1 Message Date
ea05eb2aa7 Fix tutorials not being sorted 2025-02-12 17:30:23 -05:00
ed3d576c04 Move tutorials to new site 2025-02-12 17:30:23 -05:00
bb7fa84e26 And SNW and Silo seasons 2 2025-02-12 17:30:22 -05:00
717c661c75 Fix TV index not being sorted 2025-02-12 17:30:22 -05:00
5460b73230 Move TV to new site 2025-02-12 17:30:22 -05:00
e879168188 Pull in comments from social.shadowfacts.net 2025-02-12 17:30:22 -05:00
07172d9975 Re-add blog post that got lost in the v6 rewrite 2025-02-12 11:40:32 -05:00
3c76daed1e Remove unused article-content-wide classes 2025-02-12 10:55:31 -05:00
86bd8a39ee Fix font subsetting for webring 2025-02-12 10:50:38 -05:00
e2abb6873f Polish blog posts for new design 2025-02-12 10:50:02 -05:00
471887c885 Move static content to new site 2025-02-11 21:15:03 -05:00
7e06d3060c More archive page tweaks 2025-02-11 21:08:16 -05:00
2115ba49c9 Move posts to new site 2025-02-11 21:02:19 -05:00
6cf9d85516 Fix archive not being sorted 2025-02-11 21:01:58 -05:00
76f6244814 Edit Belief 2025-02-11 20:55:32 -05:00
864b171197 Belief 2025-02-11 20:55:32 -05:00
593ed74eb7 More tweaks 2025-02-11 20:55:32 -05:00
6f6e1453f7 Use pyftsubset for font subsetting 2025-02-11 14:36:09 -05:00
1629a7e30c Character set extraction 2025-01-14 21:42:51 -05:00
0be6307a41 Test fixes 2025-01-14 12:53:19 -05:00
b865e97b29 Assorted style tweaks 2025-01-14 11:02:37 -05:00
22cbe75dc2 Add modding tutorials 2025-01-13 16:45:35 -05:00
49feefaedc Add 404 page 2025-01-13 15:15:20 -05:00
009e9cfcb1 Add tv 2025-01-13 15:07:50 -05:00
9a13efbd35 Misc stuff 2025-01-13 00:33:58 -05:00
1e772a97e2 Homepage 2025-01-12 19:04:58 -05:00
de9291cd50 Article content element styles 2025-01-12 16:34:15 -05:00
4601874c64 Fix non-inline asides on mobile 2025-01-12 16:17:21 -05:00
f0ef67f9a0 Article metadata 2025-01-12 16:08:06 -05:00
3adf6031a4 Syntax highlighting, color tweaks 2025-01-12 15:48:13 -05:00
64332a137b Add rss feed 2025-01-11 23:34:07 -05:00
f5c7c14d2e Copy static files 2025-01-11 22:09:03 -05:00
494f2ad367 Footnotes/sidenotes 2025-01-11 16:53:27 -05:00
1279a1755d Improve InputVisitable derive macro 2025-01-05 00:07:13 -04:00
55b91944b2 Fix posts not being written to new path on permalink changing 2025-01-04 14:59:28 -05:00
657f90c39c Start new design 2025-01-04 14:59:15 -05:00
a9a6b85c5f Remove unused markdown stage 2025-01-04 14:57:52 -05:00
a0c4c06de7 Live reload 2025-01-03 12:38:43 -05:00
701350a269 Use hyper instead of axum 2025-01-02 19:19:39 -05:00
876d28fac6 Dev server 2025-01-02 16:10:47 -05:00
0b8717aa2b Inject fonts into css 2025-01-02 13:15:06 -05:00
ec0746011e Add homepage 2025-01-02 11:56:43 -05:00
5d795c3084 SCSS compilation 2025-01-01 23:31:50 -05:00
60858bde24 Use tera templates, incorporate templates into the graph 2025-01-01 18:54:36 -05:00
1253999961 More compute_graph changes 2025-01-01 18:00:49 -05:00
f467025569 Don't print qualified self in node type names 2025-01-01 17:04:42 -05:00
2c1b9c620e File watching 2024-12-31 18:59:22 -05:00
640c0ab620 Allow dynamic nodes to add invalidatable rules 2024-12-31 18:49:00 -05:00
f44f525c2c Actually make removing dynamic node children work 2024-12-31 18:21:17 -05:00
6bb51638cc Output stuff 2024-12-31 14:29:11 -05:00
9ff658f719 Start on the graph generator implementation 2024-12-30 18:35:07 -05:00
20653c2da5 Merge branch 'main' into v7 2024-12-30 15:57:07 -05:00
d92ebf11b2 Dynamic rules 2024-12-29 13:37:54 -05:00
9cb6a8c6ce Start implementing graph-backed generator 2024-11-05 11:32:01 -05:00
8f0fe08ecc Pretty type names in graphviz dump, bring back PartialEq/NodeValue impl 2024-11-05 11:32:01 -05:00
b8ad929d0b Fix derive macro with generics 2024-11-03 15:02:18 -05:00
e69014d98d Assorted graph tweaks 2024-11-03 14:42:45 -05:00
712b528ca8 Add graphviz dump 2024-11-03 13:13:51 -05:00
dd1143aa9b Better cycle error 2024-11-03 10:50:05 -05:00
04bd5cf8c4 Fix async fn in trait warning 2024-11-03 01:30:49 -04:00
08a4bf87dc Derive macro 2024-11-03 01:27:51 -04:00
36bcbe3c9c Simplify Synchronicity trait 2024-11-02 23:56:51 -04:00
6d1e505590 Don't borrow_mut excessively 2024-11-02 19:29:29 -04:00
8c761fe0d4 Use Input for ValueInvalidationSignal 2024-11-02 19:21:30 -04:00
b79edeef0a Add with_output and into_builder 2024-11-02 19:19:15 -04:00
365d2db571 Make InvalidationSignal not generic over synchronicity 2024-11-02 19:08:15 -04:00
a556b14188 Add GraphBuilder::add_async_value 2024-11-02 18:49:29 -04:00
88dfef75fd Document all the things 2024-11-02 18:39:07 -04:00
c1c594d4f7 Move input to rule.rs 2024-11-02 12:10:00 -04:00
b73d205456 Rename crate 2024-11-02 11:31:38 -04:00
02b5226a90 Add invalidatable value node 2024-11-02 11:15:04 -04:00
05348a5dbc Move rules to rule.rs 2024-11-02 10:56:32 -04:00
c18c1ced59 Change modify to take ownership of the graph
If the modify fails, the graph is left in a bad state, so the client
shouldn't be able to continue using it
2024-11-01 11:43:35 -04:00
1d1673e5ee Only update downstream nodes if an input changes 2024-11-01 11:35:17 -04:00
de025dc138 Walk topological sort to update nodes 2024-11-01 11:11:27 -04:00
a6e94340ee Split graph crate into multiple files 2024-10-31 23:30:07 -04:00
6de1999b8d Rename things 2024-10-31 23:13:09 -04:00
7dbcd4963f Don't require Clone for node values 2024-10-31 23:10:42 -04:00
1ac8f4ead4 Use structs not enums for Synchronicity types 2024-10-31 22:46:36 -04:00
5998bbe116 Remove unused nightly features 2024-10-31 11:50:12 -04:00
e034f30455 Self doesn't need to be mutable when visting rule inputs 2024-10-31 11:35:43 -04:00
ca0b77349a Use associated types for rule outputs 2024-10-31 11:19:16 -04:00
d8f2a393ba Use a struct for NodeGraph 2024-10-31 11:03:58 -04:00
7c554f731a Remove Synchronicity::AnyStorage indirection 2024-10-31 11:01:11 -04:00
1530933464 Make the graph generic over whether it's sync/async 2024-10-30 23:32:45 -04:00
81cd986f77 Invalidate necessary parts of the graph after modification 2024-10-30 00:10:50 -04:00
67fb9db461 Cleanup 2024-10-29 16:20:45 -04:00
140c6a67fd Remove parts of the graph unused by the output node 2024-10-29 15:00:23 -04:00
bd2cdba5bc Move graph to separate crate 2024-10-29 15:00:23 -04:00
29838e2113 Modify graph 2024-10-29 14:18:53 -04:00
67ddf2f254 More tests 2024-10-29 11:10:13 -04:00
b7d0271f4e Consolidate external inputs with rules 2024-10-29 11:02:34 -04:00
3b943cb828 Core graph structure 2024-10-28 22:17:58 -04:00
330 changed files with 29388 additions and 3234 deletions

3
.gitmodules vendored Normal file
View 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

File diff suppressed because it is too large Load Diff

3023
Cargo.lock.bak Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,14 @@
workspace = { members = [
"crates/compute_graph",
"crates/compute_graph_macros",
"crates/derive_test",
"crates/pyftsubset",
] }
[package]
name = "v6"
name = "v7"
version = "0.1.0"
edition = "2021"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -10,63 +17,54 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[dependencies]
activitystreams = "0.7.0-alpha.22"
activitystreams-ext = "0.1.0-alpha.3"
ammonia = "3.2"
anyhow = "1.0"
askama = "0.11.1"
axum = "0.5.6"
base64 = "0.13"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "3.1", features = ["cargo"] }
env_logger = "0.9"
futures = "0.3"
html5ever = "0.26"
http-signature-normalization = "0.6"
hyper = "0.14"
log = "0.4"
markup5ever_rcdom = "0.2"
mime = "0.3"
notify = "5.0.0-pre.16"
notify-debouncer-mini = { version = "*", default-features = false }
once_cell = "1.13"
# NOTE: openssl version also needs to be updated in ios target config below
openssl = "0.10"
pulldown-cmark = "0.9"
regex = "1.5"
reqwest = { version = "0.11", features = ["json"] }
rsass = "0.25"
rss = { version = "2.0", features = ["atom"] }
ahash = "0.8.11"
anyhow = "1.0.95"
base64 = "0.22.1"
bit-vec = "0.8.0"
bitflags = "2.7.0"
chrono = { version = "0.4.39", features = ["serde"] }
clap = { version = "4.5.23", features = ["cargo"] }
compute_graph = { path = "crates/compute_graph" }
debounced = "0.2.0"
env_logger = "0.11.6"
futures = "0.3.31"
grass = { version = "0.13.4", default-features = false, git = "https://git.shadowfacts.net/shadowfacts/grass.git", branch = "custom-global-variables" }
grass_compiler = { version = "0.13.4", features = [
"custom-builtin-fns",
], git = "https://git.shadowfacts.net/shadowfacts/grass.git", branch = "custom-global-variables" }
html5ever = "0.27.0"
http = "1.2.0"
http-body = "1.0.1"
http-body-util = "0.1.2"
hyper = { version = "1.5.2", features = ["server", "http1"] }
hyper-tungstenite = "0.17.0"
hyper-util = { version = "0.1.10", features = ["tokio", "service"] }
log = "0.4.22"
markup5ever_rcdom = "0.3.0"
notify = "7.0.0"
once_cell = "1.20.2"
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_json = "1.0"
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"] }
thiserror = "1.0"
tokio = { version = "1.18", features = ["full"] }
tokio-cron-scheduler = "0.8"
tokio-stream = { version = "0.1.8", features = ["fs"] }
toml = "0.5"
tower = "0.4"
tower-http = { version = "0.3", features = ["fs"] }
# should be from crates.io
tree-sitter-bash = { version = "0.20", git = "https://git.shadowfacts.net/shadowfacts/tree-sitter-bash.git" }
tree-sitter-c = "0.20"
# should be https://github.com/tree-sitter/tree-sitter-css.git
tree-sitter-css = { version = "0.20", git = "https://git.shadowfacts.net/shadowfacts/tree-sitter-css.git" }
tree-sitter-elixir = { version = "0.19", git = "https://github.com/elixir-lang/tree-sitter-elixir.git" }
tree-sitter-highlight = "0.20"
# should be from crates.io
tree-sitter-html = { version = "0.20", git = "https://git.shadowfacts.net/shadowfacts/tree-sitter-html.git" }
# should be from crates.io
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"] }
tera = "1.20.0"
tokio = { version = "1.42.0", features = ["full"] }
tokio-stream = "0.1.17"
toml = "0.8.19"
tower = { version = "0.5.2", features = ["steer", "util"] }
tower-http = { version = "0.6.2", features = ["fs"] }
tree-sitter-bash = "0.23.3"
tree-sitter-c = "0.23.4"
tree-sitter-css = "0.23.2"
tree-sitter-elixir = "0.3.3"
tree-sitter-highlight = "0.24.6"
tree-sitter-html = "0.23.2"
tree-sitter-java = "0.23.5"
tree-sitter-javascript = "0.23.1"
tree-sitter-json = "0.24.8"
tree-sitter-objc = "3.0.2"
tree-sitter-rust = "0.23.2"
unicode-normalization = "0.1.24"

79
Cargo.toml.bak Normal file
View File

@ -0,0 +1,79 @@
workspace = { members = [
"crates/compute_graph",
"crates/compute_graph_macros",
"crates/derive_test",
] }
[package]
name = "v6"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[dependencies]
activitystreams = "0.7.0-alpha.22"
activitystreams-ext = "0.1.0-alpha.3"
ammonia = "3.2"
anyhow = "1.0"
askama = "0.11.1"
axum = "0.5.6"
base64 = "0.13"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "3.1", features = ["cargo"] }
compute_graph = { path = "crates/compute_graph" }
env_logger = "0.9"
futures = "0.3"
html5ever = "0.26"
http-signature-normalization = "0.6"
hyper = "0.14"
log = "0.4"
markup5ever_rcdom = "0.2"
mime = "0.3"
notify = "5.0.0-pre.16"
notify-debouncer-mini = { version = "*", default-features = false }
once_cell = "1.13"
# NOTE: openssl version also needs to be updated in ios target config below
openssl = "0.10"
pulldown-cmark = "0.9"
regex = "1.5"
reqwest = { version = "0.11", features = ["json"] }
rsass = "0.25"
rss = { version = "2.0", features = ["atom"] }
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" }
sqlx = { version = "0.5", features = ["runtime-tokio-native-tls", "sqlite"] }
thiserror = "1.0"
tokio = { version = "1.18", features = ["full"] }
tokio-cron-scheduler = "0.8"
tokio-stream = { version = "0.1.8", features = ["fs"] }
toml = "0.5"
tower = "0.4"
tower-http = { version = "0.3", features = ["fs"] }
# should be from crates.io
tree-sitter-bash = { version = "0.20", git = "https://git.shadowfacts.net/shadowfacts/tree-sitter-bash.git" }
tree-sitter-c = "0.20"
# should be https://github.com/tree-sitter/tree-sitter-css.git
tree-sitter-css = { version = "0.20", git = "https://git.shadowfacts.net/shadowfacts/tree-sitter-css.git" }
tree-sitter-elixir = { version = "0.19", git = "https://github.com/elixir-lang/tree-sitter-elixir.git" }
tree-sitter-highlight = "0.20"
# should be from crates.io
tree-sitter-html = { version = "0.20", git = "https://git.shadowfacts.net/shadowfacts/tree-sitter-html.git" }
# should be from crates.io
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"] }

View File

@ -1,2 +0,0 @@
[general]
dirs = ["site"]

View File

@ -2,9 +2,6 @@ use serde::Deserialize;
use std::process::Command;
fn main() {
// trigger recompilation when a new migration is added
println!("cargo:rerun-if-changed=migrations");
let target = get_swift_target_info();
if target.target.unversioned_triple.contains("linux") {
target.paths.runtime_library_paths.iter().for_each(|path| {

View 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"] }

View 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, 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, InputVisitable}};
/// let mut builder = GraphBuilder::new();
/// # #[derive(InputVisitable)]
/// # struct IncrementAfterEvaluate(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"),
}
}
}

View File

@ -0,0 +1,137 @@
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()
}
}
// 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()
}
}
// 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) {}
}

View File

@ -0,0 +1,975 @@
//! 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, 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, 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,
ctx: NodeUpdateContext<S>,
) -> UpdateStepResult {
let mut graph = self.node_graph.borrow_mut();
let mut nodes_changed = false;
for idx_to_remove in ctx.removed_nodes {
assert!(
idx_to_remove != current_idx,
"cannot remove node curently being evaluated"
);
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)
}
}
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()
}
}
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)
}
}
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);
}
}

View File

@ -0,0 +1,756 @@
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::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>,
>,
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: Vec<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: vec![],
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)
}),
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);
}
}
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 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 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 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 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 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 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 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(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 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 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())
}
}
}

View File

@ -0,0 +1,274 @@
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, 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, 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>>;
#[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()
}
}
/// 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;
#[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) {}
}

View 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(()))
}
}

View 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));
}
}

View File

@ -0,0 +1,7 @@
digraph {
0 [label ="ConstNode<i32> (id=0)"]
1 [label ="ConstNode<i32> (id=1)"]
2 [label ="RuleNode<compute_graph::tests::Add> (id=2)"]
0 -> 2 []
1 -> 2 []
}

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 10.0.1 (20240210.2158)
-->
<!-- Pages: 1 -->
<svg width="432pt" height="116pt"
viewBox="0.00 0.00 432.02 116.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 112)">
<polygon fill="white" stroke="none" points="-4,4 -4,-112 428.02,-112 428.02,4 -4,4"/>
<!-- 0 -->
<g id="node1" class="node">
<title>0</title>
<ellipse fill="none" stroke="black" cx="101.51" cy="-90" rx="101.51" ry="18"/>
<text text-anchor="middle" x="101.51" y="-84.95" font-family="Times,serif" font-size="14.00">ConstNode&lt;i32&gt; (id=0)</text>
</g>
<!-- 2 -->
<g id="node3" class="node">
<title>2</title>
<ellipse fill="none" stroke="black" cx="211.51" cy="-18" rx="185.96" ry="18"/>
<text text-anchor="middle" x="211.51" y="-12.95" font-family="Times,serif" font-size="14.00">RuleNode&lt;compute_graph::tests::Add&gt; (id=2)</text>
</g>
<!-- 0&#45;&gt;2 -->
<g id="edge1" class="edge">
<title>0&#45;&gt;2</title>
<path fill="none" stroke="black" d="M127.86,-72.23C142.07,-63.19 159.82,-51.89 175.31,-42.03"/>
<polygon fill="black" stroke="black" points="176.78,-45.25 183.33,-36.93 173.02,-39.34 176.78,-45.25"/>
</g>
<!-- 1 -->
<g id="node2" class="node">
<title>1</title>
<ellipse fill="none" stroke="black" cx="322.51" cy="-90" rx="101.51" ry="18"/>
<text text-anchor="middle" x="322.51" y="-84.95" font-family="Times,serif" font-size="14.00">ConstNode&lt;i32&gt; (id=1)</text>
</g>
<!-- 1&#45;&gt;2 -->
<g id="edge2" class="edge">
<title>1&#45;&gt;2</title>
<path fill="none" stroke="black" d="M295.92,-72.23C281.58,-63.19 263.67,-51.89 248.04,-42.03"/>
<polygon fill="black" stroke="black" points="250.26,-39.3 239.93,-36.92 246.52,-45.22 250.26,-39.3"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View 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"

View 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,
}
}

View File

@ -0,0 +1,7 @@
[package]
name = "derive_test"
version = "0.1.0"
edition = "2021"
[dependencies]
compute_graph = { path = "../compute_graph" }

View 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);
}
}

View 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"

@ -0,0 +1 @@
Subproject commit d6f40c2e453f3c07807fef7926a31717f180b660

View 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
}

View File

@ -9,8 +9,8 @@
<ul>
{% for post in posts %}
<li>
<a href="{{ post.permalink() }}">
{{ post.metadata.title }}
<a href="{{ post.permalink }}">
{{ post.title }}
</a>
</li>
{% endfor %}

View File

@ -249,6 +249,10 @@ article {
&.left { float: left; }
&.right { float: right; }
}
em {
font-style: normal;
}
}
p code {
@ -329,11 +333,15 @@ article {
font-family: var(--monospace-font);
color: var(--secondary-ui-text-color);
}
em::before, em::after {
content: "_";
font-family: var(--monospace-font);
color: var(--secondary-ui-text-color);
}
em::before, em::after {
content: "_";
font-family: var(--monospace-font);
color: var(--secondary-ui-text-color);
}
blockquote em::before,
blockquote em::after {
content: none;
}
s::before, s::after {
content: "~~";
font-family: var(--monospace-font);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

BIN
site/static/island-silent.mp4 (Stored with Git LFS)

Binary file not shown.

BIN
site/static/notif-center.mp4 (Stored with Git LFS)

Binary file not shown.

11
site_test/404.html Normal file
View File

@ -0,0 +1,11 @@
{% extends "default" %}
{% block titlevariable %}
{% set title = "Not Found" %}
{% endblock %}
{% block content -%}
<h1 class="headline">404 Not Found</h1>
{%- endblock %}

27
site_test/archive.html Normal file
View File

@ -0,0 +1,27 @@
{% extends "default" %}
{% block titlevariable %}
{% set title = "Archive" %}
{% endblock %}
{% block content -%}
<h1>Archive</h1>
{% for year in years %}
<div class="archive-list">
{% for entry in posts_by_year[year] %}
<div class="archive-entry">
<code><time datetime="{{ entry.date | iso_datetime }}">{{ entry.date | iso_date }}</time></code>
<a href="{{ entry.permalink }}">
{{ entry.title }}
</a>
</div>
{% endfor %}
</div>
{% if not loop.last %}
<hr>
{% endif %}
{% endfor %}
{%- endblock %}

View File

@ -0,0 +1,27 @@
<!--<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>-->
<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: 1.1 KiB

57
site_test/css/fonts.scss Normal file
View File

@ -0,0 +1,57 @@
@font-face {
font-family: "Valkyrie A";
font-style: normal;
font-weight: normal;
font-stretch: normal;
font-display: auto;
src: url("data:font/woff2;base64," + $valkyrie-a-regular) format("woff2");
}
@font-face {
font-family: "Valkyrie A";
font-style: italic;
font-weight: normal;
font-stretch: normal;
font-display: auto;
src: url("data:font/woff2;base64," + $valkyrie-a-italic) format("woff2");
}
@font-face {
font-family: "Valkyrie A";
font-style: normal;
font-weight: bold;
font-stretch: normal;
font-display: auto;
src: url("data:font/woff2;base64," + $valkyrie-a-bold) format("woff2");
}
@font-face {
font-family: "Valkyrie A";
font-style: italic;
font-weight: bold;
font-stretch: normal;
font-display: auto;
src: url("data:font/woff2;base64," + $valkyrie-a-bold-italic)
format("woff2");
}
@font-face {
font-family: "Berkeley Mono";
src: url("data:font/woff2;base64," + $berkeley-mono-regular) format("woff2");
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: "Berkeley Mono";
src: url("data:font/woff2;base64," + $berkeley-mono-italic) format("woff2");
font-weight: normal;
font-style: italic;
}
@font-face {
font-family: "Berkeley Mono";
src: url("data:font/woff2;base64," + $berkeley-mono-bold) format("woff2");
font-weight: bold;
font-style: normal;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

454
site_test/css/main.scss Normal file
View File

@ -0,0 +1,454 @@
@import "normalize.scss";
@import "fonts.scss";
@import "syntax-highlighting.scss";
$page-horizontal-margin: 2rem;
$mobile-breakpoint: 480px;
$container-max-width: 768px;
$link-color: #c21e1e;
:root {
// solarized-base2
--background-color: #eee8d5;
--text-color: black;
--secondary-text-color: #656565;
--link-color: #c21e1e;
--page-vertical-margin: 2rem;
}
.container {
max-width: $container-max-width;
margin: 0 $page-horizontal-margin / 2;
}
@media (min-width: calc($container-max-width + 2 * $page-horizontal-margin)) {
.container {
margin: 0 $page-horizontal-margin;
}
}
a,
a:visited {
color: var(--link-color);
text-decoration-thickness: 2px;
text-underline-offset: 2px;
}
a:hover {
text-decoration-thickness: 3px;
}
a[href^="http://"]:not(.no-external-link-decoration)::after,
a[href^="https://"]:not(.no-external-link-decoration)::after
{
background-color: currentColor;
content: "";
width: calc(max(0.667em, 12px));
height: calc(max(0.667em, 12px));
margin-left: 0.333em;
display: inline-block;
mask: url("data:image/svg+xml;base64," + $external-link) no-repeat 50% 50%;
mask-size: cover;
}
pre,
code {
font-family: "Berkeley Mono";
font-size: 0.9em;
}
pre {
overflow-x: scroll;
tab-size: 4;
word-wrap: normal;
}
blockquote {
position: relative;
font-style: italic;
&::before {
content: open-quote;
font-size: 2em;
position: absolute;
left: -25px;
top: -10px;
}
em {
font-style: normal;
}
}
img {
display: block;
margin: 0 auto;
max-width: 100%;
}
figure {
margin: 0;
figcaption {
margin-top: 4px;
font-size: 1.1rem;
font-style: italic;
color: var(--secondary-text-color);
text-align: center;
}
}
table {
width: 100%;
border-collapse: collapse;
border: 1px solid var(--secondary-text-color);
tr,
th,
td {
border: 1px solid var(--secondary-text-color);
}
td,
th {
padding: 0 0.5em;
text-align: left;
}
thead > tr,
tbody > tr:nth-child(even) {
// darkened solarized base2
background: darken(#eee8d5, 5%);
}
}
hr {
border: none;
border-top: 1px solid var(--secondary-text-color);
}
.italic {
font-style: italic;
}
html {
font-family: "Valkyrie A", Charter, serif;
font-size: 16px;
/* background-color: #dfd3c3; */
background-color: var(--background-color);
/* background-color: #e8dbc5; */
color: var(--text-color);
}
header {
margin-top: var(--page-vertical-margin);
font-style: italic;
h1 {
font-size: 5rem;
margin: 0;
a {
color: var(--text-color) !important;
transition: text-decoration-thickness 0.1s ease-in-out;
text-decoration-thickness: 4px;
text-underline-offset: 0.5rem;
&:hover {
text-decoration-thickness: 8px;
}
}
}
p {
margin-top: 0.25rem;
font-size: 1.5rem;
font-weight: lighter;
text-wrap: balance;
}
}
@media (max-width: $mobile-breakpoint) {
header h1 {
font-size: 4rem;
}
}
.headline {
// balance the number of words per line
text-wrap: balance;
&:has(+ .article-meta) {
margin-bottom: 0.2em;
}
}
.article-meta {
margin-top: 0;
color: var(--secondary-text-color);
text-wrap: pretty;
}
.body-content {
font-size: 1.25rem;
line-height: 1.4;
// Chrome only, but minimizes orphan words
text-wrap: pretty;
}
.header-anchor {
text-decoration: none;
font-size: 1rem;
vertical-align: middle;
// drag it up so it's more in the middle
padding-bottom: 0.25rem;
color: var(--secondary-text-color) !important;
background: none;
&:hover {
color: var(--link-color) !important;
}
}
.footnote-reference {
> a {
text-decoration: none;
&[href^="#fn-"] {
display: none;
}
}
}
.footnote-reference:hover + .sidenote,
.sidenote:hover,
aside:not(.inline):hover {
color: black;
a {
color: var(--link-color);
}
}
.sidenote,
aside:not(.inline) {
float: right;
margin-right: -50%;
width: 40%;
font-size: 1rem;
color: var(--secondary-text-color);
transition: color 0.2s ease-in-out;
display: block;
.sidenote-p {
margin: 20px 0;
display: block;
}
a {
color: lighten($link-color, 20%);
transition: color 0.2s ease-in-out;
}
}
aside:not(.inline) {
transform: translateY(-50%);
p:first-child {
margin-top: 0;
}
p:last-child {
margin-bottom: 0;
}
}
.tv-show-entry {
margin-bottom: 1rem;
> summary {
> h2 {
display: inline;
}
}
}
.footnote {
display: none;
flex-direction: row;
gap: 8px;
font-size: 1rem;
.footnote-marker {
flex-shrink: 0;
width: 34px;
text-align: right;
}
.footnote-backref {
text-decoration: none;
}
> div > p:first-child {
margin-top: 0;
}
> div > p:last-child {
margin-top: 0;
}
}
aside.inline {
font-size: 1rem;
background-color: lighten($link-color, 43%);
padding: 1rem;
p:first-child {
margin-top: 0;
}
p:last-child {
margin-bottom: 0;
}
}
// 1.5 to account for -50% margin-right on .sidenote
// plus page-horizontal-margin to effectively use that as the margin
@media (max-width: calc(1.5 * $container-max-width + 2 * $page-horizontal-margin)) {
.footnote-reference {
> a[href^="#fn-"] {
display: inline;
}
> a[href^="#fnref-"] {
display: none;
}
}
.sidenote {
display: none;
}
.footnote {
display: flex;
}
aside:not(.inline) {
float: none;
margin-right: 0;
width: auto;
transform: none;
background-color: lighten($link-color, 43%);
padding: 1rem;
}
}
footer {
margin-bottom: var(--page-vertical-margin);
font-style: italic;
font-size: 1.25rem;
ul {
padding: 0;
list-style: none;
}
}
@media (max-width: $mobile-breakpoint) {
footer {
font-size: 1rem;
}
}
.webring {
--webring-gradient: linear-gradient(
90deg,
#855988,
#6b4984,
#483475,
#2b2f77,
#141852
);
background: var(--webring-gradient);
background-clip: text;
position: relative;
&::after {
content: "";
width: 100%;
height: 2px;
position: absolute;
bottom: 2px;
left: 0;
background: var(--webring-gradient);
}
&:hover::after {
height: 3px;
bottom: 1px;
}
a {
color: transparent;
text-decoration: none;
position: relative;
}
}
.archive-list {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 1.25rem;
margin: 1rem 0;
.archive-entry {
display: flex;
flex-direction: row;
align-items: start;
gap: 8px;
time {
color: var(--secondary-text-color);
}
&:hover time {
color: var(--text-color);
}
}
}
#comments-container {
margin: 10px 0;
summary > h2 {
display: inline-block;
margin: 0;
cursor: pointer;
vertical-align: middle;
}
.comment {
padding: 10px;
border: 1px solid var(--secondary-text-color);
&:not(:last-child) {
margin-bottom: 15px;
}
}
.comment-user {
display: flex;
flex-direction: row;
gap: 5px;
align-items: top;
img {
width: 50px;
height: 50px;
border-radius: 5px;
}
.comment-user-name {
flex-grow: 1;
align-self: center;
h3 {
margin: 0;
}
}
}
.comment-body {
margin-top: 10px;
p {
margin: 10px 0;
&:last-child {
margin-bottom: 0;
}
}
}
}

351
site_test/css/normalize.scss vendored Normal file
View File

@ -0,0 +1,351 @@
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input {
/* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select {
/* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}

View File

@ -0,0 +1,67 @@
:root {
--solarized-base01: #586e75;
--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 {
display: block;
overflow-x: auto;
padding: 0.5em;
color: var(--solarized-base01);
// darkened base2
background: darken(#eee8d5, 5%);
}
.highlight > code {
display: block;
}
.hl-cmt {
color: var(--solarized-base00);
font-style: italic;
}
.hl-kw,
.hl-const {
color: var(--solarized-green);
}
.hl-punct-sp,
.hl-tag {
color: var(--solarized-blue);
}
.hl-emb {
color: var(--solarized-base01);
}
.hl-attr,
.hl-mod,
.hl-key,
.hl-prop {
color: var(--solarized-red);
}
.hl-str {
color: var(--solarized-cyan);
}
.hl-builtin,
.hl-type,
.hl-num {
color: var(--solarized-yellow);
}
.hl-fn {
color: var(--solarized-orange);
}

72
site_test/index.html Normal file
View File

@ -0,0 +1,72 @@
{% extends "default" %}
{% block footer_vars %}
{% set footer_links = false %}
{% endblock %}
{% block content -%}
<h2 class="headline">About Me</h2>
<div class="body-content">
<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&rsquo;t pay me to build software (myself included).
I mostly write about building software on here.
That probably doesn&rsquo;t come as a shock.
</p>
</div>
<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 }}">
{{ 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 }}&nbsp;minute&nbsp;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 %}
{% block footer_links %}
{% set additional_links = [
"Book Log", "/books/",
"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&amp;from=shadowfacts" class="no-external-link-decoration"></a>
<a title="next page in webring" href="https://metro.bieszczady.pl/cgi-bin/webring?action=next&amp;from=shadowfacts" class="no-external-link-decoration"></a>
</span>
</p>
{% endblock %}

View File

@ -0,0 +1,77 @@
{% extends "default" %}
{% block titlevariable %}
{% set title = metadata.title %}
{% endblock %}
{% block head -%}
<meta property="og:type" content="article">
{% if metadata.short_desc %}
<meta property="og:description" content="{{ metadata.short_desc }}">
{% else %}
<meta property="og:description" content="The outer part of a shadow is called the penumbra.">
{% endif %}
{%- endblock %}
{% block image %}
{% if metadata.card_image_path %}
<meta property="twitter:image" content="https://{{ _domain }}{{ metadata.card_image_path }}">
<meta property="og:image" content="https://{{ _domain }}{{ metadata.card_image_path }}">
{% else %}
<meta property="twitter:image" content="https://{{ _domain }}/shadowfacts.png">
<meta property="og:image" content="https://{{ _domain }}/shadowfacts.png">
{% endif %}
{% endblock %}
{% block content -%}
<article itemprop="blogPost" itemscope itemtype="https://schema.org/BlogPosting">
<meta itemprop="mainEntityOfPage" content="https://{{ _domain }}{{ _permalink }}">
<h1 class="headline" itemprop="name headline">
{% if metadata.html_title %}
{{ metadata.html_title }}
{% else %}
{{ metadata.title }}
{% endif %}
</h1>
<p class="article-meta">
Published on
<time itemprop="datePublished" datetime="{{ metadata.date | iso_datetime }}">
{{ metadata.date | pretty_date }},
</time>
in
{% for tag in metadata.tags %}
<span itemprop="articleSection">
<a href="/{{ tag.slug }}/">{{ tag.name }}</a>{% if loop.last %}.{% else %},{% endif %}
</span>
{% endfor %}
<span title="{{ word_count }} word{% if word_count != 1 %}s{% endif %}">
{{ word_count | reading_time }} minute read.
</span>
</p>
<div class="body-content" itemprop="articleBody">
{% if metadata.preamble %}
{{ metadata.preamble }}
{% endif %}
{{ content }}
</div>
</article>
{% if metadata.comments_post_id %}
<hr>
<script>
const commentsPostID = "{{ metadata.comments_post_id }}";
</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>
</details>
{% endif %}
<script src="/js/comments.js?{{ _stylesheet_cache_buster }}" async></script>
<hr>
{%- endblock %}

View File

@ -0,0 +1,86 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
{% block titlevariable %}
{% set title = "Shadowfacts" %}
{% endblock %}
<title>{{ title }}</title>
<link rel="cannonical" href="https://{{ _domain }}{{ _permalink }}">
<link rel="alternate" type="application/rss+xml" title="Shadowfacts" href="https://{{ _domain }}/feed.xml">
<link rel="icon" href="/favicon.ico">
<link rel="apple-touch-icon-precomposed" href="/favicon-152.png">
<meta name="msapplication-TileColor" content="#F9C72F">
<meta name="msapplication-TileImage" content="/favicon-152.png">
<meta name="twitter:card" content="summary">
<meta property="og:title" content="{{ title }}">
{% block image %}
<meta property="twitter:image" content="https://{{ _domain }}/shadowfacts.png">
<meta property="og:image" content="https://{{ _domain }}/shadowfacts.png">
{% endblock %}
<meta property="og:url" content="https://{{ _domain }}{{ _permalink }}">
<meta property="og:site_name" content="Shadowfacts">
<meta name="fediverse:creator" content="@shadowfacts@social.shadowfacts.net">
{% block head %}{% endblock %}
<link rel="stylesheet" href="/css/main.css?{{ _stylesheet_cache_buster }}">
</head>
<body itemscope itemtype="https://schema.org/Blog">
<header>
<div class="container">
<h1><a href="/">Shadowfacts</a></h1>
<p>The outer part of a shadow is called the penumbra.</p>
</div>
</header>
<main>
<div class="container">
{% block content %}{% endblock %}
</div>
</main>
<footer>
<div class="container">
<ul>
{% set footer_links = [
"Archive", "/archive/",
"Colophon", "/colophon/",
"Contact", "/elsewhere/"
] %}
{% block footer_links %}
{% set additional_links = [] %}
{% endblock %}
{% set sorted_links = footer_links | concat(with=additional_links) | zip | sort(attribute="0") %}
{% for link in sorted_links %}
<li><a href="{{ link.1 }}">{{ link.0 }}</a></li>
{% endfor %}
</ul>
{% block after_footer_links %}
{% endblock %}
<p>Generated on {{ _generated_at | pretty_date }}, by <a href="https://git.shadowfacts.net/shadowfacts/v7">v7</a>.</p>
</div>
</footer>
<script data-goatcounter="https://shadowfacts.goatcounter.com/count" async 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>
</html>

View 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 %}

27
site_test/layout/tag.html Normal file
View File

@ -0,0 +1,27 @@
{% extends "default" %}
{% block titlevariable %}
{% set title = tag_name ~ " posts" %}
{% endblock %}
{% block content -%}
<h1>Posts tagged &lsquo;{{ tag_name }}&rsquo;</h1>
{% for year in years %}
<div class="archive-list">
{% for entry in posts_by_year[year] %}
<div class="archive-entry">
<code><time datetime="{{ entry.date | iso_datetime }}">{{ entry.date | iso_date }}</time></code>
<a href="{{ entry.permalink }}">
{{ entry.title }}
</a>
</div>
{% endfor %}
</div>
{% if not loop.last %}
<hr>
{% endif %}
{% endfor %}
{%- endblock %}

View 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 %}

View 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 %}

View File

@ -4,7 +4,7 @@ tags = ["meta", "activitypub"]
date = "2019-09-18 10:34:42 -0400"
short_desc = "Stand by for reincarnation."
old_permalink = "/meta/2019/reincarnation/"
use_old_permalink_for_comments = true
comments_post_id = "9n3ZJWQsxI74wRaMzI"
```
<figure>
@ -76,7 +76,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.
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.
@ -85,5 +85,3 @@ By the way, the source code for the generator, ActivityPub integration, and cont
## 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.

View File

@ -4,7 +4,6 @@ tags = ["activitypub"]
date = "2019-09-22 17:50:42 -0400"
short_desc = "A compilation of resources I found useful in learning/implementing ActivityPub."
old_permalink = "/activitypub/2019/activity-pub-resources/"
use_old_permalink_for_comments = true
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.
- 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).

View File

@ -4,7 +4,6 @@ tags = ["elixir"]
date = "2019-10-10 12:29:42 -0400"
short_desc = "How I learned Elixir and why I love it."
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.
@ -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.
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.
@ -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.
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.

View File

@ -5,7 +5,6 @@ date = "2019-11-11 21:08:42 -0400"
short_desc = "Building a slide-over hamburger menu without using JavaScript."
old_permalink = "/web/2019/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.
@ -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 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.
@ -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.
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.

View File

@ -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."
old_permalink = "/ios/2019/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.
@ -48,7 +47,7 @@ if [ "${CONFIGURATION}" == "Debug" ]; then
echo "Embedding ${SCRIPT_INPUT_FILE_0}"
cp -R $SCRIPT_INPUT_FILE_0 $SCRIPT_OUTPUT_FILE_0
codesign --force --verbose --sign $EXPANDED_CODE_SIGN_IDENTITY $SCRIPT_OUTPUT_FILE_0
echo "Embedding ${SCRIPT_INPUT_FILE_1}"
cp -R $SCRIPT_INPUT_FILE_1 $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.
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.

View File

@ -1,6 +1,6 @@
```
title = "Algorithmic Bias"
tags = ["misc", "social media"]
tags = ["politics", "social media"]
date = "2020-06-05 09:55:42 -0400"
slug = "algorithmic-bias"
```
@ -15,4 +15,3 @@ This is what algorithmic bias looks like. **Algorithms are not neutral.**[^1]
</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.

View File

@ -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/).
[^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.
[^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.
[^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>
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 -->
@ -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.
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
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.
```elixir
def parse_frames(_, data, tag_length_remaining, frames)
def parse_frames(_, data, tag_length_remaining, frames)
when tag_length_remaining <= 0 do
{Map.new(frames), data}
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...

View File

@ -4,6 +4,7 @@ tags = ["computers"]
date = "2021-01-13 21:43:42 -0400"
short_desc = "The M1 Mac mini is my favorite computer. Let me tell you why."
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.
@ -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.
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.

View File

@ -4,6 +4,7 @@ tags = ["social media"]
date = "2021-02-25 22:46:42 -0400"
short_desc = "The technique Twitter borrows from game design to keep you engaged."
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.
@ -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.
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.

File diff suppressed because one or more lines are too long

View File

@ -4,6 +4,7 @@ tags = ["swift"]
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."
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.
@ -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.
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.

View File

@ -4,7 +4,7 @@ tags = ["build a programming language", "rust"]
date = "2021-04-13 17:00:42 -0400"
short_desc = "Turning a string into a sequence of tokens."
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).
@ -96,5 +96,3 @@ fn main() {
$ cargo run
tokens: [Integer(12), Plus, Integer(34)]
```

View File

@ -4,7 +4,7 @@ tags = ["build a programming language", "rust"]
date = "2021-04-14 17:00:42 -0400"
short_desc = "Building a small AST from the stream of tokens."
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.
@ -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.

View File

@ -4,7 +4,7 @@ tags = ["build a programming language", "rust"]
date = "2021-04-15 17:00:42 -0400"
short_desc = "A bad calculator."
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.
@ -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
fn main() {
@ -78,4 +78,3 @@ result: Integer(6)
```
Next time, I'll add some more operators and actually get around to operator precedence.

View File

@ -3,7 +3,7 @@ title = "Part 4: Operator Precedence"
tags = ["build a programming language", "rust"]
date = "2021-04-16 17:00:42 -0400"
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.
@ -189,4 +189,3 @@ fn main() {
$ cargo run
result: Integer(10)
```

View File

@ -4,7 +4,7 @@ tags = ["build a programming language", "rust"]
date = "2021-04-17 17:00:42 -0400"
short_desc = "A small gotcha in Rust's TakeWhile iterator."
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.
@ -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:
```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
I: Iterator,
P: FnMut(&I::Item) -> bool,
@ -84,5 +84,3 @@ fn parse_number<T: Iterator<Item = char>>(it: &mut T) -> Option<Token> {
// ...
}
```

View File

@ -3,7 +3,7 @@ title = "Part 6: Grouping"
tags = ["build a programming language", "rust"]
date = "2021-04-18 14:42:42 -0400"
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.
@ -97,4 +97,3 @@ node: Group {
```
(I won't bother discussing evaluating groups because it's trivial.)

View File

@ -4,7 +4,7 @@ tags = ["build a programming language", "rust"]
date = "2021-04-19 17:00:42 -0400"
short_desc = "A minor fight with the Rust borrow checker."
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.
@ -140,4 +140,3 @@ fn combine_with_binary_operator(left: Node, token: &Token, right: Node) -> Node
}
}
```

View File

@ -3,7 +3,7 @@ title = "Part 8: Variable Lookups and Function Calls"
tags = ["build a programming language", "rust"]
date = "2021-04-25 11:15:42 -0400"
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.
@ -127,4 +127,3 @@ Call {
],
}
```

View File

@ -3,7 +3,7 @@ title = "Part 9: Statements"
tags = ["build a programming language", "rust"]
date = "2021-05-03 17:46:42 -0400"
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.
@ -61,7 +61,7 @@ fn parse_statement<'a, I: Iterator<Item = &'a Token>>(it: &mut Peekable<'a, I>)
}
None => (),
}
node
}
```
@ -93,5 +93,3 @@ statements: [
),
]
```

View File

@ -3,7 +3,7 @@ title = "Part 10: Variable Declarations"
tags = ["build a programming language", "rust"]
date = "2021-05-09 19:14:42 -0400"
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.
@ -64,7 +64,7 @@ There are also a few methods for `Context`, one to construct a new context and o
```rust
impl Context {
fn new() -> Self {
fn new() -> Self {
Self {
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.

View File

@ -4,7 +4,7 @@ tags = ["build a programming language", "rust"]
date = "2021-06-29 19:14:42 -0400"
short_desc = "Evaluating if statements and dealing with nested scopes."
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.
@ -98,4 +98,3 @@ fn main() {
$ cargo run
Integer(1)
```

View File

@ -3,6 +3,7 @@ title = "Debugging My Gemini NWProtocolFramer Implementation"
tags = ["swift", "gemini"]
date = "2021-07-07 23:32:42 -0400"
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.
@ -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.
<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.
</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).

View File

@ -3,6 +3,7 @@ title = "On SwiftUI"
tags = ["swift"]
date = "2021-08-25 15:34:42 -0400"
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.
@ -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).
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.
@ -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.
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.

View File

@ -3,6 +3,7 @@ title = "A Mac Menu Bar App to Toggle Natural Scrolling"
tags = ["swift"]
date = "2021-09-02 22:31:42 -0400"
slug = "scrollswitcher"
comments_post_id = "AAyGGuMkJMm7kDglJg"
```
There are two ways you can configure the scroll direction on a computer: normal or inverted (what Apple calls "natural"). If you, like most, use a mouse with a wheel, the normal scrolling scheme is probably what you use. Under it, moving the mouse wheel down (i.e., so that a point on the top of the wheel moves down/closer to you) causes the content to move _up_ and the viewport (the window into the content) to move down. Similarly on a multitouch trackpad, under normal scrolling, as your two fingers move down, the content moves up and the viewport down.
@ -130,13 +131,13 @@ I knew the name of the notification I needed to fire, but not what to do with it
Looking at the disassembly, we can see exactly what it's doing:
<div class="article-content-wide"><img src="/2021/scrollswitcher/awakefromnib.png" alt="Hopper showing the disassembly for -[MouseController awakeFromNib]"></div>
<img src="/2021/scrollswitcher/awakefromnib.png" alt="Hopper showing the disassembly for -[MouseController awakeFromNib]">
It's adding an observer to `NSDistributedNotificationCenter`'s `defaultCenter` for the `SwipeScrollDirectionDidChangeNotification`—the notification name I saw earlier in the Console. The other place it's referenced from (`[MTMouseScrollGesture initWithDictionary:andReadPreferences:]`) is doing the same thing: adding an observer. So, it looks like this notification isn't what triggers the actual change to the actual scroll input handling deep in the guts of the system. If that were the case, I'd expect to see the preference pane _send_ the notification, not just receive it.
But, it still may be useful. Looking at the implementation of the `_swipeScrollDirectionDidChangeNotification:` method it's setting as the callback for the action, we can see that it's probably updating the checkbox value. That `setState:` call sure seems like it's on the `NSButton` used for the natural scrolling checkbox.
<div class="article-content-wide"><img src="/2021/scrollswitcher/notificationhandler.png" alt="Hopper showing the disassembly for -[MouseController _swipeScrollDirectionDidChangeNotification:]"></div>
<img src="/2021/scrollswitcher/notificationhandler.png" alt="Hopper showing the disassembly for -[MouseController _swipeScrollDirectionDidChangeNotification:]">
[`NSDistributedNotificationCenter`](https://developer.apple.com/documentation/foundation/nsdistributednotificationcenter) is described as being like the regular notification center but for inter-process communication, which sounds like what we want. It has pretty much the same API as the regular one, so we can just send that notification when we change the scroll mode.
@ -166,7 +167,7 @@ With that in place, clicking the menu bar item both sets the default and causes
That's all well and good, but clicking the menu item still doesn't actually change what happens when you move two fingers on the trackpad. It clearly works when clicking the checkbox in System Preferences, so there must be something else it's doing that we're not. Internally, this feature seems to be consistently referred to as the "swipe scroll direction" (even though it affects non-swipe scrolling), so, back in Hopper, we can search for procedures named like that. There's one that immediately looks promising `setSwipeScrollDirection`, that just delegates to an externally implemented `_setSwipeScrollDirection`.
<div class="article-content-wide"><img src="/2021/scrollswitcher/setswipescrolldirection.png" alt="Hopper showing the assembly for setSwipeScrollDirection"></div>
<img src="/2021/scrollswitcher/setswipescrolldirection.png" alt="Hopper showing the assembly for setSwipeScrollDirection">
Looking at the references to the function, I saw it was called by the `-[MouseController scrollingBehavior:]` setter. That seems like the function that I wanted, but since it was implemented elsewhere, I had no idea what parameters it took. So, where's it implemented?
@ -185,9 +186,9 @@ Actually getting the framework binaries is a bit tricky, since, starting with ma
$ dyld_shared_cache_util -extract ~/Desktop/Libraries/ /System/Library/dyld/dyld_shared_cache_x86_64
```
It took a couple guesses, but I found that the `_setSwipeScrollingDirection` function is defined in `PreferencePanesSupport.framework`.
It took a couple guesses, but I found that the `_setSwipeScrollingDirection` function is defined in `PreferencePanesSupport.framework`.
<div class="article-content-wide"><img src="/2021/scrollswitcher/preferencepanessupport.png" alt="Hopper showing the disassembly for _setSwipeScrollDirection in PreferencePanesSupport"></div>
<img src="/2021/scrollswitcher/preferencepanessupport.png" alt="Hopper showing the disassembly for _setSwipeScrollDirection in PreferencePanesSupport">
Hopper thinks it takes an int, but we can clearly see the parameter's being used as a bool. `rcx` is initialized to `kCFBooleanFalse` and set to `kCFBooleanTrue` if the parameter is true, and that's the value being passed to `CFPreferencesSetValue`. Perfect.
@ -218,4 +219,3 @@ I can't really take a screen recording of that, so you'll have to take my word t
If you're interested in the complete code, it can be found [here](https://git.shadowfacts.net/shadowfacts/ScrollSwitcher). It's not currently packaged for distribution, but you can build and run it yourself. Because it needs the sandbox disabled, it won't ever been in the App Store, but at some point I might slap an app icon on it and published a notarized, built version. So, if anyone's interested, let me know.
As it currently exists, the app—which I'm calling ScrollSwitcher—covers 90% of my needs. I don't generally dock/undock more than a one or twice a day, so just being able to click a menu bar item is plenty fast. That said, I may still extend it for "fun". One obvious improvement would be automatically changing the state when an external mouse is connected/disconnected. That shouldn't be too hard, right? Right?

View File

@ -3,6 +3,7 @@ title = "Automatically Changing Scroll Direction Based on USB Devices"
tags = ["swift"]
date = "2021-09-19 15:17:42 -0400"
slug = "auto-switch-scroll-direction"
comments_post_id = "ABWrCLtPV8sLCUHuGe"
```
[Last time](/2021/scrollswitcher/) I wrote about programmatically toggling natural scrolling on macOS and building a menu bar app to let me do that quickly. It works very well, but there's still a little bit of friction with having to click the menu bar icon—or, more accurately, forgetting to click it and then scrolling backwards and realizing you forgot. As I mentioned at the end of my previous post, one obvious way to extend it, now that the ability to programmatically set direction is in place, would be toggling it automatically based on what's currently connected. This turned out to not be terribly complicated, but dealing with IOKit was somewhat annoying, so I thought I'd write it up.
@ -101,7 +102,7 @@ The device removed callback does pretty much the same thing, just subtracting in
After the counts are updated, the delegate is notified that devices have changed and told to update the scroll direction. This is done by sending void to a `PasstroughSubject` on the app delegate. I use Combine rather than just calling a method on the app delegate because, when the `IOHIDManager` is initially added, it fires the added callback a whole bunch of times for every device that's already connected. Additionally, when a USB hub is added/removed we get callbacks for all of the individual devices and I don't want to flip the scroll direction a bunch of times. So, when the app delegate listens to the subject, it uses Combine's debouncing to only fire every 0.1 seconds at most.
Actually changing the scroll direction is next, which requires figuring out what direction to change it to, based on the current device counts. This has a slight complication: laptops.
Actually changing the scroll direction is next, which requires figuring out what direction to change it to, based on the current device counts. This has a slight complication: laptops.
On a laptop, the trackpad is always connected, so if we were to switch to natural scrolling whenever a trackpad was connected, that would be useless. Similarly, for desktops, you can imagine a case where a mouse is always connected, so just switching to normal when a mouse is present would be similarly ineffectual in some cases. The solution? Doing both, and having a preference to let the user choose.
@ -124,4 +125,3 @@ private func updateDirectionForAutoMode() {
This way, on a laptop you can set it to "normal when mouse present" and on a desktop with an always-connected mouse you can set it to "natural when trackpad present".
I've been using this updated version of ScrollSwitcher for about a week now, and it's worked flawlessly. I haven't had to open System Preferences or even click the menu bar icon. If you're interested, the full source code for the app can be found here: <https://git.shadowfacts.net/shadowfacts/ScrollSwitcher>.

View File

@ -4,9 +4,10 @@ tags = ["computers"]
date = "2022-01-06 15:37:42 -0400"
short_desc = "Apple finally made a truly great laptop."
slug = "m1-max"
comments_post_id = "AFAuHvlgiO8u6DTQcy"
```
Heres the review, if youre not going to read any farther than the first sentence: this is a damn good computer. I've had my M1 Max MBP (32 GPU cores, 64 GB RAM) for two months now and, aside from the time spent migrating things off my previous computer, its been the only “real” computer Ive used in that time.
Heres the review, if youre not going to read any farther than the first sentence: this is a damn good computer. I've had my M1 Max MBP (32 GPU cores, 64 GB RAM) for two months now and, aside from the time spent migrating things off my previous computer, its been the only “real” computer Ive used in that time.
<!-- excerpt-end -->
@ -20,7 +21,7 @@ First off, the most important part: SoC performance. This machine handily beats
I could list a bunch of artificial benchmarks for you *oooh* and *ahhh* at, but that's not generally representative of what I actually use it for.
A full release build of Tusker, my iOS app for Mastodon, takes about 94 seconds on my Intel laptop. Its 44% faster on the M1 Max, taking 53 seconds. Debug builds with incremental compilation see a similar improvement. And, as was the case with the M1, where the single-core performance really shines is in reducing the feedback loop between making a code change and being able to see that reflected in the running app.
A full release build of Tusker, my iOS app for Mastodon, takes about 94 seconds on my Intel laptop. Its 44% faster on the M1 Max, taking 53 seconds. Debug builds with incremental compilation see a similar improvement. And, as was the case with the M1, where the single-core performance really shines is in reducing the feedback loop between making a code change and being able to see that reflected in the running app.
As with the Mac mini, everything just *feels* faster. No doubt some of that is a placebo, but not entirely. I can put my old and new laptops side by side and launch the same app, and the Apple Silicon one will appear on screen several seconds sooner. This snappiness extends to within apps too, especially ones using Catalyst which always felt a little bit unresponsive before.
@ -36,7 +37,7 @@ The display on this computer is great. Having had a high-refresh rate external m
If you dont know, Apple laptops starting with the 2016 MacBook Pro have used non-integer scaling factors. That is, by default they ran at point resolutions which were more than half of the pixel resolution in each dimension. So, a software pixel mapped to some fraction of a hardware pixel, meaning everything had to be imprecisely scaled before actually going to the panel. People have been complaining about this for years, and Id always dismissed it because I never observed the issue. But, in hindsight, thats because the vast majority of my laptop usage was with it docked to an external monitor and peripherals. In that scenario, the laptops builtin display ends up physically far enough away from my eyes that I dont perceive any blurriness. But, since I've been using this laptop more as an actual laptop—bringing the screen a good foot or two closer to my eyes—Ive noticed that text is undeniably crisper.
<aside>
<aside class="inline">
Using the screen on this laptop, particularly when using it undocked and independent of an external monitor has firmly convinced me of something I previously believed: the ideal monitor would be 5k (i.e., 2560x1440 at the Retina 2x pixel density), 27" diagonally, and 120Hz. My current external monitor is 1440p, 27", and 144Hz and having used a monitors of that size for years and years, I think it's the best combination of screen real-estate and physical size of UI elements. Using a 5k iMac screen in the office[^1] convinced me that high-DPI is very nice, even if you're just looking at text all day. And finally seeing a screen that is both high DPI and high refresh rate has validated that belief. I really hope that someone makes a monitor that manages to include both.
@ -56,7 +57,7 @@ MagSafe is wonderful, Im very happy its back. I get a little spark of joy
While I didnt hate the Touch Bar as much as some people, I never found it to be better than plain old function keys. Nonetheless, Im perfectly happy that its gone. My stupid minor gripe about the Touch Bar was that, when Im using my computer docked with an external monitor and keyboard, the Touch Bar would remain on and active. That doesnt sound so bad, but it becomes an annoyance as I interact with apps and see the software buttons on the Touch Bar changing and flashing in the corner of my eye. The removal of the Touch Bar has dealt with that annoyance and has made absolutely no difference to my productivity when using the laptop on its own, so Im happy.
The hardware changes with this machine can be divided into two categories: Apple Silicon-related and not. The non-Apple Silicon changes by themselves are fairly small, but they represent a marked quality-of-life improvement when just using the computer.
The hardware changes with this machine can be divided into two categories: Apple Silicon-related and not. The non-Apple Silicon changes by themselves are fairly small, but they represent a marked quality-of-life improvement when just using the computer.
## Software
@ -95,4 +96,3 @@ One of my few complaints about the M1 Mac mini was resolved with the release of
## Conclusion
Overall, this is a fantastic computer. Apple Silicon means it's vastly faster and more efficient than any previous Mac laptop. As with last year, I'm impresed how much software is already native—just a year and a half into the Mac's ARM transition—and how well Rosetta 2 works for software that isn't. Beyond Apple Silicon, this laptop is an upgrade in every single way over the few preceding generations which felt like a big regression. Two laptops ago, I was using the 7.5 year old 2012 Retina MacBook Pro: the first laptop of a new generation of MacBooks. I'm hopeful that with all these long-standing issues resolved, this machine will last a similarly long time.

Some files were not shown because too many files have changed in this diff Show More