1//! A Rete network as a picture: left to right, with what is known marked on 2//! it. Pure: a network and what is known in, SVG out. It draws the 3//! [`Network`] that runs, so the picture cannot show rules other than the 4//! ones that run. 5//! 6//! Columns: the facts, the tests on them (alpha nodes), the joins of tests 7//! (one column per depth), and the rules. A rule's first test is its own 8//! first join, so only joins of two or more tests are drawn as nodes. 9//! 10//! What the picture has to say, beyond the wiring: 11//! - which facts came in one request (the frames around the facts, which 12//! the host names in its [`Legend`]); 13//! - what a source actually answered (the text `shown` gives for a fact); 14//! - what, of everything known, a rule that holds stands on (filled), and 15//! what was learned and not needed (outlined); 16//! - the order the rules decided in (the number on a rule); 17//! - that a rule's effect teaches a fact (the line from a rule back round 18//! to the fact, which is the loop the engine runs). 19//! 20//! The markup carries classes and no styles. `rete`, `fact`, `alpha`, `join`, 21//! `rule`, `frame`, `back`, `nth` and `fired` name the parts, and `holds`, 22//! `spare`, `fails` and `waits` the state of one; [`standalone`] writes the 23//! picture as a file with the styles a host supplies. 24#![forbid(unsafe_code)] 25 26use std::collections::BTreeSet; 27 28use maud::{Markup, html}; 29use rete::{Domain, Known, Network, State, Test, Then}; 30 31const ROW: i32 = 28; 32const TOP: i32 = 32; 33const BOX_H: i32 = 18; 34const FACT_X: i32 = 0; 35const FACT_W: i32 = 150; 36const ALPHA_X: i32 = 184; 37const ALPHA_W: i32 = 74; 38const JOIN_X: i32 = 294; 39const JOIN_STEP: i32 = 40; 40const JOIN_R: i32 = 8; 41const RULE_W: i32 = 262; 42/// Room round the network for the lines that run from a rule back to the 43/// fact it teaches: one lane per such fact. 44const GUTTER: i32 = 30; 45const LANE: i32 = 9; 46/// A frame's room above its first fact, for its caption, and round its sides. 47const CAPTION: i32 = 15; 48const PAD: i32 = 5; 49 50/// What only the host can say about its network. 51pub struct Legend<D: Domain> { 52 /// The facts that arrive together, and what brings them: a caption for 53 /// each frame drawn round those facts. 54 pub frames: Vec<(String, Vec<D::Fact>)>, 55 /// What a rule does, in words, as the rule's label ends. 56 pub then: fn(Then<D>) -> String, 57} 58 59/// Something on the diagram that can be clicked. 60#[derive(Clone, Copy)] 61pub enum Clicked<D: Domain> { 62 Fact(D::Fact), 63 Test(Test<D>), 64} 65 66fn class(state: State, used: bool) -> &'static str { 67 match (state, used) { 68 (State::Holds, true) => "holds", 69 // Known to hold, and no rule that holds needed it. 70 (State::Holds, false) => "spare", 71 (State::Fails, _) => "fails", 72 (State::Waits, _) => "waits", 73 } 74} 75 76/// The first free row at or after `y` in a column, so two nodes that want 77/// the same place do not sit on each other. 78fn place(taken: &mut BTreeSet<i32>, mut y: i32) -> i32 { 79 while !taken.insert(y) { 80 y += ROW / 2; 81 } 82 y 83} 84 85/// The network with `known` marked on it. `fired` is the rules that decided 86/// a step, in order, as indices into the network's terminals. `shown` is 87/// what to print for a known fact: Jev's own number, where there is one. 88/// `link` is where a test leads when it is clicked, for the playground. 89pub fn rete<D: Domain>( 90 network: &Network<D>, 91 known: &Known<D>, 92 fired: &[usize], 93 legend: &Legend<D>, 94 shown: impl Fn(D::Fact) -> Option<String>, 95 link: impl Fn(Clicked<D>) -> Option<String>, 96) -> Markup { 97 let linked = |clicked: Clicked<D>, node: Markup| match link(clicked) { 98 Some(href) => html! { a href=(href) { (node) } }, 99 None => node, 100 }; 101 // Alphas in fact order, so a fact's tests sit together beside it. 102 let mut order: Vec<usize> = (0..network.alphas.len()).collect(); 103 order.sort_by_key(|&alpha| network.alphas[alpha].fact()); 104 let mut alpha_y = vec![0; network.alphas.len()]; 105 for (row, &alpha) in order.iter().enumerate() { 106 alpha_y[alpha] = TOP + row as i32 * ROW; 107 } 108 let fact_y = |fact: D::Fact| { 109 let ys: Vec<i32> = 110 (0..network.alphas.len()).filter(|&a| network.alphas[a].fact() == fact).map(|a| alpha_y[a]).collect(); 111 (!ys.is_empty()).then(|| ys.iter().sum::<i32>() / ys.len() as i32) 112 }; 113 114 let depth = network.joins.iter().map(|join| join.depth).max().unwrap_or(1); 115 let join_x = |d: usize| JOIN_X + (d as i32 - 2) * JOIN_STEP; 116 let rule_x = join_x(depth) + JOIN_STEP; 117 let width = rule_x + RULE_W; 118 let height = TOP + network.alphas.len() as i32 * ROW - ROW / 2; 119 120 // Where each join's output leaves from: an alpha's right edge for a 121 // rule's first test, the join's own node otherwise. 122 let mut columns: Vec<BTreeSet<i32>> = vec![BTreeSet::new(); depth + 1]; 123 let mut out: Vec<(i32, i32)> = Vec::with_capacity(network.joins.len()); 124 for join in &network.joins { 125 out.push(if join.depth == 1 { 126 (ALPHA_X + ALPHA_W, alpha_y[join.alpha]) 127 } else { 128 (join_x(join.depth), place(&mut columns[join.depth], alpha_y[join.alpha])) 129 }); 130 } 131 let mut rule_rows = BTreeSet::new(); 132 let rule_y: Vec<i32> = network.terminals.iter().map(|t| place(&mut rule_rows, out[t.join].1)).collect(); 133 134 let (used_joins, used_alphas) = network.used(known); 135 let alpha_class = |alpha: usize| class(network.alpha(alpha, known), used_alphas[alpha]); 136 let join_class = |join: usize| class(network.join(join, known), used_joins[join]); 137 let fact_class = |fact: D::Fact| match known.get(fact) { 138 None => "waits", 139 Some(_) if (0..network.alphas.len()).any(|a| network.alphas[a].fact() == fact && used_alphas[a]) => "holds", 140 Some(_) => "spare", 141 }; 142 143 // Each effect's fact has a lane: out to the right of its rules, under 144 // the network, and up the left to the fact. 145 let taught: Vec<D::Fact> = D::facts().iter().copied().filter(|fact| !D::asked_for(*fact)).collect(); 146 let lane = |fact: D::Fact| taught.iter().position(|taught| *taught == fact).map(|lane| 10 + lane as i32 * LANE); 147 148 html! { 149 div .rete { 150 svg viewBox={ (-GUTTER) " 0 " (width + 2 * GUTTER) " " (height + GUTTER) } role="img" 151 aria-label="The rules as a Rete network" { 152 // Frames and lines first, so the nodes are drawn over them. 153 @for (caption, facts) in &legend.frames { 154 @let ys: Vec<i32> = facts.iter().filter_map(|fact| fact_y(*fact)).collect(); 155 @if let (Some(top), Some(bottom)) = (ys.iter().min(), ys.iter().max()) { 156 @let top = top - BOX_H / 2 - CAPTION; 157 g .frame { 158 rect x=(FACT_X - PAD) y=(top) width=(FACT_W + 2 * PAD) 159 height=(bottom + BOX_H / 2 + PAD - top) {} 160 text x=(FACT_X) y=(top + 11) { (caption) } 161 } 162 } 163 } 164 @for (alpha, test) in network.alphas.iter().enumerate() { 165 @if let Some(y) = fact_y(test.fact()) { 166 line class=(alpha_class(alpha)) x1=(FACT_X + FACT_W + PAD) y1=(y) x2=(ALPHA_X) y2=(alpha_y[alpha]) {} 167 } 168 } 169 // The lines into a join that a fired rule stands on are drawn 170 // last, over the ones beside them: several joins share the 171 // line that brings what came before. 172 @for last in [false, true] { 173 @for (index, join) in network.joins.iter().enumerate() { 174 @if let (Some(left), true) = (join.left, used_joins[index] == last) { 175 @let (x, y) = out[index]; 176 @let (from_x, from_y) = out[left]; 177 @let from_x = if network.joins[left].depth == 1 { from_x } else { from_x + JOIN_R }; 178 @let edge = if from_y < y { y - JOIN_R } else { y + JOIN_R }; 179 // What came before arrives from above or below; 180 // the test joined here arrives from the left. 181 path class=(class(network.join(left, known), last)) 182 d={ "M" (from_x) " " (from_y) " H" (x) " V" (edge) } {} 183 line class=(class(network.alpha(join.alpha, known), last)) 184 x1=(ALPHA_X + ALPHA_W) y1=(alpha_y[join.alpha]) x2=(x - JOIN_R) y2=(y) {} 185 } 186 } 187 } 188 @for (terminal, y) in network.terminals.iter().zip(&rule_y) { 189 @let (x, from_y) = out[terminal.join]; 190 @let x = if network.joins[terminal.join].depth == 1 { x } else { x + JOIN_R }; 191 line class=(join_class(terminal.join)) x1=(x) y1=(from_y) x2=(rule_x) y2=(*y) {} 192 @if let Then::Do(effect) = terminal.then { 193 @let fact = D::teaches(effect); 194 @if let (Some(to), Some(lane)) = (fact_y(fact), lane(fact)) { 195 @let tip = FACT_X - PAD; 196 g class={ "back " (join_class(terminal.join)) } { 197 path d={ 198 "M" (width) " " (*y) " H" (width + lane) " V" (height + lane) 199 " H" (-lane - PAD) " V" (to) " H" (tip - 5) 200 } {} 201 polygon points={ (tip) "," (to) " " (tip - 6) "," (to - 3) " " (tip - 6) "," (to + 3) } {} 202 } 203 } 204 } 205 } 206 207 @for fact in D::facts().iter().copied() { 208 @if let Some(y) = fact_y(fact) { 209 (linked(Clicked::Fact(fact), html! { 210 g class={ "fact " (fact_class(fact)) } { 211 rect x=(FACT_X) y=(y - BOX_H / 2) width=(FACT_W) height=(BOX_H) {} 212 text x=(FACT_X + 6) y=(y + 4) { (D::fact_name(fact)) } 213 @if let Some(value) = known.get(fact) { 214 text x=(FACT_X + FACT_W - 6) y=(y + 4) text-anchor="end" { 215 (shown(fact).unwrap_or_else(|| D::value_name(value).to_owned())) 216 } 217 } 218 } 219 })) 220 } 221 } 222 @for (alpha, test) in network.alphas.iter().enumerate() { 223 (linked(Clicked::Test(*test), html! { 224 g class={ "alpha " (alpha_class(alpha)) } { 225 rect x=(ALPHA_X) y=(alpha_y[alpha] - BOX_H / 2) width=(ALPHA_W) height=(BOX_H) {} 226 text x=(ALPHA_X + 6) y=(alpha_y[alpha] + 4) { (test.label()) } 227 } 228 })) 229 } 230 @for (index, join) in network.joins.iter().enumerate() { 231 @if join.depth > 1 { 232 g class={ "join " (join_class(index)) } { 233 circle cx=(out[index].0) cy=(out[index].1) r=(JOIN_R) {} 234 text x=(out[index].0) y=(out[index].1 + 4) text-anchor="middle" { "&" } 235 } 236 } 237 } 238 @for (index, (terminal, y)) in network.terminals.iter().zip(&rule_y).enumerate() { 239 @let nth = fired.iter().position(|fired| *fired == index); 240 g class={ "rule " (join_class(terminal.join)) @if nth.is_some() { " fired" } } { 241 rect x=(rule_x) y=(y - BOX_H / 2) width=(RULE_W) height=(BOX_H) {} 242 text x=(rule_x + 6) y=(y + 4) { (terminal.name) " → " ((legend.then)(terminal.then)) } 243 @if let Some(nth) = nth { 244 rect .nth x=(width - BOX_H) y=(y - BOX_H / 2) width=(BOX_H) height=(BOX_H) {} 245 text .nth x=(width - BOX_H / 2) y=(y + 4) text-anchor="middle" { (nth + 1) } 246 } 247 } 248 } 249 } 250 } 251 } 252} 253 254/// The rules that decided a step, in order, numbered, for a title bar. 255pub fn sequence<D: Domain>(network: &Network<D>, fired: &[usize]) -> String { 256 let steps: Vec<String> = 257 fired.iter().enumerate().map(|(nth, rule)| format!("{} {}", nth + 1, network.terminals[*rule].name)).collect(); 258 steps.join(" · ") 259} 260 261/// The network on its own, as an SVG file, with nothing known so every node 262/// waits. `style` is the stylesheet for the classes above, written for the 263/// file: an image is drawn without the page's stylesheet, so it comes with 264/// the picture. 265pub fn standalone<D: Domain>(network: &Network<D>, legend: &Legend<D>, style: &str) -> String { 266 let drawn = rete(network, &Known::default(), &[], legend, |_| None, |_| None).into_string(); 267 let svg = drawn.trim_start_matches(r#"<div class="rete">"#).trim_end_matches("</div>"); 268 svg.replacen("<svg ", r#"<svg xmlns="http://www.w3.org/2000/svg" "#, 1).replacen( 269 r#"aria-label="The rules as a Rete network">"#, 270 &format!(r#"aria-label="The rules as a Rete network"><style>{style}</style>"#), 271 1, 272 ) 273} 274 275#[cfg(test)] 276mod tests;