lib.rsannotatedlib.rssource276 lines · 13.3 KB · raw

A Rete network as a picture: left to right, with what is known marked on it. Pure: a network and what is known in, SVG out. It draws the [Network] that runs, so the picture cannot show rules other than the ones that run.

Columns: the facts, the tests on them (alpha nodes), the joins of tests (one column per depth), and the rules. A rule's first test is its own first join, so only joins of two or more tests are drawn as nodes.

What the picture has to say, beyond the wiring:

  • which facts came in one request (the frames around the facts, which the host names in its [Legend]);
  • what a source actually answered (the text shown gives for a fact);
  • what, of everything known, a rule that holds stands on (filled), and what was learned and not needed (outlined);
  • the order the rules decided in (the number on a rule);
  • that a rule's effect teaches a fact (the line from a rule back round to the fact, which is the loop the engine runs).

The markup carries classes and no styles. rete, fact, alpha, join, rule, frame, back, nth and fired name the parts, and holds, spare, fails and waits the state of one; [standalone] writes the picture as a file with the styles a host supplies.

24#![forbid(unsafe_code)]
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;

Room round the network for the lines that run from a rule back to the fact it teaches: one lane per such fact.

44const GUTTER: i32 = 30;
45const LANE: i32 = 9;

A frame's room above its first fact, for its caption, and round its sides.

47const CAPTION: i32 = 15;
48const PAD: i32 = 5;

What only the host can say about its network.

51pub struct Legend<D: Domain> {

The facts that arrive together, and what brings them: a caption for each frame drawn round those facts.

54    pub frames: Vec<(String, Vec<D::Fact>)>,

What a rule does, in words, as the rule's label ends.

56    pub then: fn(Then<D>) -> String,
57}

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

The first free row at or after y in a column, so two nodes that want 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}

The network with known marked on it. fired is the rules that decided a step, in order, as indices into the network's terminals. shown is what to print for a known fact: Jev's own number, where there is one. 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}

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}

The network on its own, as an SVG file, with nothing known so every node waits. style is the stylesheet for the classes above, written for the file: an image is drawn without the page's stylesheet, so it comes with 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}
275#[cfg(test)]
276mod tests;