Skip to main content

trex/
ast.rs

1//! The pattern AST: the intermediate form the parser emits and
2//! the engine consumes.
3//!
4//! Every construct in the surface language (the wiki's pattern syntax
5//! reference) has a node here. The engine matches by folding the AST over
6//! the token stream; the parser is the only producer.
7
8use crate::orbit::OrbitGroup;
9use crate::token::{BracketKind, TokenKind};
10
11/// A byte-grain character class, used when a pattern drops below the token
12/// grain to constrain a token's bytes (`\d`, `\w`, `\s`, `\h`, `\a`, `\u`,
13/// `\l`). Uppercase escapes are whole typed tokens; these lowercase ones are
14/// the regex-style byte classes.
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum ByteClass {
17    /// `\d`: every byte is an ASCII digit.
18    Digit,
19    /// `\w`: every byte is an ASCII word byte (alphanumeric or `_`).
20    Word,
21    /// `\s`: every byte is ASCII whitespace.
22    Space,
23    /// `\h`: every byte is an ASCII hex digit (`0-9a-fA-F`).
24    Hex,
25    /// `\a`: every byte is an ASCII letter.
26    Alpha,
27    /// `\u`: every byte is an ASCII uppercase letter.
28    Upper,
29    /// `\l`: every byte is an ASCII lowercase letter.
30    Lower,
31}
32
33/// How a reading compares against the number written beside it.
34#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
35pub enum Cmp {
36    Lt,
37    Le,
38    Gt,
39    Ge,
40    Eq,
41    Ne,
42}
43
44impl Cmp {
45    /// Whether an ordering of the reading against the written number
46    /// satisfies this comparison.
47    #[must_use]
48    pub fn holds(self, o: std::cmp::Ordering) -> bool {
49        use std::cmp::Ordering;
50        match self {
51            Cmp::Lt => o == Ordering::Less,
52            Cmp::Le => o != Ordering::Greater,
53            Cmp::Gt => o == Ordering::Greater,
54            Cmp::Ge => o != Ordering::Less,
55            Cmp::Eq => o == Ordering::Equal,
56            Cmp::Ne => o != Ordering::Equal,
57        }
58    }
59
60    /// The surface operator, for diagnostics.
61    #[must_use]
62    pub fn glyph(self) -> &'static str {
63        match self {
64            Cmp::Lt => "<",
65            Cmp::Le => "<=",
66            Cmp::Gt => ">",
67            Cmp::Ge => ">=",
68            Cmp::Eq => "=",
69            Cmp::Ne => "!=",
70        }
71    }
72}
73
74/// A reading of the echo axis at one token: how often its content recurs in
75/// the input, which occurrence this one is, and whether the recurrence is
76/// regularly spaced.
77#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
78pub enum EchoPred {
79    /// `@echo>5`: occurrences of this token's content in the input, one for
80    /// a token whose content appears once.
81    Count(Cmp, u32),
82    /// `@echo:nth=3`: this occurrence's place among them, counting from one,
83    /// or from the last backward where the index is negative. Never zero.
84    Nth(Cmp, i32),
85    /// `@echo:period`: the occurrences are regularly spaced.
86    Period,
87    /// `@echo:period=k`: regularly spaced, at `k` bytes to the nearest byte.
88    PeriodAt(Cmp, u32),
89}
90
91impl AnchorKind {
92    /// Whether the anchor reads the input beyond the bytes beside one
93    /// token, so a chunked scanner cannot commit a match under it before
94    /// the input ends: the input's ends, which a retained buffer would
95    /// misjudge at its own; and every field built over the whole stream.
96    /// A line anchor reads to the nearest newline, and a join reads the
97    /// other input, so neither does.
98    #[must_use]
99    pub fn reads_whole_input(&self) -> bool {
100        !matches!(
101            self,
102            AnchorKind::LineStart
103                | AnchorKind::LineEnd
104                | AnchorKind::Resume
105                | AnchorKind::ResetStart
106                | AnchorKind::Joined { .. }
107        )
108    }
109}
110
111/// How a timestamp compares with the one before it in the stream.
112#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
113pub enum TimeOrder {
114    /// At or after it: the records run forward.
115    Asc,
116    /// Before it: the records run backward, a clock skew or an out-of-order
117    /// record.
118    Desc,
119}
120
121impl TimeOrder {
122    /// The direction a name denotes.
123    #[must_use]
124    pub fn parse(name: &str) -> Option<TimeOrder> {
125        match name {
126            "asc" => Some(TimeOrder::Asc),
127            "desc" => Some(TimeOrder::Desc),
128            _ => None,
129        }
130    }
131
132    /// The name it is written under.
133    #[must_use]
134    pub fn label(self) -> &'static str {
135        match self {
136            TimeOrder::Asc => "asc",
137            TimeOrder::Desc => "desc",
138        }
139    }
140}
141
142/// A texture class a spectral predicate can match.
143#[derive(Clone, Copy, Debug, PartialEq, Eq)]
144pub enum SpecTexture {
145    /// Natural-language prose.
146    Prose,
147    /// Source code.
148    Code,
149    /// Mathematics.
150    Math,
151    /// Compressed / packed / binary data.
152    Data,
153}
154
155/// A spectral-axis predicate over a token's pooled field signature
156/// (`\F{...}`): the temporal substrate read as a match condition.
157#[derive(Clone, Copy, Debug, PartialEq, Eq)]
158pub enum SpectralPred {
159    /// Pooled entropy (x100) is at least this (high-entropy / packed).
160    EntropyGe(u8),
161    /// Pooled entropy (x100) is at most this (plain / low-information).
162    EntropyLe(u8),
163    /// The dominant byte-period equals this many bytes.
164    PeriodEq(u16),
165    /// Any dominant byte-period was detected.
166    PeriodAny,
167    /// The pooled texture class matches.
168    Texture(SpecTexture),
169    /// A change-point is inside the token's span.
170    Onset,
171}
172
173/// A magnitude-axis predicate over a token's order of magnitude
174/// (`\M{...}`): the scale substrate read as a match condition. A
175/// Number's magnitude is `log10(|value|)`, any other token's is
176/// `log2(byte length)`, so a threshold of `6` is a numeric value over
177/// ~1e6 or a token over ~64 bytes. The threshold is stored as the
178/// magnitude times 100, an integer, so [`Atom`] keeps deriving `Eq` and
179/// `Hash` (a raw `f32` implements neither), the same fixed-point trick
180/// [`SpectralPred::EntropyGe`] uses.
181#[derive(Clone, Debug, PartialEq, Eq)]
182pub enum MagPred {
183    /// The token's magnitude (x100) is at least this: `\M{>6}` / `\M{>=6}`.
184    Ge(i32),
185    /// The token's magnitude (x100) is at most this: `\M{<3}` / `\M{<=3}`.
186    Le(i32),
187    /// The token's magnitude is at least the mean magnitude of a context
188    /// plus a delta: `\N{>+1}` is an order of magnitude above the mean of
189    /// the rolling window before the token, `\N{>+2s}` two of the window's
190    /// standard deviations above it, `\N{>+1:phase}` an order above the
191    /// token's column, `\N{>+1:k}` an order above the values bound to
192    /// earlier occurrences of the key register `k` holds. A threshold read
193    /// from the stream rather than written into the pattern, which a regular
194    /// expression has no way to state.
195    Above(Scope, Delta),
196    /// The token's magnitude is at most a context's mean plus a delta:
197    /// `\N{<-1}` is an order of magnitude below the window's mean.
198    Below(Scope, Delta),
199}
200
201/// The context a relative magnitude predicate takes its baseline from.
202///
203/// Each is a fold the rolling context keeps at every token
204/// ([`crate::context`]); the predicate reads its magnitude and compares.
205#[derive(Clone, Debug, PartialEq, Eq)]
206pub enum Scope {
207    /// The rolling window of significant tokens before the token. The
208    /// default when no scope is named.
209    Window,
210    /// The earlier tokens at the token's phase of the dominant token-kind
211    /// period: its column, in periodic records.
212    Phase,
213    /// The tokens since the byte grain's last regime change.
214    Regime,
215    /// The earlier occurrences of the token's own key.
216    Echo,
217    /// The heads of the brackets enclosing the token.
218    Enclosing,
219    /// The values bound by `=` or `:` to earlier occurrences of the token
220    /// the named register holds: the history of that key's values.
221    Key(String),
222}
223
224impl Scope {
225    /// The scope a name denotes. The five context names are reserved; any
226    /// other name is a register.
227    #[must_use]
228    pub fn parse(name: &str) -> Scope {
229        match name {
230            "window" => Scope::Window,
231            "phase" => Scope::Phase,
232            "regime" => Scope::Regime,
233            "echo" => Scope::Echo,
234            "enclosing" => Scope::Enclosing,
235            other => Scope::Key(other.to_string()),
236        }
237    }
238}
239
240/// The contexts a pattern reads, one flag per [`Scope`] (the phase anchor
241/// counts as reading the phase).
242#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
243pub struct ContextUses {
244    /// The rolling window (`\N{>+1}`).
245    pub window: bool,
246    /// The phase fold or anchor (`\N{>+1:phase}`, `@phase:2`).
247    pub phase: bool,
248    /// The regime fold (`\N{>+1:regime}`).
249    pub regime: bool,
250    /// The echo fold (`\N{>+1:echo}`).
251    pub echo: bool,
252    /// The enclosure fold (`\N{>+1:enclosing}`).
253    pub enclosing: bool,
254    /// A key's value history (`\N{>+1:k}`).
255    pub key: bool,
256}
257
258impl ContextUses {
259    /// Whether any relation-admitted context or the phase is read.
260    #[must_use]
261    pub fn any_related(&self) -> bool {
262        self.phase || self.regime || self.echo || self.enclosing || self.key
263    }
264}
265
266/// How far a relative predicate's threshold is from the context's mean,
267/// signed, stored times 100 like the absolute thresholds.
268#[derive(Clone, Copy, Debug, PartialEq, Eq)]
269pub enum Delta {
270    /// Orders of magnitude.
271    Orders(i32),
272    /// Standard deviations of the context's magnitudes.
273    Sigmas(i32),
274}
275
276impl MagPred {
277    /// Whether a token whose order of magnitude is `mag` satisfies this
278    /// predicate, given the magnitude fold of the predicate's context for a
279    /// relative form. `None`, or an empty fold, matches nothing: there is no
280    /// baseline to be above. A sigma form also needs a spread to measure in,
281    /// so a context of one value, or of equal values, matches nothing
282    /// either. Both engines resolve the token's magnitude with
283    /// [`crate::magnitude::token_magnitude`] and call this, so the compare
284    /// lives in one place.
285    #[must_use]
286    pub fn matches(&self, mag: f32, context: Option<&crate::profile::MagnitudeProfile>) -> bool {
287        match self {
288            MagPred::Ge(centi) => mag * 100.0 >= *centi as f32,
289            MagPred::Le(centi) => mag * 100.0 <= *centi as f32,
290            MagPred::Above(_, delta) | MagPred::Below(_, delta) => {
291                let Some(c) = context.filter(|c| c.count > 0) else { return false };
292                let offset = match delta {
293                    Delta::Orders(centi) => *centi as f32 / 100.0,
294                    Delta::Sigmas(centi) => {
295                        let sigma = c.std_dev();
296                        if c.count < 2 || sigma <= 0.0 {
297                            return false;
298                        }
299                        *centi as f32 / 100.0 * sigma
300                    }
301                };
302                let threshold = c.mean() + offset;
303                if matches!(self, MagPred::Above(..)) { mag >= threshold } else { mag <= threshold }
304            }
305        }
306    }
307
308    /// The context a relative form reads; `None` for an absolute one.
309    #[must_use]
310    pub fn scope(&self) -> Option<&Scope> {
311        match self {
312            MagPred::Ge(_) | MagPred::Le(_) => None,
313            MagPred::Above(s, _) | MagPred::Below(s, _) => Some(s),
314        }
315    }
316}
317
318/// Which stream a grain-qualified reading runs over.
319///
320/// An axis that reads a sequence can read any of trex's three, and they are
321/// different sequences rather than coarser views of one: a byte-grain seam is
322/// a break in the bytes, a token-grain seam a break in the sequence of kinds,
323/// a supertoken-grain seam a break in the sequence of roles. A pattern names
324/// the one it means.
325#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
326pub enum Grain {
327    /// The byte stream. The unqualified reading, and the default.
328    #[default]
329    Byte,
330    /// The significant-token stream, read by kind.
331    Token,
332    /// The supertoken stream, read by role.
333    Super,
334}
335
336impl Grain {
337    /// The grain a name denotes, or `None` when it names none.
338    #[must_use]
339    pub fn parse(name: &str) -> Option<Grain> {
340        Some(match name {
341            "byte" => Grain::Byte,
342            "token" => Grain::Token,
343            "super" => Grain::Super,
344            _ => return None,
345        })
346    }
347
348    /// The name [`Self::parse`] reads, as a pattern writes it after a colon.
349    #[must_use]
350    pub fn name(self) -> &'static str {
351        match self {
352            Grain::Byte => "byte",
353            Grain::Token => "token",
354            Grain::Super => "super",
355        }
356    }
357}
358
359/// A second input a join anchor reads: its bytes and their lex, taken once
360/// when the pattern is parsed, so every anchor naming it shares one lex.
361#[derive(Debug, PartialEq, Eq)]
362pub struct OtherInput {
363    /// The name the pattern wrote after `@`: a path, or a name the caller
364    /// supplied the bytes for.
365    pub name: String,
366    pub bytes: Vec<u8>,
367    pub tokens: Vec<crate::token::Token>,
368}
369
370/// An `f64` held by its bit pattern, so a pattern node carrying one keeps
371/// `Eq`.
372#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
373pub struct Real(u64);
374
375impl Real {
376    /// The real `v`.
377    #[must_use]
378    pub fn new(v: f64) -> Real {
379        Real(v.to_bits())
380    }
381
382    /// The value held.
383    #[must_use]
384    pub fn get(self) -> f64 {
385        f64::from_bits(self.0)
386    }
387}
388
389/// What a gravity reading is held against: a percentile of the input's own
390/// readings at the grain, or a value in bits.
391#[derive(Clone, Copy, Debug, PartialEq, Eq)]
392pub enum Level {
393    /// `@strain>90`: the reading at that percentile, `0..=100`, of this
394    /// input's readings.
395    Percentile(Real),
396    /// `@strain>2.5b`: that many bits.
397    Bits(Real),
398}
399
400/// The cut an `@ambiguous` or `@seam` anchor holds its reading against: the
401/// `>` or `>=` and the number written after it.
402#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
403pub struct AnchorCut {
404    /// [`Cmp::Gt`] or [`Cmp::Ge`].
405    pub cmp: Cmp,
406    /// The number written after the comparison: a disagreement for
407    /// `@ambiguous`, standard deviations above the input's mean seam strength
408    /// for `@seam`.
409    pub value: Real,
410}
411
412impl AnchorCut {
413    /// Whether the comparison is strict, `>` rather than `>=`.
414    #[must_use]
415    pub fn strict(self) -> bool {
416        self.cmp == Cmp::Gt
417    }
418
419    /// The cut as written, `>0.3` or `>=1.5`.
420    #[must_use]
421    pub fn written(self) -> String {
422        format!("{}{}", self.cmp.glyph(), self.value.get())
423    }
424
425    /// The smallest `f32` reading that passes this cut, so a reader holding
426    /// its readings as `f32` keeps a reading at or above it and compares
427    /// exactly against the number as written.
428    #[must_use]
429    pub fn least_passing(self) -> f32 {
430        let v = self.value.get();
431        let near = v as f32;
432        let passes = |t: f32| if self.strict() { f64::from(t) > v } else { f64::from(t) >= v };
433        if passes(near) { near } else { near.next_up() }
434    }
435}
436
437/// Which reading of the input's pair field an anchor holds against a level
438/// (see [`crate::gravity`]).
439#[derive(Clone, Copy, Debug, PartialEq, Eq)]
440pub enum GravityReading {
441    /// `@strain`: a unit's mean potential against the units before it.
442    Strain,
443    /// `@bound`: the attraction across the cut before a unit.
444    Bound,
445}
446
447/// A zero-width position anchor: it asserts a property of the current
448/// position (a boundary in a precomputed axis field) without consuming a
449/// token, the statistical analog of regex's `^` / `$` / `\b`.
450#[derive(Clone, Debug, PartialEq, Eq)]
451pub enum AnchorKind {
452    /// `@seam`: the current token starts at a predictive-segmentation cut -
453    /// a point where the past stops predicting the future (branching-entropy
454    /// boundary), with no delimiter to anchor on.
455    ///
456    /// The grain says which sequence has to stop predicting itself.
457    /// `@seam` reads the sequence of token kinds, `@seam:byte` the bytes,
458    /// `@seam:super` the sequence of construct roles. A byte-grain cut is at a
459    /// word boundary; a token-grain cut is where the shape of the statement
460    /// changes, which is a different question about the same input.
461    ///
462    /// The cut is the one written after the grain, `@seam:byte>1.5`, in
463    /// standard deviations above the mean strength at that grain, or the
464    /// reader's own where none is: at least one standard deviation above the
465    /// mean at the byte grain, any cut of positive strength at the others.
466    Seam(Grain, Option<AnchorCut>),
467    /// `@strain>90`, `@bound<10`, `@strain:byte>2.5b`, `@bound:super<=5`: a
468    /// reading of the input's pair field at the current token, held against a
469    /// percentile of the input's own readings or a value in bits.
470    ///
471    /// The grain says which units the field is learned over and read at:
472    /// `@strain` reads the token, `:byte` the token's first byte, `:super` the
473    /// supertoken holding the token. Strain is that unit's; bound is the cut
474    /// before it, so at the supertoken grain it holds only at a token that
475    /// opens its supertoken.
476    Gravity(GravityReading, Grain, Cmp, Level),
477    /// `@kin("x")`, `@kin:byte("e")`, `@kin:super("f(x)")`: the current unit's
478    /// type is `x`'s type or shares its gravity class in this input - a type
479    /// the input's pair field treats like the one `x` lexes to.
480    ///
481    /// The unit is read at the grain as [`Self::Gravity`] reads it. `x` names
482    /// its type by example: at the byte grain it is one byte, at the token
483    /// grain the first token it lexes to, at the supertoken grain the first
484    /// supertoken it forms. An example the input never holds matches nothing.
485    Kin(Grain, String),
486    /// `@nested>k` / `@nested>=k`: the current token is at least this many
487    /// brackets deep (the stress-axis structural-load regime). `@nested>2`
488    /// stores `3`; `@nested>=2` stores `2`.
489    Nested(u16),
490    /// `@ambiguous`: the current token's span contains a contested point - a
491    /// position whose reading depends on the observer's vantage (the causal
492    /// and anticausal readings disagree), the observation axis.
493    ///
494    /// The grain says which sequence's two readings have to disagree.
495    /// `@ambiguous` reads the bytes, where the contest is over how characters
496    /// group; `@ambiguous:token` and `@ambiguous:super` read the sequences
497    /// above, where it is over how structure groups.
498    ///
499    /// The cut is the one written after the grain, `@ambiguous>0.3`, or the
500    /// reader's own disagreement of at least 0.2 where none is.
501    Ambiguous(Grain, Option<AnchorCut>),
502    /// `@novel`: the current token is the first occurrence of its content in
503    /// the input - a keyed token with no prior echo (the echo axis).
504    Novel,
505    /// `@echoed`: the current token's content recurs elsewhere in the input
506    /// (before or after) - a keyed token whose echo count is at least two.
507    Echoed,
508    /// `@echo>5`, `@echo:nth=3`, `@echo:period`: a reading of the echo axis
509    /// at the current token, counted at the orbit rung an `(?orbit:G ...)`
510    /// scope around it names.
511    Echo(EchoPred, OrbitGroup),
512    /// `@order:asc` / `@order:desc`: the timestamp here is no earlier than
513    /// the timestamp token before it in the stream, or no later. A token
514    /// that is not a timestamp, and the first timestamp of an input, satisfy
515    /// neither: there is no pair to order.
516    Order(TimeOrder),
517    /// `@shape:rare`, `@shape:rare<5`, `@shape:rare<1%`: this token's line
518    /// has a template rarer than the cut, the templates being the input's
519    /// lines grouped by token-kind silhouette.
520    Rare(crate::templates::Rarity),
521    /// `@echoed:@other.log` / `@novel:@other.log`: the current token's
522    /// content occurs somewhere in a second input, or nowhere in it, keyed
523    /// at the orbit rung an `(?orbit:G ...)` scope around it names. A token
524    /// the echo axis does not key satisfies neither.
525    Joined { other: std::sync::Arc<OtherInput>, recurs: bool, group: OrbitGroup },
526    /// `^`: the current token is the first significant token of a line,
527    /// or of the input.
528    ///
529    /// The positional counterpart to the property anchors above: every other
530    /// anchor describes what a token is, and this one describes where it is.
531    /// Line-leading position separates a token being used from a token being
532    /// mentioned: in most line-oriented input the first token of a line is
533    /// the thing acting, and the same word later in the line is an argument
534    /// or prose.
535    ///
536    /// Whitespace does not count as significant, so an indented token
537    /// still leads its line.
538    LineStart,
539    /// `$`: the current token is the last significant token of a line, or
540    /// of the input. The mirror of [`Self::LineStart`].
541    LineEnd,
542    /// `@super`: the current token is the first of its supertoken - a
543    /// construct boundary.
544    ///
545    /// The upper-grain counterpart of `@seam`. Where a seam is a statistical
546    /// break in the byte stream, this is a structural one in the tower: the
547    /// point where one construct ends and the next begins.
548    SuperStart,
549    /// `@super:role`: the supertoken containing the current token has this
550    /// role.
551    ///
552    /// The one thing a regular expression cannot state at all, because it has
553    /// no notion of a container: an atom conditioned on what encloses it. A
554    /// number is a number anywhere, but a number inside an assignment
555    /// is a value and a number inside a call is an argument, and this is what
556    /// tells them apart without a grammar for the language.
557    SuperRole(crate::supertoken::Role),
558    /// `\K`: report the match as beginning here, discarding what was matched
559    /// before it.
560    ///
561    /// Everything to its left is still required, so it reads as a lookbehind
562    /// whose width need not be known: `"key" ":" \K \W` requires the key and
563    /// colon and reports only the value. [`Look::Behind`] is the other way to
564    /// say that and needs a bounded sub-pattern, since it tries start
565    /// positions in a fixed window; this has no such limit because it
566    /// consumes what it looks at rather than searching backward for it.
567    ///
568    /// It moves the reported start only. The scan resumes from the match's
569    /// real end, so a run does not overlap the part a `\K` hid.
570    ResetStart,
571    /// `\G`: the current token is exactly where the previous match ended, so
572    /// this match abuts it with nothing skipped.
573    ///
574    /// A scan reports the leftmost non-overlapping matches, which lets it
575    /// skip whatever fails to match. This refuses that: a run of matches
576    /// carrying `\G` covers a contiguous stretch, and the run stops at the
577    /// first token the pattern cannot take. That is the difference between
578    /// finding occurrences and tokenizing, and it is the one thing a scan
579    /// cannot express by filtering its results afterward - the gap has to
580    /// forbid the match rather than be noticed once the match exists.
581    ///
582    /// At the first attempt there is no previous match, so it holds at the
583    /// start of the input.
584    Resume,
585    /// `\A`: the current token is the first significant token of the whole
586    /// input.
587    ///
588    /// Distinct from [`Self::LineStart`], which holds at the head of every
589    /// line. The two are separate because trex's input is usually
590    /// line-oriented, so the line reading is the useful default and keeps the
591    /// unmarked spelling; a pattern that means the start of the stream says so.
592    /// The scope is the input, not any enclosing balanced group, so `\A` inside
593    /// `\B(...)` still asks about the stream.
594    InputStart,
595    /// `\z`: the current token is the last significant token of the whole
596    /// input. The mirror of [`Self::InputStart`].
597    InputEnd,
598    /// `@phase:k`: the current token is at phase `k` of the dominant
599    /// token-kind period - column `k` of a periodic record, counted in
600    /// significant tokens from the stream start, with no delimiter named.
601    /// Holds nowhere when the stream has no period.
602    Phase(u16),
603    /// `@phase:k/p`, `@phase:k#n`: the current token is at column `k` of a
604    /// named period - the `p`-token period, or the `n`-th strongest - where
605    /// that period is live in the stream by the gate [`Self::Phase`]'s period
606    /// clears ([`crate::context::live_periods`]). Holds nowhere when it is not.
607    PhaseIn(u16, PeriodRef),
608}
609
610/// Which of a stream's live periods a phase anchor counts against.
611#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
612pub enum PeriodRef {
613    /// The period of this many significant tokens.
614    Length(u16),
615    /// The `n`-th strongest live period, counting from one. Two periods of
616    /// nearly one strength can trade ranks between slices of one input.
617    Rank(u16),
618}
619
620/// A set expression over token matchers: `[\N \W]`, `[^\N]`, `[\W && \h]`,
621/// `[\W -- \u]`.
622///
623/// A token is a member when it matches one of `any`, and also one of `all`
624/// when that is non-empty, and none of `none`; `negated` then flips the
625/// answer. The three lists are flat rather than a nested expression tree,
626/// which covers union, complement, intersection and difference without a
627/// precedence rule for the reader to learn.
628///
629/// Complement is what makes the token grain closed under the regular
630/// operations, and it is cheaper here than in a byte regex: the alphabet is a
631/// handful of typed kinds rather than 256 byte values, so a class is a
632/// predicate over members and negation is one flag.
633#[derive(Clone, Debug, PartialEq, Eq)]
634pub struct TokenClass {
635    /// Unioned members; a token must match one of these.
636    pub any: Vec<Atom>,
637    /// Intersected members (`&&`); when non-empty a token must also match one.
638    pub all: Vec<Atom>,
639    /// Subtracted members (`--`); a token must match none of these.
640    pub none: Vec<Atom>,
641    /// `[^...]`: the whole membership answer is inverted.
642    pub negated: bool,
643}
644
645impl TokenClass {
646    /// Every member atom across the three lists.
647    pub fn members(&self) -> impl Iterator<Item = &Atom> {
648        self.any.iter().chain(&self.all).chain(&self.none)
649    }
650
651    /// Every member atom, mutably.
652    pub fn members_mut(&mut self) -> impl Iterator<Item = &mut Atom> {
653        self.any.iter_mut().chain(&mut self.all).chain(&mut self.none)
654    }
655}
656
657impl Atom {
658    /// A literal compared byte-for-byte, the default reading.
659    #[must_use]
660    pub fn literal(text: &str) -> Atom {
661        Atom::Literal(text.to_string(), OrbitGroup::Identity)
662    }
663
664    /// Rewrite every literal in this atom to compare under `group`, recursing
665    /// into class members. Applied once at parse time by an `(?orbit:G ...)`
666    /// scope, so the engines see a literal that already knows how it compares
667    /// and neither has to carry a scope stack. The single-pass engine bakes
668    /// atoms into instructions at compile time and could not carry one.
669    pub fn set_orbit(&mut self, group: OrbitGroup) {
670        match self {
671            Atom::Literal(_, g) | Atom::LiteralWithin(_, _, g) | Atom::RegisterWithin(_, _, g) => {
672                *g = group;
673            }
674            Atom::Class(c) => {
675                for m in c.members_mut() {
676                    m.set_orbit(group);
677                }
678            }
679            _ => {}
680        }
681    }
682}
683
684/// A single-token matcher.
685#[derive(Clone, Debug, PartialEq, Eq)]
686pub enum Atom {
687    /// A token of a specific typed class (`\N`, `\W`, `\Q`, ...).
688    Kind(TokenKind),
689    /// Any one significant token (`.`).
690    Any,
691    /// A literal token whose text equals this string (`"lit"` or a bare
692    /// punctuation character), compared under a symmetry group.
693    ///
694    /// [`OrbitGroup::Identity`] is byte equality, the default everywhere. A
695    /// `(?orbit:G ...)` scope rewrites the literals inside it to carry `G`, so
696    /// `(?orbit:case "Cat")` matches `cat`, and the notation rung matches a
697    /// Greek glyph against its TeX name. This is the generalization regex
698    /// spells `(?i)`, which has only the one rung.
699    Literal(String, OrbitGroup),
700    /// A token whose text matches the value bound to a register. Plain
701    /// `=name` compares byte-for-byte (the [`OrbitGroup::Identity`] orbit);
702    /// `=shape name` / `=case name` / `=notation name` compare under a
703    /// symmetry group, so the reference matches every token in the bound
704    /// token's orbit - a fuzzy backreference, not just an exact repeat.
705    RegisterEq(String, OrbitGroup),
706    /// A token in a typed relation to the value bound to a register:
707    /// `=subnet a` the same network, `=domain e` the same mail domain,
708    /// `=day t` the same calendar day, `=major v` the same major version.
709    /// Both sides are projected through the relation's typed field and the
710    /// projections compared, so the relation is decided by the values the
711    /// lexer recognized rather than by the bytes.
712    RegisterRelated(String, crate::typed::Relation),
713    /// A literal token within `k` edits of this string (`"lit"~k`): an
714    /// insertion, a deletion or a substitution of one character each, over
715    /// the two texts as the group canonicalizes them.
716    LiteralWithin(String, u8, OrbitGroup),
717    /// A token within `k` edits of the value bound to a register
718    /// (`=editk name`), over the two texts as the group canonicalizes them.
719    RegisterWithin(String, u8, OrbitGroup),
720    /// `=kin a`, `=kin:byte a`, `=kin:super a`: a token whose unit the input's
721    /// pair field places with the unit the value bound to the register starts
722    /// in - the same type, or one placed gravity class ([`crate::gravity`]).
723    /// The grain is read as [`AnchorKind::Kin`] reads it.
724    RegisterKin(String, Grain),
725    /// A token whose bytes all satisfy a byte class (`\d`, `\w`,
726    /// `\s`).
727    Byte(ByteClass),
728    /// A token whose bytes match a whole-anchored byte-pattern
729    /// (`` `[A-Z][a-z]+` ``). The low grain composed into the token
730    /// grain: regex-style byte matching inside one token.
731    BytePattern(crate::bytepat::BytePat),
732    /// A token whose pooled spectral signature satisfies a predicate
733    /// (`\F{...}`): the temporal substrate (entropy, period, texture,
734    /// change-point) composed into the token grain.
735    Spectral(SpectralPred),
736    /// A token whose order of magnitude satisfies a predicate (`\M{...}`):
737    /// the scale substrate composed into the token grain. Stateless (a pure
738    /// function of the token's kind and bytes), so it matches in the linear
739    /// single-pass engine with no side field, unlike [`Atom::Spectral`].
740    Magnitude(MagPred),
741    /// A token satisfying a class set-expression (`[\N \W]`, `[^\N]`).
742    Class(Box<TokenClass>),
743    /// A token of a specific kind whose magnitude also satisfies a predicate
744    /// (`\N{>6}`): the kind atom intersected with the magnitude axis on the
745    /// same token ("a number, specifically, over a million"). Stateless, like
746    /// [`Atom::Magnitude`]; the `{...}` here is a predicate, not a repeat
747    /// count, which the parser tells apart by the comparison operator.
748    KindMag(TokenKind, MagPred),
749    /// A token of a specific kind whose value, read in the kind's own units,
750    /// satisfies a typed predicate (`\I{in:10.0.0.0/8}`, `\V{>=2.0,<3}`,
751    /// `\T{age<24h}`): the kind atom intersected with a comparison the
752    /// lexer's recognition of the token makes possible. A function of the
753    /// token's bytes, and of the clock for a timestamp clause, which each
754    /// engine reads once per scan.
755    KindPred(TokenKind, crate::typed::TypedPred),
756    /// A timestamp a written distance away from the one a register holds
757    /// (`\T{>+1h:t}`): the difference between this instant and the bound one,
758    /// compared against a signed duration. The threshold is read from the
759    /// stream rather than written into the pattern, so a gap and a burst are
760    /// the same construct with two operators.
761    Since(Cmp, Signed, String),
762}
763
764/// A duration written with a sign, in nanoseconds.
765#[derive(Clone, Debug, PartialEq, Eq, Hash)]
766pub struct Signed {
767    /// Whether the duration runs backward, before rather than after.
768    pub negative: bool,
769    /// Its magnitude in nanoseconds.
770    pub nanos: crate::typed::Decimal,
771}
772
773impl Atom {
774    /// The magnitude predicate this atom carries, if any; a class answers
775    /// for its first member that carries one.
776    #[must_use]
777    pub fn magnitude_pred(&self) -> Option<&MagPred> {
778        match self {
779            Atom::Magnitude(p) | Atom::KindMag(_, p) => Some(p),
780            Atom::Class(c) => c.members().find_map(Atom::magnitude_pred),
781            _ => None,
782        }
783    }
784
785    /// The register a relative magnitude predicate on this atom reads its
786    /// baseline through, if it reads one.
787    #[must_use]
788    pub fn key_scope(&self) -> Option<&str> {
789        match self.magnitude_pred().and_then(MagPred::scope) {
790            Some(Scope::Key(name)) => Some(name),
791            _ => None,
792        }
793    }
794
795    /// `rename` applied to every register name the atom reads: a
796    /// back-reference, a typed relation, an edit distance, a timestamp gap,
797    /// a magnitude history, and the members of a class.
798    pub(crate) fn rename_registers(&mut self, rename: &dyn Fn(&mut String)) {
799        match self {
800            Atom::RegisterEq(name, _)
801            | Atom::RegisterRelated(name, _)
802            | Atom::RegisterWithin(name, _, _)
803            | Atom::RegisterKin(name, _)
804            | Atom::Since(_, _, name) => rename(name),
805            Atom::Magnitude(p) | Atom::KindMag(_, p) => p.rename_key(rename),
806            Atom::Class(c) => {
807                for member in c.members_mut() {
808                    member.rename_registers(rename);
809                }
810            }
811            _ => {}
812        }
813    }
814}
815
816impl MagPred {
817    /// `rename` applied to the register a relative predicate reads its
818    /// history through, where it reads one.
819    fn rename_key(&mut self, rename: &dyn Fn(&mut String)) {
820        if let MagPred::Above(Scope::Key(name), _) | MagPred::Below(Scope::Key(name), _) = self {
821            rename(name);
822        }
823    }
824}
825
826/// How an alternation chooses among its branches.
827///
828/// The three are genuinely different answers, not shades of one. For
829/// `("a" | "a" "b") "c"` over `a b c`: [`Self::First`] and [`Self::Longest`]
830/// both match the whole span, [`Self::Committed`] matches nothing, because it
831/// takes the short branch and never reconsiders when `"c"` fails. For
832/// `\W | \W \W` over `a b`: [`Self::First`] and [`Self::Committed`] match `a`
833/// then `b`, [`Self::Longest`] matches `a b`.
834#[derive(Clone, Copy, Debug, PartialEq, Eq)]
835pub enum AltMode {
836    /// `|`: leftmost-first, the default. Branches are preferred in order, but
837    /// a branch whose continuation fails yields to the next. Perl, PCRE and
838    /// Rust's `regex` all match this way, so a pattern ported from one of them
839    /// keeps its meaning.
840    First,
841    /// `||`: leftmost-longest. A true regular union with no preference; the
842    /// longest overall match wins. POSIX and RE2 match this way.
843    Longest,
844    /// `|>`: committed choice. The first branch that matches wins outright and
845    /// the rest are never tried, whatever follows. PEG semantics, and what the
846    /// grammar engine uses internally.
847    Committed,
848}
849
850impl AltMode {
851    /// The surface operator, for diagnostics.
852    #[must_use]
853    pub fn glyph(self) -> &'static str {
854        match self {
855            AltMode::First => "|",
856            AltMode::Longest => "||",
857            AltMode::Committed => "|>",
858        }
859    }
860}
861
862/// Which way a zero-width assertion reads from the current position.
863#[derive(Clone, Copy, Debug, PartialEq, Eq)]
864pub enum Look {
865    /// `~(P)`: `P` matches starting here.
866    Ahead,
867    /// `~<(P)`: `P` matches ending here. Requires a bounded `P`, so the
868    /// start positions to try are a fixed window rather than the whole prefix.
869    Behind,
870    /// `~>k(P)` and `~>k{m,n}(P)`: of the `k` significant tokens starting at
871    /// the position the assertion holds at, between `m` and `n` are positions
872    /// where `P` starts a match. `~>k(P)` is `m` of one and no `n`.
873    ///
874    /// The position is the one reached, not the one last consumed, so in
875    /// `\W ~>2(P)` the window opens on the token after the word rather than on
876    /// the word. Every assertion reads from its own position, this one
877    /// included.
878    ///
879    /// Two questions a regular expression cannot ask, in one shape. Distance
880    /// in tokens is the unit a token stream has and a byte stream does not -
881    /// `.{0,n}` over bytes is a different question, since a token is not
882    /// bounded in bytes. Counting how many times something occurs nearby is
883    /// not a regular property at all.
884    ///
885    /// `window` is never zero and `at_least` never exceeds `at_most`; the
886    /// parser refuses a window that can hold nothing and a range that nothing
887    /// can satisfy.
888    ///
889    /// The window is why the assertion is bounded, and a bounded assertion
890    /// does not make a match depend on the whole input the way `~(P)` and a
891    /// content guard do.
892    Within { window: usize, at_least: usize, at_most: Option<usize> },
893    /// `~#(P)` and `~#{m,n}(P)`: of the significant tokens inside the balanced
894    /// group that opens at the position the assertion holds at, between `m` and
895    /// `n` are positions where `P` starts a match. `~#(P)` is `m` of one and no
896    /// `n`.
897    ///
898    /// The region is the group, so `P` is confined to it: a match cannot run
899    /// past the closing bracket, and no token after it is a position `P` is
900    /// tried at. Nested groups are inside the region, so their tokens count;
901    /// counting at one bracket depth only is what `split_at_depth` asks.
902    ///
903    /// A position that opens no group has an empty region and a count of zero,
904    /// which leaves the two polarities exact complements of each other at every
905    /// position rather than only where a bracket is.
906    ///
907    /// [`Self::Within`] bounds the same count by a token distance the pattern
908    /// names; this one bounds it by a structure the input has. That is the
909    /// difference that puts it past a regular language: the extent is the
910    /// matching bracket, which is found by counting brackets, and a regular
911    /// language cannot count them.
912    ///
913    /// The reach is therefore not a number the pattern carries, so no scanner
914    /// can hold enough tokens past a match to finalize one.
915    /// [`Pattern::has_assert`] reports it for that reason, which makes
916    /// [`Pattern::depends_on_whole_input`] true and the prefix machinery
917    /// decline the pattern rather than read a group a cut truncated.
918    InGroup { at_least: usize, at_most: Option<usize> },
919}
920
921impl Look {
922    /// Whether absence satisfies the assertion: a count whose floor is zero is
923    /// met by a region holding nothing, so the sub-pattern under it requires
924    /// nothing of the input at all.
925    ///
926    /// A shortcut that reads such a sub-pattern's literals as required refuses
927    /// an input unscanned that would have matched. The counting forms are the
928    /// only ones that can be satisfied this way: every other assertion runs its
929    /// sub-pattern and needs it to succeed.
930    #[must_use]
931    pub fn satisfied_by_absence(self) -> bool {
932        matches!(self, Self::Within { at_least: 0, .. } | Self::InGroup { at_least: 0, .. })
933    }
934}
935
936/// Which way a quantifier leans when several match lengths are possible.
937///
938/// The language a quantifier accepts is the same either way; the difference is
939/// which accepted length is reported. `\W*? \N` and `\W* \N` accept the same
940/// inputs and return different spans on most of them.
941#[derive(Clone, Copy, Debug, PartialEq, Eq)]
942pub enum Greed {
943    /// `*` `+` `?` `{m,n}`: prefer the longest match, the default everywhere.
944    Greedy,
945    /// `*?` `+?` `??` `{m,n}?`: prefer the shortest.
946    Lazy,
947}
948
949/// Which reading a repetition takes when its body can match without
950/// consuming a token.
951///
952/// Allowing an empty iteration to repeat leaves a greedy star with no
953/// most-preferred derivation at all, so every engine removes something to make
954/// an answer exist, and the two families remove different things. The
955/// difference is visible only where the body's highest-priority alternation
956/// branch is nullable and a lower-priority one consumes; reverse those two and
957/// the readings agree.
958///
959/// This is a property of the whole pattern rather than of one node. A pattern
960/// names it with a leading `(?empty:perl)` or `(?empty:thompson)`, and
961/// [`crate::parser::parse_with_empty_loop`] hands it back beside the tree;
962/// the tree itself does not carry it, so no consumer can forget to unwrap it
963/// and read the wrong one.
964#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
965pub enum EmptyLoop {
966    /// The crate's reading, and the default. A thread whose body matched empty
967    /// returns to a state it already reached, and a simulation that carries
968    /// one thread per state drops it, so the branch that consumed wins.
969    #[default]
970    Thompson,
971    /// The backtracking reading. The empty iteration is taken and the loop
972    /// then breaks, so the branch that matched empty is on the path and the
973    /// match ends earlier.
974    Perl,
975}
976
977/// A pattern node.
978#[derive(Clone, Debug, PartialEq, Eq)]
979pub enum Pattern {
980    /// Matches the empty token sequence.
981    Empty,
982    /// One token.
983    Atom(Atom),
984    /// `P:name` / `P::name`: match `P` and bind the matched span text to
985    /// `name`. The flag is scope: `false` (`:name`) binds for the rest of the
986    /// match; `true` (`::name`) is scoped to the enclosing balanced group, so
987    /// the binding is dropped when that group closes and a reference cannot
988    /// leak out of it.
989    Bind(String, bool, Box<Pattern>),
990    /// `\B(P)`, `\B[P]`, `\B{P}`, or bare `\B`: a balanced bracket
991    /// group whose interior matches `P`. `None` accepts any bracket
992    /// kind.
993    Balanced(Option<BracketKind>, Box<Pattern>),
994    /// `~"lit"` / `!~"lit"`: a zero-width assertion on `lit` in the
995    /// forward window. The flag is the negation: `false` asserts `lit`
996    /// occurs (`~"lit"`), `true` asserts it does not (`!~"lit"`, a
997    /// negative lookahead that stays linear and ReDoS-free).
998    Guard(String, bool),
999    /// `~(P)` / `!~(P)` / `~<(P)` / `!~<(P)`: a zero-width assertion that `P`
1000    /// matches at the current position, consuming nothing. The flag is the
1001    /// negation. This is the sub-pattern generalization of [`Self::Guard`],
1002    /// which asks only whether a literal occurs somewhere in the forward
1003    /// window; the literal form is kept because a prefilter can answer it
1004    /// without a positional scan.
1005    Assert(Box<Pattern>, bool, Look),
1006    /// `@seam` (and future axis anchors): a zero-width assertion that the
1007    /// current position is at a boundary in a precomputed axis field. It
1008    /// consumes no token; it filters the reachable set by position.
1009    Anchor(AnchorKind),
1010    /// `@k P`: position at the k-th field, then match `P`.
1011    Field(usize, Box<Pattern>),
1012    /// `P Q ...`: a sequence matched left to right.
1013    Concat(Vec<Pattern>),
1014    /// `P | Q ...`: alternation, with the mode fixing how a branch is chosen.
1015    Alt(Vec<Pattern>, AltMode),
1016    /// `P*` / `P*?`.
1017    Star(Box<Pattern>, Greed),
1018    /// `P+` / `P+?`.
1019    Plus(Box<Pattern>, Greed),
1020    /// `P?` / `P??`.
1021    Opt(Box<Pattern>, Greed),
1022    /// `P{m,n}` / `P{m,n}?`; `n` is `None` for an open upper bound (`P{m,}`).
1023    Repeat(Box<Pattern>, usize, Option<usize>, Greed),
1024    /// `(?>P)`, and the possessive quantifiers that expand to it: match `P`,
1025    /// keep only the length `P` itself preferred, and never offer another.
1026    ///
1027    /// In a backtracking engine this exists to stop catastrophic
1028    /// backtracking. That reason does not apply here - neither engine
1029    /// backtracks - so what is left is the meaning: `(?>\W*) \W` cannot
1030    /// match, because the star takes every word and the atom is then offered
1031    /// nothing, where `\W* \W` would hand back one.
1032    ///
1033    /// It is a cut over lengths, as `|>` is a cut over branches, and it goes
1034    /// to the same engine for the same reason: the single-pass engine's
1035    /// thread priority expresses a preference and has no way to discard the
1036    /// alternatives it has already queued.
1037    Atomic(Box<Pattern>),
1038    /// `(A B C)~k`: a run of tokens within `k` token edits of the atom
1039    /// sequence, an edit being a token the run lacks, a token it has extra,
1040    /// or a token that matches no atom in its place. The token-grain form of
1041    /// [`Atom::LiteralWithin`], which counts characters within one token.
1042    ///
1043    /// Every run within `k` is offered, ranked by edit cost and then by
1044    /// length, so what follows the group chooses among the alignments the
1045    /// way it chooses among an alternation's branches.
1046    Within(Vec<EditAtom>, u8),
1047}
1048
1049/// One element of a `(A B C)~k` group.
1050///
1051/// A single-token atom, because the walk aligns one atom against one token
1052/// and a cell of it is one atom test; the parser refuses anything else
1053/// inside the group rather than reading it as something it is not.
1054#[derive(Clone, Debug, PartialEq, Eq)]
1055pub struct EditAtom {
1056    /// The atom, matched against its aligned token as it would be alone.
1057    pub atom: Atom,
1058    /// The register the token this atom aligned with binds to, and whether
1059    /// the binding is scoped to the enclosing balanced group, as
1060    /// [`Pattern::Bind`] carries them. An atom the alignment deleted binds
1061    /// nothing, as a register under an untaken optional does.
1062    pub bind: Option<(String, bool)>,
1063}
1064
1065impl Pattern {
1066    /// Wrap a pattern in a box. Small helper to keep the parser
1067    /// terse.
1068    #[must_use]
1069    pub fn boxed(self) -> Box<Pattern> {
1070        Box::new(self)
1071    }
1072
1073    /// Whether any atom reads a register back, as `=name` does.
1074    ///
1075    /// This is what decides whether a binding can be erased without changing
1076    /// which spans match: a binding records what a match consumed and
1077    /// constrains nothing, so a pattern nothing reads back matches the same
1078    /// spans with every binding removed.
1079    #[must_use]
1080    pub fn reads_a_register(&self) -> bool {
1081        fn atom_reads(a: &Atom) -> bool {
1082            match a {
1083                Atom::RegisterEq(..)
1084                | Atom::RegisterRelated(..)
1085                | Atom::RegisterWithin(..)
1086                | Atom::RegisterKin(..)
1087                | Atom::Since(..) => true,
1088                Atom::Class(c) => {
1089                    c.any.iter().chain(&c.all).chain(&c.none).any(atom_reads)
1090                }
1091                _ => false,
1092            }
1093        }
1094        match self {
1095            Pattern::Atom(a) => atom_reads(a),
1096            Pattern::Within(v, _) => v.iter().any(|e| atom_reads(&e.atom)),
1097            Pattern::Empty | Pattern::Guard(..) | Pattern::Anchor(_) => false,
1098            Pattern::Bind(_, _, p)
1099            | Pattern::Balanced(_, p)
1100            | Pattern::Field(_, p)
1101            | Pattern::Star(p, _)
1102            | Pattern::Plus(p, _)
1103            | Pattern::Opt(p, _)
1104            | Pattern::Repeat(p, _, _, _)
1105            | Pattern::Atomic(p)
1106            | Pattern::Assert(p, _, _) => p.reads_a_register(),
1107            Pattern::Concat(v) | Pattern::Alt(v, _) => v.iter().any(Pattern::reads_a_register),
1108        }
1109    }
1110
1111    /// Whether the pattern binds anything at all.
1112    #[must_use]
1113    pub fn binds_anything(&self) -> bool {
1114        match self {
1115            Pattern::Bind(..) => true,
1116            Pattern::Within(v, _) => v.iter().any(|e| e.bind.is_some()),
1117            Pattern::Empty | Pattern::Atom(_) | Pattern::Guard(..) | Pattern::Anchor(_) => false,
1118            Pattern::Balanced(_, p)
1119            | Pattern::Field(_, p)
1120            | Pattern::Star(p, _)
1121            | Pattern::Plus(p, _)
1122            | Pattern::Opt(p, _)
1123            | Pattern::Repeat(p, _, _, _)
1124            | Pattern::Atomic(p)
1125            | Pattern::Assert(p, _, _) => p.binds_anything(),
1126            Pattern::Concat(v) | Pattern::Alt(v, _) => v.iter().any(Pattern::binds_anything),
1127        }
1128    }
1129
1130    /// The same pattern with every binding removed, or `None` where it binds
1131    /// nothing or an atom reads a binding back.
1132    ///
1133    /// The spans are the same, which is the whole of what a scan reports: the
1134    /// routes are written against the shapes the language spells without
1135    /// bindings, so `\W:name "="` reaches the route `\W "="` takes only once
1136    /// its binding is off. A caller wanting the registers resolves them against
1137    /// the original pattern over these spans.
1138    #[must_use]
1139    pub fn without_bindings(&self) -> Option<Pattern> {
1140        if !self.binds_anything() || self.reads_a_register() {
1141            return None;
1142        }
1143        fn strip(p: &Pattern) -> Pattern {
1144            match p {
1145                Pattern::Bind(_, _, inner) => strip(inner),
1146                Pattern::Balanced(k, p) => Pattern::Balanced(*k, strip(p).boxed()),
1147                Pattern::Field(k, p) => Pattern::Field(*k, strip(p).boxed()),
1148                Pattern::Star(p, g) => Pattern::Star(strip(p).boxed(), *g),
1149                Pattern::Plus(p, g) => Pattern::Plus(strip(p).boxed(), *g),
1150                Pattern::Opt(p, g) => Pattern::Opt(strip(p).boxed(), *g),
1151                Pattern::Repeat(p, lo, hi, g) => Pattern::Repeat(strip(p).boxed(), *lo, *hi, *g),
1152                Pattern::Atomic(p) => Pattern::Atomic(strip(p).boxed()),
1153                Pattern::Assert(p, neg, look) => Pattern::Assert(strip(p).boxed(), *neg, *look),
1154                Pattern::Concat(v) => Pattern::Concat(v.iter().map(strip).collect()),
1155                Pattern::Alt(v, mode) => Pattern::Alt(v.iter().map(strip).collect(), *mode),
1156                Pattern::Within(v, k) => Pattern::Within(
1157                    v.iter().map(|e| EditAtom { atom: e.atom.clone(), bind: None }).collect(),
1158                    *k,
1159                ),
1160                other => other.clone(),
1161            }
1162        }
1163        Some(strip(self))
1164    }
1165
1166    /// Whether the pattern begins with `\G`, so its matches must form a
1167    /// contiguous run rather than the leftmost non-overlapping selection.
1168    ///
1169    /// Only the head position counts, and [`Pattern::resume_is_misplaced`]
1170    /// rejects any other, so this is the whole of what the scan has to ask.
1171    #[must_use]
1172    pub fn starts_with_resume(&self) -> bool {
1173        match self {
1174            Pattern::Anchor(AnchorKind::Resume) => true,
1175            Pattern::Concat(v) => v.first().is_some_and(Pattern::starts_with_resume),
1176            Pattern::Bind(_, _, p) => p.starts_with_resume(),
1177            _ => false,
1178        }
1179    }
1180
1181    /// Whether `\G` appears anywhere it cannot mean anything: that is, at all
1182    /// except the head of the pattern.
1183    #[must_use]
1184    pub fn resume_is_misplaced(&self) -> bool {
1185        // The head occurrence is the legal one, so it is not searched.
1186        match self {
1187            Pattern::Concat(v) => {
1188                let head_ok = v.first().is_some_and(Pattern::starts_with_resume);
1189                let skip = usize::from(head_ok);
1190                v.iter().skip(skip).any(Pattern::mentions_resume)
1191                    || v.first().is_some_and(|p| !head_ok && p.mentions_resume())
1192            }
1193            Pattern::Anchor(AnchorKind::Resume) => false,
1194            other => other.mentions_resume(),
1195        }
1196    }
1197
1198    /// Whether an anchor reading the whole stream's ends - `\A` or `\z` -
1199    /// occurs anywhere in the pattern.
1200    ///
1201    /// The two read whether any significant token precedes or follows the
1202    /// one at hand, which a caller scanning a slice of the token stream
1203    /// cannot answer: the slice's first token looks like the input's.
1204    /// `^` and `$` are not here, reading the bytes either side of a token
1205    /// rather than the stream, so a slice answers them as the whole does.
1206    #[must_use]
1207    pub fn mentions_stream_end_anchor(&self) -> bool {
1208        match self {
1209            Pattern::Anchor(AnchorKind::InputStart | AnchorKind::InputEnd) => true,
1210            Pattern::Empty
1211            | Pattern::Atom(_)
1212            | Pattern::Within(..)
1213            | Pattern::Anchor(_)
1214            | Pattern::Guard(..) => false,
1215            Pattern::Star(p, _)
1216            | Pattern::Plus(p, _)
1217            | Pattern::Opt(p, _)
1218            | Pattern::Bind(_, _, p)
1219            | Pattern::Balanced(_, p)
1220            | Pattern::Field(_, p)
1221            | Pattern::Repeat(p, _, _, _)
1222            | Pattern::Atomic(p)
1223            | Pattern::Assert(p, _, _) => p.mentions_stream_end_anchor(),
1224            Pattern::Concat(v) | Pattern::Alt(v, _) => {
1225                v.iter().any(Pattern::mentions_stream_end_anchor)
1226            }
1227        }
1228    }
1229
1230    /// Whether `\K` occurs anywhere in the pattern.
1231    #[must_use]
1232    pub fn mentions_reset_start(&self) -> bool {
1233        match self {
1234            Pattern::Anchor(AnchorKind::ResetStart) => true,
1235            Pattern::Empty
1236            | Pattern::Atom(_)
1237            | Pattern::Within(..)
1238            | Pattern::Anchor(_)
1239            | Pattern::Guard(..) => false,
1240            Pattern::Star(p, _)
1241            | Pattern::Plus(p, _)
1242            | Pattern::Opt(p, _)
1243            | Pattern::Bind(_, _, p)
1244            | Pattern::Balanced(_, p)
1245            | Pattern::Field(_, p)
1246            | Pattern::Repeat(p, _, _, _) | Pattern::Atomic(p)
1247            | Pattern::Assert(p, _, _) => p.mentions_reset_start(),
1248            Pattern::Concat(v) | Pattern::Alt(v, _) => {
1249                v.iter().any(Pattern::mentions_reset_start)
1250            }
1251        }
1252    }
1253
1254    /// Whether `\G` occurs anywhere at all in this sub-pattern.
1255    fn mentions_resume(&self) -> bool {
1256        match self {
1257            Pattern::Anchor(AnchorKind::Resume) => true,
1258            Pattern::Empty
1259            | Pattern::Atom(_)
1260            | Pattern::Within(..)
1261            | Pattern::Anchor(_)
1262            | Pattern::Guard(..) => false,
1263            Pattern::Star(p, _)
1264            | Pattern::Plus(p, _)
1265            | Pattern::Opt(p, _)
1266            | Pattern::Bind(_, _, p)
1267            | Pattern::Balanced(_, p)
1268            | Pattern::Field(_, p)
1269            | Pattern::Repeat(p, _, _, _) | Pattern::Atomic(p)
1270            | Pattern::Assert(p, _, _) => p.mentions_resume(),
1271            Pattern::Concat(v) | Pattern::Alt(v, _) => v.iter().any(Pattern::mentions_resume),
1272        }
1273    }
1274
1275    /// Whether the pattern contains a content guard (`~"lit"`) anywhere.
1276    /// A guard's forward window is unbounded, so a streaming or pipelined
1277    /// scanner cannot finalize a match carrying one until the whole input
1278    /// is seen.
1279    #[must_use]
1280    pub fn contains_guard(&self) -> bool {
1281        match self {
1282            Pattern::Guard(..) => true,
1283            Pattern::Assert(p, _, _) => p.contains_guard(),
1284            Pattern::Empty | Pattern::Atom(_) | Pattern::Within(..) | Pattern::Anchor(_) => false,
1285            Pattern::Star(p, _) | Pattern::Plus(p, _) | Pattern::Opt(p, _) | Pattern::Bind(_, _, p) => {
1286                p.contains_guard()
1287            }
1288            Pattern::Repeat(p, _, _, _) | Pattern::Atomic(p) | Pattern::Balanced(_, p) | Pattern::Field(_, p) => {
1289                p.contains_guard()
1290            }
1291            Pattern::Concat(v) | Pattern::Alt(v, _) => v.iter().any(Pattern::contains_guard),
1292        }
1293    }
1294
1295    /// Whether the pattern needs whole-input, non-droppable context: a field
1296    /// anchor (`@k`, which counts commas from the input start) or a statistical
1297    /// anchor (`@seam`, whose axis field is computed over the whole stream). In
1298    /// either case a committed prefix cannot be dropped, so a streaming or
1299    /// pipelined scanner must defer finalizing a match that carries one.
1300    #[must_use]
1301    pub fn contains_field(&self) -> bool {
1302        match self {
1303            Pattern::Field(..) | Pattern::Anchor(_) => true,
1304            Pattern::Assert(p, _, _) => p.contains_field(),
1305            Pattern::Empty | Pattern::Atom(_) | Pattern::Within(..) | Pattern::Guard(..) => false,
1306            Pattern::Star(p, _) | Pattern::Plus(p, _) | Pattern::Opt(p, _) | Pattern::Bind(_, _, p) => {
1307                p.contains_field()
1308            }
1309            Pattern::Repeat(p, _, _, _) | Pattern::Atomic(p) | Pattern::Balanced(_, p) => p.contains_field(),
1310            Pattern::Concat(v) | Pattern::Alt(v, _) => v.iter().any(Pattern::contains_field),
1311        }
1312    }
1313
1314    /// Whether a match's outcome can depend on input outside its own span, so
1315    /// a chunked scanner must not commit it before the whole input is seen.
1316    ///
1317    /// Three sources: a content guard reads an unbounded forward window; a
1318    /// field anchor counts commas from the input start; an axis atom or anchor
1319    /// reads a field computed over the whole stream, and truncating the input
1320    /// moves that reading. A new axis belongs in this predicate, which is the
1321    /// single place a chunked surface consults.
1322    #[must_use]
1323    pub fn depends_on_whole_input(&self) -> bool {
1324        self.contains_guard()
1325            || self.contains_field()
1326            || self.has_spectral()
1327            || self.has_assert()
1328            || self.reads_context_window()
1329            || self.reads_related_context()
1330            || self.any_node(&|p| matches!(p, Pattern::Anchor(k) if k.reads_whole_input()))
1331    }
1332
1333    /// Whether a match's outcome can depend on input beyond the lines it
1334    /// spans, so a scanner that cuts its input only just after a newline
1335    /// must not commit it before the whole input is seen:
1336    /// [`Self::depends_on_whole_input`] less the line anchors, which read no
1337    /// further than the newlines around the match, and less a lookbehind
1338    /// that reads only tokens of the match itself.
1339    #[must_use]
1340    pub fn depends_on_more_than_its_lines(&self) -> bool {
1341        self.contains_guard()
1342            || self.any_node(&|p| match p {
1343                Pattern::Field(..) => true,
1344                Pattern::Anchor(k) => !matches!(k, AnchorKind::LineStart | AnchorKind::LineEnd),
1345                Pattern::Assert(_, _, look) => !matches!(look, Look::Within { .. } | Look::Behind),
1346                _ => false,
1347            })
1348            || self.looks_behind_its_match()
1349            || self.has_spectral()
1350            || self.reads_context_window()
1351            || self.reads_related_context()
1352    }
1353
1354    /// Whether a lookbehind can read a token before its match's start: one at
1355    /// a position where the match may have taken fewer tokens than its
1356    /// sub-pattern can span, or one whose sub-pattern is unbounded.
1357    #[must_use]
1358    pub fn looks_behind_its_match(&self) -> bool {
1359        self.looks_behind_from(0)
1360    }
1361
1362    /// [`Self::looks_behind_its_match`] for a node the match reaches having
1363    /// taken at least `before` tokens.
1364    fn looks_behind_from(&self, before: usize) -> bool {
1365        match self {
1366            Pattern::Assert(p, _, Look::Behind) => match p.max_tokens() {
1367                Some(reach) => reach > before || p.looks_behind_from(0),
1368                None => true,
1369            },
1370            Pattern::Assert(p, _, _) => p.looks_behind_from(0),
1371            Pattern::Empty | Pattern::Atom(_) | Pattern::Guard(..) | Pattern::Anchor(_) | Pattern::Within(..) => false,
1372            Pattern::Bind(_, _, p)
1373            | Pattern::Atomic(p)
1374            | Pattern::Opt(p, _)
1375            | Pattern::Star(p, _)
1376            | Pattern::Plus(p, _)
1377            | Pattern::Repeat(p, ..)
1378            | Pattern::Field(_, p)
1379            | Pattern::Balanced(_, p) => p.looks_behind_from(before),
1380            Pattern::Concat(v) => {
1381                let mut taken = before;
1382                for p in v {
1383                    if p.looks_behind_from(taken) {
1384                        return true;
1385                    }
1386                    taken = taken.saturating_add(p.min_tokens());
1387                }
1388                false
1389            }
1390            Pattern::Alt(v, _) => v.iter().any(|p| p.looks_behind_from(before)),
1391        }
1392    }
1393
1394    /// The fewest tokens a match of the pattern can span. A lower bound: where
1395    /// the count is not known exactly it is too small, never too large, so a
1396    /// reader asking what a match has taken errs toward less.
1397    #[must_use]
1398    pub fn min_tokens(&self) -> usize {
1399        match self {
1400            Pattern::Empty | Pattern::Guard(..) | Pattern::Anchor(_) | Pattern::Assert(..) => 0,
1401            Pattern::Atom(_) => 1,
1402            // A run within `k` of `m` atoms drops at most `k` of them and is
1403            // never empty.
1404            Pattern::Within(v, k) => v.len().saturating_sub(usize::from(*k)).max(1),
1405            Pattern::Bind(_, _, p) | Pattern::Atomic(p) | Pattern::Field(_, p) | Pattern::Plus(p, _) => {
1406                p.min_tokens()
1407            }
1408            Pattern::Opt(..) | Pattern::Star(..) | Pattern::Balanced(..) => 0,
1409            Pattern::Repeat(p, least, _, _) => p.min_tokens().saturating_mul(*least),
1410            Pattern::Concat(v) => v.iter().map(Pattern::min_tokens).fold(0, usize::saturating_add),
1411            // An alternation with no branch takes no token.
1412            Pattern::Alt(v, _) if v.is_empty() => 0,
1413            Pattern::Alt(v, _) => v.iter().map(Pattern::min_tokens).fold(usize::MAX, usize::min),
1414        }
1415    }
1416
1417    /// Whether the pattern compares a token to the rolling window before it
1418    /// (`\N{>+1}`), so the set engine folds the window field once per scan.
1419    #[must_use]
1420    pub fn reads_context_window(&self) -> bool {
1421        self.context_uses().window
1422    }
1423
1424    /// Whether the pattern reads a relation-admitted context or a phase
1425    /// (`\N{>+1:phase}`, `\N{>+1:k}`, `@phase:2`), so the set engine builds
1426    /// the related-context field once per scan.
1427    #[must_use]
1428    pub fn reads_related_context(&self) -> bool {
1429        self.context_uses().any_related()
1430    }
1431
1432    /// Which contexts the pattern reads, so a scan builds each one's inputs
1433    /// only when something asks for it.
1434    #[must_use]
1435    pub fn context_uses(&self) -> ContextUses {
1436        // The walk takes a `Fn`, so the flags are gathered through a cell.
1437        let uses = std::cell::Cell::new(ContextUses::default());
1438        self.any_atom(&|a| {
1439            if let Some(scope) = a.magnitude_pred().and_then(MagPred::scope) {
1440                let mut u = uses.get();
1441                match scope {
1442                    Scope::Window => u.window = true,
1443                    Scope::Phase => u.phase = true,
1444                    Scope::Regime => u.regime = true,
1445                    Scope::Echo => u.echo = true,
1446                    Scope::Enclosing => u.enclosing = true,
1447                    Scope::Key(_) => u.key = true,
1448                }
1449                uses.set(u);
1450            }
1451            false
1452        });
1453        let mut u = uses.get();
1454        if self.any_node(&|p| matches!(p, Pattern::Anchor(AnchorKind::Phase(_)))) {
1455            u.phase = true;
1456        }
1457        u
1458    }
1459
1460    /// Whether any atom in the pattern satisfies `pred`, class members
1461    /// included.
1462    /// Whether the pattern reads a register back, as `=name` and
1463    /// `=shape name` do.
1464    ///
1465    /// Distinct from [`Self::binds`], which says only that a register is
1466    /// written. A thread's future depends on what it has bound only where
1467    /// something reads it: with a back-reference, two threads at one counter
1468    /// holding different bindings go on to match different tokens and are both
1469    /// live. Without one, they match identically from here and the
1470    /// higher-priority thread settles which captures are reported.
1471    ///
1472    /// The engine's thread list keys on this. Keying on a binding instead
1473    /// makes a pattern that only binds hash its whole save array per thread
1474    /// per step to keep threads apart that nothing can tell apart.
1475    #[must_use]
1476    pub fn reads_registers(&self) -> bool {
1477        self.any_atom(&|a| {
1478            matches!(
1479                a,
1480                Atom::RegisterEq(..)
1481                    | Atom::RegisterRelated(..)
1482                    | Atom::RegisterWithin(..)
1483                    | Atom::RegisterKin(..)
1484                    | Atom::Since(..)
1485            )
1486        })
1487    }
1488
1489    /// Whether the pattern orders timestamps against the one before them
1490    /// (`@order:asc` / `@order:desc`), so the scan reads each timestamp once
1491    /// and records how it compares with the one before.
1492    #[must_use]
1493    pub fn has_order(&self) -> bool {
1494        self.any_node(&|p| matches!(p, Pattern::Anchor(AnchorKind::Order(_))))
1495    }
1496
1497    /// Whether the pattern reads a whitespace token itself (`\S`, or the
1498    /// `\s` byte class), so a run of whitespace split at a cut would read
1499    /// differently from the same run whole.
1500    #[must_use]
1501    pub fn reads_whitespace(&self) -> bool {
1502        self.any_atom(&|a| {
1503            matches!(a, Atom::Kind(TokenKind::Whitespace) | Atom::Byte(ByteClass::Space))
1504        })
1505    }
1506
1507    /// The most tokens a match of the pattern can span, or `None` when an
1508    /// open repeat or a balanced group leaves it unbounded. A chunked
1509    /// scanner commits a match of a bounded pattern once the input holds
1510    /// that many tokens past the match's start, since no alternative at
1511    /// that start can reach further; an assertion consumes nothing, so it
1512    /// spans nothing here, and the assertions that read past a match defer
1513    /// every commit on their own.
1514    #[must_use]
1515    pub fn max_tokens(&self) -> Option<usize> {
1516        match self {
1517            Pattern::Empty | Pattern::Guard(..) | Pattern::Anchor(_) | Pattern::Assert(..) => Some(0),
1518            Pattern::Atom(_) => Some(1),
1519            // The longest run within `k` of `m` atoms is `m + k` tokens: every
1520            // atom aligned and `k` more the run carried extra.
1521            Pattern::Within(v, k) => v.len().checked_add(usize::from(*k)),
1522            Pattern::Bind(_, _, p) | Pattern::Atomic(p) | Pattern::Opt(p, _) | Pattern::Field(_, p) => {
1523                p.max_tokens()
1524            }
1525            Pattern::Balanced(..) | Pattern::Star(..) | Pattern::Plus(..) => None,
1526            Pattern::Repeat(p, _, most, _) => {
1527                most.and_then(|n| p.max_tokens().and_then(|w| w.checked_mul(n)))
1528            }
1529            Pattern::Concat(v) => {
1530                v.iter().try_fold(0usize, |acc, p| p.max_tokens().and_then(|w| acc.checked_add(w)))
1531            }
1532            Pattern::Alt(v, _) => {
1533                v.iter().try_fold(0usize, |acc, p| p.max_tokens().map(|w| acc.max(w)))
1534            }
1535        }
1536    }
1537
1538    /// Whether a match of the pattern can span no tokens at all.
1539    ///
1540    /// The opening-kind route reads this. A pattern that can match nothing
1541    /// matches at every anchor, so it forces no opening kind; and inside a
1542    /// concatenation, a leading element that can take nothing leaves the match
1543    /// opening on whatever follows it, so the kinds it begins with are its own
1544    /// and the next element's together.
1545    ///
1546    /// Answering `false` for a pattern that can in fact take nothing is the
1547    /// dangerous direction: the route would then refuse anchors where a
1548    /// zero-width match begins. Every variant is matched rather than defaulted,
1549    /// so a new one has to be decided rather than inheriting an answer.
1550    #[must_use]
1551    pub fn takes_no_tokens(&self) -> bool {
1552        match self {
1553            // An assertion runs a sub-pattern as a filter and consumes none of
1554            // what it reads; a guard and an anchor are positions.
1555            Pattern::Empty | Pattern::Guard(..) | Pattern::Anchor(_) | Pattern::Assert(..) => true,
1556            // Every atom covers exactly one token.
1557            Pattern::Atom(_) => false,
1558            // The shortest run within `k` of `m` atoms deletes `k` of them, so
1559            // it is empty only where the budget covers every atom.
1560            Pattern::Within(v, k) => v.len() <= usize::from(*k),
1561            Pattern::Bind(_, _, p) | Pattern::Atomic(p) | Pattern::Field(_, p) => p.takes_no_tokens(),
1562            Pattern::Opt(..) | Pattern::Star(..) => true,
1563            Pattern::Plus(p, _) => p.takes_no_tokens(),
1564            // A balanced group is its brackets at least.
1565            Pattern::Balanced(..) => false,
1566            Pattern::Repeat(p, least, _, _) => *least == 0 || p.takes_no_tokens(),
1567            Pattern::Concat(v) => v.iter().all(Pattern::takes_no_tokens),
1568            Pattern::Alt(v, _) => v.iter().any(Pattern::takes_no_tokens),
1569        }
1570    }
1571
1572    /// Whether the pattern reads the rarity of a line's template
1573    /// (`@shape:rare`), so the scan mines the input's templates once.
1574    #[must_use]
1575    pub fn has_rare(&self) -> bool {
1576        self.any_node(&|p| matches!(p, Pattern::Anchor(AnchorKind::Rare(_))))
1577    }
1578
1579    /// The second inputs the pattern's join anchors read, each with the rung
1580    /// it is keyed at, once per distinct pair, so the scan keys each once.
1581    #[must_use]
1582    pub fn joins(&self) -> Vec<(std::sync::Arc<OtherInput>, OrbitGroup)> {
1583        use std::sync::Arc;
1584        let out = std::cell::RefCell::new(Vec::<(Arc<OtherInput>, OrbitGroup)>::new());
1585        self.any_node(&|p| {
1586            if let Pattern::Anchor(AnchorKind::Joined { other, group, .. }) = p {
1587                let mut seen = out.borrow_mut();
1588                if !seen.iter().any(|(o, g)| Arc::ptr_eq(o, other) && g == group) {
1589                    seen.push((Arc::clone(other), *group));
1590                }
1591            }
1592            false
1593        });
1594        out.into_inner()
1595    }
1596
1597    fn any_atom(&self, pred: &impl Fn(&Atom) -> bool) -> bool {
1598        fn atom_has(a: &Atom, pred: &impl Fn(&Atom) -> bool) -> bool {
1599            pred(a)
1600                || match a {
1601                    Atom::Class(c) => c.members().any(|m| atom_has(m, pred)),
1602                    _ => false,
1603                }
1604        }
1605        self.any_node(&|p| matches!(p, Pattern::Atom(a) if atom_has(a, pred)))
1606    }
1607
1608    /// The most significant tokens past a match's own end that deciding the
1609    /// match can read, for the bounded assertions that read forward at all.
1610    ///
1611    /// A `~>k(P)` assertion tries `P` at each of `k` positions from its own
1612    /// position, and `P` itself can run on, so its reach is `k` plus `P`'s
1613    /// own token length. Zero where the pattern has no such assertion.
1614    ///
1615    /// A caller working from a prefix must hold this many tokens beyond a
1616    /// match before it can trust the verdict: reserving only the match's own
1617    /// length leaves the assertion reading a stream the cut truncated, and
1618    /// that reports no match where the whole input has one.
1619    ///
1620    /// `~#{m,n}(P)` reads to a closing bracket rather than a token count, so no
1621    /// number here describes its reach and it contributes none. It is reported
1622    /// by [`Self::has_assert`] instead, which declines the prefix outright.
1623    #[must_use]
1624    pub fn widest_forward_window(&self) -> usize {
1625        // A cell because the walker takes an `Fn`: it visits every node and
1626        // never needs to mutate, so the accumulator carries its own.
1627        let widest = std::cell::Cell::new(0usize);
1628        self.any_node(&|p| {
1629            if let Pattern::Assert(inner, _, Look::Within { window, .. }) = p {
1630                let inner_len = crate::nfa::bounded_max_len(inner).unwrap_or(0);
1631                widest.set(widest.get().max(window.saturating_add(inner_len)));
1632            }
1633            false
1634        });
1635        widest.get()
1636    }
1637
1638    /// Whether the pattern carries a zero-width sub-pattern assertion whose
1639    /// reach is not bounded. Such an assertion reads input outside the match
1640    /// span in either direction and as far as it needs to, so a chunked
1641    /// scanner cannot finalize a match carrying one.
1642    ///
1643    /// A `~>k(P)` assertion reads at most `k` significant tokens past the
1644    /// position and is not counted here: its reach is part of the pattern, so
1645    /// a scanner holding that many tokens beyond a match can finalize it.
1646    ///
1647    /// `~#{m,n}(P)` is counted, because its reach is the enclosing bracket and
1648    /// the input decides where that is. A cut inside the group leaves
1649    /// the opening token with no mate, which reads as an empty region and a
1650    /// count of zero - a verdict of no match on an input that has one.
1651    #[must_use]
1652    pub fn has_assert(&self) -> bool {
1653        self.any_node(&|p| {
1654            matches!(p, Pattern::Assert(_, _, look) if !matches!(look, Look::Within { .. }))
1655        })
1656    }
1657
1658    /// Whether the pattern reads the supertoken tower, so a chunked scanner
1659    /// must not commit a match carrying it before the whole input is seen.
1660    ///
1661    /// A unit is a run of tokens, so one cut by a chunk boundary is the same
1662    /// hazard [`Self::depends_on_whole_input`] already declares for the other
1663    /// whole-stream readings. This is covered there through
1664    /// [`Self::contains_field`], which answers true for every anchor rather
1665    /// than enumerating them, and the test on this module pins that so the
1666    /// coverage is not accidental.
1667    #[must_use]
1668    pub fn reads_supertokens(&self) -> bool {
1669        self.has_super()
1670    }
1671
1672    /// Whether the pattern queries the seam axis (`@seam`) anywhere, so the
1673    /// set engine builds the seam field once and threads it read-only.
1674    #[must_use]
1675    pub fn has_seam(&self) -> bool {
1676        self.has_seam_at(Grain::Byte)
1677    }
1678
1679    /// Whether the pattern reads the seam axis at one particular grain, so
1680    /// only the streams a pattern names are segmented.
1681    #[must_use]
1682    pub fn has_seam_at(&self, grain: Grain) -> bool {
1683        self.any_node(&|p| matches!(p, Pattern::Anchor(AnchorKind::Seam(g, _)) if *g == grain))
1684    }
1685
1686    /// Every `@seam` anchor's grain and cut, each pair once, in the order met,
1687    /// so each cut list a pattern reads is built once a scan.
1688    #[must_use]
1689    pub fn seam_cuts(&self) -> Vec<(Grain, Option<AnchorCut>)> {
1690        self.anchor_cuts(|k| match k {
1691            AnchorKind::Seam(g, cut) => Some((*g, *cut)),
1692            _ => None,
1693        })
1694    }
1695
1696    /// Every `@ambiguous` anchor's grain and cut, each pair once, in the order
1697    /// met.
1698    #[must_use]
1699    pub fn contested_cuts(&self) -> Vec<(Grain, Option<AnchorCut>)> {
1700        self.anchor_cuts(|k| match k {
1701            AnchorKind::Ambiguous(g, cut) => Some((*g, *cut)),
1702            _ => None,
1703        })
1704    }
1705
1706    /// The distinct grain and cut pairs `pick` reads off this pattern's
1707    /// anchors, in the order met.
1708    fn anchor_cuts(
1709        &self,
1710        pick: impl Fn(&AnchorKind) -> Option<(Grain, Option<AnchorCut>)>,
1711    ) -> Vec<(Grain, Option<AnchorCut>)> {
1712        let found = std::cell::RefCell::new(Vec::new());
1713        self.any_node(&|p| {
1714            if let Pattern::Anchor(k) = p
1715                && let Some(pair) = pick(k)
1716            {
1717                let mut found = found.borrow_mut();
1718                if !found.contains(&pair) {
1719                    found.push(pair);
1720                }
1721            }
1722            false
1723        });
1724        found.into_inner()
1725    }
1726
1727    /// Whether any anchor counts a column against a named period
1728    /// (`@phase:k/p`, `@phase:k#n`), so the live periods are read once.
1729    #[must_use]
1730    pub fn has_phase_in(&self) -> bool {
1731        self.any_node(&|p| matches!(p, Pattern::Anchor(AnchorKind::PhaseIn(..))))
1732    }
1733
1734    /// Whether any anchor reads the pair field at `grain` (`@strain`,
1735    /// `@bound`, `@kin`), so the field is learned only at the grains a
1736    /// pattern names.
1737    #[must_use]
1738    pub fn has_gravity_at(&self, grain: Grain) -> bool {
1739        self.any_node(&|p| {
1740            matches!(
1741                p,
1742                Pattern::Anchor(AnchorKind::Gravity(_, g, ..) | AnchorKind::Kin(g, _)) if *g == grain
1743            )
1744        }) || self.any_atom(&|a| matches!(a, Atom::RegisterKin(_, g) if *g == grain))
1745            || self.any_node(&|p| {
1746                matches!(p, Pattern::Within(v, _)
1747                    if v.iter().any(|e| matches!(&e.atom, Atom::RegisterKin(_, g) if *g == grain)))
1748            })
1749    }
1750
1751    /// Every `@kin` anchor's grain and example, each once, in the order met.
1752    #[must_use]
1753    pub fn kin_examples(&self) -> Vec<(Grain, String)> {
1754        let found = std::cell::RefCell::new(Vec::new());
1755        self.any_node(&|p| {
1756            if let Pattern::Anchor(AnchorKind::Kin(g, x)) = p {
1757                let mut found = found.borrow_mut();
1758                if !found.iter().any(|(fg, fx)| fg == g && fx == x) {
1759                    found.push((*g, x.clone()));
1760                }
1761            }
1762            false
1763        });
1764        found.into_inner()
1765    }
1766
1767    /// Whether this subtree makes a preference-bearing choice of its own: a
1768    /// leftmost-first alternation, or a quantifier.
1769    ///
1770    /// A quantifier whose body answers `false` is fully described by how many
1771    /// times it ran, so the set engine records one number for it. A body that
1772    /// answers `true` made choices inside each iteration, and those have to be
1773    /// comparable position by position against another derivation's, so that
1774    /// quantifier records one entry per iteration instead.
1775    #[must_use]
1776    pub fn has_choice(&self) -> bool {
1777        self.any_node(&|p| {
1778            matches!(
1779                p,
1780                Pattern::Alt(_, AltMode::First)
1781                    | Pattern::Star(..)
1782                    | Pattern::Plus(..)
1783                    | Pattern::Opt(..)
1784                    | Pattern::Repeat(..)
1785            )
1786        })
1787    }
1788
1789    /// Whether the pattern carries a field anchor (`@k`), so the set engine
1790    /// builds the per-token field index once. Narrower than
1791    /// [`Self::contains_field`], which also answers true for an axis anchor.
1792    #[must_use]
1793    pub fn has_field_anchor(&self) -> bool {
1794        self.any_node(&|p| matches!(p, Pattern::Field(..)))
1795    }
1796
1797    /// Whether the pattern queries the stress axis (`@nested>k`) anywhere, so
1798    /// the set engine builds the stress field once and threads it read-only.
1799    #[must_use]
1800    pub fn has_stress(&self) -> bool {
1801        self.any_node(&|p| matches!(p, Pattern::Anchor(AnchorKind::Nested(_))))
1802    }
1803
1804    /// Whether the pattern queries the observation axis (`@ambiguous`)
1805    /// anywhere, so the set engine builds the observation field once.
1806    #[must_use]
1807    pub fn has_observation(&self) -> bool {
1808        self.has_observation_at(Grain::Byte)
1809    }
1810
1811    /// Whether the pattern reads the observation axis at one grain, so only
1812    /// the streams a pattern names are read.
1813    #[must_use]
1814    pub fn has_observation_at(&self, grain: Grain) -> bool {
1815        self.any_node(&|p| matches!(p, Pattern::Anchor(AnchorKind::Ambiguous(g, _)) if *g == grain))
1816    }
1817
1818    /// Whether the pattern queries the echo axis (`@novel` / `@echoed` /
1819    /// `@echo...`) anywhere, so the set engine builds the recurrence field.
1820    #[must_use]
1821    pub fn has_echo(&self) -> bool {
1822        self.any_node(&|p| {
1823            matches!(p, Pattern::Anchor(AnchorKind::Novel | AnchorKind::Echoed | AnchorKind::Echo(..)))
1824        })
1825    }
1826
1827    /// The orbit rungs the pattern counts recurrence at, in the order they
1828    /// first appear and each once. A scan builds one recurrence field per
1829    /// rung; `Identity` is the rung `@novel` and `@echoed` read and the one
1830    /// an `@echo` under no orbit scope reads.
1831    #[must_use]
1832    pub fn echo_orbits(&self) -> Vec<OrbitGroup> {
1833        let out = std::cell::RefCell::new(Vec::new());
1834        self.any_node(&|p| {
1835            let group = match p {
1836                Pattern::Anchor(AnchorKind::Novel | AnchorKind::Echoed) => OrbitGroup::Identity,
1837                Pattern::Anchor(AnchorKind::Echo(_, g)) => *g,
1838                _ => return false,
1839            };
1840            let mut seen = out.borrow_mut();
1841            if !seen.contains(&group) {
1842                seen.push(group);
1843            }
1844            false
1845        });
1846        out.into_inner()
1847    }
1848
1849    /// Whether the pattern queries the supertoken tower (`@super` /
1850    /// `@super:role`) anywhere, so the set engine builds the upper-grain
1851    /// window once.
1852    #[must_use]
1853    pub fn has_super(&self) -> bool {
1854        self.any_node(&|p| {
1855            matches!(p, Pattern::Anchor(AnchorKind::SuperStart | AnchorKind::SuperRole(_)))
1856        })
1857    }
1858
1859    /// Whether any node in the pattern satisfies `pred`. The shared subtree
1860    /// walk the per-axis `has_*` queries delegate to, so each is a one-line
1861    /// predicate rather than a repeated match.
1862    fn any_node(&self, pred: &impl Fn(&Pattern) -> bool) -> bool {
1863        if pred(self) {
1864            return true;
1865        }
1866        match self {
1867            Pattern::Assert(p, _, _) => p.any_node(pred),
1868            // An edit-distance group's elements are atoms rather than
1869            // patterns, so there is no node beneath it for a predicate over
1870            // nodes to reach.
1871            Pattern::Empty
1872            | Pattern::Atom(_)
1873            | Pattern::Within(..)
1874            | Pattern::Guard(..)
1875            | Pattern::Anchor(_) => false,
1876            Pattern::Star(p, _) | Pattern::Plus(p, _) | Pattern::Opt(p, _) | Pattern::Bind(_, _, p) => {
1877                p.any_node(pred)
1878            }
1879            Pattern::Repeat(p, _, _, _) | Pattern::Atomic(p) | Pattern::Balanced(_, p) | Pattern::Field(_, p) => {
1880                p.any_node(pred)
1881            }
1882            Pattern::Concat(v) | Pattern::Alt(v, _) => v.iter().any(|c| c.any_node(pred)),
1883        }
1884    }
1885
1886    /// The capture names the pattern can bind (`:name` suffixes), in
1887    /// first-seen order with duplicates removed. A rewrite template is
1888    /// validated against this set so a reference to an unbound name is a
1889    /// template error rather than a silent empty substitution.
1890    #[must_use]
1891    pub fn capture_names(&self) -> Vec<String> {
1892        let mut out = Vec::new();
1893        self.collect_capture_names(&mut out);
1894        out
1895    }
1896
1897    /// Nest every register bound inside this pattern under `prefix`: each is
1898    /// renamed `prefix.name`, and every reference to one of them inside the
1899    /// pattern follows, so a back-reference, a typed relation, an edit
1900    /// distance, a timestamp gap or a magnitude history keyed by the register
1901    /// still reads it. A reference to a register bound outside is left as it
1902    /// is.
1903    pub fn nest_registers(&mut self, prefix: &str) {
1904        let inner = self.capture_names();
1905        let rename = |name: &mut String| {
1906            if inner.iter().any(|n| n == name) {
1907                *name = format!("{prefix}.{name}");
1908            }
1909        };
1910        self.rename_registers(&rename);
1911    }
1912
1913    /// `rename` applied to every register name the pattern binds or reads.
1914    fn rename_registers(&mut self, rename: &dyn Fn(&mut String)) {
1915        match self {
1916            Pattern::Bind(name, _, p) => {
1917                rename(name);
1918                p.rename_registers(rename);
1919            }
1920            Pattern::Atom(a) => a.rename_registers(rename),
1921            Pattern::Within(v, _) => {
1922                for e in v {
1923                    e.atom.rename_registers(rename);
1924                    if let Some((name, _)) = &mut e.bind {
1925                        rename(name);
1926                    }
1927                }
1928            }
1929            Pattern::Empty | Pattern::Guard(..) | Pattern::Anchor(_) => {}
1930            Pattern::Balanced(_, p)
1931            | Pattern::Field(_, p)
1932            | Pattern::Star(p, _)
1933            | Pattern::Plus(p, _)
1934            | Pattern::Opt(p, _)
1935            | Pattern::Repeat(p, _, _, _)
1936            | Pattern::Atomic(p)
1937            | Pattern::Assert(p, _, _) => p.rename_registers(rename),
1938            Pattern::Concat(v) | Pattern::Alt(v, _) => {
1939                for p in v {
1940                    p.rename_registers(rename);
1941                }
1942            }
1943        }
1944    }
1945
1946    /// The names bound under a repetition, in first-seen order: the
1947    /// registers that hold every binding a match made, in order, rather than
1948    /// the last.
1949    #[must_use]
1950    pub fn list_registers(&self) -> Vec<String> {
1951        let mut out = Vec::new();
1952        self.collect_list_registers(false, &mut out);
1953        out
1954    }
1955
1956    /// Whether any register is bound under a repetition.
1957    #[must_use]
1958    pub fn has_list_registers(&self) -> bool {
1959        !self.list_registers().is_empty()
1960    }
1961
1962    /// The registers bound inside each repetition, one list per repetition
1963    /// in the order they open, each in first-seen order; a repetition
1964    /// inside another gives its own list as well as adding to the outer's.
1965    #[must_use]
1966    pub fn repetition_registers(&self) -> Vec<Vec<String>> {
1967        let mut out = Vec::new();
1968        self.collect_repetition_registers(&mut out);
1969        out
1970    }
1971
1972    fn collect_repetition_registers(&self, out: &mut Vec<Vec<String>>) {
1973        let repetition = |p: &Pattern, out: &mut Vec<Vec<String>>| {
1974            out.push(p.capture_names());
1975            p.collect_repetition_registers(out);
1976        };
1977        match self {
1978            Pattern::Star(p, _) | Pattern::Plus(p, _) => repetition(p, out),
1979            Pattern::Repeat(p, _, hi, _) if *hi != Some(1) => repetition(p, out),
1980            Pattern::Bind(_, _, p)
1981            | Pattern::Repeat(p, _, _, _)
1982            | Pattern::Opt(p, _)
1983            | Pattern::Balanced(_, p)
1984            | Pattern::Field(_, p)
1985            | Pattern::Atomic(p)
1986            | Pattern::Assert(p, _, _) => p.collect_repetition_registers(out),
1987            Pattern::Concat(v) | Pattern::Alt(v, _) => {
1988                for p in v {
1989                    p.collect_repetition_registers(out);
1990                }
1991            }
1992            Pattern::Within(..) | Pattern::Empty | Pattern::Atom(_) | Pattern::Guard(..) | Pattern::Anchor(_) => {}
1993        }
1994    }
1995
1996    fn collect_list_registers(&self, repeated: bool, out: &mut Vec<String>) {
1997        match self {
1998            Pattern::Bind(name, _, p) => {
1999                if repeated && !out.contains(name) {
2000                    out.push(name.clone());
2001                }
2002                p.collect_list_registers(repeated, out);
2003            }
2004            Pattern::Star(p, _) | Pattern::Plus(p, _) => p.collect_list_registers(true, out),
2005            Pattern::Repeat(p, _, hi, _) => p.collect_list_registers(repeated || *hi != Some(1), out),
2006            Pattern::Opt(p, _)
2007            | Pattern::Balanced(_, p)
2008            | Pattern::Field(_, p)
2009            | Pattern::Atomic(p)
2010            | Pattern::Assert(p, _, _) => p.collect_list_registers(repeated, out),
2011            Pattern::Concat(v) | Pattern::Alt(v, _) => {
2012                for p in v {
2013                    p.collect_list_registers(repeated, out);
2014                }
2015            }
2016            // An edit-distance group holds no repetition of its own, so its
2017            // bindings are a list only where one encloses it.
2018            Pattern::Within(v, _) => {
2019                for name in v.iter().filter_map(|e| e.bind.as_ref()).map(|(n, _)| n) {
2020                    if repeated && !out.contains(name) {
2021                        out.push(name.clone());
2022                    }
2023                }
2024            }
2025            Pattern::Empty | Pattern::Atom(_) | Pattern::Guard(..) | Pattern::Anchor(_) => {}
2026        }
2027    }
2028
2029    fn collect_capture_names(&self, out: &mut Vec<String>) {
2030        match self {
2031            // An assertion consumes nothing and its bindings are discarded
2032            // with the probe, so it contributes no capture a template could
2033            // reference.
2034            Pattern::Assert(..) => {}
2035            Pattern::Bind(name, _, p) => {
2036                if !out.contains(name) {
2037                    out.push(name.clone());
2038                }
2039                p.collect_capture_names(out);
2040            }
2041            Pattern::Within(v, _) => {
2042                for name in v.iter().filter_map(|e| e.bind.as_ref()).map(|(n, _)| n) {
2043                    if !out.contains(name) {
2044                        out.push(name.clone());
2045                    }
2046                }
2047            }
2048            Pattern::Empty | Pattern::Atom(_) | Pattern::Guard(..) | Pattern::Anchor(_) => {}
2049            Pattern::Star(p, _) | Pattern::Plus(p, _) | Pattern::Opt(p, _) => {
2050                p.collect_capture_names(out);
2051            }
2052            Pattern::Repeat(p, _, _, _) | Pattern::Atomic(p) | Pattern::Balanced(_, p) | Pattern::Field(_, p) => {
2053                p.collect_capture_names(out);
2054            }
2055            Pattern::Concat(v) | Pattern::Alt(v, _) => {
2056                for c in v {
2057                    c.collect_capture_names(out);
2058                }
2059            }
2060        }
2061    }
2062
2063    /// The token kind each register binds, in the order
2064    /// [`Pattern::capture_names`] reports, `None` where the register binds
2065    /// anything but a single kind atom.
2066    ///
2067    /// A register over a concatenation, an alternation or a repetition binds
2068    /// text that may span several kinds, and there is no one typed value to
2069    /// read from it, so it answers `None` rather than the first kind it
2070    /// happens to contain. A magnitude or a predicate on the atom does not
2071    /// change which kind it is, so those bind their kind like a bare one.
2072    #[must_use]
2073    pub fn capture_kinds(&self) -> Vec<(String, Option<TokenKind>)> {
2074        let mut out = Vec::new();
2075        self.collect_capture_kinds(&mut out);
2076        out
2077    }
2078
2079    fn collect_capture_kinds(&self, out: &mut Vec<(String, Option<TokenKind>)>) {
2080        match self {
2081            Pattern::Assert(..) => {}
2082            Pattern::Bind(name, _, p) => {
2083                if !out.iter().any(|(n, _)| n == name) {
2084                    out.push((name.clone(), p.one_kind()));
2085                }
2086                p.collect_capture_kinds(out);
2087            }
2088            // Each element is one atom, so a binding on one carries that
2089            // atom's kind exactly as a bare `\N:n` does.
2090            Pattern::Within(v, _) => {
2091                for e in v {
2092                    if let Some((name, _)) = &e.bind
2093                        && !out.iter().any(|(n, _)| n == name)
2094                    {
2095                        out.push((name.clone(), Pattern::Atom(e.atom.clone()).one_kind()));
2096                    }
2097                }
2098            }
2099            Pattern::Empty | Pattern::Atom(_) | Pattern::Guard(..) | Pattern::Anchor(_) => {}
2100            Pattern::Star(p, _) | Pattern::Plus(p, _) | Pattern::Opt(p, _) => {
2101                p.collect_capture_kinds(out);
2102            }
2103            Pattern::Repeat(p, _, _, _)
2104            | Pattern::Atomic(p)
2105            | Pattern::Balanced(_, p)
2106            | Pattern::Field(_, p) => {
2107                p.collect_capture_kinds(out);
2108            }
2109            Pattern::Concat(v) | Pattern::Alt(v, _) => {
2110                for c in v {
2111                    c.collect_capture_kinds(out);
2112                }
2113            }
2114        }
2115    }
2116
2117    /// The kind this pattern is one atom of, or `None` for anything else.
2118    ///
2119    /// A sequence or an alternation of exactly one member is that member, so
2120    /// a register reads its kind through either. Without that, a lone atom
2121    /// the parser happened to wrap would report no value at all, which reads
2122    /// identically to a register that genuinely has none.
2123    ///
2124    /// Every other shape answers `None` deliberately. A run, a repetition and
2125    /// an alternation of two or more all bind text whose kind varies between
2126    /// matches or spans several kinds at once, and there is no single value
2127    /// to read from them. An unsure shape answers `None`, because a value
2128    /// printed for text that is not that kind is worse than no value.
2129    fn one_kind(&self) -> Option<TokenKind> {
2130        match self {
2131            Pattern::Atom(Atom::Kind(k) | Atom::KindMag(k, _) | Atom::KindPred(k, _)) => Some(*k),
2132            Pattern::Atomic(p) => p.one_kind(),
2133            Pattern::Concat(v) | Pattern::Alt(v, _) if v.len() == 1 => v[0].one_kind(),
2134            _ => None,
2135        }
2136    }
2137
2138    /// The ids of every declared or library kind the pattern names, each
2139    /// once, in order of first mention.
2140    #[must_use]
2141    pub fn custom_kinds(&self) -> Vec<u8> {
2142        fn atom_into(a: &Atom, out: &mut Vec<u8>) {
2143            match a {
2144                Atom::Kind(TokenKind::Custom(id))
2145                | Atom::KindMag(TokenKind::Custom(id), _)
2146                | Atom::KindPred(TokenKind::Custom(id), _) => {
2147                    if !out.contains(id) {
2148                        out.push(*id);
2149                    }
2150                }
2151                Atom::Class(c) => c.members().for_each(|m| atom_into(m, out)),
2152                _ => {}
2153            }
2154        }
2155        fn into(p: &Pattern, out: &mut Vec<u8>) {
2156            match p {
2157                Pattern::Atom(a) => atom_into(a, out),
2158                Pattern::Within(v, _) => v.iter().for_each(|e| atom_into(&e.atom, out)),
2159                Pattern::Empty | Pattern::Guard(..) | Pattern::Anchor(_) => {}
2160                Pattern::Assert(p, _, _)
2161                | Pattern::Star(p, _)
2162                | Pattern::Plus(p, _)
2163                | Pattern::Opt(p, _)
2164                | Pattern::Bind(_, _, p)
2165                | Pattern::Repeat(p, _, _, _)
2166                | Pattern::Atomic(p)
2167                | Pattern::Balanced(_, p)
2168                | Pattern::Field(_, p) => into(p, out),
2169                Pattern::Concat(v) | Pattern::Alt(v, _) => v.iter().for_each(|p| into(p, out)),
2170            }
2171        }
2172        let mut out = Vec::new();
2173        into(self, &mut out);
2174        out
2175    }
2176
2177    /// The ids of the shipped library's kinds the pattern names, which a lex
2178    /// has to be told to produce.
2179    #[must_use]
2180    pub fn library_kinds(&self) -> Vec<u8> {
2181        self.custom_kinds().into_iter().filter(|&id| id >= crate::custom::LIBRARY_ID_BASE).collect()
2182    }
2183
2184    /// Whether the pattern drops to the byte grain anywhere: a byte
2185    /// pattern (`` `...` ``), a byte class (`\d` / `\w` / `\s`), or a
2186    /// content guard (a byte-level forward-window assertion). This is the
2187    /// sub-token byte constraint that the dual-grain split routes to the
2188    /// byte grain.
2189    #[must_use]
2190    pub fn has_byte_constraint(&self) -> bool {
2191        fn atom_has(a: &Atom) -> bool {
2192            match a {
2193                Atom::Byte(_) | Atom::BytePattern(_) => true,
2194                Atom::Class(c) => c.members().any(atom_has),
2195                _ => false,
2196            }
2197        }
2198        match self {
2199            Pattern::Atom(a) if atom_has(a) => true,
2200            Pattern::Within(v, _) if v.iter().any(|e| atom_has(&e.atom)) => true,
2201            Pattern::Guard(..) => true,
2202            Pattern::Assert(p, _, _) => p.has_byte_constraint(),
2203            Pattern::Empty | Pattern::Atom(_) | Pattern::Within(..) | Pattern::Anchor(_) => false,
2204            Pattern::Star(p, _) | Pattern::Plus(p, _) | Pattern::Opt(p, _) | Pattern::Bind(_, _, p) => {
2205                p.has_byte_constraint()
2206            }
2207            Pattern::Repeat(p, _, _, _) | Pattern::Atomic(p) | Pattern::Balanced(_, p) | Pattern::Field(_, p) => {
2208                p.has_byte_constraint()
2209            }
2210            Pattern::Concat(v) | Pattern::Alt(v, _) => v.iter().any(Pattern::has_byte_constraint),
2211        }
2212    }
2213
2214    /// Whether the pattern queries the spectral axis (`\F{...}`) anywhere.
2215    /// Such a pattern routes to the set-reachability engine, which builds
2216    /// the spectral field once and threads it read-only to the matcher.
2217    #[must_use]
2218    pub fn has_spectral(&self) -> bool {
2219        fn atom_has(a: &Atom) -> bool {
2220            match a {
2221                Atom::Spectral(_) => true,
2222                Atom::Class(c) => c.members().any(atom_has),
2223                _ => false,
2224            }
2225        }
2226        match self {
2227            Pattern::Atom(a) if atom_has(a) => true,
2228            Pattern::Within(v, _) if v.iter().any(|e| atom_has(&e.atom)) => true,
2229            Pattern::Assert(p, _, _) => p.has_spectral(),
2230            Pattern::Empty
2231            | Pattern::Atom(_)
2232            | Pattern::Within(..)
2233            | Pattern::Guard(..)
2234            | Pattern::Anchor(_) => false,
2235            Pattern::Star(p, _) | Pattern::Plus(p, _) | Pattern::Opt(p, _) | Pattern::Bind(_, _, p) => {
2236                p.has_spectral()
2237            }
2238            Pattern::Repeat(p, _, _, _) | Pattern::Atomic(p) | Pattern::Balanced(_, p) | Pattern::Field(_, p) => {
2239                p.has_spectral()
2240            }
2241            Pattern::Concat(v) | Pattern::Alt(v, _) => v.iter().any(Pattern::has_spectral),
2242        }
2243    }
2244
2245    /// The readings the pattern's `\F{...}` atoms take, so a scan builds the
2246    /// spectral field with those and no others.
2247    ///
2248    /// The field takes a step per byte over the whole input, so computing a
2249    /// reading that no atom needs costs time at every byte: an atom naming the
2250    /// entropy alone would otherwise also decay a filterbank, count a k-gram
2251    /// through a map and take a square root, at each of them.
2252    #[must_use]
2253    pub fn spectral_needs(&self) -> crate::spectral::Needs {
2254        fn atom_into(a: &Atom, out: &mut crate::spectral::Needs) {
2255            match a {
2256                Atom::Spectral(p) => *out = out.and(crate::spectral::Needs::of(p)),
2257                Atom::Class(c) => c.members().for_each(|m| atom_into(m, out)),
2258                _ => {}
2259            }
2260        }
2261        let out = std::cell::RefCell::new(crate::spectral::Needs::none());
2262        self.any_node(&|p| {
2263            match p {
2264                Pattern::Atom(a) => atom_into(a, &mut out.borrow_mut()),
2265                Pattern::Within(v, _) => {
2266                    for e in v {
2267                        atom_into(&e.atom, &mut out.borrow_mut());
2268                    }
2269                }
2270                _ => {}
2271            }
2272            false
2273        });
2274        out.into_inner()
2275    }
2276
2277    /// Whether a bind appears anywhere in the pattern, so a scan of it has
2278    /// captures to resolve.
2279    #[must_use]
2280    pub fn binds(&self) -> bool {
2281        match self {
2282            Pattern::Bind(..) => true,
2283            Pattern::Within(v, _) => v.iter().any(|e| e.bind.is_some()),
2284            Pattern::Empty | Pattern::Atom(_) | Pattern::Guard(..) | Pattern::Anchor(_) => false,
2285            Pattern::Assert(p, _, _)
2286            | Pattern::Star(p, _)
2287            | Pattern::Plus(p, _)
2288            | Pattern::Opt(p, _)
2289            | Pattern::Repeat(p, _, _, _)
2290            | Pattern::Atomic(p)
2291            | Pattern::Balanced(_, p)
2292            | Pattern::Field(_, p) => p.binds(),
2293            Pattern::Concat(v) | Pattern::Alt(v, _) => v.iter().any(Pattern::binds),
2294        }
2295    }
2296}
2297
2298/// Collapse nested quantifiers using Kleene-algebra identities that
2299/// preserve the matched language:
2300/// `(r*)* = (r+)* = (r?)* = r*`, `(r*)+ = r*`, `(r+)+ = r+`,
2301/// `(r?)+ = r*`, `(r*)? = (r+)? = r*`, `(r?)? = r?`.
2302///
2303/// The collapse fires only when the inner quantified subtree carries
2304/// no capture (binding, register-equality, or guard), so the reported
2305/// capture values are never changed. This removes the canonical
2306/// nested-quantifier blowup: `(.*)*` becomes `.*`, which the engine
2307/// scans without the per-position recompute the nested form forces.
2308///
2309/// It also fires only when the two quantifiers lean the same way. The
2310/// identities hold for the language a pattern accepts, not for which of the
2311/// accepted lengths it reports, so collapsing `(r*?)*` to `r*` would keep the
2312/// same inputs matching and change the spans returned for them. Three lazy
2313/// pairs stay nested even so, for the reason [`foldable`] gives.
2314#[must_use]
2315pub fn normalize(p: Pattern) -> Pattern {
2316    match p {
2317        Pattern::Star(inner, g) => collapse_outer_star(normalize(*inner), g),
2318        Pattern::Plus(inner, g) => collapse_outer_plus(normalize(*inner), g),
2319        Pattern::Opt(inner, g) => collapse_outer_opt(normalize(*inner), g),
2320        Pattern::Concat(v) => Pattern::Concat(v.into_iter().map(normalize).collect()),
2321        Pattern::Alt(v, m) => Pattern::Alt(v.into_iter().map(normalize).collect(), m),
2322        Pattern::Bind(n, s, x) => Pattern::Bind(n, s, normalize(*x).boxed()),
2323        Pattern::Balanced(k, x) => Pattern::Balanced(k, normalize(*x).boxed()),
2324        Pattern::Repeat(x, lo, hi, g) => Pattern::Repeat(normalize(*x).boxed(), lo, hi, g),
2325        Pattern::Field(k, x) => Pattern::Field(k, normalize(*x).boxed()),
2326        Pattern::Assert(x, neg, look) => Pattern::Assert(normalize(*x).boxed(), neg, look),
2327        // The body normalizes, but the cut is opaque to the identities above:
2328        // an outer quantifier folding through it would restore the lengths
2329        // the atomic group exists to discard.
2330        Pattern::Atomic(x) => Pattern::Atomic(normalize(*x).boxed()),
2331        leaf @ (Pattern::Empty
2332        | Pattern::Atom(_)
2333        | Pattern::Within(..)
2334        | Pattern::Guard(..)
2335        | Pattern::Anchor(_)) => leaf,
2336    }
2337}
2338
2339/// The three quantifier shapes a fold pairs.
2340#[derive(Clone, Copy, PartialEq, Eq)]
2341enum Quant {
2342    Star,
2343    Plus,
2344    Opt,
2345}
2346
2347/// Whether an inner quantifier may fold into an outer one: the same lean, no
2348/// capture inside to have its reported value changed, and, for a lazy pair,
2349/// the same position after the body has matched.
2350///
2351/// A greedy pair offers the body's next token first in either form. A lazy
2352/// pair offers an enclosing loop's exit first, and the form's position after
2353/// the body has matched decides what that loop's re-entry reaches: an
2354/// optional or a plus is past its body, so the re-entry reaches the body
2355/// afresh and offers its next token before the exit; a star is at its own
2356/// head, which the re-entry reaches a second time and drops, so the exit
2357/// comes first. `(r??)*?`, `(r??)+?` and `(r+?)??` each fold to a form at a
2358/// head where the original is past the body, and inside a further loop the
2359/// two rank the body's next token and the exit in opposite orders. The regex crate ranks the nested form's way, so those stay as
2360/// written.
2361fn foldable(inner: Quant, i: Greed, outer: Quant, g: Greed, x: &Pattern) -> bool {
2362    if i != g || has_capture(x) {
2363        return false;
2364    }
2365    match g {
2366        Greed::Greedy => true,
2367        Greed::Lazy => !matches!(
2368            (inner, outer),
2369            (Quant::Opt, Quant::Star | Quant::Plus) | (Quant::Plus, Quant::Opt)
2370        ),
2371    }
2372}
2373
2374fn collapse_outer_star(inner: Pattern, g: Greed) -> Pattern {
2375    match inner {
2376        Pattern::Star(x, i) if foldable(Quant::Star, i, Quant::Star, g, &x) => Pattern::Star(x, g),
2377        Pattern::Plus(x, i) if foldable(Quant::Plus, i, Quant::Star, g, &x) => Pattern::Star(x, g),
2378        Pattern::Opt(x, i) if foldable(Quant::Opt, i, Quant::Star, g, &x) => Pattern::Star(x, g),
2379        other => Pattern::Star(other.boxed(), g),
2380    }
2381}
2382
2383fn collapse_outer_plus(inner: Pattern, g: Greed) -> Pattern {
2384    match inner {
2385        Pattern::Star(x, i) if foldable(Quant::Star, i, Quant::Plus, g, &x) => Pattern::Star(x, g),
2386        Pattern::Plus(x, i) if foldable(Quant::Plus, i, Quant::Plus, g, &x) => Pattern::Plus(x, g),
2387        Pattern::Opt(x, i) if foldable(Quant::Opt, i, Quant::Plus, g, &x) => Pattern::Star(x, g),
2388        other => Pattern::Plus(other.boxed(), g),
2389    }
2390}
2391
2392fn collapse_outer_opt(inner: Pattern, g: Greed) -> Pattern {
2393    match inner {
2394        Pattern::Star(x, i) if foldable(Quant::Star, i, Quant::Opt, g, &x) => Pattern::Star(x, g),
2395        Pattern::Plus(x, i) if foldable(Quant::Plus, i, Quant::Opt, g, &x) => Pattern::Star(x, g),
2396        Pattern::Opt(x, i) if foldable(Quant::Opt, i, Quant::Opt, g, &x) => Pattern::Opt(x, g),
2397        other => Pattern::Opt(other.boxed(), g),
2398    }
2399}
2400
2401/// Whether `p` carries any capture-affecting node (binding, register-
2402/// equality, or content guard) in its subtree. Used to keep the
2403/// quantifier-collapse from changing reported captures.
2404fn has_capture(p: &Pattern) -> bool {
2405    match p {
2406        // An assertion is a condition on the match, so collapsing a quantifier
2407        // around one could change which spans are reported.
2408        Pattern::Bind(..) | Pattern::Guard(..) | Pattern::Assert(..) => true,
2409        Pattern::Atom(
2410            Atom::RegisterEq(..) | Atom::RegisterRelated(..) | Atom::RegisterWithin(..) | Atom::RegisterKin(..),
2411        ) => true,
2412        Pattern::Within(v, _) => v.iter().any(|e| {
2413            e.bind.is_some()
2414                || matches!(
2415                    e.atom,
2416                    Atom::RegisterEq(..) | Atom::RegisterRelated(..) | Atom::RegisterWithin(..) | Atom::RegisterKin(..)
2417                )
2418        }),
2419        Pattern::Atom(_) | Pattern::Empty | Pattern::Anchor(_) => false,
2420        Pattern::Star(x, _)
2421        | Pattern::Plus(x, _)
2422        | Pattern::Opt(x, _)
2423        | Pattern::Balanced(_, x)
2424        | Pattern::Repeat(x, _, _, _)
2425        | Pattern::Atomic(x)
2426        | Pattern::Field(_, x) => has_capture(x),
2427        Pattern::Concat(v) | Pattern::Alt(v, _) => v.iter().any(has_capture),
2428    }
2429}
2430
2431#[cfg(test)]
2432mod tests {
2433    use super::*;
2434
2435    #[test]
2436    fn a_supertoken_pattern_is_whole_input_dependent() {
2437        // A unit is a run of tokens, so a chunk boundary can cut one. A
2438        // chunked scanner must therefore not commit a match that read the
2439        // tower before it has seen the whole input.
2440        let p = crate::parser::parse("@super:assign \\N").expect("parses");
2441        assert!(p.reads_supertokens());
2442        assert!(
2443            p.depends_on_whole_input(),
2444            "the tower is a whole-stream reading and has to be declared as one"
2445        );
2446        // A pattern that does not read the tower is not dragged in with it.
2447        let plain = crate::parser::parse("\\N").expect("parses");
2448        assert!(!plain.reads_supertokens());
2449        assert!(!plain.depends_on_whole_input());
2450    }
2451
2452    #[test]
2453    fn constructs_one_of_every_variant() {
2454        // Acceptance: the AST represents every construct in the
2455        // spec; build one instance of each node.
2456        let nodes = vec![
2457            Pattern::Empty,
2458            Pattern::Atom(Atom::Kind(TokenKind::Word)),
2459            Pattern::Atom(Atom::Any),
2460            Pattern::Atom(Atom::literal("<")),
2461            Pattern::Atom(Atom::RegisterEq("t".to_string(), OrbitGroup::Identity)),
2462            Pattern::Atom(Atom::Byte(ByteClass::Digit)),
2463            Pattern::Bind("t".to_string(), false, Pattern::Atom(Atom::Kind(TokenKind::Word)).boxed()),
2464            Pattern::Balanced(
2465                Some(BracketKind::Paren),
2466                Pattern::Star(Pattern::Atom(Atom::Any).boxed(), Greed::Greedy).boxed(),
2467            ),
2468            Pattern::Balanced(None, Pattern::Empty.boxed()),
2469            Pattern::Guard("ERROR".to_string(), false),
2470            Pattern::Field(3, Pattern::Empty.boxed()),
2471            Pattern::Concat(vec![Pattern::Empty, Pattern::Empty]),
2472            Pattern::Alt(vec![Pattern::Empty, Pattern::Empty], AltMode::First),
2473            Pattern::Star(Pattern::Empty.boxed(), Greed::Greedy),
2474            Pattern::Plus(Pattern::Empty.boxed(), Greed::Greedy),
2475            Pattern::Opt(Pattern::Empty.boxed(), Greed::Lazy),
2476            Pattern::Repeat(Pattern::Empty.boxed(), 1, Some(3), Greed::Greedy),
2477        ];
2478        // Debug renders each node without panicking.
2479        for n in &nodes {
2480            assert!(!format!("{n:?}").is_empty());
2481        }
2482        assert_eq!(nodes.len(), 17);
2483    }
2484
2485    #[test]
2486    fn nested_quantifiers_collapse() {
2487        let g = Greed::Greedy;
2488        let any = || Pattern::Atom(Atom::Any).boxed();
2489        let star = Pattern::Star(any(), g);
2490        // (.*)* -> .*
2491        assert_eq!(normalize(Pattern::Star(star.clone().boxed(), g)), star);
2492        // (.+)* -> .*
2493        let plus = Pattern::Plus(any(), g);
2494        assert_eq!(normalize(Pattern::Star(plus.boxed(), g)), star);
2495        // (.?)+ -> .*
2496        let opt = Pattern::Opt(any(), g);
2497        assert_eq!(normalize(Pattern::Plus(opt.clone().boxed(), g)), star);
2498        // (.?)? -> .?
2499        assert_eq!(normalize(Pattern::Opt(opt.clone().boxed(), g)), opt);
2500    }
2501
2502    #[test]
2503    fn quantifiers_leaning_opposite_ways_do_not_collapse() {
2504        // The Kleene identities hold for the accepted language, not for which
2505        // accepted length is reported, so folding a lazy inner into a greedy
2506        // outer would keep the same inputs matching and change their spans.
2507        let lazy_star = Pattern::Star(Pattern::Atom(Atom::Any).boxed(), Greed::Lazy);
2508        let nested = Pattern::Star(lazy_star.clone().boxed(), Greed::Greedy);
2509        assert_eq!(normalize(nested.clone()), nested, "(.*?)* keeps its nesting");
2510
2511        let greedy_star = Pattern::Star(Pattern::Atom(Atom::Any).boxed(), Greed::Greedy);
2512        let nested = Pattern::Star(greedy_star.boxed(), Greed::Lazy);
2513        assert_eq!(normalize(nested.clone()), nested, "(.*)*? keeps its nesting");
2514
2515        // Matching leans still collapse.
2516        let lazy = Pattern::Star(Pattern::Atom(Atom::Any).boxed(), Greed::Lazy);
2517        assert_eq!(normalize(Pattern::Star(lazy.clone().boxed(), Greed::Lazy)), lazy);
2518    }
2519
2520    #[test]
2521    fn a_lazy_fold_that_changes_the_position_after_the_body_is_not_made() {
2522        // `(.??)+?`, `(.??)*?` and `(.+?)??` each fold to a star, which is at
2523        // its head after the body has matched where they are past it, and
2524        // inside a further loop that ranks the next token and the exit the
2525        // other way round. They keep their nesting.
2526        let any = || Pattern::Atom(Atom::Any).boxed();
2527        let lazy_opt = Pattern::Opt(any(), Greed::Lazy);
2528        let lazy_plus = Pattern::Plus(any(), Greed::Lazy);
2529        for nested in [
2530            Pattern::Plus(lazy_opt.clone().boxed(), Greed::Lazy),
2531            Pattern::Star(lazy_opt.clone().boxed(), Greed::Lazy),
2532            Pattern::Opt(lazy_plus.clone().boxed(), Greed::Lazy),
2533        ] {
2534            assert_eq!(normalize(nested.clone()), nested, "{nested:?} keeps its nesting");
2535        }
2536        // The lazy folds that keep the position still collapse.
2537        let lazy_star = Pattern::Star(any(), Greed::Lazy);
2538        assert_eq!(normalize(Pattern::Plus(lazy_star.clone().boxed(), Greed::Lazy)), lazy_star);
2539        assert_eq!(normalize(Pattern::Star(lazy_plus.clone().boxed(), Greed::Lazy)), lazy_star);
2540        assert_eq!(normalize(Pattern::Plus(lazy_plus.clone().boxed(), Greed::Lazy)), lazy_plus);
2541        assert_eq!(normalize(Pattern::Opt(lazy_star.clone().boxed(), Greed::Lazy)), lazy_star);
2542        assert_eq!(normalize(Pattern::Opt(lazy_opt.clone().boxed(), Greed::Lazy)), lazy_opt);
2543        // And every greedy fold.
2544        let greedy_opt = Pattern::Opt(any(), Greed::Greedy);
2545        let greedy_plus = Pattern::Plus(any(), Greed::Greedy);
2546        let star = Pattern::Star(any(), Greed::Greedy);
2547        assert_eq!(normalize(Pattern::Plus(greedy_opt.boxed(), Greed::Greedy)), star);
2548        assert_eq!(normalize(Pattern::Opt(greedy_plus.boxed(), Greed::Greedy)), star);
2549    }
2550
2551    #[test]
2552    fn nested_quantifier_with_capture_is_preserved() {
2553        // ((\W:t))* keeps its nesting so reported captures are unchanged.
2554        let bind =
2555            Pattern::Bind("t".to_string(), false, Pattern::Atom(Atom::Kind(TokenKind::Word)).boxed());
2556        let g = Greed::Greedy;
2557        let inner_star = Pattern::Star(bind.boxed(), g);
2558        let nested = Pattern::Star(inner_star.clone().boxed(), g);
2559        assert_eq!(normalize(nested), Pattern::Star(inner_star.boxed(), g));
2560    }
2561}