lib.rsannotatedlib.rssource276 lines · 13.3 KB · raw
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;