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