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}