Skip to main content

abnf_oracle/
generate.rs

1//! The deterministic generator (SCOPE.md 6.8).
2//!
3//! A walk over the arena that emits a string the grammar accepts. Two things make it terminate
4//! on grammars where a naive walk would not:
5//!
6//! * **Unproductive branches are never entered**, in any mode. Once inside `bad = "x" bad`
7//!   there may be no alternation left to steer by, so the ban is absolute rather than a
8//!   fallback for an exhausted budget (D20).
9//! * **Witness mode.** When the depth budget runs out the walk stops choosing and starts
10//!   following witnesses, which form a well-founded derivation and so reach terminals in
11//!   finitely many steps (D28). Choosing "the branch with the smallest `min_len`" is not a
12//!   substitute: saturated values compare meaninglessly, and the choice would turn on node
13//!   numbering rather than on the grammar.
14//!
15//! Termination is not the same as practicality. `start = 1000000000*"a"` is productive and has
16//! no shorter expansion, so the walk terminates and would still produce a gigabyte;
17//! [`GenOptions::max_output_len`] is what makes that an error instead.
18//!
19//! Coverage mode ([`GenOptions::coverage`]) steers towards branches not yet taken, with a
20//! guarantee rather than a probability: while any reachable branch is untaken, each successful
21//! call takes at least one.
22
23use std::collections::{HashMap, VecDeque};
24
25use crate::ast::{CharVal, Node, NodeId, NumVal, Witness};
26use crate::check::CheckedGrammar;
27use crate::error::GenError;
28use crate::rng::SplitMix64;
29
30/// How many extra repetitions a repetition may take beyond its minimum.
31pub const DEFAULT_SPREAD: usize = 3;
32/// How deep the walk may recurse before it starts following witnesses.
33///
34/// Named apart from [`crate::DEFAULT_MAX_DEPTH`], which is the recognizer's: that one is a
35/// safety limit whose breach is an error, this one is a steering budget whose exhaustion just
36/// changes how choices are made.
37pub const DEFAULT_GEN_DEPTH: usize = 16;
38/// How long a generated string may get before generation gives up.
39pub const DEFAULT_MAX_OUTPUT_LEN: usize = 1 << 20;
40/// How many nodes the walk may visit before it gives up.
41pub const DEFAULT_MAX_STEPS: u64 = 1 << 24;
42
43/// How to generate.
44#[derive(Clone, Debug, PartialEq, Eq)]
45pub struct GenOptions {
46    /// How deep to recurse before switching to witness mode. Default [`DEFAULT_GEN_DEPTH`].
47    ///
48    /// This is a *steering* budget, not a safety limit: exhausting it does not fail, it makes
49    /// the walk take the shortest derivation it knows. The safety limits are
50    /// [`GenOptions::max_output_len`] and [`GenOptions::max_steps`].
51    pub max_depth: usize,
52
53    /// How many repetitions beyond the minimum to consider. Default [`DEFAULT_SPREAD`].
54    ///
55    /// A repetition takes a count uniformly from `min..=min(max, min + spread)`, so `*"a"`
56    /// with the default produces nought to three characters rather than an unbounded run.
57    pub spread: usize,
58
59    /// Steer towards branches that have not been taken yet, rather than choosing randomly.
60    ///
61    /// With this set, and subject to the resource limits, **each successful call covers at
62    /// least one new coverage unit while any reachable one remains uncovered** — so full
63    /// coverage takes at most `uncovered(rule)` calls. That is a guarantee rather than a
64    /// probability, and it does not depend on [`GenOptions::max_depth`] (D10, D38).
65    pub coverage: bool,
66
67    /// Emit case-insensitive strings exactly as written, rather than varying their case.
68    ///
69    /// Varying case is the more useful default for a fuzzer — `"abc"` really does match `AbC` —
70    /// but it makes output unpredictable, so a test that compares generated text against an
71    /// expected string wants this set.
72    pub preserve_case: bool,
73
74    /// Give up once the output reaches this many scalar values. Default
75    /// [`DEFAULT_MAX_OUTPUT_LEN`].
76    ///
77    /// Finite by default, because termination alone does not make generation practical: a
78    /// grammar can demand a gigabyte without being wrong (D29).
79    pub max_output_len: Option<usize>,
80
81    /// Give up after visiting this many nodes. Default [`DEFAULT_MAX_STEPS`].
82    pub max_steps: Option<u64>,
83}
84
85impl Default for GenOptions {
86    fn default() -> Self {
87        Self {
88            max_depth: DEFAULT_GEN_DEPTH,
89            spread: DEFAULT_SPREAD,
90            coverage: false,
91            preserve_case: false,
92            max_output_len: Some(DEFAULT_MAX_OUTPUT_LEN),
93            max_steps: Some(DEFAULT_MAX_STEPS),
94        }
95    }
96}
97
98/// Generates strings that a rule accepts.
99///
100/// Deterministic: the same seed, options and sequence of calls produce the same strings, within
101/// a crate version (D15). Constructible only from a [`CheckedGrammar`] (D1).
102pub struct Generator<'g> {
103    grammar: &'g CheckedGrammar,
104    rng: SplitMix64,
105    options: GenOptions,
106    coverage: Coverage,
107    /// Units taken during the call in progress.
108    ///
109    /// Held apart until the call succeeds: a call that ends in `OutputLimit` produced no
110    /// output, so counting what it touched would let the bound be satisfied by strings nobody
111    /// ever saw.
112    pending: Vec<Unit>,
113    steps: u64,
114    /// Scalars emitted so far in the current call.
115    ///
116    /// Counted rather than measured: `String::chars().count()` walks the whole buffer, so
117    /// checking the limit that way costs O(n) per character and makes filling it O(n^2). A
118    /// million-scalar default would take tens of seconds to refuse.
119    emitted: usize,
120}
121
122impl<'g> Generator<'g> {
123    /// Creates a generator seeded with `seed`.
124    #[must_use]
125    pub fn new(grammar: &'g CheckedGrammar, seed: u64) -> Self {
126        Self {
127            grammar,
128            rng: SplitMix64::new(seed),
129            options: GenOptions::default(),
130            coverage: Coverage::new(grammar),
131            pending: Vec::new(),
132            steps: 0,
133            emitted: 0,
134        }
135    }
136
137    /// Sets the generation options.
138    #[must_use]
139    pub fn with_options(mut self, options: GenOptions) -> Self {
140        self.options = options;
141        self
142    }
143
144    /// How many nodes have been visited during the most recent call.
145    ///
146    /// Reset by every [`Generator::generate`], so one call's budget is never spent by the last.
147    /// That differs from [`crate::Recognizer::steps`], which counts over the whole life of a
148    /// recognizer: a generator is asked for many independent strings, while a recognizer is
149    /// bound to one input and answers questions about it.
150    ///
151    /// The counter [`GenOptions::max_steps`] limits, and what tests assert against rather than
152    /// wall-clock time (D31).
153    #[must_use]
154    pub const fn steps(&self) -> u64 {
155        self.steps
156    }
157
158    /// How many coverage units `rule` can reach that have not been covered yet.
159    ///
160    /// Counted over the generatable graph, so a branch the generator would never take is not
161    /// counted against it (D10). Zero means every branch reachable from this rule has been
162    /// taken by some successful call.
163    ///
164    /// With [`GenOptions::coverage`] set, this is also the *bound*: each successful call
165    /// reduces it by at least one, so it reaches zero within this many calls.
166    ///
167    /// # Errors
168    ///
169    /// As [`Generator::generate`], for the same reasons: a rule that cannot be generated from
170    /// has no coverage to report.
171    pub fn uncovered(&mut self, rule: &str) -> Result<usize, GenError> {
172        let body = self.start_rule(rule)?;
173        Ok(self.coverage.uncovered_reachable(self.grammar, body))
174    }
175
176    /// Generates one string that `rule` accepts.
177    ///
178    /// # Errors
179    ///
180    /// [`GenError::UnknownRule`] if there is no such rule; the compatibility limits of
181    /// SCOPE.md 6.6 if the rule can reach a prose value or an unrepresentable terminal;
182    /// [`GenError::NoFiniteExpansion`] if it matches nothing at all; or a resource limit.
183    pub fn generate(&mut self, rule: &str) -> Result<String, GenError> {
184        let body = self.start_rule(rule)?;
185
186        let mut out = String::new();
187        // Fresh per call, so one call's limit is not spent by the last.
188        self.steps = 0;
189        self.emitted = 0;
190        self.pending.clear();
191        self.walk(body, 0, &mut out)?;
192
193        // Committed only now that the call has succeeded.
194        for unit in std::mem::take(&mut self.pending) {
195            self.coverage.cover(unit);
196        }
197        Ok(out)
198    }
199
200    /// Resolves a start rule, refusing the ones that cannot be generated from.
201    fn start_rule(&self, rule: &str) -> Result<NodeId, GenError> {
202        let found = self
203            .grammar
204            .rule(rule)
205            .ok_or_else(|| GenError::UnknownRule(rule.to_owned()))?;
206
207        // Prose and unrepresentable terminals are refused before anything is emitted, the same
208        // way the recognizer refuses them before reading input (D11, D4).
209        self.grammar.can_generate(rule)?;
210
211        // An unproductive rule is refused immediately rather than discovered by walking into
212        // it: there is no string to produce, so there is nothing to attempt (D9).
213        if !self.grammar.min_len(found.body).is_finite() {
214            return Err(GenError::NoFiniteExpansion {
215                rule: found.name.as_str().to_owned(),
216            });
217        }
218        Ok(found.body)
219    }
220
221    /// Emits one node's contribution to the output.
222    fn walk(&mut self, node: NodeId, depth: usize, out: &mut String) -> Result<(), GenError> {
223        self.step()?;
224
225        match self.grammar.node(node) {
226            Node::Alt { branches } => {
227                let branches = branches.clone();
228                let chosen = self.choose_branch(node, &branches, depth);
229                self.walk(branches[chosen], depth, out)
230            }
231            Node::Concat { items } => {
232                for item in items.clone() {
233                    self.walk(item, depth, out)?;
234                }
235                Ok(())
236            }
237            Node::Repeat { repeat, body, .. } => {
238                let (repeat, body) = (*repeat, *body);
239                let count = self.choose_count(repeat.min, repeat.max, body, depth);
240                for _ in 0..count {
241                    self.walk(body, depth, out)?;
242                }
243                Ok(())
244            }
245            Node::Optional { body } => {
246                let body = *body;
247                // `[x]` is `x / empty`: branch 0 takes the body, branch 1 skips it.
248                if self.choose_branch_of_optional(node, body, depth) == 0 {
249                    self.walk(body, depth, out)?;
250                }
251                Ok(())
252            }
253            Node::RuleRef { .. } => {
254                let target = self
255                    .grammar
256                    .target(node)
257                    .expect("`check` resolved every reference");
258                let body = self.grammar.rule_by_id(target).body;
259                self.walk(body, depth + 1, out)
260            }
261            Node::CharVal(value) => {
262                let value = value.clone();
263                self.emit_char_val(&value, out)
264            }
265            Node::NumVal { value, .. } => {
266                let value = value.clone();
267                self.emit_num_val(&value, out)
268            }
269            Node::ProseVal(_) => {
270                // Unreachable: the start rule was refused if it could reach prose, and the
271                // reachability that decides that walks the same nodes this does.
272                debug_assert!(false, "prose reached despite the start-rule gate");
273                Ok(())
274            }
275        }
276    }
277
278    /// Picks a branch of an alternation, and records the unit that choice covers.
279    fn choose_branch(&mut self, node: NodeId, branches: &[NodeId], depth: usize) -> usize {
280        // Never a branch that matches nothing, in any mode (D20).
281        let usable: Vec<usize> = (0..branches.len())
282            .filter(|index| self.grammar.min_len(branches[*index]).is_finite())
283            .collect();
284        debug_assert!(
285            !usable.is_empty(),
286            "a productive alternation has a productive branch"
287        );
288
289        let targets: Vec<NodeId> = usable.iter().map(|branch| branches[*branch]).collect();
290        let chosen = self.select(node, &usable, &targets, depth);
291        self.pending.push(Unit {
292            node,
293            branch: chosen as u32,
294        });
295        chosen
296    }
297
298    /// Picks between taking an optional's body and skipping it.
299    ///
300    /// `[x]` is `x / empty`, so this is an alternation with branch 0 the body and branch 1 the
301    /// empty arm. The empty arm leads nowhere, which the selection rule sees as an unreachable
302    /// target.
303    fn choose_branch_of_optional(&mut self, node: NodeId, body: NodeId, depth: usize) -> usize {
304        let (usable, targets) = if self.grammar.min_len(body).is_finite() {
305            // `node` stands in for "leads nowhere": it holds no uncovered unit of its own that
306            // taking the empty arm could reach, so its distance is never the deciding one.
307            (vec![0, 1], vec![body, node])
308        } else {
309            // Skipping is always available, and is all that is left.
310            (vec![1], vec![node])
311        };
312
313        let chosen = self.select(node, &usable, &targets, depth);
314        self.pending.push(Unit {
315            node,
316            branch: chosen as u32,
317        });
318        chosen
319    }
320
321    /// The selection rule of SCOPE.md 6.8, shared by alternations and optionals.
322    ///
323    /// `targets[i]` is the node `usable[i]` leads to.
324    fn select(
325        &mut self,
326        node: NodeId,
327        usable: &[usize],
328        targets: &[NodeId],
329        depth: usize,
330    ) -> usize {
331        if self.options.coverage {
332            // 1. A branch not yet taken. Taking it covers a unit outright, which is what makes
333            //    the per-call guarantee hold.
334            let untaken = usable.iter().copied().find(|branch| {
335                let unit = Unit {
336                    node,
337                    branch: *branch as u32,
338                };
339                !self.coverage.is_covered(unit) && !self.pending.contains(&unit)
340            });
341            if let Some(branch) = untaken {
342                return branch;
343            }
344
345            // 2. Otherwise the branch closest to something uncovered — the *chase*. Distance
346            //    strictly decreases along it, so it ends; "any branch that reaches one, ties at
347            //    random" would not, which is the failure D38 exists to prevent. Ties by branch
348            //    index keep it deterministic.
349            let mut nearest: Option<(u32, usize)> = None;
350            for (position, branch) in usable.iter().copied().enumerate() {
351                let target = targets[position];
352                // An optional's empty arm leads back to the node itself: nothing new lies
353                // beyond it, so it never wins the chase.
354                if target == node {
355                    continue;
356                }
357                let distance = self.coverage.distance_to_uncovered(target);
358                if distance != UNREACHABLE && nearest.is_none_or(|(best, _)| distance < best) {
359                    nearest = Some((distance, branch));
360                }
361            }
362            if let Some((_, branch)) = nearest {
363                return branch;
364            }
365            // 3. Nothing reachable is uncovered: fall through to the ordinary rules below.
366        }
367
368        if depth >= self.options.max_depth {
369            // Witness mode: the shortest derivation this node knows, which terminates because
370            // witnesses were recorded in the order `min_len` was settled (D28).
371            //
372            // In coverage mode this is reached only once every reachable unit is covered, so
373            // the depth budget can never cut a chase short (D38).
374            if let Some(Witness::Branch(branch)) = self.grammar.witness(node)
375                && usable.contains(&(branch as usize))
376            {
377                return branch as usize;
378            }
379        }
380        let pick = self.rng.below(usable.len() as u64) as usize;
381        usable[pick]
382    }
383
384    /// Picks how many times to repeat.
385    fn choose_count(&mut self, min: u64, max: Option<u64>, body: NodeId, depth: usize) -> u64 {
386        // In coverage mode a repetition whose body can reach something uncovered must run at
387        // least once, or a zero count would skip past units the bound is counting on (D25).
388        if self.options.coverage
389            && max.is_none_or(|max| max >= 1)
390            && self.coverage.distance_to_uncovered(body) != UNREACHABLE
391        {
392            return min.max(1);
393        }
394        if depth >= self.options.max_depth {
395            return min; // witness mode: a repetition's witness is always its minimum
396        }
397        let ceiling = max
398            .unwrap_or(u64::MAX)
399            .min(min.saturating_add(self.options.spread as u64));
400        if ceiling <= min {
401            return min;
402        }
403        min + self.rng.below(ceiling - min + 1)
404    }
405
406    fn emit_char_val(&mut self, value: &CharVal, out: &mut String) -> Result<(), GenError> {
407        for ch in value.value.chars() {
408            // A case-insensitive string matches either case, so vary it — that is the whole
409            // point of the distinction, and a generator that always emitted one case would
410            // never exercise the other.
411            let emitted = if value.case_sensitive || self.options.preserve_case {
412                ch
413            } else if self.rng.bool() {
414                ch.to_ascii_uppercase()
415            } else {
416                ch.to_ascii_lowercase()
417            };
418            self.push(emitted, out)?;
419        }
420        Ok(())
421    }
422
423    fn emit_num_val(&mut self, value: &NumVal, out: &mut String) -> Result<(), GenError> {
424        match value {
425            NumVal::Scalar(scalar) => {
426                let ch = scalar_at(*scalar).expect("`check` refused unrepresentable terminals");
427                self.push(ch, out)
428            }
429            NumVal::Range { lo, hi } => {
430                // Uniform among the scalars the range holds, which is not the same as uniform
431                // over `lo..=hi`: the surrogate block is skipped (SCOPE.md 6.1).
432                let count = NumVal::scalars_in(*lo, *hi);
433                debug_assert!(count > 0, "`check` refused empty ranges");
434                let index = self.rng.below(count);
435                let ch = NumVal::nth_scalar(*lo, *hi, index).expect("index is in range");
436                self.push(ch, out)
437            }
438            NumVal::Concat(values) => {
439                for scalar in values {
440                    let ch = scalar_at(*scalar).expect("`check` refused unrepresentable terminals");
441                    self.push(ch, out)?;
442                }
443                Ok(())
444            }
445        }
446    }
447
448    /// Appends one scalar, respecting the output limit.
449    fn push(&mut self, ch: char, out: &mut String) -> Result<(), GenError> {
450        if self
451            .options
452            .max_output_len
453            .is_some_and(|limit| self.emitted >= limit)
454        {
455            return Err(GenError::OutputLimit);
456        }
457        self.emitted += 1;
458        out.push(ch);
459        Ok(())
460    }
461
462    /// Counts one node visit, and stops if that exceeds the limit.
463    fn step(&mut self) -> Result<(), GenError> {
464        self.steps += 1;
465        if self
466            .options
467            .max_steps
468            .is_some_and(|limit| self.steps > limit)
469        {
470            return Err(GenError::StepLimit);
471        }
472        Ok(())
473    }
474}
475
476/// The scalar value `value` names, if it names one.
477fn scalar_at(value: u64) -> Option<char> {
478    char::from_u32(u32::try_from(value).ok()?)
479}
480
481/// Distance meaning "no uncovered unit is reachable from here".
482const UNREACHABLE: u32 = u32::MAX;
483
484/// One thing the generator can be asked to exercise: a branch of an alternation.
485///
486/// `[x]` contributes two, via its `x / empty` model, and since `*1a` canonicalizes to `[a]`
487/// the two spellings are indistinguishable here (SCOPE.md 6.8, D10).
488#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
489struct Unit {
490    node: NodeId,
491    branch: u32,
492}
493
494/// The coverage machinery: which branches exist, which have been taken, and how far away the
495/// nearest untaken one is.
496///
497/// Everything here is computed over the **generatable graph** — the arena minus every branch
498/// with infinite `min_len` and every `max == 0` repetition body — because the generator never
499/// enters either, so counting what is inside them would make the coverage bound unreachable by
500/// construction (SCOPE.md 6.8).
501struct Coverage {
502    /// Every candidate unit, in node order.
503    units: Vec<Unit>,
504    /// Where each unit sits in `units`.
505    index: HashMap<Unit, usize>,
506    /// Which units have been covered by a successful call.
507    covered: Vec<bool>,
508    /// Which units are reachable from each rule body, cached on first use.
509    ///
510    /// Keyed by the body's [`NodeId`], which is unique per rule across *both* tables. Keying by
511    /// an index into the user-rule slice instead meant every implicit core rule -- which has no
512    /// such index -- collapsed onto one shared entry, so asking about `HEXDIG` after `ALPHA`
513    /// returned `ALPHA`'s answer (D46).
514    reachable: HashMap<NodeId, Vec<usize>>,
515    /// Successors of each node over the generatable graph.
516    edges: Vec<Vec<NodeId>>,
517    /// Distance from each node to the nearest uncovered unit.
518    distance: Vec<u32>,
519    /// Whether `distance` needs recomputing before it is next read.
520    stale: bool,
521}
522
523impl Coverage {
524    /// Enumerates the units of a grammar and builds the graph they live on.
525    fn new(grammar: &CheckedGrammar) -> Self {
526        let node_count = grammar.node_count();
527        let mut units = Vec::new();
528        let mut edges = vec![Vec::new(); node_count];
529
530        for (index, outgoing) in edges.iter_mut().enumerate() {
531            let node = NodeId::from_index(index as u32);
532            match grammar.node(node) {
533                Node::Alt { branches } => {
534                    for (branch, child) in branches.iter().enumerate() {
535                        // A branch that matches nothing is not a unit and not an edge: the
536                        // generator will never take it, so neither may the bound (D20).
537                        if grammar.min_len(*child).is_finite() {
538                            units.push(Unit {
539                                node,
540                                branch: branch as u32,
541                            });
542                            outgoing.push(*child);
543                        }
544                    }
545                }
546                Node::Optional { body } => {
547                    // Two units: branch 0 takes the body, branch 1 skips it. Skipping is always
548                    // available, so it is a unit even when the body is not.
549                    if grammar.min_len(*body).is_finite() {
550                        units.push(Unit { node, branch: 0 });
551                        outgoing.push(*body);
552                    }
553                    units.push(Unit { node, branch: 1 });
554                }
555                Node::Concat { items } => outgoing.extend(items.iter().copied()),
556                Node::Repeat { repeat, body, .. } => {
557                    // `*0(x)` can never run its body, so nothing inside it is reachable (D36).
558                    if !repeat.is_never() {
559                        outgoing.push(*body);
560                    }
561                }
562                Node::RuleRef { .. } => {
563                    if let Some(target) = grammar.target(node) {
564                        outgoing.push(grammar.rule_by_id(target).body);
565                    }
566                }
567                Node::CharVal(_) | Node::NumVal { .. } | Node::ProseVal(_) => {}
568            }
569        }
570
571        let index = units
572            .iter()
573            .enumerate()
574            .map(|(position, unit)| (*unit, position))
575            .collect();
576        let count = units.len();
577        Self {
578            units,
579            index,
580            covered: vec![false; count],
581            reachable: HashMap::new(),
582            edges,
583            distance: vec![UNREACHABLE; node_count],
584            stale: true,
585        }
586    }
587
588    /// How many units reachable from `body` are still uncovered.
589    fn uncovered_reachable(&mut self, grammar: &CheckedGrammar, body: NodeId) -> usize {
590        // Populate the cache first, so the borrow of `reachable` ends before `covered` is read.
591        self.reachable_from(grammar, body);
592        self.reachable[&body]
593            .iter()
594            .filter(|position| !self.covered[**position])
595            .count()
596    }
597
598    /// The units reachable from a rule `body`, cached.
599    ///
600    /// Reachability does not change as coverage grows — the generatable graph is fixed — so
601    /// this is computed once per body.
602    fn reachable_from(&mut self, grammar: &CheckedGrammar, body: NodeId) -> &[usize] {
603        if !self.reachable.contains_key(&body) {
604            let mut seen = vec![false; grammar.node_count()];
605            let mut stack = vec![body];
606            seen[body.index()] = true;
607            let mut found = Vec::new();
608
609            while let Some(node) = stack.pop() {
610                // A unit belongs to the node it is a branch of, so reaching the node is what
611                // makes its units reachable.
612                for (position, unit) in self.units.iter().enumerate() {
613                    if unit.node == node {
614                        found.push(position);
615                    }
616                }
617                for next in &self.edges[node.index()] {
618                    if !seen[next.index()] {
619                        seen[next.index()] = true;
620                        stack.push(*next);
621                    }
622                }
623            }
624            found.sort_unstable();
625            self.reachable.insert(body, found);
626        }
627        &self.reachable[&body]
628    }
629
630    /// Marks a unit covered, if it is one.
631    fn cover(&mut self, unit: Unit) {
632        if let Some(position) = self.index.get(&unit)
633            && !self.covered[*position]
634        {
635            self.covered[*position] = true;
636            // Covering a unit can only *raise* distances, so a stale table still decreases
637            // strictly along a chase — it may merely chase something already covered. Recompute
638            // lazily rather than on every step.
639            self.stale = true;
640        }
641    }
642
643    /// Recomputes `distance` if coverage has changed since it was last read.
644    ///
645    /// Multi-source reverse breadth-first search: every node holding an uncovered unit is at
646    /// distance zero, and every other node is one more than its nearest successor.
647    fn refresh(&mut self) {
648        if !self.stale {
649            return;
650        }
651        self.stale = false;
652
653        let node_count = self.distance.len();
654        self.distance.fill(UNREACHABLE);
655
656        // Reverse edges, so the search can walk from a target back towards its predecessors.
657        let mut incoming: Vec<Vec<usize>> = vec![Vec::new(); node_count];
658        for (from, targets) in self.edges.iter().enumerate() {
659            for target in targets {
660                incoming[target.index()].push(from);
661            }
662        }
663
664        let mut queue = VecDeque::new();
665        for (position, unit) in self.units.iter().enumerate() {
666            if !self.covered[position] {
667                let index = unit.node.index();
668                if self.distance[index] != 0 {
669                    self.distance[index] = 0;
670                    queue.push_back(index);
671                }
672            }
673        }
674        while let Some(node) = queue.pop_front() {
675            let next = self.distance[node] + 1;
676            for previous in &incoming[node] {
677                if self.distance[*previous] > next {
678                    self.distance[*previous] = next;
679                    queue.push_back(*previous);
680                }
681            }
682        }
683    }
684
685    /// How far `node` is from the nearest uncovered unit.
686    fn distance_to_uncovered(&mut self, node: NodeId) -> u32 {
687        self.refresh();
688        self.distance[node.index()]
689    }
690
691    fn is_covered(&self, unit: Unit) -> bool {
692        self.index
693            .get(&unit)
694            .is_some_and(|position| self.covered[*position])
695    }
696}