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