Skip to main content

praxis_runtime/
graph.rs

1//! The graph walks behind §6.5's prelude helpers (ADR-060).
2//!
3//! §6.5 asks for "closure-based algorithms that do not require materializing a
4//! graph object": the caller supplies a start state and a function from a state
5//! to its neighbours, and the helper walks whatever that function describes.
6//! There is no graph value, no adjacency table and no node type — the graph is
7//! the closure.
8//!
9//! # Why the walks do not call the closures themselves
10//!
11//! Calling a Praxis closure means transmuting a JIT'd function pointer and
12//! passing it a live `RuntimeContext`, which no unit test can supply. So the
13//! walks below never touch a closure: they ask a [`GraphOracle`], and
14//! `praxis_runtime::abi` supplies the one implementation that calls closures.
15//! A test supplies one backed by an adjacency table, which is what makes
16//! "`dijkstra` relaxes an edge it has already settled" a question that can be
17//! asked without a compiler in the room.
18//!
19//! # States are values, and the walks hold them
20//!
21//! A state is a `GcRef` and the walks keep every state they have seen — in a
22//! visited set, in a queue, in a cost table, in a parent table. Those are Rust
23//! structures the collector cannot see, so **the caller must root every state
24//! it hands in and every state an oracle hands back** before the next call that
25//! may allocate.
26//! [`GraphOracle::retain`] is where that happens: the walks call it once per
27//! newly discovered state, immediately, and the ABI implementation roots it in
28//! its [`NativeScope`](crate::roots::NativeScope).
29//!
30//! # Identity
31//!
32//! Two states are the same state when [`DynamicKey`] says so — the same
33//! descriptor and a structural `equals` — which is exactly the rule a `Set`
34//! element and a `Map` key follow. That is why inference requires
35//! `CapKind::HashStable` of the state type at every call site: a state that can
36//! change after the walk has stored it cannot be found again, and the walk
37//! would revisit it forever.
38
39use std::cmp::Reverse;
40use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque};
41
42use crate::GcRef;
43use crate::context::FaultKind;
44use crate::dynamic_key::DynamicKey;
45
46/// A walk stopped before it had an answer, because a fault is pending.
47///
48/// Never constructed by a walk directly: it comes back from an oracle call that
49/// faulted, or from [`GraphOracle::abort`], so "a walk returned `Err` and left
50/// no fault behind" is not a state a walk can produce.
51#[derive(Clone, Copy, PartialEq, Eq, Debug)]
52pub struct Aborted;
53
54/// What a walk asks about the graph it is walking.
55///
56/// Every method may fault — the closures are arbitrary Praxis code — so every
57/// answer is a `Result`. An `Err(Aborted)` means a fault is already pending on
58/// the context and the walk must stop; it never means "no answer".
59pub trait GraphOracle {
60    /// The states reachable in one step from `state`.
61    fn neighbours(&mut self, state: GcRef) -> Result<Vec<GcRef>, Aborted>;
62
63    /// The cost of the edge from `from` to `to`. Only called for a pair the
64    /// oracle itself reported adjacent.
65    fn weight(&mut self, from: GcRef, to: GcRef) -> Result<i64, Aborted>;
66
67    /// The estimated remaining cost from `state` to a goal.
68    fn heuristic(&mut self, state: GcRef) -> Result<i64, Aborted>;
69
70    /// Whether `state` is a goal.
71    fn is_goal(&mut self, state: GcRef) -> Result<bool, Aborted>;
72
73    /// Keep `state` alive for the rest of the walk. Called once per state, the
74    /// moment the walk decides to remember it.
75    fn retain(&mut self, state: GcRef);
76
77    /// Raise `kind` and stop the walk. The `Aborted` it returns is the only way
78    /// a walk reports a fault of its own.
79    fn abort(&mut self, kind: FaultKind) -> Aborted;
80}
81
82/// Remembered states, in the order they were first seen.
83///
84/// The visited set and the visit order are one structure because every walk
85/// needs both and keeping them apart is how a state ends up in one and not the
86/// other. `insert` answers whether the state was new, which is the only
87/// question the walks ask.
88struct Seen {
89    keys: HashSet<DynamicKey>,
90    order: Vec<GcRef>,
91}
92
93impl Seen {
94    fn new() -> Seen {
95        Seen {
96            keys: HashSet::new(),
97            order: Vec::new(),
98        }
99    }
100
101    /// Record `state` if it is new. Returns whether it was.
102    fn insert(&mut self, state: GcRef) -> bool {
103        if self.keys.insert(DynamicKey::new(state)) {
104            self.order.push(state);
105            true
106        } else {
107            false
108        }
109    }
110}
111
112/// What a goal-directed search found: the route from the start to the goal it
113/// stopped at, and what that route cost.
114///
115/// One answer for both halves of a goal-directed family. `X_distance` projects
116/// `cost` and `X_path` projects `states`, so the number and the route are
117/// always the *same* route's — two searches, one run apart, could disagree
118/// about which goal they stopped at, and this makes that unrepresentable.
119///
120/// `states` runs from the start to the goal, both included, so a start that is
121/// itself a goal is a one-element route at cost 0. `cost` is the route's own
122/// price in the units the search counts: edges for the unweighted walks, the
123/// sum of the weights for the weighted ones.
124#[derive(Clone, PartialEq, Eq, Debug)]
125pub struct Route {
126    /// What the route cost, in the search's own units.
127    pub cost: i64,
128    /// The states from the start to the goal, in order, inclusive of both.
129    pub states: Vec<GcRef>,
130}
131
132/// Where each state was first reached from, for the states a route can pass
133/// through. The start is absent: it was reached from nowhere, which is what
134/// terminates [`route_to`].
135type Parents = HashMap<DynamicKey, GcRef>;
136
137/// The route to `goal`, walked back through `parents` and reversed.
138///
139/// Every state on the way is a key whose value is one step closer to the start,
140/// and the start has no entry — so the walk back is finite and ends exactly
141/// there.
142fn route_to(parents: &Parents, goal: GcRef) -> Vec<GcRef> {
143    let mut states = vec![goal];
144    let mut at = goal;
145    while let Some(parent) = parents.get(&DynamicKey::new(at)) {
146        states.push(*parent);
147        at = *parent;
148    }
149    states.reverse();
150    states
151}
152
153/// `bfs(start, neighbours)` — every reachable state, in breadth-first order.
154///
155/// The start is the first element: a walk always reaches where it began, which
156/// is why this result needs no `Option`.
157pub fn bfs_order(oracle: &mut dyn GraphOracle, start: GcRef) -> Result<Vec<GcRef>, Aborted> {
158    let mut seen = Seen::new();
159    oracle.retain(start);
160    seen.insert(start);
161    let mut queue = VecDeque::new();
162    queue.push_back(start);
163    while let Some(state) = queue.pop_front() {
164        for next in oracle.neighbours(state)? {
165            oracle.retain(next);
166            if seen.insert(next) {
167                queue.push_back(next);
168            }
169        }
170    }
171    Ok(seen.order)
172}
173
174/// `dfs(start, neighbours)` — every reachable state, in depth-first pre-order.
175///
176/// The neighbours are pushed in reverse so the *first* neighbour a state
177/// reports is the first one descended into. Without that the order is the
178/// mirror image of the one the program wrote, which is the kind of difference
179/// only an end-to-end test sees.
180pub fn dfs_order(oracle: &mut dyn GraphOracle, start: GcRef) -> Result<Vec<GcRef>, Aborted> {
181    let mut seen = Seen::new();
182    oracle.retain(start);
183    let mut stack = vec![start];
184    while let Some(state) = stack.pop() {
185        if !seen.insert(state) {
186            continue;
187        }
188        let next = oracle.neighbours(state)?;
189        for n in next.into_iter().rev() {
190            oracle.retain(n);
191            stack.push(n);
192        }
193    }
194    Ok(seen.order)
195}
196
197/// `flood_fill(start, neighbours)` — every reachable state.
198///
199/// The same walk as [`bfs_order`]; only the result type differs, and the ABI
200/// wrapper is what turns the states into a `Set` rather than a `Vec`. Sharing
201/// the walk is deliberate: "which states are reachable" has one answer, and two
202/// implementations of it would eventually disagree.
203pub fn reachable(oracle: &mut dyn GraphOracle, start: GcRef) -> Result<Vec<GcRef>, Aborted> {
204    bfs_order(oracle, start)
205}
206
207/// `bfs_distance`/`bfs_path`'s one walk: the shortest route from `start` to a
208/// state satisfying `is_goal`, or `None` when no such state is reachable.
209///
210/// Every edge counts one, so the first time the walk *dequeues* a goal it has
211/// reached it by a shortest route, and the [`Route::cost`] is that route's edge
212/// count. `is_goal` is asked on dequeue rather than on discovery, which is what
213/// makes a start that is already a goal zero steps rather than one.
214///
215/// A state's parent is recorded the moment [`Seen::insert`] first accepts it,
216/// and breadth-first order means that first sighting is along a shortest route
217/// — so the parent chain is final as soon as it is written.
218pub fn bfs_route(oracle: &mut dyn GraphOracle, start: GcRef) -> Result<Option<Route>, Aborted> {
219    let mut seen = Seen::new();
220    let mut parents = Parents::new();
221    oracle.retain(start);
222    seen.insert(start);
223    let mut queue = VecDeque::new();
224    queue.push_back((start, 0_i64));
225    while let Some((state, steps)) = queue.pop_front() {
226        if oracle.is_goal(state)? {
227            return Ok(Some(Route {
228                cost: steps,
229                states: route_to(&parents, state),
230            }));
231        }
232        // A step count cannot overflow before the visited set exhausts memory,
233        // but the addition is still checked: `saturating_add` would report a
234        // distance nobody walked.
235        let Some(next_steps) = steps.checked_add(1) else {
236            return Err(oracle.abort(FaultKind::IntOverflow));
237        };
238        for next in oracle.neighbours(state)? {
239            oracle.retain(next);
240            if seen.insert(next) {
241                parents.insert(DynamicKey::new(next), state);
242                queue.push_back((next, next_steps));
243            }
244        }
245    }
246    Ok(None)
247}
248
249/// `dfs_distance`/`dfs_path`'s one walk: the route depth-first search found to
250/// a state satisfying `is_goal`, or `None` when no such state is reachable.
251///
252/// **The route is the one the descent happened to reach first, which need not
253/// be a shortest one** — that is the whole difference from [`bfs_route`], and
254/// the reason both families exist rather than one.
255///
256/// The stack carries each entry's parent, because the parent that counts is the
257/// one on the entry that was *popped and accepted*: a state can be pushed from
258/// several predecessors before it is first visited, and only the push the walk
259/// actually descended from is on its route.
260pub fn dfs_route(oracle: &mut dyn GraphOracle, start: GcRef) -> Result<Option<Route>, Aborted> {
261    let mut seen = Seen::new();
262    let mut parents = Parents::new();
263    oracle.retain(start);
264    let mut stack: Vec<(GcRef, Option<GcRef>)> = vec![(start, None)];
265    while let Some((state, parent)) = stack.pop() {
266        if !seen.insert(state) {
267            continue;
268        }
269        if let Some(parent) = parent {
270            parents.insert(DynamicKey::new(state), parent);
271        }
272        if oracle.is_goal(state)? {
273            let states = route_to(&parents, state);
274            // A route through `n` states crosses `n - 1` edges, and a route
275            // always holds at least its own goal.
276            let cost = (states.len() - 1) as i64;
277            return Ok(Some(Route { cost, states }));
278        }
279        let next = oracle.neighbours(state)?;
280        for n in next.into_iter().rev() {
281            oracle.retain(n);
282            stack.push((n, Some(state)));
283        }
284    }
285    Ok(None)
286}
287
288/// How a search turns the cost of reaching a state into the priority its
289/// frontier entry is filed under: the cost itself for Dijkstra
290/// ([`cost_itself`]), `g + h` for A\* ([`estimate`]).
291///
292/// It takes the oracle because A\*'s half of the answer comes from the
293/// program's heuristic closure, and returns a `Result` because that closure can
294/// fault like any other.
295type PriorityOf = fn(&mut dyn GraphOracle, GcRef, i64) -> Result<i64, Aborted>;
296
297/// The priority queue Dijkstra and A\* share, with the three tables that make a
298/// pop mean something: the least cost known per state, the states already
299/// settled, and the insertion counter that breaks ties.
300///
301/// The two searches differ in one expression — the priority an entry is filed
302/// under — and in what they do with a state once it settles. Everything else is
303/// here, both refusals included, because written twice they would eventually
304/// disagree and two searches that fault differently on the same graph is the
305/// bug this shape prevents. It is the argument [`Seen`] was factored out for,
306/// and the one [`reachable`] delegates to [`bfs_order`] for.
307struct Frontier {
308    /// Ordered by `(priority, sequence)`, carrying the state alongside: a state
309    /// is not orderable — nothing requires it to be — so the tie-break is
310    /// insertion order, which also makes the walk deterministic.
311    heap: BinaryHeap<Reverse<(i64, usize, StateEntry)>>,
312    /// The least cost known to reach each state so far.
313    best: HashMap<DynamicKey, i64>,
314    /// The states already settled; nothing relaxes into one again.
315    done: HashSet<DynamicKey>,
316    /// The predecessor on the cheapest route known to each state.
317    ///
318    /// **A settled state's parent is final.** [`Frontier::push`] writes here
319    /// only for an entry that improved on what was known, and
320    /// [`Frontier::relax`] skips a state already in `done` — so nothing can
321    /// push a settled state again, and nothing overwrites its parent. What
322    /// settles a state is its cheapest entry (a lower `g` is a lower priority
323    /// under both [`cost_itself`] and [`estimate`], because `h` is a property
324    /// of the state alone), and that entry's push is the one that wrote the
325    /// parent. So the chain from a settled goal is the chain of the route the
326    /// cost belongs to.
327    parents: Parents,
328    /// Pushes so far — the heap's tie-break.
329    seq: usize,
330}
331
332impl Frontier {
333    fn new() -> Frontier {
334        Frontier {
335            heap: BinaryHeap::new(),
336            best: HashMap::new(),
337            done: HashSet::new(),
338            parents: Parents::new(),
339            seq: 0,
340        }
341    }
342
343    /// Record that `state` is reachable at `cost` by way of `parent` and queue
344    /// it under `priority`. For Dijkstra the cost and the priority are one
345    /// number; for A\* the priority is `g + h` and the cost is `g`.
346    ///
347    /// `parent` is `None` only for the start, which is reached from nowhere.
348    fn push(&mut self, state: GcRef, parent: Option<GcRef>, cost: i64, priority: i64) {
349        let key = DynamicKey::new(state);
350        self.best.insert(key, cost);
351        if let Some(parent) = parent {
352            self.parents.insert(key, parent);
353        }
354        self.heap
355            .push(Reverse((priority, self.seq, StateEntry(state))));
356        self.seq += 1;
357    }
358
359    /// The next state to settle and the cost it settled at, or `None` when the
360    /// frontier is spent.
361    ///
362    /// A state queued again at a lower cost leaves its old entry behind, so a
363    /// pop is a loop: the later entries for a settled state are stale.
364    fn settle(&mut self) -> Option<(GcRef, i64)> {
365        while let Some(Reverse((_, _, StateEntry(state)))) = self.heap.pop() {
366            let key = DynamicKey::new(state);
367            if !self.done.insert(key) {
368                // Already settled by a cheaper entry; this one is stale.
369                continue;
370            }
371            // `best` is the settled cost: the entry that popped is the cheapest
372            // one for this state, and nothing lowers it after it is settled.
373            // The number in the entry is not it — for A\* that is `g + h`.
374            let cost = *self
375                .best
376                .get(&key)
377                .expect("a popped state has a known cost");
378            return Some((state, cost));
379        }
380        None
381    }
382
383    /// Relax every edge out of `state`, which settled at `cost`, queueing each
384    /// neighbour the edge improves under `priority_of`.
385    ///
386    /// Both refusals [`dijkstra_costs`] and [`best_route`] document are made
387    /// here, once: a negative edge weight is a [`FaultKind::NoAnswer`], because
388    /// a settled state is never reconsidered and a cost nobody paid is worse
389    /// than a stop; a cost that leaves the `Int` range is a
390    /// [`FaultKind::IntOverflow`] rather than a wrap, which is the same rule
391    /// ADR-058 applied to `abs(Int::MIN)`.
392    fn relax(
393        &mut self,
394        oracle: &mut dyn GraphOracle,
395        state: GcRef,
396        cost: i64,
397        priority_of: PriorityOf,
398    ) -> Result<(), Aborted> {
399        for next in oracle.neighbours(state)? {
400            oracle.retain(next);
401            let step = oracle.weight(state, next)?;
402            if step < 0 {
403                return Err(oracle.abort(FaultKind::NoAnswer));
404            }
405            let Some(through) = cost.checked_add(step) else {
406                return Err(oracle.abort(FaultKind::IntOverflow));
407            };
408            let next_key = DynamicKey::new(next);
409            if self.done.contains(&next_key) {
410                continue;
411            }
412            let improved = match self.best.get(&next_key) {
413                Some(known) => through < *known,
414                None => true,
415            };
416            if improved {
417                let priority = priority_of(oracle, next, through)?;
418                self.push(next, Some(state), through, priority);
419            }
420        }
421        Ok(())
422    }
423}
424
425/// `dijkstra(start, neighbours, weight)` — the least cost from `start` to every
426/// reachable state, as `(state, cost)` pairs.
427///
428/// The start is present at cost 0. An unreachable state is simply absent, which
429/// is why this answers with a table rather than with an `Option` per state.
430///
431/// A **negative edge weight faults**. Dijkstra settles a state the first time it
432/// pops it and never reconsiders, so a negative edge makes the answer quietly
433/// too large — and a cost nobody paid is worse than a stop (the rule ADR-058
434/// applied to `abs(Int::MIN)`). The refusal itself lives in `Frontier::relax`,
435/// which is why A\* makes it identically.
436pub fn dijkstra_costs(
437    oracle: &mut dyn GraphOracle,
438    start: GcRef,
439) -> Result<Vec<(GcRef, i64)>, Aborted> {
440    let mut frontier = Frontier::new();
441    let mut settled: Vec<(GcRef, i64)> = Vec::new();
442
443    oracle.retain(start);
444    frontier.push(start, None, 0, 0);
445
446    while let Some((state, cost)) = frontier.settle() {
447        settled.push((state, cost));
448        frontier.relax(oracle, state, cost, cost_itself)?;
449    }
450    Ok(settled)
451}
452
453/// The priority Dijkstra files an entry under: the cost, with nothing added. It
454/// takes the oracle it never asks so that it and [`estimate`] are one shape.
455fn cost_itself(_oracle: &mut dyn GraphOracle, _state: GcRef, cost: i64) -> Result<i64, Aborted> {
456    Ok(cost)
457}
458
459/// The one weighted goal-directed search, filed under `priority_of`: the
460/// cheapest route from `start` to a goal, or `None` when no goal is reachable.
461///
462/// Dijkstra and A\* are this loop under [`cost_itself`] and [`estimate`]
463/// respectively — the same argument [`Frontier`] itself is made for, at the one
464/// remaining seam. A search stops at the first goal it *settles*, and a settled
465/// state's cost is final, so that goal is the cheapest reachable one.
466///
467/// Both refusals live in `Frontier::relax`, and A\*'s third — a negative
468/// heuristic — in [`estimate`], so a distance and a path fault on exactly the
469/// same graphs.
470fn best_route(
471    oracle: &mut dyn GraphOracle,
472    start: GcRef,
473    priority_of: PriorityOf,
474) -> Result<Option<Route>, Aborted> {
475    let mut frontier = Frontier::new();
476
477    oracle.retain(start);
478    let start_priority = priority_of(oracle, start, 0)?;
479    frontier.push(start, None, 0, start_priority);
480
481    while let Some((state, cost)) = frontier.settle() {
482        if oracle.is_goal(state)? {
483            return Ok(Some(Route {
484                cost,
485                states: route_to(&frontier.parents, state),
486            }));
487        }
488        frontier.relax(oracle, state, cost, priority_of)?;
489    }
490    Ok(None)
491}
492
493/// `dijkstra_distance`/`dijkstra_path`'s one walk: the cheapest route from
494/// `start` to a goal, or `None` when no goal is reachable.
495///
496/// The same search [`dijkstra_costs`] runs, stopped at the first goal it
497/// settles instead of run to exhaustion — so it makes the same two refusals, a
498/// negative edge weight and a cost with no `Int`, through the same code.
499pub fn dijkstra_route(
500    oracle: &mut dyn GraphOracle,
501    start: GcRef,
502) -> Result<Option<Route>, Aborted> {
503    best_route(oracle, start, cost_itself)
504}
505
506/// `a_star_distance`/`a_star_path`'s one walk: the cheapest route from `start`
507/// to a goal, or `None` when no goal is reachable.
508///
509/// The frontier is ordered by `g + h`; a state is settled when it is popped, and
510/// the first goal popped is the cheapest one **provided the heuristic never
511/// overestimates**. A heuristic that does is the caller's error and the search
512/// cannot detect it — but a *negative* one can be detected, and is, for the same
513/// reason a negative weight is: it makes `f` decrease along a path, which is the
514/// condition the ordering relies on.
515pub fn a_star_route(oracle: &mut dyn GraphOracle, start: GcRef) -> Result<Option<Route>, Aborted> {
516    best_route(oracle, start, estimate)
517}
518
519/// `g + h` for a state, with the two refusals A\*'s ordering depends on: a
520/// negative estimate, and a sum with no `Int`.
521fn estimate(oracle: &mut dyn GraphOracle, state: GcRef, cost: i64) -> Result<i64, Aborted> {
522    let h = oracle.heuristic(state)?;
523    if h < 0 {
524        return Err(oracle.abort(FaultKind::NoAnswer));
525    }
526    match cost.checked_add(h) {
527        Some(f) => Ok(f),
528        None => Err(oracle.abort(FaultKind::IntOverflow)),
529    }
530}
531
532/// A state in a priority-queue entry, ordered as **equal to every other state**.
533///
534/// The queue's real key is the `(cost, sequence)` pair in front of this; a state
535/// has no order of its own and requiring one would exclude every type that is a
536/// legal `Map` key but not orderable — tuples and records, which is what a grid
537/// position is. Making the comparison total-and-constant here is what lets the
538/// tuple derive its `Ord` from the two fields that do order.
539#[derive(Clone, Copy)]
540struct StateEntry(GcRef);
541
542impl PartialEq for StateEntry {
543    fn eq(&self, _other: &Self) -> bool {
544        true
545    }
546}
547impl Eq for StateEntry {}
548impl PartialOrd for StateEntry {
549    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
550        Some(self.cmp(other))
551    }
552}
553impl Ord for StateEntry {
554    fn cmp(&self, _other: &Self) -> std::cmp::Ordering {
555        std::cmp::Ordering::Equal
556    }
557}
558
559#[cfg(test)]
560mod tests {
561    use super::*;
562    use crate::abi::{praxis_alloc_int, praxis_int_load};
563    use crate::context::{Runtime, RuntimeContext};
564
565    /// A graph written down as a table, so a walk can be tested without a JIT.
566    ///
567    /// States are boxed `Int`s allocated from a real runtime — real `GcRef`s
568    /// with real descriptors, so `DynamicKey` does the same structural
569    /// comparison it does for a program's own states. The adjacency, weights,
570    /// heuristic and goals are all keyed on the integer the state holds.
571    struct Table {
572        ctx: *mut RuntimeContext,
573        edges: Vec<(i64, Vec<i64>)>,
574        weights: Vec<((i64, i64), i64)>,
575        heuristics: Vec<(i64, i64)>,
576        goals: Vec<i64>,
577        /// Set by `abort`; the fault the walk raised, which a real context
578        /// would carry on its fault slot.
579        raised: Option<FaultKind>,
580        /// Every state handed to `retain`, in order — the rooting the ABI
581        /// implementation performs.
582        retained: Vec<i64>,
583    }
584
585    impl Table {
586        fn value(&self, state: GcRef) -> i64 {
587            // SAFETY: every state in these tests is an `Int` allocated below.
588            unsafe { praxis_int_load(self.ctx, state) }
589        }
590
591        fn state(&self, n: i64) -> GcRef {
592            // SAFETY: `ctx` is wired for the test's lifetime.
593            unsafe { praxis_alloc_int(self.ctx, n) }
594        }
595    }
596
597    impl GraphOracle for Table {
598        fn neighbours(&mut self, state: GcRef) -> Result<Vec<GcRef>, Aborted> {
599            let n = self.value(state);
600            let out = self
601                .edges
602                .iter()
603                .find(|(from, _)| *from == n)
604                .map(|(_, to)| to.clone())
605                .unwrap_or_default();
606            Ok(out.into_iter().map(|m| self.state(m)).collect())
607        }
608
609        fn weight(&mut self, from: GcRef, to: GcRef) -> Result<i64, Aborted> {
610            let pair = (self.value(from), self.value(to));
611            Ok(self
612                .weights
613                .iter()
614                .find(|(p, _)| *p == pair)
615                .map(|(_, w)| *w)
616                .unwrap_or(1))
617        }
618
619        fn heuristic(&mut self, state: GcRef) -> Result<i64, Aborted> {
620            let n = self.value(state);
621            Ok(self
622                .heuristics
623                .iter()
624                .find(|(s, _)| *s == n)
625                .map(|(_, h)| *h)
626                .unwrap_or(0))
627        }
628
629        fn is_goal(&mut self, state: GcRef) -> Result<bool, Aborted> {
630            Ok(self.goals.contains(&self.value(state)))
631        }
632
633        fn retain(&mut self, state: GcRef) {
634            let n = self.value(state);
635            self.retained.push(n);
636        }
637
638        fn abort(&mut self, kind: FaultKind) -> Aborted {
639            self.raised = Some(kind);
640            Aborted
641        }
642    }
643
644    /// A runtime plus a leaked context, and the table over it. The runtime has
645    /// to outlive every state, so both are returned together.
646    ///
647    /// The `Runtime` is **boxed**, and that is load-bearing: a context holds
648    /// `&mut rt.heap` as a raw pointer, so returning an unboxed `Runtime` by
649    /// value moves the heap out from under every context already minted from
650    /// it. Boxing keeps the address stable across the move.
651    fn table(edges: &[(i64, &[i64])]) -> (Box<Runtime>, Table) {
652        let mut rt = Box::new(Runtime::new());
653        let ctx: *mut RuntimeContext = Box::leak(Box::new(rt.context()));
654        let t = Table {
655            ctx,
656            edges: edges
657                .iter()
658                .map(|(from, to)| (*from, to.to_vec()))
659                .collect(),
660            weights: Vec::new(),
661            heuristics: Vec::new(),
662            goals: Vec::new(),
663            raised: None,
664            retained: Vec::new(),
665        };
666        (rt, t)
667    }
668
669    fn values(t: &Table, states: &[GcRef]) -> Vec<i64> {
670        states.iter().map(|s| t.value(*s)).collect()
671    }
672
673    /// The two orders are different walks over the same graph, and each one has
674    /// to be the order it names. A diamond `1 -> {2, 3}`, both to `4`,
675    /// distinguishes them: breadth-first is `1 2 3 4`, depth-first is
676    /// `1 2 4 3`.
677    #[test]
678    fn breadth_first_and_depth_first_visit_in_the_orders_they_name() {
679        let (_rt, mut t) = table(&[(1, &[2, 3]), (2, &[4]), (3, &[4]), (4, &[])]);
680        let start = t.state(1);
681        let bfs = bfs_order(&mut t, start).expect("no fault");
682        assert_eq!(values(&t, &bfs), vec![1, 2, 3, 4]);
683
684        let (_rt2, mut t2) = table(&[(1, &[2, 3]), (2, &[4]), (3, &[4]), (4, &[])]);
685        let start2 = t2.state(1);
686        let dfs = dfs_order(&mut t2, start2).expect("no fault");
687        assert_eq!(values(&t2, &dfs), vec![1, 2, 4, 3]);
688    }
689
690    /// A depth-first walk descends into the *first* neighbour a state reports.
691    /// The stack reverses the neighbour list, so a walk that pushed them in
692    /// order would visit the last one first and still look plausible on a
693    /// symmetric graph.
694    #[test]
695    fn a_depth_first_walk_takes_the_first_neighbour_first() {
696        let (_rt, mut t) = table(&[(1, &[2, 3]), (2, &[]), (3, &[])]);
697        let start = t.state(1);
698        let order = dfs_order(&mut t, start).expect("no fault");
699        assert_eq!(values(&t, &order), vec![1, 2, 3]);
700    }
701
702    /// A cycle terminates, and every state appears once. Without the visited
703    /// set both walks run forever; with a set that is consulted but not
704    /// *updated* on the queue path, a diamond enqueues its join twice.
705    #[test]
706    fn a_cycle_is_walked_once_and_terminates() {
707        let (_rt, mut t) = table(&[(1, &[2]), (2, &[3]), (3, &[1, 2])]);
708        let start = t.state(1);
709        let bfs = bfs_order(&mut t, start).expect("no fault");
710        assert_eq!(values(&t, &bfs), vec![1, 2, 3]);
711
712        let (_rt2, mut t2) = table(&[(1, &[2]), (2, &[3]), (3, &[1, 2])]);
713        let start2 = t2.state(1);
714        let dfs = dfs_order(&mut t2, start2).expect("no fault");
715        assert_eq!(values(&t2, &dfs), vec![1, 2, 3]);
716    }
717
718    /// A state with no neighbours is still reached: the walk answers with the
719    /// start alone rather than with nothing.
720    #[test]
721    fn a_lone_state_is_its_own_walk() {
722        let (_rt, mut t) = table(&[(1, &[])]);
723        let start = t.state(1);
724        let order = bfs_order(&mut t, start).unwrap();
725        assert_eq!(values(&t, &order), vec![1]);
726
727        let (_rt2, mut t2) = table(&[(1, &[])]);
728        let start2 = t2.state(1);
729        let reached = reachable(&mut t2, start2).unwrap();
730        assert_eq!(values(&t2, &reached), vec![1]);
731    }
732
733    /// Identity is structural, not by pointer. Two separately allocated `Int`s
734    /// holding `2` are the same state, so a graph whose neighbour function
735    /// mints a fresh object per call still terminates — which is what every
736    /// real neighbour closure does (`|p| [(p.0 + 1, p.1), …]` allocates).
737    #[test]
738    fn two_equal_states_are_one_state_however_they_were_allocated() {
739        let (_rt, mut t) = table(&[(1, &[2]), (2, &[1])]);
740        let start = t.state(1);
741        let order = bfs_order(&mut t, start).expect("no fault");
742        assert_eq!(values(&t, &order), vec![1, 2]);
743        // The neighbour function allocated a fresh `1` on the second step, and
744        // the walk recognized it as the state it started from.
745        assert!(t.retained.len() >= 3, "the fresh states were retained");
746    }
747
748    /// Every state the walk remembers was handed to `retain` first. This is the
749    /// rooting contract: a state in the visited set that the collector cannot
750    /// see is a dangling reference the next allocation creates.
751    #[test]
752    fn every_remembered_state_was_retained_first() {
753        let (_rt, mut t) = table(&[(1, &[2, 3]), (2, &[4]), (3, &[]), (4, &[])]);
754        let start = t.state(1);
755        let order = bfs_order(&mut t, start).expect("no fault");
756        for state in &order {
757            assert!(
758                t.retained.contains(&t.value(*state)),
759                "a visited state was never retained"
760            );
761        }
762    }
763
764    /// The cost of a route, which is what a `_distance` helper projects out of
765    /// it. Written once because every distance assertion below is this.
766    fn cost(found: Result<Option<Route>, Aborted>) -> Option<i64> {
767        found.expect("no fault").map(|r| r.cost)
768    }
769
770    /// The distance is the number of *steps*, the start is zero steps away, and
771    /// an unreachable goal is `None` rather than a sentinel.
772    #[test]
773    fn a_distance_counts_steps_and_absence_is_none() {
774        let (_rt, mut t) = table(&[(1, &[2]), (2, &[3]), (3, &[]), (9, &[])]);
775        t.goals = vec![3];
776        let start = t.state(1);
777        assert_eq!(cost(bfs_route(&mut t, start)), Some(2));
778
779        let (_rt2, mut t2) = table(&[(1, &[2]), (2, &[3]), (3, &[])]);
780        t2.goals = vec![1];
781        let start2 = t2.state(1);
782        assert_eq!(
783            cost(bfs_route(&mut t2, start2)),
784            Some(0),
785            "a start that is already a goal is zero steps, not one"
786        );
787
788        let (_rt3, mut t3) = table(&[(1, &[2]), (2, &[])]);
789        t3.goals = vec![99];
790        let start3 = t3.state(1);
791        assert_eq!(cost(bfs_route(&mut t3, start3)), None);
792    }
793
794    /// A breadth-first distance is the *shortest* one. The long way round is
795    /// enqueued first, so a walk that returned the first goal it enqueued
796    /// rather than the first it dequeued would answer 3 here.
797    #[test]
798    fn a_distance_is_the_shortest_path_not_the_first_found() {
799        let (_rt, mut t) = table(&[(1, &[2, 5]), (2, &[3]), (3, &[4]), (4, &[]), (5, &[4])]);
800        t.goals = vec![4];
801        let start = t.state(1);
802        assert_eq!(cost(bfs_route(&mut t, start)), Some(2));
803    }
804
805    /// The cost table holds the least cost to every reachable state, the start
806    /// at zero, and nothing for what cannot be reached. The cheap three-hop
807    /// path has to beat the expensive one-hop edge, which is the whole of
808    /// Dijkstra and the half a step-counting BFS gets wrong.
809    #[test]
810    fn a_cost_table_prefers_a_cheap_long_path_to_an_expensive_short_one() {
811        let (_rt, mut t) = table(&[(1, &[2, 4]), (2, &[3]), (3, &[4]), (4, &[]), (7, &[])]);
812        t.weights = vec![((1, 4), 10), ((1, 2), 1), ((2, 3), 1), ((3, 4), 1)];
813        let start = t.state(1);
814        let costs = dijkstra_costs(&mut t, start).expect("no fault");
815        let mut by_state: Vec<(i64, i64)> = costs.iter().map(|(s, c)| (t.value(*s), *c)).collect();
816        by_state.sort_unstable();
817        assert_eq!(by_state, vec![(1, 0), (2, 1), (3, 2), (4, 3)]);
818        assert!(
819            !by_state.iter().any(|(s, _)| *s == 7),
820            "an unreachable state is absent, not present at some cost"
821        );
822    }
823
824    /// A settled state is settled: a later, longer route to it does not add a
825    /// second entry to the table.
826    #[test]
827    fn each_state_is_settled_once() {
828        let (_rt, mut t) = table(&[(1, &[2, 3]), (2, &[4]), (3, &[4]), (4, &[])]);
829        let start = t.state(1);
830        let costs = dijkstra_costs(&mut t, start).expect("no fault");
831        assert_eq!(costs.len(), 4, "one entry per reachable state");
832    }
833
834    /// A negative edge weight faults rather than answering. Dijkstra never
835    /// reconsiders a settled state, so a negative edge makes the answer quietly
836    /// too large — and the same refusal covers A\*, which settles the same way.
837    #[test]
838    fn a_negative_edge_weight_faults_rather_than_answering() {
839        let (_rt, mut t) = table(&[(1, &[2]), (2, &[])]);
840        t.weights = vec![((1, 2), -1)];
841        let start = t.state(1);
842        assert_eq!(dijkstra_costs(&mut t, start), Err(Aborted));
843        assert_eq!(t.raised, Some(FaultKind::NoAnswer));
844
845        let (_rt2, mut t2) = table(&[(1, &[2]), (2, &[])]);
846        t2.weights = vec![((1, 2), -1)];
847        t2.goals = vec![2];
848        let start2 = t2.state(1);
849        assert_eq!(a_star_route(&mut t2, start2), Err(Aborted));
850        assert_eq!(t2.raised, Some(FaultKind::NoAnswer));
851    }
852
853    /// A path whose cost leaves the `Int` range faults rather than wrapping —
854    /// the rule ADR-058 applied to `abs(Int::MIN)`, at the one place a walk
855    /// does arithmetic the program did not write.
856    #[test]
857    fn a_cost_with_no_int_faults_rather_than_wrapping() {
858        let (_rt, mut t) = table(&[(1, &[2]), (2, &[3]), (3, &[])]);
859        t.weights = vec![((1, 2), i64::MAX), ((2, 3), 1)];
860        let start = t.state(1);
861        assert_eq!(dijkstra_costs(&mut t, start), Err(Aborted));
862        assert_eq!(t.raised, Some(FaultKind::IntOverflow));
863    }
864
865    /// A\* answers the cheapest cost to a goal, and the heuristic only changes
866    /// the order states are examined in — not the answer. The same graph is
867    /// searched twice, once with a zero heuristic (which is Dijkstra) and once
868    /// with an exact one.
869    #[test]
870    fn a_star_finds_the_cheapest_goal_whatever_the_heuristic_estimates() {
871        let edges: &[(i64, &[i64])] = &[(1, &[2, 4]), (2, &[3]), (3, &[4]), (4, &[])];
872        let weights = vec![((1, 4), 10), ((1, 2), 1), ((2, 3), 1), ((3, 4), 1)];
873
874        let (_rt, mut t) = table(edges);
875        t.weights = weights.clone();
876        t.goals = vec![4];
877        let start = t.state(1);
878        assert_eq!(cost(a_star_route(&mut t, start)), Some(3));
879
880        let (_rt2, mut t2) = table(edges);
881        t2.weights = weights;
882        t2.goals = vec![4];
883        // An exact remaining-cost estimate: still admissible, so still 3.
884        t2.heuristics = vec![(1, 3), (2, 2), (3, 1), (4, 0)];
885        let start2 = t2.state(1);
886        assert_eq!(cost(a_star_route(&mut t2, start2)), Some(3));
887    }
888
889    /// An unreachable goal is `None`, and a start that is already a goal costs
890    /// nothing.
891    #[test]
892    fn a_star_answers_nothing_for_an_unreachable_goal() {
893        let (_rt, mut t) = table(&[(1, &[2]), (2, &[])]);
894        t.goals = vec![99];
895        let start = t.state(1);
896        assert_eq!(cost(a_star_route(&mut t, start)), None);
897
898        let (_rt2, mut t2) = table(&[(1, &[2]), (2, &[])]);
899        t2.goals = vec![1];
900        let start2 = t2.state(1);
901        assert_eq!(cost(a_star_route(&mut t2, start2)), Some(0));
902    }
903
904    /// A negative heuristic faults. It is the one caller error A\* *can* see:
905    /// an inadmissible-but-positive estimate produces a wrong answer nothing
906    /// can detect, while a negative one breaks the ordering the search is built
907    /// on and is one comparison away.
908    #[test]
909    fn a_negative_heuristic_faults_rather_than_misordering_the_search() {
910        let (_rt, mut t) = table(&[(1, &[2]), (2, &[])]);
911        t.goals = vec![2];
912        t.heuristics = vec![(1, -5)];
913        let start = t.state(1);
914        assert_eq!(a_star_route(&mut t, start), Err(Aborted));
915        assert_eq!(t.raised, Some(FaultKind::NoAnswer));
916    }
917
918    // --- the route the distance is the cost of ------------------------------
919
920    /// A route runs from the start to the goal, both included, and its cost is
921    /// the price of *that* route: the edge count for a step-counting walk, the
922    /// weight sum for a weighted one. Three families, one shape.
923    #[test]
924    fn a_route_is_start_to_goal_inclusive_and_its_cost_is_its_own() {
925        let edges: &[(i64, &[i64])] = &[(1, &[2]), (2, &[3]), (3, &[])];
926        let weights = vec![((1, 2), 4), ((2, 3), 6)];
927
928        let (_rt, mut t) = table(edges);
929        t.goals = vec![3];
930        let start = t.state(1);
931        let route = bfs_route(&mut t, start).expect("no fault").expect("a goal");
932        assert_eq!(values(&t, &route.states), vec![1, 2, 3]);
933        assert_eq!(route.cost, 2, "a breadth-first cost counts edges");
934
935        let (_rt2, mut t2) = table(edges);
936        t2.weights = weights.clone();
937        t2.goals = vec![3];
938        let start2 = t2.state(1);
939        let route = dijkstra_route(&mut t2, start2)
940            .expect("no fault")
941            .expect("a goal");
942        assert_eq!(values(&t2, &route.states), vec![1, 2, 3]);
943        assert_eq!(route.cost, 10, "a weighted cost sums the weights");
944
945        let (_rt3, mut t3) = table(edges);
946        t3.weights = weights;
947        t3.goals = vec![3];
948        let start3 = t3.state(1);
949        let route = a_star_route(&mut t3, start3)
950            .expect("no fault")
951            .expect("a goal");
952        assert_eq!(values(&t3, &route.states), vec![1, 2, 3]);
953        assert_eq!(route.cost, 10);
954    }
955
956    /// A start that is already a goal is a route of one state at cost zero, not
957    /// an empty route and not `None`. All four searches agree, because "the
958    /// route to where I already am" has one answer.
959    #[test]
960    fn a_start_that_is_the_goal_is_a_route_of_one_state() {
961        for search in [bfs_route, dfs_route, dijkstra_route, a_star_route] {
962            let (_rt, mut t) = table(&[(1, &[2]), (2, &[])]);
963            t.goals = vec![1];
964            let start = t.state(1);
965            let route = search(&mut t, start).expect("no fault").expect("a goal");
966            assert_eq!(values(&t, &route.states), vec![1]);
967            assert_eq!(route.cost, 0);
968        }
969    }
970
971    /// An unreachable goal is `None` from every one of the four. A route the
972    /// search never found has no states *and* no cost, which is the whole
973    /// reason both forms answer an `Option`.
974    #[test]
975    fn an_unreachable_goal_is_nothing_from_every_search() {
976        for search in [bfs_route, dfs_route, dijkstra_route, a_star_route] {
977            let (_rt, mut t) = table(&[(1, &[2]), (2, &[])]);
978            t.goals = vec![99];
979            let start = t.state(1);
980            assert!(search(&mut t, start).expect("no fault").is_none());
981        }
982    }
983
984    /// The breadth-first route is *a* shortest one, and its length agrees with
985    /// the cost the same call reports. The long way round is discovered first,
986    /// so a walk that recorded a parent on every sighting rather than on the
987    /// first would reconstruct the three-hop route here.
988    #[test]
989    fn a_breadth_first_route_is_a_shortest_one() {
990        let (_rt, mut t) = table(&[(1, &[2, 5]), (2, &[3]), (3, &[4]), (4, &[]), (5, &[4])]);
991        t.goals = vec![4];
992        let start = t.state(1);
993        let route = bfs_route(&mut t, start).expect("no fault").expect("a goal");
994        assert_eq!(values(&t, &route.states), vec![1, 5, 4]);
995        assert_eq!(route.states.len() as i64 - 1, route.cost);
996    }
997
998    /// **The two families ask different questions.** On a graph whose cheapest
999    /// route is not its shortest, Dijkstra takes the cheap three-hop one and
1000    /// breadth-first takes the dear one-hop one — and each is right about the
1001    /// question it was asked.
1002    #[test]
1003    fn the_cheapest_route_and_the_shortest_route_are_different_routes() {
1004        let edges: &[(i64, &[i64])] = &[(1, &[2, 4]), (2, &[3]), (3, &[4]), (4, &[])];
1005        let weights = vec![((1, 4), 10), ((1, 2), 1), ((2, 3), 1), ((3, 4), 1)];
1006
1007        let (_rt, mut t) = table(edges);
1008        t.weights = weights.clone();
1009        t.goals = vec![4];
1010        let start = t.state(1);
1011        let cheap = dijkstra_route(&mut t, start)
1012            .expect("no fault")
1013            .expect("a goal");
1014        assert_eq!(values(&t, &cheap.states), vec![1, 2, 3, 4]);
1015        assert_eq!(cheap.cost, 3);
1016
1017        let (_rt2, mut t2) = table(edges);
1018        t2.weights = weights;
1019        t2.goals = vec![4];
1020        let start2 = t2.state(1);
1021        let short = bfs_route(&mut t2, start2)
1022            .expect("no fault")
1023            .expect("a goal");
1024        assert_eq!(values(&t2, &short.states), vec![1, 4]);
1025        assert_eq!(short.cost, 1, "one edge, whatever it costs");
1026    }
1027
1028    /// A state first queued expensively and then relaxed to a lower cost ends
1029    /// up with the **cheap** route's parent. `4` is reached from `1` at 10 and
1030    /// then from `3` at 3; the route has to go through `3`, which is only true
1031    /// if the improving push overwrote the first one's parent.
1032    #[test]
1033    fn a_relaxed_state_keeps_the_cheap_routes_parent() {
1034        let (_rt, mut t) = table(&[(1, &[4, 2]), (2, &[3]), (3, &[4]), (4, &[])]);
1035        t.weights = vec![((1, 4), 10), ((1, 2), 1), ((2, 3), 1), ((3, 4), 1)];
1036        t.goals = vec![4];
1037        let start = t.state(1);
1038        let route = dijkstra_route(&mut t, start)
1039            .expect("no fault")
1040            .expect("a goal");
1041        assert_eq!(values(&t, &route.states), vec![1, 2, 3, 4]);
1042        assert_eq!(route.cost, 3);
1043    }
1044
1045    /// A depth-first route is the one the descent found, **not** a shortest
1046    /// one. `1 -> 4` is one edge, and depth-first descends into `2` first and
1047    /// arrives at `4` three edges later — which is the honest reason
1048    /// `dfs_path` exists beside `bfs_path` rather than being the same helper.
1049    #[test]
1050    fn a_depth_first_route_need_not_be_a_short_one() {
1051        let edges: &[(i64, &[i64])] = &[(1, &[2, 4]), (2, &[3]), (3, &[4]), (4, &[])];
1052
1053        let (_rt, mut t) = table(edges);
1054        t.goals = vec![4];
1055        let start = t.state(1);
1056        let deep = dfs_route(&mut t, start).expect("no fault").expect("a goal");
1057        assert_eq!(values(&t, &deep.states), vec![1, 2, 3, 4]);
1058        assert_eq!(deep.cost, 3);
1059
1060        let (_rt2, mut t2) = table(edges);
1061        t2.goals = vec![4];
1062        let start2 = t2.state(1);
1063        let wide = bfs_route(&mut t2, start2)
1064            .expect("no fault")
1065            .expect("a goal");
1066        assert_eq!(values(&t2, &wide.states), vec![1, 4]);
1067        assert!(wide.cost < deep.cost);
1068    }
1069
1070    /// The refusals are the search's, not the wrapper's: a negative weight and
1071    /// a negative heuristic fault the route-answering forms too, because there
1072    /// is one search behind both the number and the route.
1073    #[test]
1074    fn a_route_refuses_the_graphs_a_cost_refuses() {
1075        let (_rt, mut t) = table(&[(1, &[2]), (2, &[])]);
1076        t.weights = vec![((1, 2), -1)];
1077        t.goals = vec![2];
1078        let start = t.state(1);
1079        assert_eq!(dijkstra_route(&mut t, start), Err(Aborted));
1080        assert_eq!(t.raised, Some(FaultKind::NoAnswer));
1081
1082        let (_rt2, mut t2) = table(&[(1, &[2]), (2, &[3]), (3, &[])]);
1083        t2.weights = vec![((1, 2), i64::MAX), ((2, 3), 1)];
1084        t2.goals = vec![99];
1085        let start2 = t2.state(1);
1086        assert_eq!(dijkstra_route(&mut t2, start2), Err(Aborted));
1087        assert_eq!(t2.raised, Some(FaultKind::IntOverflow));
1088
1089        let (_rt3, mut t3) = table(&[(1, &[2]), (2, &[])]);
1090        t3.goals = vec![2];
1091        t3.heuristics = vec![(1, -5)];
1092        let start3 = t3.state(1);
1093        assert_eq!(a_star_route(&mut t3, start3), Err(Aborted));
1094        assert_eq!(t3.raised, Some(FaultKind::NoAnswer));
1095    }
1096
1097    /// Every state on a route was handed to `retain` first. The parent table is
1098    /// a Rust structure like the visited set, and a state reachable only
1099    /// through it is one the collector would otherwise reclaim while the walk
1100    /// is still going to answer with it.
1101    #[test]
1102    fn every_state_on_a_route_was_retained_first() {
1103        let (_rt, mut t) = table(&[(1, &[2, 3]), (2, &[4]), (3, &[]), (4, &[])]);
1104        t.goals = vec![4];
1105        let start = t.state(1);
1106        let route = bfs_route(&mut t, start).expect("no fault").expect("a goal");
1107        for state in &route.states {
1108            assert!(
1109                t.retained.contains(&t.value(*state)),
1110                "a state on the route was never retained"
1111            );
1112        }
1113    }
1114}