use std::collections::{BTreeMap, HashSet};
use std::rc::Rc;
use crate::ast::{Atom, ByteClass, EmptyLoop, Greed, Pattern};
use crate::lexer::text;
use crate::spectral::SpectralField;
use crate::token::{Token, TokenKind};
#[derive(Clone, Copy, Default)]
struct Fields<'a> {
spectral: Option<&'a SpectralField>,
seam_cuts: Option<&'a [usize]>,
depths: Option<&'a [u16]>,
obs_contested: Option<&'a [usize]>,
echo: &'a [(crate::orbit::OrbitGroup, crate::echo::EchoField)],
supers: Option<&'a crate::supertoken::SuperContext>,
order: Option<&'a [Option<bool>]>,
templates: Option<&'a crate::templates::Mining>,
joins: &'a [Join],
seam_token: Option<&'a [usize]>,
seam_super: Option<&'a [usize]>,
obs_token: Option<&'a [usize]>,
obs_super: Option<&'a [usize]>,
gravity: [Option<&'a crate::gravity::Readings>; 3],
kin: &'a [(crate::ast::Grain, String, Option<u32>)],
bands: Option<&'a Bands>,
field_starts: Option<&'a [u32]>,
context: Option<&'a crate::context::ContextField>,
relation: Option<&'a crate::context::RelationContext>,
clock: crate::typed::Clock,
lists: &'a [u16],
}
struct ContextBuild {
window: Option<crate::context::ContextField>,
relation: Option<crate::context::RelationContext>,
}
fn build_context(
pattern: &Pattern,
input: &[u8],
toks: &[Token],
spectral: Option<&SpectralField>,
echo: Option<&crate::echo::EchoField>,
) -> ContextBuild {
use crate::context::{ContextConfig, fold_windows_parallel, record_period, relate};
use crate::profile::AxisCtx;
let uses = pattern.context_uses();
let ctx = AxisCtx::new(input);
let window = uses.window.then(|| {
let supers = crate::supertoken::SuperContext::build(toks, input);
fold_windows_parallel(toks, &ctx, &supers, &ContextConfig::default())
});
let relation = uses.any_related().then(|| {
let own_spectral;
let cuts: &[usize] = if uses.regime {
match spectral {
Some(s) => &s.boundaries,
None => {
let mut needs = crate::spectral::Needs::none();
needs.onset = true;
needs.entropy = true;
needs.bands = true;
own_spectral =
crate::spectral::analyze_needing(input, &Default::default(), needs);
&own_spectral.boundaries
}
}
} else {
&[]
};
let own_echo;
let echo = if uses.echo || uses.key {
match echo {
Some(e) => e,
None => {
own_echo = crate::echo::analyze(toks, input);
&own_echo
}
}
} else {
own_echo =
crate::echo::EchoField { frames: Vec::new(), keyed: 0, distinct: 0, novel: 0, echoed: 0 };
&own_echo
};
let period = if uses.phase { record_period(toks, input) } else { None };
relate(toks, &ctx, cuts, echo, period)
});
ContextBuild { window, relation }
}
pub(crate) fn field_start_index(input: &[u8], toks: &[Token]) -> Vec<u32> {
let mut out = vec![0u32; toks.len()];
let mut commas = 0u32;
let mut any_significant = false;
let mut prev_was_comma = false;
for (p, t) in toks.iter().enumerate() {
if t.kind == TokenKind::Whitespace {
if !any_significant || prev_was_comma {
out[p] = commas + 1;
}
continue;
}
if !any_significant || prev_was_comma {
out[p] = commas + 1;
}
any_significant = true;
if t.kind == TokenKind::Punct && text(input, t) == b"," {
commas += 1;
prev_was_comma = true;
} else {
prev_was_comma = false;
}
}
out
}
type Env = Rc<BTreeMap<u16, (usize, usize)>>;
fn collect_registers(pat: &Pattern) -> Vec<String> {
fn walk(pat: &Pattern, out: &mut Vec<String>) {
let push = |name: &str, out: &mut Vec<String>| {
if !out.iter().any(|r| r == name) {
out.push(name.to_string());
}
};
match pat {
Pattern::Empty | Pattern::Guard(..) | Pattern::Anchor(_) => {}
Pattern::Assert(p, _, _) => walk(p, out),
Pattern::Atom(
Atom::RegisterEq(name, _)
| Atom::RegisterRelated(name, _)
| Atom::RegisterWithin(name, _, _)
| Atom::RegisterKin(name, _),
) => {
push(name, out);
}
Pattern::Atom(a) => {
if let Some(name) = a.key_scope() {
push(name, out);
}
}
Pattern::Concat(ps) | Pattern::Alt(ps, _) => ps.iter().for_each(|p| walk(p, out)),
Pattern::Opt(p, _)
| Pattern::Star(p, _)
| Pattern::Plus(p, _)
| Pattern::Repeat(p, _, _, _)
| Pattern::Atomic(p)
| Pattern::Balanced(_, p)
| Pattern::Field(_, p) => walk(p, out),
Pattern::Bind(name, _, p) => {
push(name, out);
walk(p, out);
}
Pattern::Within(v, _) => {
for e in v {
if let Some((name, _)) = &e.bind {
push(name, out);
}
walk(&Pattern::Atom(e.atom.clone()), out);
}
}
}
}
let mut out = Vec::new();
walk(pat, &mut out);
out
}
fn reg_id(regs: &[String], name: &str) -> Option<u16> {
regs.iter().position(|r| r == name).map(|i| i as u16)
}
#[derive(Clone, Debug)]
enum Rank {
Inline { len: u8, bytes: [u8; 7] },
Heap(Rc<[u8]>),
}
const RANK_INLINE: usize = 7;
impl Default for Rank {
fn default() -> Self {
Rank::Inline { len: 0, bytes: [0; RANK_INLINE] }
}
}
impl Rank {
fn as_slice(&self) -> &[u8] {
match self {
Rank::Inline { len, bytes } => &bytes[..*len as usize],
Rank::Heap(path) => path,
}
}
fn extended(&self, branch: u8) -> Rank {
let path = self.as_slice();
let n = path.len();
if n < RANK_INLINE {
let mut bytes = [0u8; RANK_INLINE];
bytes[..n].copy_from_slice(path);
bytes[n] = branch;
return Rank::Inline { len: (n + 1) as u8, bytes };
}
Rank::Heap((0..n + 1).map(|i| if i < n { path[i] } else { branch }).collect())
}
fn of(path: &[u8]) -> Rank {
if path.len() <= RANK_INLINE {
let mut bytes = [0u8; RANK_INLINE];
bytes[..path.len()].copy_from_slice(path);
return Rank::Inline { len: path.len() as u8, bytes };
}
Rank::Heap(Rc::from(path))
}
}
#[derive(Debug)]
struct HistNode {
id: u16,
span: (usize, usize),
prev: Hist,
}
type Hist = Option<Rc<HistNode>>;
#[derive(Clone, Debug)]
struct State {
pos: usize,
env: Env,
rank: Rank,
hist: Hist,
}
impl PartialEq for State {
fn eq(&self, other: &Self) -> bool {
self.pos == other.pos && self.env == other.env
}
}
impl Eq for State {}
impl std::hash::Hash for State {
fn hash<H: std::hash::Hasher>(&self, h: &mut H) {
self.pos.hash(h);
self.env.hash(h);
}
}
fn empty_env() -> Env {
thread_local! {
static EMPTY: Env = Rc::new(BTreeMap::new());
}
EMPTY.with(Clone::clone)
}
impl State {
fn start(pos: usize) -> Self {
State { pos, env: empty_env(), rank: Rank::default(), hist: None }
}
fn history(&self) -> Vec<(u16, (usize, usize))> {
let mut out = Vec::new();
let mut cur = self.hist.as_ref();
while let Some(node) = cur {
out.push((node.id, node.span));
cur = node.prev.as_ref();
}
out.reverse();
out
}
fn rank_slice(&self) -> &[u8] {
self.rank.as_slice()
}
fn with_branch(&self, branch: u8) -> Self {
note_extended(self.rank_slice().len() + 1);
State {
pos: self.pos,
env: self.env.clone(),
rank: self.rank.extended(branch),
hist: self.hist.clone(),
}
}
}
fn cmp_rank(a: &[u8], b: &[u8]) -> std::cmp::Ordering {
note_compared();
a.cmp(b)
}
fn best_index(states: &[State]) -> Option<usize> {
states
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| cmp_rank(a.rank_slice(), b.rank_slice()))
.map(|(at, _)| at)
}
fn count_key(g: Greed, iters: usize) -> u8 {
let k = u8::try_from(iters.min(u8::MAX as usize)).expect("clamped to a u8");
match g {
Greed::Greedy => u8::MAX - k,
Greed::Lazy => k,
}
}
pub const INLINE_REGS: usize = 2;
#[derive(Clone)]
pub enum Regs {
None,
Inline(u8, [Span; INLINE_REGS]),
Shared(std::sync::Arc<[Span]>),
}
impl Regs {
#[must_use]
#[inline]
pub const fn none() -> Self {
Regs::None
}
#[must_use]
#[inline]
pub fn from_slice(from: &[Span]) -> Self {
if from.is_empty() {
return Regs::None;
}
if from.len() <= INLINE_REGS {
let mut held = [Span { start: 0, end: 0 }; INLINE_REGS];
held[..from.len()].copy_from_slice(from);
let n = u8::try_from(from.len()).expect("at most INLINE_REGS");
Regs::Inline(n, held)
} else {
Regs::Shared(from.into())
}
}
#[must_use]
#[inline]
pub fn as_slice(&self) -> &[Span] {
match self {
Regs::None => &[],
Regs::Inline(n, spans) => &spans[..*n as usize],
Regs::Shared(spans) => spans,
}
}
#[inline]
pub fn as_mut_slice(&mut self) -> &mut [Span] {
match self {
Regs::None => &mut [],
Regs::Inline(n, spans) => &mut spans[..*n as usize],
Regs::Shared(spans) => {
if std::sync::Arc::get_mut(spans).is_none() {
*spans = spans.to_vec().into();
}
std::sync::Arc::get_mut(spans).expect("no other holder remains")
}
}
}
}
impl std::ops::Deref for Regs {
type Target = [Span];
#[inline]
fn deref(&self) -> &[Span] {
self.as_slice()
}
}
impl From<Vec<Span>> for Regs {
#[inline]
fn from(v: Vec<Span>) -> Self {
Regs::from_slice(&v)
}
}
impl FromIterator<Span> for Regs {
fn from_iter<I: IntoIterator<Item = Span>>(iter: I) -> Self {
let mut it = iter.into_iter();
let mut held = [Span { start: 0, end: 0 }; INLINE_REGS];
let mut n = 0usize;
for slot in &mut held {
let Some(span) = it.next() else { break };
*slot = span;
n += 1;
}
match it.next() {
None if n == 0 => Regs::None,
None => Regs::Inline(u8::try_from(n).expect("at most INLINE_REGS"), held),
Some(over) => {
let mut all: Vec<Span> = Vec::with_capacity(n + 2);
all.extend_from_slice(&held[..n]);
all.push(over);
all.extend(it);
Regs::Shared(all.into())
}
}
}
}
impl PartialEq for Regs {
fn eq(&self, other: &Self) -> bool {
self.as_slice() == other.as_slice()
}
}
impl Eq for Regs {}
impl std::fmt::Debug for Regs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.as_slice().fmt(f)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Match {
pub start: usize,
pub end: usize,
pub(crate) captures: Regs,
names: Option<std::sync::Arc<[String]>>,
lists: Option<Box<Bindings>>,
}
type Bindings = Vec<(usize, Vec<Span>)>;
impl Match {
#[must_use]
#[inline]
pub fn plain(start: usize, end: usize) -> Self {
Match { start, end, captures: Regs::none(), names: None, lists: None }
}
#[must_use]
pub(crate) fn with_lists(mut self, lists: Bindings) -> Self {
self.lists = (!lists.is_empty()).then(|| Box::new(lists));
self
}
#[must_use]
pub fn list(&self, name: &str) -> Option<&[Span]> {
let i = self.names().iter().position(|n| n == name)?;
self.lists.as_ref()?.iter().find(|(k, _)| *k == i).map(|(_, spans)| spans.as_slice())
}
pub fn lists(&self) -> impl Iterator<Item = (&str, &[Span])> {
self.lists
.as_deref()
.map_or(&[][..], Vec::as_slice)
.iter()
.map(|(k, spans)| (self.names()[*k].as_str(), spans.as_slice()))
}
#[must_use]
#[inline]
pub fn bound(
start: usize,
end: usize,
captures: Regs,
names: std::sync::Arc<[String]>,
) -> Self {
Match { start, end, captures, names: (!names.is_empty()).then_some(names), lists: None }
}
#[must_use]
pub fn names(&self) -> &[String] {
self.names.as_deref().unwrap_or(&[])
}
#[must_use]
#[inline]
pub fn captures(&self) -> &[Span] {
self.captures.as_slice()
}
#[inline]
pub fn captures_mut(&mut self) -> &mut [Span] {
self.captures.as_mut_slice()
}
#[must_use]
pub fn shifted(mut self, by: usize) -> Match {
let by32 = u32::try_from(by).expect("a window begins within the widest offset a span holds");
let shift = |s: &mut Span| {
let moved = |at: u32| at.checked_add(by32).expect("a register ends within the widest offset a span holds");
s.start = moved(s.start);
s.end = moved(s.end);
};
self.start += by;
self.end += by;
self.captures.as_mut_slice().iter_mut().for_each(shift);
if let Some(lists) = self.lists.as_mut() {
lists.iter_mut().flat_map(|(_, spans)| spans.iter_mut()).for_each(shift);
}
self
}
#[must_use]
pub fn group<'h>(&self, name: &str, input: &'h [u8]) -> Option<&'h [u8]> {
self.group_span(name).map(|s| &input[s.range()])
}
#[must_use]
pub fn group_span(&self, name: &str) -> Option<Span> {
let i = self.names().iter().position(|n| n == name)?;
self.captures.get(i).copied()
}
#[must_use]
pub fn group_at<'h>(&self, i: usize, input: &'h [u8]) -> Option<&'h [u8]> {
self.captures.get(i).map(|s| &input[s.range()])
}
#[must_use]
#[inline]
pub fn span(&self) -> Span {
Span { start: self.start as u32, end: self.end as u32 }
}
#[must_use]
pub fn extract<'h, const N: usize>(&self, input: &'h [u8]) -> (&'h [u8], [&'h [u8]; N]) {
assert_eq!(
self.captures.len(),
N,
"extract::<{N}> on a match that bound {} registers",
self.captures.len()
);
let mut out = [&input[0..0]; N];
for (slot, span) in out.iter_mut().zip(self.captures()) {
*slot = &input[span.range()];
}
(&input[self.start..self.end], out)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Span {
pub start: u32,
pub end: u32,
}
impl Span {
#[must_use]
pub fn start(&self) -> usize {
self.start as usize
}
#[must_use]
pub fn end(&self) -> usize {
self.end as usize
}
#[must_use]
pub fn len(&self) -> usize {
self.end().saturating_sub(self.start())
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.end() <= self.start()
}
#[must_use]
pub fn range(&self) -> std::ops::Range<usize> {
self.start as usize..self.end as usize
}
}
impl From<Span> for Match {
#[inline]
fn from(s: Span) -> Self {
Match::plain(s.start as usize, s.end as usize)
}
}
#[must_use]
pub fn is_match(pattern: &Pattern, input: &[u8]) -> bool {
if let Some(shapes) = crate::library::shapes_for(pattern) {
return !scan_with_shapes(pattern, input, &shapes).is_empty();
}
if let Some(found) = routed_is_match_early(pattern, input) {
return found;
}
if let Some(bare) = pattern.without_bindings()
&& let Some(found) = routed_is_match_early(&bare, input)
{
crate::trace::rung("is_match", "a route, with the bindings off", input.len());
return found;
}
if let Some(found) = crate::prefilter::any_required_window(pattern, input) {
crate::trace::rung("is_match", "the windows a literal opens", input.len());
return found;
}
if crate::prefilter::any_in_prefix(pattern, input) == Some(true) {
crate::trace::rung("is_match", "a prefix that found one", input.len());
return true;
}
if let Some(found) = crate::nfa::SerialWalk::any_match(pattern, input) {
crate::trace::rung("is_match", "the held walk, windowed from the first ask", input.len());
return found;
}
crate::trace::rung("is_match", "every match of the whole scan", input.len());
!scan(pattern, input).is_empty()
}
fn routed_is_match_early(pattern: &Pattern, input: &[u8]) -> Option<bool> {
if crate::prefilter::requires_absent(pattern, input) {
crate::trace::rung("is_match", "an absent literal, no lex", input.len());
return Some(false);
}
if let Some(lits) = crate::prefilter::byte_routable_literals(pattern)
&& let Some(found) = crate::prefilter::byte_route_any_word_literal(&lits, input)
{
crate::trace::rung("is_match", "a word literal route, no lex", input.len());
return Some(found);
}
if let Some(lit) = crate::prefilter::byte_routable_line_anchored_literal(pattern)
&& let Some(first) = crate::prefilter::byte_route_first_line_anchored_literal(lit, input, 0)
{
crate::trace::rung("is_match", "a line-anchored literal route, no lex", input.len());
return Some(first.is_some());
}
if let Some(punct) = crate::prefilter::byte_routable_word_then_punct(pattern)
&& let Some(found) = crate::prefilter::byte_route_any_word_then_punct(punct, input)
{
crate::trace::rung("is_match", "a word-then-punct route, no lex", input.len());
return Some(found);
}
if let Some((bp, prefix)) = crate::prefilter::byte_routable_byte_pattern(pattern)
&& let Some(found) = crate::prefilter::byte_route_any_byte_pattern(bp, &prefix, input)
{
crate::trace::rung("is_match", "a byte-pattern route, no lex", input.len());
return Some(found);
}
if let Some(first) = crate::prefilter::byte_route_kind_from(pattern, input, 0) {
crate::trace::rung("is_match", "a kind route, no lex", input.len());
return Some(first.is_some());
}
if let Some(found) = crate::prefilter::first_by_literal_windows(pattern, input) {
crate::trace::rung("is_match", "windows to the first match", input.len());
return Some(found.is_some());
}
None
}
#[must_use]
pub fn scan(pattern: &Pattern, input: &[u8]) -> Vec<Span> {
if let Some(shapes) = crate::library::shapes_for(pattern) {
return scan_with_shapes(pattern, input, &shapes);
}
if let Some(spans) = routed_spans(pattern, input) {
crate::trace::rung("scan", "a route, not the engine", input.len());
return spans;
}
if let Some(matches) = crate::nfa::scan_nfa(pattern, input) {
crate::trace::rung("scan", "the single-pass engine over a whole lex", input.len());
return matches;
}
crate::trace::rung("scan", "the set engine over a whole lex", input.len());
scan_set_reachability(pattern, input)
}
pub(crate) fn routed_spans(pattern: &Pattern, input: &[u8]) -> Option<Vec<Span>> {
if let Some(bare) = pattern.without_bindings()
&& let Some(spans) = routed_spans(&bare, input)
{
crate::trace::rung("scan", "a route, with the bindings off", input.len());
return Some(spans);
}
routed_spans_positional(pattern, input)
.or_else(|| routed_spans_selective(pattern, input))
.or_else(|| {
let spans = crate::prefilter::scan_by_literal_windows(pattern, input)?;
crate::trace::rung("scan", "windows around the opening literal", input.len());
Some(spans)
})
}
pub(crate) fn routed_first(pattern: &Pattern, input: &[u8]) -> Option<Option<Span>> {
routed_first_early(pattern, input)
.or_else(|| routed_spans_positional(pattern, input).map(|v| v.into_iter().next()))
.or_else(|| {
let first = crate::prefilter::first_by_literal_windows(pattern, input)?;
crate::trace::rung("routed_first", "windows to the first match", input.len());
Some(first)
})
.or_else(|| crate::prefilter::first_required_window(pattern, input))
}
pub(crate) fn routed_first_positional(pattern: &Pattern, input: &[u8]) -> Option<Option<Span>> {
routed_first_early(pattern, input)
.or_else(|| routed_spans_positional(pattern, input).map(|v| v.into_iter().next()))
.or_else(|| {
let first = crate::prefilter::first_by_literal_windows(pattern, input)?;
crate::trace::rung("routed_first_positional", "windows to the first match", input.len());
Some(first)
})
}
pub(crate) fn routed_first_at(pattern: &Pattern, input: &[u8], at: usize) -> Option<Option<Span>> {
if crate::prefilter::requires_absent(pattern, input) {
return Some(None);
}
if let Some(first) = routed_first_at_reading(pattern, input, at, &mut None) {
crate::trace::rung("routed_first_at", "an anchored byte route, no lex", input.len());
return Some(first);
}
if let Some(bare) = pattern.without_bindings()
&& let Some(first) = routed_first_at_reading(&bare, input, at, &mut None)
{
crate::trace::rung("routed_first_at", "an anchored route, with the bindings off", input.len());
return Some(first);
}
if let Some(first) = crate::prefilter::first_by_literal_windows_at(pattern, input, at) {
crate::trace::rung("routed_first_at", "windows from the offset", input.len());
return Some(first);
}
let whole = routed_spans_positional(pattern, input)?;
crate::trace::rung("routed_first_at", "the whole positional set, filtered", input.len());
Some(whole.into_iter().find(|s| s.start() >= at))
}
#[must_use]
pub(crate) fn routed_spans_at(pattern: &Pattern, input: &[u8], at: usize) -> Option<Vec<Span>> {
if crate::prefilter::requires_absent(pattern, input) {
crate::trace::rung("routed_spans_at", "a literal the input does not hold", input.len());
return Some(Vec::new());
}
let spans = crate::prefilter::scan_by_literal_windows_at(pattern, input, at)?;
crate::trace::rung("routed_spans_at", "windows from the offset", input.len());
Some(spans)
}
pub(crate) fn routed_first_at_reading<'i>(
pattern: &Pattern,
input: &'i [u8],
at: usize,
reader: &mut Option<crate::prefilter::ByteReader<'i>>,
) -> Option<Option<Span>> {
if let Some(lits) = crate::prefilter::byte_routable_literals(pattern) {
let held = reader.get_or_insert_with(|| crate::prefilter::ByteReader::new(input));
if let Some(first) =
crate::prefilter::byte_route_first_word_literal_reading(&lits, held, at)
{
return Some(first);
}
}
if let Some(lit) = crate::prefilter::byte_routable_line_anchored_literal(pattern) {
let held = reader.get_or_insert_with(|| crate::prefilter::ByteReader::new(input));
if let Some(first) =
crate::prefilter::byte_route_first_line_anchored_literal_reading(lit, held, at)
{
return Some(first);
}
}
if let Some(punct) = crate::prefilter::byte_routable_word_then_punct(pattern) {
let held = reader.get_or_insert_with(|| crate::prefilter::ByteReader::new(input));
if let Some(first) =
crate::prefilter::byte_route_first_word_then_punct_reading(punct, held, at)
{
return Some(first);
}
}
if let Some((bp, prefix)) = crate::prefilter::byte_routable_byte_pattern(pattern) {
let held = reader.get_or_insert_with(|| crate::prefilter::ByteReader::new(input));
if let Some(first) =
crate::prefilter::byte_route_first_byte_pattern_reading(bp, &prefix, held, at)
{
return Some(first);
}
}
crate::prefilter::byte_route_kind_reading(pattern, input, reader, at)
}
fn routed_first_early(pattern: &Pattern, input: &[u8]) -> Option<Option<Span>> {
if crate::prefilter::requires_absent(pattern, input) {
return Some(None);
}
if let Some(bare) = pattern.without_bindings()
&& let Some(first) = routed_first_early(&bare, input)
{
crate::trace::rung("routed_first", "a route, with the bindings off", input.len());
return Some(first);
}
if let Some(lits) = crate::prefilter::byte_routable_literals(pattern)
&& let Some(first) = crate::prefilter::byte_route_first_word_literal(&lits, input)
{
return Some(first);
}
if let Some(lit) = crate::prefilter::byte_routable_line_anchored_literal(pattern)
&& let Some(first) = crate::prefilter::byte_route_first_line_anchored_literal(lit, input, 0)
{
return Some(first);
}
if let Some(punct) = crate::prefilter::byte_routable_word_then_punct(pattern)
&& let Some(first) = crate::prefilter::byte_route_first_word_then_punct(punct, input)
{
return Some(first);
}
if let Some((bp, prefix)) = crate::prefilter::byte_routable_byte_pattern(pattern)
&& let Some(first) = crate::prefilter::byte_route_first_byte_pattern(bp, &prefix, input)
{
return Some(first);
}
if let Some(first) = crate::prefilter::byte_route_kind_from(pattern, input, 0) {
return Some(first);
}
None
}
#[doc(hidden)]
#[must_use]
pub fn routed_spans_public(pattern: &Pattern, input: &[u8]) -> Option<Vec<Span>> {
routed_spans(pattern, input)
}
pub(crate) fn routed_spans_positional(pattern: &Pattern, input: &[u8]) -> Option<Vec<Span>> {
if crate::prefilter::requires_absent(pattern, input) {
return Some(Vec::new());
}
if let Some(lits) = crate::prefilter::byte_routable_literals(pattern)
&& let Some(spans) = crate::prefilter::byte_route_word_literals(&lits, input)
{
crate::trace::rung("scan", "a word literal route, no lex", input.len());
return Some(spans);
}
if let Some(lit) = crate::prefilter::byte_routable_line_anchored_literal(pattern)
&& let Some(spans) = crate::prefilter::byte_route_line_anchored_literal(lit, input)
{
crate::trace::rung("scan", "a line-anchored literal route, no lex", input.len());
return Some(spans);
}
if let Some(punct) = crate::prefilter::byte_routable_word_then_punct(pattern)
&& let Some(spans) = crate::prefilter::byte_route_word_then_punct(punct, input)
{
crate::trace::rung("scan", "a word-then-punctuation route, no lex", input.len());
return Some(spans);
}
if let Some((bp, prefix)) = crate::prefilter::byte_routable_byte_pattern(pattern)
&& let Some(spans) = crate::prefilter::byte_route_byte_pattern(bp, &prefix, input)
{
crate::trace::rung("scan", "a byte-pattern route, no lex", input.len());
return Some(spans);
}
None
}
pub(crate) fn routed_spans_selective(pattern: &Pattern, input: &[u8]) -> Option<Vec<Span>> {
let literal_word_punct = || {
let (lit, punct) = crate::prefilter::byte_routable_literal_word_punct(pattern)?;
let spans = crate::prefilter::byte_route_literal_word_punct(lit, punct, input)?;
crate::trace::rung("scan", "a literal, word, punctuation route, no lex", input.len());
Some(spans)
};
literal_word_punct()
.or_else(|| {
let spans = crate::prefilter::scan_required_windows(pattern, input)?;
crate::trace::rung("scan", "the windows a literal opens", input.len());
Some(spans)
})
.or_else(|| routed_spans_kind(pattern, input))
.or_else(|| routed_spans_balanced(pattern, input))
}
fn routed_spans_balanced(pattern: &Pattern, input: &[u8]) -> Option<Vec<Span>> {
let kind = crate::kind_route::bare_balanced(pattern)?;
Some(crate::parallel_lex::lex_paired_parts_held(input, |parts| {
crate::kind_route::scan_balanced_parts(kind, parts)
}))
}
fn routed_spans_kind(pattern: &Pattern, input: &[u8]) -> Option<Vec<Span>> {
if let Some(codes) = crate::kind_route::kind_sequence(pattern) {
return Some(crate::parallel_lex::lex_significant_parts_held(input, |parts| {
crate::kind_route::scan_kind_sequence(&codes, parts)
}));
}
let (code, lo, hi) = crate::kind_route::kind_run(pattern)?;
Some(crate::parallel_lex::lex_significant_parts_held(input, |parts| {
crate::kind_route::scan_kind_run(code, lo, hi, parts)
}))
}
#[must_use]
pub fn scan_with_empty_loop(pattern: &Pattern, input: &[u8], empty: EmptyLoop) -> Vec<Span> {
if empty == EmptyLoop::Perl && crate::nfa::empty_loop_needs_set_engine(pattern) {
return scan_set_reachability(pattern, input);
}
scan(pattern, input)
}
#[must_use]
pub fn scan_with_shapes(
pattern: &Pattern,
input: &[u8],
shapes: &crate::custom::ShapeSet,
) -> Vec<Span> {
let shapes = shapes.with_library_shapes(&pattern.library_kinds());
if shapes.is_empty() {
return scan(pattern, input);
}
crate::trace::rung("scan", "the engines over a lex under the declared shapes", input.len());
let blobs = crate::lexer::blob_runs(input);
let toks = crate::lexer::lex_with_shapes(input, &blobs, &shapes, 0);
if let Some(matches) = crate::nfa::scan_nfa_over(pattern, input, &toks) {
return matches;
}
scan_tokens_from(pattern, input, &toks, 0)
}
#[must_use]
pub(crate) fn scan_with_shapes_from(
pattern: &Pattern,
input: &[u8],
shapes: &crate::custom::ShapeSet,
at: usize,
) -> Vec<Span> {
let preparing = crate::trace::phase("the resumed scan: its shapes and blob runs");
let shapes = shapes.with_library_shapes(&pattern.library_kinds());
drop(preparing);
if shapes.is_empty()
&& let Some(spans) = routed_spans_at(pattern, input, at)
{
return spans;
}
let tabling = crate::trace::phase("the resumed scan: its blob runs");
let blobs = crate::lexer::blob_runs(input);
drop(tabling);
let lexing = crate::trace::phase("the resumed scan: its own lex");
let toks = crate::lexer::lex_with_shapes(input, &blobs, &shapes, 0);
drop(lexing);
crate::trace::counted(
"the resumed scan: bytes it lexes",
u64::try_from(input.len()).expect("an input within the counter's width"),
);
scan_over_tokens_from(pattern, input, &toks, at)
}
#[must_use]
pub(crate) fn scan_over_tokens_from(
pattern: &Pattern,
input: &[u8],
toks: &[Token],
at: usize,
) -> Vec<Span> {
let walking = crate::trace::phase("the resumed scan: its walk from the anchor");
let start = toks.partition_point(|t| t.start() < at);
let found = match crate::nfa::scan_nfa_over_serial_from(pattern, input, toks, start) {
Some(matches) => matches,
None => scan_tokens_from(pattern, input, toks, start),
};
drop(walking);
found
}
#[must_use]
pub fn captures(pattern: &Pattern, input: &[u8], spans: &[Span]) -> Vec<Match> {
if !pattern.binds() {
crate::trace::rung("captures", "nothing bound, the spans back", input.len());
return spans.iter().copied().map(Match::from).collect();
}
if let Some(shapes) = crate::library::shapes_for(pattern) {
return captures_with_shapes(pattern, input, &shapes, spans);
}
if let [span] = spans
&& let Some(m) = captures_in_region(pattern, input, *span)
{
crate::trace::rung("captures", "one span, over its own region", input.len());
return vec![m];
}
if let Some((ms, names)) = crate::prefilter::flat_captures_by_byte_bounds(pattern, input, spans) {
crate::trace::rung("captures", "the registers off each match's own bytes", input.len());
return ms
.into_iter()
.map(|m| {
Match::bound(
m.span.start(),
m.span.end(),
Regs::from_slice(&m.regs[..names.len()]),
names.clone(),
)
})
.collect();
}
if let Some(ms) = crate::prefilter::captures_by_windows(pattern, input, spans) {
crate::trace::rung("captures", "a window at each span", input.len());
return ms;
}
if let Some(ms) = crate::nfa::captures_over_parts(pattern, input, spans) {
crate::trace::rung("captures", "an attempt a span, over the lexer's parts", input.len());
return ms;
}
crate::trace::rung("captures", "the set engine over a whole stitched lex", input.len());
crate::parallel_lex::lex_parallel_held(input, |toks| {
captures_routed(pattern, input, toks, spans, EmptyLoop::Thompson, false)
})
}
#[must_use]
pub fn captures_with_lists(pattern: &Pattern, input: &[u8], spans: &[Span]) -> Vec<Match> {
if !pattern.has_list_registers() {
return captures(pattern, input, spans);
}
if let Some(shapes) = crate::library::shapes_for(pattern) {
return captures_with_shapes_and_lists(pattern, input, &shapes, spans);
}
crate::trace::rung("captures", "the set engine, keeping every binding under a repetition", input.len());
crate::parallel_lex::lex_parallel_held(input, |toks| {
captures_routed(pattern, input, toks, spans, EmptyLoop::Thompson, true)
})
}
#[must_use]
pub fn captures_with_shapes_and_lists(
pattern: &Pattern,
input: &[u8],
shapes: &crate::custom::ShapeSet,
spans: &[Span],
) -> Vec<Match> {
if !pattern.has_list_registers() {
return captures_with_shapes(pattern, input, shapes, spans);
}
let shapes = shapes.with_library_shapes(&pattern.library_kinds());
if shapes.is_empty() {
return captures_with_lists(pattern, input, spans);
}
let blobs = crate::lexer::blob_runs(input);
let toks = crate::lexer::lex_with_shapes(input, &blobs, &shapes, 0);
captures_routed(pattern, input, &toks, spans, EmptyLoop::Thompson, true)
}
fn captures_in_region(pattern: &Pattern, input: &[u8], span: Span) -> Option<Match> {
match crate::prefilter::tokens_around(pattern, input, span) {
Ok(toks) => crate::nfa::captures_over(pattern, input, &toks, &[span])
.and_then(|v| v.into_iter().next()),
Err(why) => {
debug_assert!(
!matches!(why, crate::prefilter::RegionRefusal::NoRegionForm)
|| crate::nfa::bounded_max_len(pattern).is_none_or(|n| n == 0)
|| pattern.starts_with_resume()
|| pattern.mentions_reset_start()
|| pattern.mentions_stream_end_anchor(),
"a region was refused as `{why}` for a pattern that has one"
);
None
}
}
}
#[must_use]
pub fn captures_with_empty_loop(
pattern: &Pattern,
input: &[u8],
empty: EmptyLoop,
spans: &[Span],
) -> Vec<Match> {
if !pattern.binds() {
return spans.iter().copied().map(Match::from).collect();
}
crate::parallel_lex::lex_parallel_held(input, |toks| {
captures_routed(pattern, input, toks, spans, empty, false)
})
}
#[must_use]
pub fn captures_with_shapes(
pattern: &Pattern,
input: &[u8],
shapes: &crate::custom::ShapeSet,
spans: &[Span],
) -> Vec<Match> {
let shapes = shapes.with_library_shapes(&pattern.library_kinds());
if shapes.is_empty() {
return captures(pattern, input, spans);
}
if !pattern.binds() {
return spans.iter().copied().map(Match::from).collect();
}
let blobs = crate::lexer::blob_runs(input);
let toks = crate::lexer::lex_with_shapes(input, &blobs, &shapes, 0);
captures_routed(pattern, input, &toks, spans, EmptyLoop::Thompson, false)
}
#[must_use]
pub fn captures_over(pattern: &Pattern, input: &[u8], toks: &[Token], spans: &[Span]) -> Vec<Match> {
if !pattern.binds() {
return spans.iter().copied().map(Match::from).collect();
}
captures_routed(pattern, input, toks, spans, EmptyLoop::Thompson, false)
}
#[must_use]
pub fn captures_over_with_lists(
pattern: &Pattern,
input: &[u8],
toks: &[Token],
spans: &[Span],
) -> Vec<Match> {
if !pattern.has_list_registers() {
return captures_over(pattern, input, toks, spans);
}
captures_routed(pattern, input, toks, spans, EmptyLoop::Thompson, true)
}
fn captures_routed(
pattern: &Pattern,
input: &[u8],
toks: &[Token],
spans: &[Span],
empty: EmptyLoop,
keep_lists: bool,
) -> Vec<Match> {
let set_engine = (empty == EmptyLoop::Perl && crate::nfa::empty_loop_needs_set_engine(pattern))
|| (keep_lists && pattern.has_list_registers());
if !set_engine && let Some(matches) = crate::nfa::captures_over(pattern, input, toks, spans) {
return matches;
}
let n = toks.len();
let analyses = Analyses::build(pattern, input, toks);
let fields = analyses.fields();
let mut order: Vec<usize> = (0..analyses.regs.len()).collect();
order.sort_by(|&a, &b| analyses.regs[a].cmp(&analyses.regs[b]));
let names: std::sync::Arc<[String]> =
order.iter().map(|&i| analyses.regs[i].clone()).collect();
spans
.iter()
.map(|&span| {
let i = toks.partition_point(|t| t.start < span.start);
let (env, history) = (i < n)
.then(|| {
best_at(pattern, input, toks, n, i, &analyses.absent, &analyses.regs, fields, &mut 0, |best| {
(best.pos, best.env.clone(), best.history())
})
})
.flatten()
.filter(|&(end, _, _)| toks[end - 1].end == span.end)
.map(|(_, env, history)| (env, history))
.expect("a span a scan of this pattern returned is reproduced by the attempt at its start");
let captures = order
.iter()
.map(|&id| {
env.iter()
.find(|entry| *entry.0 as usize == id)
.map_or(Span { start: 0, end: 0 }, |entry| Span {
start: entry.1.0 as u32,
end: entry.1.1 as u32,
})
})
.collect();
if !keep_lists {
return Match::bound(span.start(), span.end(), captures, names.clone());
}
let mut lists: Bindings = analyses
.lists
.iter()
.filter_map(|&id| order.iter().position(|&o| o == usize::from(id)).map(|at| (at, Vec::new())))
.collect();
for (id, (s, e)) in history {
if let Some(at) = order.iter().position(|&o| o == usize::from(id))
&& let Some(slot) = lists.iter_mut().find(|(k, _)| *k == at)
{
slot.1.push(Span { start: s as u32, end: e as u32 });
}
}
Match::bound(span.start(), span.end(), captures, names.clone()).with_lists(lists)
})
.collect()
}
struct Analyses {
absent: HashSet<Vec<u8>>,
clock: crate::typed::Clock,
regs: Vec<String>,
spectral: Option<SpectralField>,
seam_cuts: Option<Vec<usize>>,
depths: Option<Vec<u16>>,
obs_contested: Option<Vec<usize>>,
echo: Vec<(crate::orbit::OrbitGroup, crate::echo::EchoField)>,
supers: Option<crate::supertoken::SuperContext>,
order: Option<Vec<Option<bool>>>,
templates: Option<crate::templates::Mining>,
joins: Vec<Join>,
seam_token: Option<Vec<usize>>,
seam_super: Option<Vec<usize>>,
obs_token: Option<Vec<usize>>,
obs_super: Option<Vec<usize>>,
gravity: [Option<crate::gravity::Readings>; 3],
kin: Vec<(crate::ast::Grain, String, Option<u32>)>,
bands: Option<Bands>,
field_starts: Option<Vec<u32>>,
context: ContextBuild,
lists: Vec<u16>,
}
struct Bands {
live: Vec<u16>,
sig_index: Vec<u32>,
}
impl Bands {
fn build(toks: &[Token], input: &[u8]) -> Bands {
let mut next = 0u32;
let sig_index = toks
.iter()
.map(|t| {
if t.is_significant() {
next += 1;
next - 1
} else {
u32::MAX
}
})
.collect();
Bands { live: crate::context::live_periods(toks, input), sig_index }
}
fn at(&self, p: usize, k: u16, period: crate::ast::PeriodRef) -> bool {
let named = match period {
crate::ast::PeriodRef::Length(len) => self.live.contains(&len).then_some(len),
crate::ast::PeriodRef::Rank(n) => usize::from(n).checked_sub(1).and_then(|i| self.live.get(i).copied()),
};
match (named, self.sig_index.get(p)) {
(Some(len), Some(&i)) if i != u32::MAX => i % u32::from(len) == u32::from(k),
(Some(_) | None, Some(_) | None) => false,
}
}
}
fn grain_slot(grain: crate::ast::Grain) -> usize {
match grain {
crate::ast::Grain::Byte => 0,
crate::ast::Grain::Token => 1,
crate::ast::Grain::Super => 2,
}
}
impl Analyses {
fn build(pattern: &Pattern, input: &[u8], toks: &[Token]) -> Self {
let absent = crate::prefilter::absent_guard_literals(pattern, input);
let regs = collect_registers(pattern);
let lists: Vec<u16> =
pattern.list_registers().iter().filter_map(|name| reg_id(®s, name)).collect();
let spectral = pattern.has_spectral().then(|| {
let _building = crate::trace::phase("the spectral field");
crate::spectral::analyze_needing(input, &Default::default(), pattern.spectral_needs())
});
let seam_cuts = pattern.has_seam().then(|| {
let _building = crate::trace::phase("the seam cuts");
crate::seam::analyze(input).strong_cuts()
});
let depths = pattern.has_stress().then(|| {
let _building = crate::trace::phase("the nesting depths");
crate::stress::depths(toks)
});
let obs_contested = pattern.has_observation().then(|| {
let _building = crate::trace::phase("the contested points");
crate::observation::analyze(input).contested
});
let echo: Vec<(crate::orbit::OrbitGroup, crate::echo::EchoField)> = {
let _building = crate::trace::phase("the recurrence fields");
pattern
.echo_orbits()
.into_iter()
.map(|g| {
let cfg = crate::echo::EchoConfig { orbit: g, ..Default::default() };
(g, crate::echo::analyze_with(toks, input, &cfg))
})
.collect()
};
let supers = pattern.has_super().then(|| {
let _building = crate::trace::phase("the supertoken tower");
crate::supertoken::SuperContext::build(toks, input)
});
let clock = crate::typed::Clock::current();
let order = pattern.has_order().then(|| {
let _building = crate::trace::phase("the timestamp order");
timestamp_order(toks, input, clock)
});
let templates = pattern.has_rare().then(|| {
let _building = crate::trace::phase("the mined templates");
crate::templates::Mining::mine_tokens(toks, input)
});
let joins: Vec<Join> = {
let _building = crate::trace::phase("the joins, one a named input");
pattern
.joins()
.into_iter()
.map(|(other, group)| Join::build(other, group, toks, input))
.collect()
};
let obs_cfg = crate::observation::ObservationConfig::default();
let seam_cfg = crate::seam::SeamConfig::default();
let seam_token = pattern.has_seam_at(crate::ast::Grain::Token).then(|| {
let _building = crate::trace::phase("the seam cuts, over the tokens");
crate::seam::analyze_tokens(toks, &seam_cfg)
});
let seam_super = pattern.has_seam_at(crate::ast::Grain::Super).then(|| {
let _building = crate::trace::phase("the seam cuts, over the supertokens");
crate::seam::analyze_supertokens(
&crate::supertoken::supertokens_from(toks, input),
&seam_cfg,
)
});
let obs_token = pattern.has_observation_at(crate::ast::Grain::Token).then(|| {
let _building = crate::trace::phase("the contested tokens");
crate::observation::contested_tokens(toks, &obs_cfg)
});
let obs_super = pattern.has_observation_at(crate::ast::Grain::Super).then(|| {
let _building = crate::trace::phase("the contested supertokens");
crate::observation::contested_supertokens(
&crate::supertoken::supertokens_from(toks, input),
&obs_cfg,
)
});
let gravity = [crate::ast::Grain::Byte, crate::ast::Grain::Token, crate::ast::Grain::Super].map(|g| {
pattern.has_gravity_at(g).then(|| {
let _building = crate::trace::phase(match g {
crate::ast::Grain::Byte => "the pair field, over the bytes",
crate::ast::Grain::Token => "the pair field, over the tokens",
crate::ast::Grain::Super => "the pair field, over the supertokens",
});
crate::gravity::Readings::read(g, input, toks)
})
});
let kin = pattern
.kin_examples()
.into_iter()
.map(|(g, x)| {
let named = gravity[grain_slot(g)]
.as_ref()
.and_then(|r| crate::gravity::example_key(g, x.as_bytes()).and_then(|k| r.type_named(&k)));
(g, x, named)
})
.collect();
let bands = pattern.has_phase_in().then(|| {
let _building = crate::trace::phase("the live periods");
Bands::build(toks, input)
});
let field_starts = pattern.has_field_anchor().then(|| {
let _building = crate::trace::phase("the field starts");
field_start_index(input, toks)
});
let context = {
let _building = crate::trace::phase("the context field");
build_context(pattern, input, toks, spectral.as_ref(), echo_at(&echo, crate::orbit::OrbitGroup::Identity))
};
Analyses {
absent,
clock,
regs,
order,
templates,
joins,
spectral,
seam_cuts,
depths,
obs_contested,
echo,
supers,
seam_token,
seam_super,
obs_token,
obs_super,
gravity,
kin,
bands,
field_starts,
context,
lists,
}
}
fn fields(&self) -> Fields<'_> {
Fields {
spectral: self.spectral.as_ref(),
seam_cuts: self.seam_cuts.as_deref(),
depths: self.depths.as_deref(),
obs_contested: self.obs_contested.as_deref(),
echo: &self.echo,
supers: self.supers.as_ref(),
order: self.order.as_deref(),
templates: self.templates.as_ref(),
joins: &self.joins,
seam_token: self.seam_token.as_deref(),
seam_super: self.seam_super.as_deref(),
obs_token: self.obs_token.as_deref(),
obs_super: self.obs_super.as_deref(),
gravity: self.gravity.each_ref().map(Option::as_ref),
kin: &self.kin,
bands: self.bands.as_ref(),
field_starts: self.field_starts.as_deref(),
context: self.context.window.as_ref(),
relation: self.context.relation.as_ref(),
clock: self.clock,
lists: &self.lists,
}
}
}
pub(crate) fn scan_set_reachability(pattern: &Pattern, input: &[u8]) -> Vec<Span> {
let _whole = crate::trace::phase("the set engine");
let lexing = crate::trace::phase("its lex, stitched");
crate::parallel_lex::lex_parallel_held(input, |toks| {
drop(lexing);
let building = crate::trace::phase("its axis fields");
let analyses = Analyses::build(pattern, input, toks);
drop(building);
let _sweeping = crate::trace::phase("its sweep over the anchors");
scan_tokens(pattern, input, toks, &analyses)
})
}
#[must_use]
pub fn scan_tokens_from(
pattern: &Pattern,
input: &[u8],
toks: &[Token],
start: usize,
) -> Vec<Span> {
let n = toks.len();
let analyses = Analyses::build(pattern, input, toks);
let fields = analyses.fields();
let results: Vec<StartMatch> = (0..n)
.map(|i| {
if i < start {
None
} else {
attempt_at(
pattern,
input,
toks,
n,
i,
&analyses.absent,
&analyses.regs,
fields,
&mut 0,
)
}
})
.collect();
let mut matches = Vec::new();
let mut s = start;
while s < n {
let s0 = skip_ws(toks, s, n);
if s0 >= n {
break;
}
if let Some(best_pos) = results[s0] {
matches.push(Span { start: toks[s0].start, end: toks[best_pos - 1].end });
s = best_pos;
} else {
s = s0 + 1;
}
}
matches
}
const PARALLEL_SCAN_THRESHOLD_COLD: usize = 24_576;
const PARALLEL_SCAN_THRESHOLD_WARM: usize = 1024;
static POOL_WARM: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
static TOKENS_SWEPT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
const TOKENS_BEFORE_WARMING: usize = 350_000;
type StartMatch = Option<usize>;
fn scan_tokens(pattern: &Pattern, input: &[u8], toks: &[Token], analyses: &Analyses) -> Vec<Span> {
let n = toks.len();
let attempting = crate::trace::phase("its sweep: an attempt a start");
let results = per_start_matches(
pattern,
input,
toks,
n,
&analyses.absent,
&analyses.regs,
analyses.fields(),
);
drop(attempting);
let _selecting = crate::trace::phase("its sweep: the leftmost selection");
let contiguous = pattern.starts_with_resume();
let mut matches = Vec::new();
let mut start = 0;
while start < n {
let s0 = skip_ws(toks, start, n);
if s0 >= n {
break;
}
if let Some(best_pos) = results[s0] {
matches.push(Span { start: toks[s0].start, end: toks[best_pos - 1].end });
start = best_pos;
} else if contiguous {
break;
} else {
start = s0 + 1;
}
}
matches
}
#[allow(clippy::too_many_arguments)]
fn per_start_matches(
pattern: &Pattern,
input: &[u8],
toks: &[Token],
n: usize,
absent: &HashSet<Vec<u8>>,
regs: &[String],
fields: Fields<'_>,
) -> Vec<StartMatch> {
let cores = std::thread::available_parallelism().map_or(1, std::num::NonZero::get);
let opens = crate::kind_route::first_kinds(pattern);
let warm = POOL_WARM.load(std::sync::atomic::Ordering::Relaxed)
|| TOKENS_SWEPT.fetch_add(n, std::sync::atomic::Ordering::Relaxed) + n
>= TOKENS_BEFORE_WARMING;
let threshold =
if warm { PARALLEL_SCAN_THRESHOLD_WARM } else { PARALLEL_SCAN_THRESHOLD_COLD };
if n < threshold || cores <= 1 {
let mut advanced = 0u64;
let got: Vec<StartMatch> = (0..n)
.map(|i| {
if worth_attempting(opens, toks[i].kind) {
attempt_at(pattern, input, toks, n, i, absent, regs, fields, &mut advanced)
} else {
None
}
})
.collect();
count_starts(opens, toks, 0, &got, advanced);
return got;
}
POOL_WARM.store(true, std::sync::atomic::Ordering::Relaxed);
use flynnel::JobPlan;
use flynnel::sched::par_iter::for_each_chunk_indexed_min_leaf;
let mut results: Vec<StartMatch> = (0..n).map(|_| None).collect();
let min_leaf = n.div_ceil(cores * 8).max(64);
let plan = JobPlan::new(0, n as u32)
.with_leaf_shape(flynnel::LeafShape::PortCompute);
for_each_chunk_indexed_min_leaf(&plan, &mut results, min_leaf, |start, slots| {
let mut advanced = 0u64;
for (j, slot) in slots.iter_mut().enumerate() {
let i = start + j;
if worth_attempting(opens, toks[i].kind) {
*slot = attempt_at(pattern, input, toks, n, i, absent, regs, fields, &mut advanced);
}
}
count_starts(opens, toks, start, slots, advanced);
});
results
}
fn worth_attempting(opens: Option<crate::kind_route::OpeningKinds>, kind: TokenKind) -> bool {
kind != TokenKind::Whitespace && opens.is_none_or(|set| set.admits(kind))
}
fn count_starts(
opens: Option<crate::kind_route::OpeningKinds>,
toks: &[Token],
start: usize,
got: &[StartMatch],
advanced: u64,
) {
if !crate::trace::keeping() {
return;
}
let (mut skipped, mut refused, mut matched) = (0u64, 0u64, 0u64);
for (j, slot) in got.iter().enumerate() {
let kind = toks[start + j].kind;
if kind == TokenKind::Whitespace {
skipped += 1;
} else if !worth_attempting(opens, kind) {
refused += 1;
}
matched += u64::from(slot.is_some());
}
let walked = u64::try_from(got.len()).expect("a leaf shorter than the counter's width");
crate::trace::counted("the sweep: starts the fan-out walks", walked);
crate::trace::counted("the sweep: starts skipped as whitespace", skipped);
crate::trace::counted("the sweep: starts the opening kind refuses", refused);
crate::trace::counted("the sweep: attempts made", walked - skipped - refused);
crate::trace::counted("the sweep: attempts whose state set survived", advanced);
crate::trace::counted("the sweep: attempts that match", matched);
report_lent();
}
#[allow(clippy::too_many_arguments)]
fn attempt_at(
pattern: &Pattern,
input: &[u8],
toks: &[Token],
n: usize,
i: usize,
absent: &HashSet<Vec<u8>>,
regs: &[String],
fields: Fields<'_>,
advanced: &mut u64,
) -> StartMatch {
best_at(pattern, input, toks, n, i, absent, regs, fields, advanced, |best| best.pos)
}
#[allow(clippy::too_many_arguments)]
fn best_at<R>(
pattern: &Pattern,
input: &[u8],
toks: &[Token],
n: usize,
i: usize,
absent: &HashSet<Vec<u8>>,
regs: &[String],
fields: Fields<'_>,
advanced: &mut u64,
read: impl FnOnce(&State) -> R,
) -> Option<R> {
if toks[i].kind == TokenKind::Whitespace {
return None;
}
let mut init = SCRATCH.with(|s| std::mem::take(&mut *s.borrow_mut()));
init.clear();
let lent = init.capacity();
init.push(State::start(i));
let mut results = advance(pattern, input, toks, n, init, absent, regs, fields);
note_lent(lent, results.capacity());
*advanced += u64::from(!results.is_empty());
let answer = results
.iter()
.min_by(|a, b| cmp_rank(a.rank_slice(), b.rank_slice()).then(b.pos.cmp(&a.pos)))
.filter(|best| best.pos > i)
.map(read);
results.clear();
SCRATCH.with(|s| *s.borrow_mut() = results);
answer
}
thread_local! {
static SCRATCH: std::cell::RefCell<Vec<State>> =
const { std::cell::RefCell::new(Vec::new()) };
static LENT: std::cell::Cell<(u64, u64, u64)> = const { std::cell::Cell::new((0, 0, 0)) };
static UNROLLED: std::cell::Cell<(u64, u64, u64)> = const { std::cell::Cell::new((0, 0, 0)) };
static RANKED: std::cell::Cell<(u64, u64, u64, u64, u64, u64)> =
const { std::cell::Cell::new((0, 0, 0, 0, 0, 0)) };
}
thread_local! {
static FIXPOINT_MAP: std::cell::Cell<(u64, u64, u64, u64, u64, u64, u64)> =
const { std::cell::Cell::new((0, 0, 0, 0, 0, 0, 0)) };
}
fn note_extended(len: usize) {
RANKED.with(|r| {
let (extended, compared, sum, over4, over16, over64) = r.get();
r.set((
extended + 1,
compared,
sum + len as u64,
over4 + u64::from(len > 4),
over16 + u64::from(len > 16),
over64 + u64::from(len > 64),
));
});
}
fn note_compared() {
RANKED.with(|r| {
let (extended, compared, sum, over4, over16, over64) = r.get();
r.set((extended, compared + 1, sum, over4, over16, over64));
});
}
fn note_lent(given: usize, back: usize) {
LENT.with(|l| {
let (attempts, sum_given, sum_back) = l.get();
l.set((attempts + 1, sum_given + given as u64, sum_back + back as u64));
});
}
fn note_fixpoint_map(entries: usize) {
FIXPOINT_MAP.with(|m| {
let (calls, sum, over4, over16, over64, over256, over1024) = m.get();
m.set((
calls + 1,
sum + entries as u64,
over4 + u64::from(entries > 4),
over16 + u64::from(entries > 16),
over64 + u64::from(entries > 64),
over256 + u64::from(entries > 256),
over1024 + u64::from(entries > 1024),
));
});
}
fn note_unrolled(grown: usize, marked: usize) {
UNROLLED.with(|u| {
let (iterations, sum_grown, sum_marked) = u.get();
u.set((iterations + 1, sum_grown + grown as u64, sum_marked + marked as u64));
});
}
fn report_lent() {
let (attempts, given, back) = LENT.with(|l| l.replace((0, 0, 0)));
if attempts == 0 {
return;
}
crate::trace::counted("the state vector: times it was borrowed", attempts);
crate::trace::counted("the state vector: slots lent out", given);
crate::trace::counted("the state vector: slots handed back", back);
let (iterations, grown, marked) = UNROLLED.with(|u| u.replace((0, 0, 0)));
if iterations == 0 {
return;
}
crate::trace::counted("the quantifier fixpoint: iterations", iterations);
crate::trace::counted("the quantifier fixpoint: slots its frontier grew to", grown);
crate::trace::counted("the quantifier fixpoint: slots handed to the body", marked);
let (calls, entries, over4, over16, over64, over256, over1024) =
FIXPOINT_MAP.with(|m| m.replace((0, 0, 0, 0, 0, 0, 0)));
if calls > 0 {
crate::trace::counted("the fixpoint's seen-map: times one was built", calls);
crate::trace::counted("the fixpoint's seen-map: entries they held", entries);
crate::trace::counted("the fixpoint's seen-map: those past four entries", over4);
crate::trace::counted("the fixpoint's seen-map: those past sixteen entries", over16);
crate::trace::counted("the fixpoint's seen-map: those past sixty-four entries", over64);
crate::trace::counted("the fixpoint's seen-map: those past two hundred and fifty-six", over256);
crate::trace::counted("the fixpoint's seen-map: those past one thousand and twenty-four", over1024);
}
let (extended, compared, sum, over4, over16, over64) =
RANKED.with(|r| r.replace((0, 0, 0, 0, 0, 0)));
if extended == 0 && compared == 0 {
return;
}
crate::trace::counted("the preference path: times one was extended", extended);
crate::trace::counted("the preference path: times two were compared", compared);
crate::trace::counted("the preference path: bytes those extensions produced", sum);
crate::trace::counted("the preference path: extensions past four bytes", over4);
crate::trace::counted("the preference path: extensions past sixteen bytes", over16);
crate::trace::counted("the preference path: extensions past sixty-four bytes", over64);
}
fn skip_ws(toks: &[Token], mut pos: usize, end: usize) -> usize {
while pos < end && toks[pos].kind == TokenKind::Whitespace {
pos += 1;
}
pos
}
#[allow(clippy::too_many_arguments)]
fn advance(
pat: &Pattern,
input: &[u8],
toks: &[Token],
end: usize,
states: Vec<State>,
absent: &HashSet<Vec<u8>>,
regs: &[String],
fields: Fields<'_>,
) -> Vec<State> {
match pat {
Pattern::Empty => states,
Pattern::Atom(a) => states
.into_iter()
.filter_map(|s| match_atom(a, input, toks, end, &s, regs, fields))
.collect(),
Pattern::Concat(parts) => {
let mut acc = states;
for p in parts {
if acc.is_empty() {
break;
}
acc = advance(p, input, toks, end, acc, absent, regs, fields);
}
acc
}
Pattern::Alt(parts, mode) => {
alt(parts, *mode, input, toks, end, states, absent, regs, fields)
}
Pattern::Opt(p, g) => {
repeat(p, (0, Some(1)), *g, input, toks, end, states, absent, regs, fields)
}
Pattern::Star(p, g) => star(p, *g, input, toks, end, states, absent, regs, fields),
Pattern::Plus(p, g) => {
let once = advance(p, input, toks, end, states, absent, regs, fields);
star(p, *g, input, toks, end, once, absent, regs, fields)
}
Pattern::Repeat(p, m, n, g) => {
repeat(p, (*m, *n), *g, input, toks, end, states, absent, regs, fields)
}
Pattern::Atomic(p) => {
if states.len() == 1 {
let mut out = advance(p, input, toks, end, states, absent, regs, fields);
match best_index(&out) {
Some(at) => {
out.swap(0, at);
out.truncate(1);
}
None => out.clear(),
}
return out;
}
let mut kept = Vec::with_capacity(states.len());
for s in states {
let out = advance(p, input, toks, end, vec![s], absent, regs, fields);
if let Some(best) =
out.into_iter().min_by(|a, b| cmp_rank(a.rank_slice(), b.rank_slice()))
{
kept.push(best);
}
}
dedup(kept)
}
Pattern::Bind(name, _, p) => bind(name, p, input, toks, end, states, absent, regs, fields),
Pattern::Balanced(kind, p) => balanced(*kind, p, input, toks, end, states, absent, regs, fields),
Pattern::Guard(lit, neg) => guard(lit, *neg, input, toks, end, states, absent),
Pattern::Assert(inner, neg, look) => {
assert_zero_width(inner, *neg, *look, input, toks, end, states, absent, regs, fields)
}
Pattern::Field(k, inner) => field_match(*k, inner, input, toks, end, states, absent, regs, fields),
Pattern::Anchor(kind) => anchor(kind, input, toks, end, states, fields),
Pattern::Within(atoms, k) => {
let mut out = Vec::new();
let mut scratch = EDITS.with(|e| std::mem::take(&mut *e.borrow_mut()));
for s in states {
within_edits_of(
atoms,
*k,
input,
toks,
end,
&s,
regs,
fields,
&mut scratch,
&mut out,
);
}
EDITS.with(|e| *e.borrow_mut() = scratch);
dedup(out)
}
}
}
#[allow(clippy::too_many_arguments)]
fn within_edits_of(
atoms: &[crate::ast::EditAtom],
k: u8,
input: &[u8],
toks: &[Token],
end: usize,
s: &State,
regs: &[String],
fields: Fields<'_>,
scratch: &mut EditScratch,
out: &mut Vec<State>,
) {
let k = usize::from(k);
let m = atoms.len();
if m == 0 {
return;
}
let run = &mut scratch.run;
run.clear();
let mut p = skip_ws(toks, s.pos, end);
while run.len() < m + k && p < end {
run.push(p);
p = skip_ws(toks, p + 1, end);
}
let n = run.len();
let past = k + 1;
let w = n + 1;
let table = &mut scratch.table;
table.clear();
table.resize((m + 1) * w, (past, Step::Start));
for (j, cell) in table.iter_mut().take(w).enumerate() {
cell.0 = if j <= k { j } else { past };
}
for i in 1..=m {
let lo = i.saturating_sub(k);
let hi = (i + k).min(n);
if lo == 0 {
table[i * w] = (i, Step::AtomDeleted);
}
for j in lo.max(1)..=hi {
let matched = atom_test(&atoms[i - 1].atom, input, toks, run[j - 1], s, regs, fields);
let diagonal = table[(i - 1) * w + j - 1].0 + usize::from(!matched);
let atom_deleted = table[(i - 1) * w + j].0 + 1;
let token_extra = table[i * w + j - 1].0 + 1;
let cost = diagonal.min(atom_deleted).min(token_extra);
if cost > k {
continue;
}
let step = if cost == diagonal {
if matched { Step::Matched } else { Step::Substituted }
} else if cost == atom_deleted {
Step::AtomDeleted
} else {
Step::TokenExtra
};
table[i * w + j] = (cost, step);
}
}
let taken = &mut scratch.taken;
taken.clear();
taken.resize(m, None);
for j in 1..=n {
let cost = table[m * w + j].0;
if cost > k {
continue;
}
taken.iter_mut().for_each(|t| *t = None);
let (mut i, mut col) = (m, j);
while i > 0 {
match table[i * w + col].1 {
Step::Matched => {
taken[i - 1] = Some(run[col - 1]);
i -= 1;
col -= 1;
}
Step::Substituted => {
i -= 1;
col -= 1;
}
Step::AtomDeleted => i -= 1,
Step::TokenExtra => col -= 1,
Step::Start => {
debug_assert!(false, "an alignment stepped through an unreached cell");
break;
}
}
}
let mut env = s.env.clone();
for (e, at) in atoms.iter().zip(taken.iter()) {
if let (Some((name, _)), Some(at)) = (&e.bind, at)
&& let Some(id) = reg_id(regs, name)
{
let span = (toks[*at].start(), toks[*at].end());
Rc::make_mut(&mut env).insert(id, span);
}
}
let mut rank = s.rank_slice().to_vec();
rank.push(u8::try_from(cost).unwrap_or(u8::MAX));
rank.push(u8::try_from(n - j).unwrap_or(u8::MAX));
out.push(State {
pos: run[j - 1] + 1,
env,
rank: Rank::of(&rank),
hist: s.hist.clone(),
});
}
}
#[derive(Clone, Copy)]
enum Step {
Start,
Matched,
Substituted,
AtomDeleted,
TokenExtra,
}
#[derive(Default)]
struct EditScratch {
run: Vec<usize>,
table: Vec<(usize, Step)>,
taken: Vec<Option<usize>>,
}
impl EditScratch {
const fn new() -> Self {
Self { run: Vec::new(), table: Vec::new(), taken: Vec::new() }
}
}
thread_local! {
static EDITS: std::cell::RefCell<EditScratch> =
const { std::cell::RefCell::new(EditScratch::new()) };
}
fn leads_its_line(input: &[u8], start: usize) -> bool {
let mut i = start;
while i > 0 {
let b = input[i - 1];
if b == b'\n' {
return true;
}
if !b.is_ascii_whitespace() {
return false;
}
i -= 1;
}
true
}
fn ends_its_line(input: &[u8], end: usize) -> bool {
let mut i = end;
while i < input.len() {
let b = input[i];
if b == b'\n' {
return true;
}
if !b.is_ascii_whitespace() {
return false;
}
i += 1;
}
true
}
pub(crate) fn positional_anchor_holds(
kind: &crate::ast::AnchorKind,
input: &[u8],
toks: &[Token],
p: usize,
) -> bool {
let t = &toks[p];
anchor_holds_at(
kind,
input,
t.start(),
t.end(),
!toks[..p].iter().any(Token::is_significant),
!toks[p + 1..].iter().any(Token::is_significant),
)
}
pub(crate) fn anchor_holds_at(
kind: &crate::ast::AnchorKind,
input: &[u8],
start: usize,
end: usize,
first: bool,
last: bool,
) -> bool {
use crate::ast::AnchorKind;
match kind {
AnchorKind::LineStart => leads_its_line(input, start),
AnchorKind::LineEnd => ends_its_line(input, end),
AnchorKind::InputStart => first,
AnchorKind::InputEnd => last,
AnchorKind::Resume => true,
AnchorKind::ResetStart => true,
_ => false,
}
}
fn gravity_holds(
r: &crate::gravity::Readings,
reading: crate::ast::GravityReading,
cmp: crate::ast::Cmp,
level: crate::ast::Level,
start: usize,
) -> bool {
use crate::ast::{GravityReading, Level};
let unit = match reading {
GravityReading::Strain => r.unit_at(start),
GravityReading::Bound => r.unit_starting_at(start),
};
let Some(u) = unit else { return false };
let value = match reading {
GravityReading::Strain => r.strain[u],
GravityReading::Bound => r.binding[u],
};
let bar = match (level, reading) {
(Level::Bits(v), _) => Some(v.get()),
(Level::Percentile(q), GravityReading::Strain) => r.strain_percentile(q.get()).map(f64::from),
(Level::Percentile(q), GravityReading::Bound) => r.binding_percentile(q.get()).map(f64::from),
};
bar.is_some_and(|b| cmp.holds(f64::from(value).total_cmp(&b)))
}
fn anchor(
kind: &crate::ast::AnchorKind,
input: &[u8],
toks: &[Token],
end: usize,
states: Vec<State>,
fields: Fields<'_>,
) -> Vec<State> {
use crate::ast::AnchorKind;
states
.into_iter()
.filter(|s| {
let p = skip_ws(toks, s.pos, end);
if p >= end {
return false;
}
match kind {
AnchorKind::LineStart
| AnchorKind::LineEnd
| AnchorKind::InputStart
| AnchorKind::InputEnd
| AnchorKind::Resume => positional_anchor_holds(kind, input, toks, p),
AnchorKind::Seam(grain) => {
let cuts = match grain {
crate::ast::Grain::Byte => fields.seam_cuts,
crate::ast::Grain::Token => fields.seam_token,
crate::ast::Grain::Super => fields.seam_super,
};
cuts.is_some_and(|c| c.binary_search(&toks[p].start()).is_ok())
}
AnchorKind::Gravity(reading, grain, cmp, level) => fields.gravity[grain_slot(*grain)]
.is_some_and(|r| gravity_holds(r, *reading, *cmp, *level, toks[p].start())),
AnchorKind::Kin(grain, example) => fields.gravity[grain_slot(*grain)].is_some_and(|r| {
let named = fields
.kin
.iter()
.find(|(g, x, _)| g == grain && x == example)
.and_then(|(_, _, t)| *t);
match (named, r.unit_at(toks[p].start())) {
(Some(want), Some(unit)) => r.kin(r.type_of(unit), want),
(None, _) | (_, None) => false,
}
}),
AnchorKind::Nested(min) => fields.depths.is_some_and(|d| d[p] >= *min),
AnchorKind::Ambiguous(grain) => {
let contested = match grain {
crate::ast::Grain::Byte => fields.obs_contested,
crate::ast::Grain::Token => fields.obs_token,
crate::ast::Grain::Super => fields.obs_super,
};
contested.is_some_and(|c| {
let i = c.partition_point(|&x| x < toks[p].start());
c.get(i).is_some_and(|&x| x < toks[p].end())
})
}
AnchorKind::Novel => echo_at(fields.echo, crate::orbit::OrbitGroup::Identity)
.is_some_and(|f| f.frames.get(p).is_some_and(crate::echo::EchoFrame::novel)),
AnchorKind::Echoed => echo_at(fields.echo, crate::orbit::OrbitGroup::Identity)
.is_some_and(|f| f.frames.get(p).is_some_and(crate::echo::EchoFrame::echoed)),
AnchorKind::Echo(pred, group) => echo_at(fields.echo, *group)
.and_then(|f| f.frames.get(p))
.is_some_and(|fr| echo_pred_holds(pred, fr)),
AnchorKind::Order(want) => fields
.order
.and_then(|o| o.get(p).copied())
.flatten()
.is_some_and(|forward| forward == (*want == crate::ast::TimeOrder::Asc)),
AnchorKind::Rare(cut) => {
fields.templates.is_some_and(|m| m.token_is_rare(p, *cut))
}
AnchorKind::Joined { other, recurs, group } => fields
.joins
.iter()
.find(|j| std::sync::Arc::ptr_eq(&j.other, other) && j.group == *group)
.and_then(|j| j.recurs.get(p).copied().flatten())
.is_some_and(|in_other| in_other == *recurs),
AnchorKind::ResetStart => true,
AnchorKind::SuperStart => fields.supers.is_some_and(|s| s.starts_unit(p)),
AnchorKind::SuperRole(want) => {
fields.supers.is_some_and(|s| s.unit_of(p).is_some_and(|u| u.role == *want))
}
AnchorKind::Phase(k) => {
fields.relation.is_some_and(|r| r.phase_of.get(p) == Some(k))
}
AnchorKind::PhaseIn(k, period) => fields.bands.is_some_and(|b| b.at(p, *k, *period)),
}
})
.collect()
}
fn match_atom(
a: &Atom,
input: &[u8],
toks: &[Token],
end: usize,
s: &State,
regs: &[String],
fields: Fields<'_>,
) -> Option<State> {
let wants_ws = matches!(
a,
Atom::Kind(TokenKind::Whitespace) | Atom::Byte(crate::ast::ByteClass::Space)
);
let p = if wants_ws { s.pos } else { skip_ws(toks, s.pos, end) };
if p >= end {
return None;
}
let ok = atom_test(a, input, toks, p, s, regs, fields);
if ok {
Some(State { pos: p + 1, env: s.env.clone(), rank: s.rank.clone(), hist: s.hist.clone() })
} else {
None
}
}
fn atom_test(
a: &Atom,
input: &[u8],
toks: &[Token],
p: usize,
s: &State,
regs: &[String],
fields: Fields<'_>,
) -> bool {
let t = &toks[p];
let txt = text(input, t);
match a {
Atom::Kind(k) => t.kind == *k,
Atom::Any => true,
Atom::Literal(lit, group) => register_eq_matches(lit.as_bytes(), txt, *group),
Atom::RegisterEq(name, group) => reg_id(regs, name)
.and_then(|id| s.env.get(&id))
.is_some_and(|&(a, b)| register_eq_matches(&input[a..b], txt, *group)),
Atom::RegisterKin(name, grain) => reg_id(regs, name).and_then(|id| s.env.get(&id)).is_some_and(|&(a, _)| {
fields.gravity[grain_slot(*grain)].is_some_and(|r| match (r.unit_at(a), r.unit_at(t.start())) {
(Some(bound), Some(here)) => r.kin(r.type_of(bound), r.type_of(here)),
(None, _) | (_, None) => false,
})
}),
Atom::RegisterRelated(name, relation) => reg_id(regs, name)
.and_then(|id| s.env.get(&id))
.is_some_and(|&(a, b)| relation.related(&input[a..b], txt)),
Atom::LiteralWithin(lit, k, group) => within_edits(lit.as_bytes(), txt, *k, *group),
Atom::RegisterWithin(name, k, group) => reg_id(regs, name)
.and_then(|id| s.env.get(&id))
.is_some_and(|&(a, b)| within_edits(&input[a..b], txt, *k, *group)),
Atom::Byte(bc) => byte_class_matches(*bc, txt),
Atom::BytePattern(bp) => bp.matches_whole(txt),
Atom::Spectral(pred) => spectral_pred_matches(pred, fields.spectral, t.start(), t.end()),
Atom::Magnitude(pred) => magnitude_matches(pred, toks, p, txt, s, regs, fields),
Atom::KindMag(k, pred) => {
t.kind == *k && magnitude_matches(pred, toks, p, txt, s, regs, fields)
}
Atom::KindPred(k, pred) => t.kind == *k && pred.matches_as(t.kind, txt, || fields.clock),
Atom::Since(op, signed, name) => {
t.kind == crate::token::TokenKind::Timestamp
&& reg_id(regs, name).and_then(|id| s.env.get(&id)).is_some_and(|&(a, b)| {
crate::typed::since_holds(*op, signed, &input[a..b], txt, fields.clock)
})
}
Atom::Class(c) => {
let hit = c.any.iter().any(|m| atom_test(m, input, toks, p, s, regs, fields))
&& (c.all.is_empty()
|| c.all.iter().any(|m| atom_test(m, input, toks, p, s, regs, fields)))
&& !c.none.iter().any(|m| atom_test(m, input, toks, p, s, regs, fields));
hit != c.negated
}
}
}
fn magnitude_matches(
pred: &crate::ast::MagPred,
toks: &[Token],
p: usize,
txt: &[u8],
s: &State,
regs: &[String],
fields: Fields<'_>,
) -> bool {
let mag = crate::magnitude::token_magnitude(toks[p].kind, txt);
let context = pred.scope().and_then(|scope| magnitude_context(scope, toks, p, s, regs, fields));
pred.matches(mag, context)
}
fn magnitude_context<'a>(
scope: &crate::ast::Scope,
toks: &[Token],
p: usize,
s: &State,
regs: &[String],
fields: Fields<'a>,
) -> Option<&'a crate::profile::MagnitudeProfile> {
use crate::ast::Scope;
match scope {
Scope::Window => {
let field = fields.context?;
let prev = (0..p).rev().find(|&j| toks[j].is_significant())?;
Some(&field.at_token.get(prev)?.magnitude)
}
Scope::Phase => Some(&fields.relation?.at_token.get(p)?.phase.magnitude),
Scope::Regime => Some(&fields.relation?.at_token.get(p)?.regime.magnitude),
Scope::Echo => Some(&fields.relation?.at_token.get(p)?.echoing.magnitude),
Scope::Enclosing => Some(&fields.relation?.at_token.get(p)?.enclosing.magnitude),
Scope::Key(name) => {
let field = fields.relation?;
let id = reg_id(regs, name)?;
let &(start, _) = s.env.get(&id)?;
let key = toks.partition_point(|t| t.start() < start);
Some(&field.value_history.get(key)?.magnitude)
}
}
}
pub(crate) struct Join {
other: std::sync::Arc<crate::ast::OtherInput>,
group: crate::orbit::OrbitGroup,
recurs: Vec<Option<bool>>,
}
impl Join {
fn build(
other: std::sync::Arc<crate::ast::OtherInput>,
group: crate::orbit::OrbitGroup,
toks: &[Token],
input: &[u8],
) -> Join {
let keys: std::collections::HashSet<String> = other
.tokens
.iter()
.filter(|t| crate::echo::keyed_kind(t.kind))
.map(|t| crate::orbit::canonical(&other.bytes[t.span()], group))
.collect();
let recurs = toks
.iter()
.map(|t| {
crate::echo::keyed_kind(t.kind)
.then(|| keys.contains(&crate::orbit::canonical(&input[t.span()], group)))
})
.collect();
Join { other, group, recurs }
}
}
pub(crate) fn timestamp_order(
toks: &[Token],
input: &[u8],
clock: crate::typed::Clock,
) -> Vec<Option<bool>> {
let mut out = vec![None; toks.len()];
let mut prev: Option<(i64, u32)> = None;
for (i, t) in toks.iter().enumerate() {
if t.kind != crate::token::TokenKind::Timestamp {
continue;
}
let text = String::from_utf8_lossy(&input[t.span()]);
let Some(here) = crate::typed::parse_civil(&text).and_then(|c| c.epoch(clock)) else {
continue;
};
if let Some(before) = prev {
out[i] = Some(here >= before);
}
prev = Some(here);
}
out
}
fn echo_at(
fields: &[(crate::orbit::OrbitGroup, crate::echo::EchoField)],
group: crate::orbit::OrbitGroup,
) -> Option<&crate::echo::EchoField> {
fields.iter().find(|(g, _)| *g == group).map(|(_, f)| f)
}
fn echo_pred_holds(pred: &crate::ast::EchoPred, fr: &crate::echo::EchoFrame) -> bool {
use crate::ast::EchoPred;
if !fr.keyed {
return false;
}
match pred {
EchoPred::Count(op, k) => op.holds(fr.count.cmp(k)),
EchoPred::Nth(op, k) => {
let want = if *k < 0 {
let from_end = k.unsigned_abs();
if from_end > fr.count {
return false;
}
fr.count - from_end + 1
} else {
k.unsigned_abs()
};
op.holds(fr.nth.cmp(&want))
}
EchoPred::Period => fr.period > 0.0,
EchoPred::PeriodAt(op, k) => {
fr.period > 0.0 && op.holds((fr.period.round() as i64).cmp(&i64::from(*k)))
}
}
}
fn spectral_pred_matches(
pred: &crate::ast::SpectralPred,
field: Option<&SpectralField>,
start: usize,
end: usize,
) -> bool {
use crate::ast::{SpecTexture, SpectralPred};
use crate::spectral::Texture;
let Some(f) = field else { return false };
f.assert_carries(crate::spectral::Needs::of(pred), "a spectral atom");
match pred {
SpectralPred::Onset => f.boundary_in(start, end),
SpectralPred::EntropyGe(p) => f.signature(start, end).entropy * 100.0 >= f32::from(*p),
SpectralPred::EntropyLe(p) => f.signature(start, end).entropy * 100.0 <= f32::from(*p),
SpectralPred::PeriodEq(n) => f.signature(start, end).period == *n,
SpectralPred::PeriodAny => f.signature(start, end).period != 0,
SpectralPred::Texture(want) => matches!(
(want, crate::spectral::texture_of(&f.signature(start, end))),
(SpecTexture::Prose, Texture::Prose)
| (SpecTexture::Code, Texture::Code)
| (SpecTexture::Math, Texture::Math)
| (SpecTexture::Data, Texture::Data)
),
}
}
pub(crate) fn register_eq_matches(bound: &[u8], txt: &[u8], group: crate::orbit::OrbitGroup) -> bool {
use crate::orbit::{OrbitGroup, canonical};
match group {
OrbitGroup::Identity => bound == txt,
g => canonical(bound, g) == canonical(txt, g),
}
}
pub(crate) fn within_edits(bound: &[u8], txt: &[u8], k: u8, group: crate::orbit::OrbitGroup) -> bool {
use crate::orbit::{OrbitGroup, canonical};
match group {
OrbitGroup::Identity => crate::edit::within(bound, txt, k),
g => crate::edit::within(canonical(bound, g).as_bytes(), canonical(txt, g).as_bytes(), k),
}
}
pub(crate) fn byte_class_matches(bc: ByteClass, txt: &[u8]) -> bool {
if txt.is_empty() {
return false;
}
match bc {
ByteClass::Digit => txt.iter().all(u8::is_ascii_digit),
ByteClass::Word => txt.iter().all(|b| *b == b'_' || b.is_ascii_alphanumeric()),
ByteClass::Space => txt.iter().all(u8::is_ascii_whitespace),
ByteClass::Hex => txt.iter().all(u8::is_ascii_hexdigit),
ByteClass::Alpha => txt.iter().all(u8::is_ascii_alphabetic),
ByteClass::Upper => txt.iter().all(u8::is_ascii_uppercase),
ByteClass::Lower => txt.iter().all(u8::is_ascii_lowercase),
}
}
#[allow(clippy::too_many_arguments)]
fn alt(
parts: &[Pattern],
mode: crate::ast::AltMode,
input: &[u8],
toks: &[Token],
end: usize,
mut states: Vec<State>,
absent: &HashSet<Vec<u8>>,
regs: &[String],
fields: Fields<'_>,
) -> Vec<State> {
use crate::ast::AltMode;
let last = parts.len().saturating_sub(1);
match mode {
AltMode::Committed => {
for (i, p) in parts.iter().enumerate() {
let taken =
if i == last { std::mem::take(&mut states) } else { states.clone() };
let out = advance(p, input, toks, end, taken, absent, regs, fields);
if !out.is_empty() {
return dedup(out);
}
}
Vec::new()
}
AltMode::Longest => {
let mut out = Vec::new();
for (i, p) in parts.iter().enumerate() {
let taken =
if i == last { std::mem::take(&mut states) } else { states.clone() };
out.extend(advance(p, input, toks, end, taken, absent, regs, fields));
}
dedup(out)
}
AltMode::First => {
let mut out = Vec::new();
for (i, p) in parts.iter().enumerate() {
let branch = u8::try_from(i).unwrap_or(u8::MAX);
let tagged: Vec<State> = states.iter().map(|s| s.with_branch(branch)).collect();
out.extend(advance(p, input, toks, end, tagged, absent, regs, fields));
}
dedup(out)
}
}
}
#[allow(clippy::too_many_arguments)]
fn star(
p: &Pattern,
g: Greed,
input: &[u8],
toks: &[Token],
end: usize,
states: Vec<State>,
absent: &HashSet<Vec<u8>>,
regs: &[String],
fields: Fields<'_>,
) -> Vec<State> {
unroll(p, g, None, input, toks, end, states, absent, regs, fields)
}
const SEEN_LINEAR: usize = 16;
struct Seen {
few: Vec<(State, usize, Rank)>,
many: Option<std::collections::HashMap<State, (usize, Rank), crate::fxhash::FxBuild>>,
}
impl Seen {
fn get(&self, s: &State) -> Option<(usize, Rank)> {
match &self.many {
Some(m) => m.get(s).map(|(at, rank)| (*at, rank.clone())),
None => self
.few
.iter()
.find(|(held, _, _)| held == s)
.map(|(_, at, rank)| (*at, rank.clone())),
}
}
fn insert(&mut self, s: State, at: usize, rank: Rank) {
if let Some(m) = &mut self.many {
m.insert(s, (at, rank));
return;
}
if let Some(slot) = self.few.iter_mut().find(|(held, _, _)| *held == s) {
slot.1 = at;
slot.2 = rank;
return;
}
self.few.push((s, at, rank));
if self.few.len() > SEEN_LINEAR {
let mut m = std::collections::HashMap::with_capacity_and_hasher(
self.few.len() * 2,
crate::fxhash::FxBuild::process(),
);
for (state, at, rank) in self.few.drain(..) {
m.insert(state, (at, rank));
}
self.many = Some(m);
}
}
fn held(&self) -> usize {
match &self.many {
Some(m) => m.len(),
None => self.few.len(),
}
}
}
thread_local! {
static SEEN: std::cell::RefCell<Vec<(State, usize, Rank)>> =
const { std::cell::RefCell::new(Vec::new()) };
}
#[allow(clippy::too_many_arguments)]
fn unroll(
p: &Pattern,
g: Greed,
bound: Option<usize>,
input: &[u8],
toks: &[Token],
end: usize,
states: Vec<State>,
absent: &HashSet<Vec<u8>>,
regs: &[String],
fields: Fields<'_>,
) -> Vec<State> {
let choiceful = p.has_choice();
let (cont, stop) = match g {
Greed::Greedy => (0u8, 1u8),
Greed::Lazy => (1u8, 0u8),
};
let mut best =
Seen { few: SEEN.with(|s| std::mem::take(&mut *s.borrow_mut())), many: None };
let mut result: Vec<State> = Vec::new();
let mut frontier: Vec<State> = states;
let mut iters: usize = 0;
let mut fresh: Vec<State> = Vec::new();
loop {
fresh.clear();
for s in frontier.drain(..) {
let entry = if choiceful {
s.with_branch(stop)
} else {
s.with_branch(count_key(g, iters))
};
let Some((i, held)) = best.get(&s) else {
best.insert(s.clone(), result.len(), s.rank.clone());
result.push(entry);
fresh.push(s);
continue;
};
if cmp_rank(entry.rank_slice(), result[i].rank_slice()) == std::cmp::Ordering::Less {
result[i] = entry;
}
let held = held.as_slice();
if cmp_rank(s.rank_slice(), held) == std::cmp::Ordering::Less {
best.insert(s.clone(), i, s.rank.clone());
fresh.push(s);
}
}
if fresh.is_empty() || bound.is_some_and(|nn| iters >= nn) {
note_fixpoint_map(best.held());
let mut few = best.few;
few.clear();
SEEN.with(|s| *s.borrow_mut() = few);
return result;
}
let grown = fresh.capacity();
let marked: Vec<State> = if choiceful {
fresh.iter().map(|s| s.with_branch(cont)).collect()
} else {
std::mem::take(&mut fresh)
};
note_unrolled(grown, marked.capacity());
std::mem::swap(&mut fresh, &mut frontier);
frontier = advance(p, input, toks, end, marked, absent, regs, fields);
iters += 1;
}
}
#[allow(clippy::too_many_arguments)]
fn repeat(
p: &Pattern,
bounds: (usize, Option<usize>),
g: Greed,
input: &[u8],
toks: &[Token],
end: usize,
states: Vec<State>,
absent: &HashSet<Vec<u8>>,
regs: &[String],
fields: Fields<'_>,
) -> Vec<State> {
let (m, n) = bounds;
let mut cur = states;
for _ in 0..m {
cur = advance(p, input, toks, end, cur, absent, regs, fields);
if cur.is_empty() {
return cur;
}
}
let optional = n.map(|nn| nn.saturating_sub(m));
unroll(p, g, optional, input, toks, end, cur, absent, regs, fields)
}
#[allow(clippy::too_many_arguments)]
fn bind(
name: &str,
p: &Pattern,
input: &[u8],
toks: &[Token],
end: usize,
states: Vec<State>,
absent: &HashSet<Vec<u8>>,
regs: &[String],
fields: Fields<'_>,
) -> Vec<State> {
let id = reg_id(regs, name).expect("bind register is collected");
let listed = fields.lists.contains(&id);
let record = |r: &mut State, span: (usize, usize)| {
if listed {
r.hist = Some(Rc::new(HistNode { id, span, prev: r.hist.take() }));
}
};
if let Pattern::Atom(atom) = p {
let out: Vec<State> = states
.into_iter()
.filter_map(|s| {
let mut r = match_atom(atom, input, toks, end, &s, regs, fields)?;
let tok = r.pos - 1;
let span = (toks[tok].start(), toks[tok].end());
Rc::make_mut(&mut r.env).insert(id, span);
record(&mut r, span);
Some(r)
})
.collect();
return dedup(out);
}
let mut out = Vec::new();
for s in states {
let start_pos = skip_ws(toks, s.pos, end);
for mut r in advance(p, input, toks, end, vec![s], absent, regs, fields) {
let span = if r.pos > start_pos {
(toks[start_pos].start(), toks[r.pos - 1].end())
} else {
(0, 0)
};
Rc::make_mut(&mut r.env).insert(id, span);
record(&mut r, span);
out.push(r);
}
}
dedup(out)
}
#[allow(clippy::too_many_arguments)]
fn balanced(
kind: Option<crate::token::BracketKind>,
p: &Pattern,
input: &[u8],
toks: &[Token],
end: usize,
states: Vec<State>,
absent: &HashSet<Vec<u8>>,
regs: &[String],
fields: Fields<'_>,
) -> Vec<State> {
let mut scoped_ids: Vec<u16> = Vec::new();
collect_scoped_ids(p, regs, &mut scoped_ids);
let mut out = Vec::new();
for s in states {
let p0 = skip_ws(toks, s.pos, end);
if p0 >= end {
continue;
}
let TokenKind::Open(bk) = toks[p0].kind else {
continue;
};
if let Some(want) = kind
&& want != bk
{
continue;
}
let Some(m) = toks[p0].mate() else {
continue;
};
if m >= end {
continue;
}
let pre_env = s.env.clone();
let inner_start =
vec![State { pos: p0 + 1, env: s.env.clone(), rank: s.rank.clone(), hist: s.hist.clone() }];
for ist in advance(p, input, toks, m, inner_start, absent, regs, fields) {
if skip_ws(toks, ist.pos, m) == m {
let rank = ist.rank.clone();
let hist = ist.hist.clone();
let env = restore_scoped(ist.env, &pre_env, &scoped_ids);
out.push(State { pos: m + 1, env, rank, hist });
}
}
}
dedup(out)
}
fn collect_scoped_ids(pat: &Pattern, regs: &[String], out: &mut Vec<u16>) {
match pat {
Pattern::Bind(name, scoped, p) => {
if *scoped
&& let Some(id) = reg_id(regs, name)
&& !out.contains(&id)
{
out.push(id);
}
collect_scoped_ids(p, regs, out);
}
Pattern::Within(v, _) => {
for (name, scoped) in v.iter().filter_map(|e| e.bind.as_ref()) {
if *scoped
&& let Some(id) = reg_id(regs, name)
&& !out.contains(&id)
{
out.push(id);
}
}
}
Pattern::Empty | Pattern::Atom(_) | Pattern::Guard(..) | Pattern::Anchor(_) => {}
Pattern::Assert(p, _, _) | Pattern::Atomic(p) => collect_scoped_ids(p, regs, out),
Pattern::Star(p, _) | Pattern::Plus(p, _) | Pattern::Opt(p, _) => {
collect_scoped_ids(p, regs, out);
}
Pattern::Repeat(p, _, _, _) | Pattern::Balanced(_, p) | Pattern::Field(_, p) => {
collect_scoped_ids(p, regs, out);
}
Pattern::Concat(v) | Pattern::Alt(v, _) => {
for c in v {
collect_scoped_ids(c, regs, out);
}
}
}
}
fn restore_scoped(env: Env, pre: &Env, scoped_ids: &[u16]) -> Env {
if scoped_ids.is_empty() {
return env;
}
let mut e = env;
let map = Rc::make_mut(&mut e);
for &id in scoped_ids {
match pre.get(&id) {
Some(&v) => {
map.insert(id, v);
}
None => {
map.remove(&id);
}
}
}
e
}
thread_local! {
static PROBE: std::cell::RefCell<Vec<State>> =
const { std::cell::RefCell::new(Vec::new()) };
}
#[allow(clippy::too_many_arguments)]
fn probe_matches(
inner: &Pattern,
at: usize,
env: &Env,
input: &[u8],
toks: &[Token],
end: usize,
absent: &HashSet<Vec<u8>>,
regs: &[String],
fields: Fields<'_>,
) -> bool {
let mut probe = PROBE.with(|p| std::mem::take(&mut *p.borrow_mut()));
probe.clear();
probe.push(State { pos: at, env: env.clone(), rank: Rank::default(), hist: None });
let mut out = advance(inner, input, toks, end, probe, absent, regs, fields);
let hit = !out.is_empty();
out.clear();
PROBE.with(|p| *p.borrow_mut() = out);
hit
}
#[allow(clippy::too_many_arguments)]
fn assert_zero_width(
inner: &Pattern,
neg: bool,
look: crate::ast::Look,
input: &[u8],
toks: &[Token],
end: usize,
states: Vec<State>,
absent: &HashSet<Vec<u8>>,
regs: &[String],
fields: Fields<'_>,
) -> Vec<State> {
use crate::ast::Look;
let window = crate::nfa::bounded_max_len(inner).unwrap_or(0);
states
.into_iter()
.filter(|s| {
let p = skip_ws(toks, s.pos, end);
let hit = match look {
Look::Ahead => probe_matches(inner, p, &s.env, input, toks, end, absent, regs, fields),
Look::Within { window, at_least, at_most } => {
let mut tried = 0usize;
let mut found = 0usize;
let mut j = p;
let enough = at_most.map_or(at_least, |hi| hi + 1);
while j < end && tried < window && found < enough {
if toks[j].kind != TokenKind::Whitespace {
tried += 1;
if probe_matches(
inner, j, &s.env, input, toks, end, absent, regs, fields,
) {
found += 1;
}
}
j += 1;
}
found >= at_least && at_most.is_none_or(|hi| found <= hi)
}
Look::InGroup { at_least, at_most } => {
let mut interior = p..p;
if p < end
&& matches!(toks[p].kind, TokenKind::Open(_))
&& let Some(m) = toks[p].mate()
&& m < end
{
interior = p + 1..m;
}
let close = interior.end;
let mut found = 0usize;
let enough = at_most.map_or(at_least, |hi| hi + 1);
for j in interior {
if found >= enough {
break;
}
if toks[j].kind == TokenKind::Whitespace {
continue;
}
if probe_matches(
inner, j, &s.env, input, toks, close, absent, regs, fields,
) {
found += 1;
}
}
found >= at_least && at_most.is_none_or(|hi| found <= hi)
}
Look::Behind => {
let lo = p.saturating_sub(window.saturating_mul(2));
(lo..p).any(|j| {
if toks[j].kind == TokenKind::Whitespace {
return false;
}
let mut probe = PROBE.with(|b| std::mem::take(&mut *b.borrow_mut()));
probe.clear();
probe.push(State {
pos: j,
env: s.env.clone(),
rank: Rank::default(),
hist: None,
});
let mut out = advance(inner, input, toks, p, probe, absent, regs, fields);
let hit = out.iter().any(|r| skip_ws(toks, r.pos, p) == p);
out.clear();
PROBE.with(|b| *b.borrow_mut() = out);
hit
})
}
};
hit != neg
})
.collect()
}
fn guard(
lit: &str,
neg: bool,
input: &[u8],
toks: &[Token],
end: usize,
states: Vec<State>,
absent: &HashSet<Vec<u8>>,
) -> Vec<State> {
if absent.contains(lit.as_bytes()) {
return if neg { states } else { Vec::new() };
}
states
.into_iter()
.filter(|s| {
let p = skip_ws(toks, s.pos, end);
let from = if p < toks.len() { toks[p].start() } else { input.len() };
crate::byte_simd::contains(&input[from..], lit.as_bytes()) != neg
})
.collect()
}
fn dedup(states: Vec<State>) -> Vec<State> {
if states.len() <= 1 {
return states;
}
let mut at: std::collections::HashMap<State, usize, crate::fxhash::FxBuild> =
std::collections::HashMap::with_capacity_and_hasher(
states.len(),
crate::fxhash::FxBuild::process(),
);
let mut out: Vec<State> = Vec::with_capacity(states.len());
for s in states {
match at.get(&s) {
Some(&i) => {
if cmp_rank(s.rank_slice(), out[i].rank_slice()) == std::cmp::Ordering::Less {
out[i] = s;
}
}
None => {
at.insert(s.clone(), out.len());
out.push(s);
}
}
}
out
}
#[allow(clippy::too_many_arguments)]
fn field_match(
k: usize,
inner: &Pattern,
input: &[u8],
toks: &[Token],
end: usize,
states: Vec<State>,
absent: &HashSet<Vec<u8>>,
regs: &[String],
fields: Fields<'_>,
) -> Vec<State> {
let mut out = Vec::new();
for s in states {
let p = skip_ws(toks, s.pos, end);
if is_field_start(end, p, k, fields) {
let init = vec![State { pos: p, env: s.env, rank: s.rank, hist: s.hist }];
out.extend(advance(inner, input, toks, end, init, absent, regs, fields));
}
}
dedup(out)
}
fn is_field_start(end: usize, p: usize, k: usize, fields: Fields<'_>) -> bool {
if p >= end {
return false;
}
fields.field_starts.is_some_and(|f| f.get(p).copied() == Some(k.max(1) as u32))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse;
#[test]
fn holding_registers_inline_does_not_widen_a_match() {
assert_eq!(
size_of::<Regs>(),
size_of::<Vec<Span>>(),
"Regs is {} bytes against a Vec's {}",
size_of::<Regs>(),
size_of::<Vec<Span>>()
);
assert_eq!(size_of::<Match>(), 64, "a match is {} bytes", size_of::<Match>());
}
#[test]
fn registers_read_the_same_inline_and_shared() {
let spans: Vec<Span> =
(0..8u32).map(|i| Span { start: i * 10, end: i * 10 + 4 }).collect();
for n in 0..=spans.len() {
let regs = Regs::from_slice(&spans[..n]);
assert_eq!(regs.as_slice(), &spans[..n], "{n} registers read back");
assert_eq!(regs.len(), n, "{n} registers counted");
let shared = Regs::Shared(spans[..n].into());
assert_eq!(regs, shared, "{n} registers compare alike however held");
let held = match n {
0 => matches!(regs, Regs::None),
1..=INLINE_REGS => matches!(regs, Regs::Inline(..)),
_ => matches!(regs, Regs::Shared(_)),
};
assert!(held, "{n} registers held the way its count calls for");
assert!(matches!(Regs::none(), Regs::None), "no registers holds nothing");
let mut mine = shared.clone();
let theirs = Regs::Shared(spans[..n].into());
for sp in mine.as_mut_slice() {
sp.start += 1;
}
assert_eq!(theirs.as_slice(), &spans[..n], "{n} registers left alone");
assert!(
mine.iter().zip(&spans[..n]).all(|(a, b)| a.start == b.start + 1),
"{n} registers written through"
);
}
}
fn run(pattern: &str, input: &str) -> Vec<Match> {
let p = parse(pattern).unwrap();
let spans = scan(&p, input.as_bytes());
captures(&p, input.as_bytes(), &spans)
}
#[test]
fn the_anchored_ladders_answer_a_binding_pattern_as_they_answer_its_twin() {
let mut text = String::new();
for i in 0..300u32 {
text.push_str(&format!("let value_{i} = {} ; call_{i}(alpha, beta) ;\n", i * 37));
}
let input = text.as_bytes();
for (bound, bare) in [
("\\W:name \"=\"", "\\W \"=\""),
("\"let\" \\W:v \"=\"", "\"let\" \\W \"=\""),
("\\W:w", "\\W"),
("\\N:n", "\\N"),
("\\W:w \"!\"", "\\W \"!\""),
] {
let (b, u) = (parse(bound).expect(bound), parse(bare).expect(bare));
let spans = scan(&u, input);
assert_eq!(scan(&b, input), spans, "scan {bound}");
assert_eq!(is_match(&b, input), !spans.is_empty(), "is_match {bound}");
assert_eq!(is_match(&u, input), !spans.is_empty(), "is_match {bare}");
let first = spans.first().copied();
assert_eq!(routed_first(&b, input).flatten(), first, "first {bound}");
assert_eq!(routed_first(&u, input).flatten(), first, "first {bare}");
let at = first.map_or(0, |s| s.end());
let next = spans.iter().find(|s| s.start() >= at).copied();
assert_eq!(routed_first_at(&b, input, at).flatten(), next, "at {bound}");
assert_eq!(routed_first_at(&u, input, at).flatten(), next, "at {bare}");
}
}
#[test]
fn the_positional_ladder_takes_the_windows_and_answers_what_the_scan_does() {
let mut text = String::new();
for i in 0..300u32 {
text.push_str(&format!("call_{i}(alpha) ; let value_{i} = {i} ;\n"));
}
let input = text.as_bytes();
for src in ["\"let\" \\W \"=\"", "\"let\" \\W:v \"=\"", "\"let\" \\W"] {
let p = parse(src).expect(src);
let first = scan(&p, input).first().copied();
assert!(first.is_some(), "{src} matches the corpus");
assert_eq!(routed_first_positional(&p, input).flatten(), first, "{src}");
assert_eq!(crate::shortest_match(&p, input), first.map(|s| s.end()), "{src}");
}
}
#[test]
fn a_pattern_names_the_reading_its_empty_loops_take() {
use crate::parser::parse_with_empty_loop;
let spans = |ms: Vec<Span>| -> Vec<(usize, usize)> {
ms.iter().map(|m| (m.start(), m.end())).collect()
};
let input = b"42 baz bar ";
for src in [r"(\N? | \W)+ .", r"(\N? | \W)* ."] {
let p = parse(src).expect("parses");
assert_eq!(
spans(scan_with_empty_loop(&p, input, EmptyLoop::Thompson)),
vec![(0, 10)],
"{src} under the crate's reading"
);
assert_eq!(
spans(scan_with_empty_loop(&p, input, EmptyLoop::Perl)),
vec![(0, 6), (7, 10)],
"{src} under the backtracking reading"
);
assert_eq!(spans(scan(&p, input)), vec![(0, 10)], "{src} unnamed");
}
for src in [r"(\W | \N?)+ .", r"(\N?)+ \W"] {
let p = parse(src).expect("parses");
assert_eq!(
spans(scan_with_empty_loop(&p, input, EmptyLoop::Thompson)),
spans(scan_with_empty_loop(&p, input, EmptyLoop::Perl)),
"{src} reads the same either way"
);
}
let flat = parse(r"\W+ \N").expect("parses");
assert!(!crate::nfa::empty_loop_needs_set_engine(&flat));
let (p, mode) = parse_with_empty_loop(r"(?empty:perl)(\N? | \W)+ .").expect("parses");
assert_eq!(mode, EmptyLoop::Perl);
assert_eq!(p, parse(r"(\N? | \W)+ .").expect("parses"));
assert_eq!(spans(scan_with_empty_loop(&p, input, mode)), vec![(0, 6), (7, 10)]);
let (_, mode) = parse_with_empty_loop(r"\W+").expect("parses");
assert_eq!(mode, EmptyLoop::Thompson, "the crate's reading is the default");
let e = parse_with_empty_loop(r"(?empty:pcre)\W+").expect_err("names no reading");
assert!(e.msg.contains("thompson"), "the error names the readings: {}", e.msg);
}
fn log_with_an_outlier() -> (String, usize) {
let mut s = String::new();
let mut planted = 0;
for i in 0..240 {
match i % 3 {
0 => {
let v = if i == 150 { 5_000_000 } else { 90 + (i * 37) % 21 };
if i == 150 {
planted = s.len() + format!("svc_{} latency = ", i % 7).len();
}
s.push_str(&format!("svc_{} latency = {v} ;\n", i % 7));
}
1 => s.push_str(&format!("svc_{} size = {} ;\n", i % 7, 1_000_000 + i)),
_ => s.push_str(&format!("state: {} ;\n", if i % 2 == 0 { "ready" } else { "busy" })),
}
}
(s, planted)
}
#[test]
fn a_relative_magnitude_predicate_reads_its_threshold_from_the_window() {
let (log, planted) = log_with_an_outlier();
let m = run("\\N{>+2}", &log);
assert!(m.iter().any(|m| m.start == planted), "the planted latency is found: {m:?}");
assert!(m.iter().all(|m| log[m.start..m.end].len() >= 7), "only the large numbers: {m:?}");
let m = run("\\N{>+2s}", &log);
assert!(m.iter().any(|m| m.start == planted), "the planted latency clears two sigmas: {m:?}");
let m = run("\\N{mag>6}", &log);
assert!(m.iter().all(|m| log[m.start..m.end].len() >= 7) && m.len() > 70, "{}", m.len());
}
#[test]
fn a_keyed_relative_predicate_reads_the_history_of_that_keys_values() {
let (log, planted) = log_with_an_outlier();
let m = run("\"latency\":k \"=\" \\N{>+1:k}", &log);
assert_eq!(m.len(), 1, "{m:?}");
assert!(log[m[0].start..m[0].end].starts_with("latency = 5000000"), "{m:?}");
assert_eq!(m[0].end, planted + "5000000".len(), "the match ends at the planted value");
let m = run("\"size\":k \"=\" \\N{>+0:k}", &log);
assert!(m.iter().all(|m| !log[..m.start].is_empty()), "the first size never matches: {m:?}");
assert!(m.len() > 30, "later sizes sit at the mean, so `>+0` holds where a value repeats: {}", m.len());
}
#[test]
fn a_phase_anchor_selects_a_column_of_a_periodic_record() {
let mut rows = String::new();
for i in 0..200 {
rows.push_str(&format!("r{i} , {} , x{} ;\n", i * 3, i % 4));
}
let toks = crate::lexer::lex(rows.as_bytes());
assert_eq!(crate::context::record_period(&toks, rows.as_bytes()), Some(6), "the row is the period");
let m = run("@phase:2 \\N", &rows);
assert_eq!(m.len(), 200, "every row's number sits at phase two: {}", m.len());
assert!(run("@phase:0 \\N", &rows).is_empty(), "no number sits at phase zero");
assert_eq!(run("@phase:4 \\W", &rows).len(), 200, "the second word is at phase four");
}
#[test]
fn a_declared_shape_becomes_a_token_the_default_lexer_would_split() {
use crate::custom::{Precedence, ShapeSet};
let input = b"ticket ABC-1234 done";
let split = crate::lexer::lex(input);
assert!(
split.iter().filter(|t| t.is_significant()).count() > 3,
"the default lexer splits ABC-1234"
);
let mut shapes = ShapeSet::new();
shapes.declare("order = `[A-Z]{3}-[0-9]{4}`", Precedence::Before).expect("declares");
let pat = crate::parser::parse_with_shapes("\\{order}", &shapes).expect("parses");
let m = crate::engine::scan_with_shapes(&pat, input, &shapes);
assert_eq!(m.len(), 1, "the shape matches once");
assert_eq!(&input[m[0].range()], b"ABC-1234");
}
#[test]
fn both_engines_agree_on_custom_kinds() {
use crate::custom::{Precedence, ShapeSet};
let mut shapes = ShapeSet::new();
shapes.declare("order = `[A-Z]{3}-[0-9]{4}`", Precedence::Before).expect("declares");
shapes.declare("level = `(DEBUG|INFO|WARN|ERROR)`", Precedence::Before).expect("declares");
let inputs: &[&str] = &[
"",
"ABC-1234",
"ticket ABC-1234 done",
"ERROR ABC-1234 and WARN XYZ-9999 here",
"no shapes at all in this line",
"ABC-1234 ABC-1234 ABC-1234",
"INFO 12 ABC-1234 (nested DEF-5678) tail",
];
let patterns: &[&str] = &[
"\\{order}",
"\\{level}",
"\\{level} \\{order}",
"\\{order}+",
"\\{order} | \\{level}",
"\\{level} .* \\{order}",
"\\W \\{order}",
"\\{order}:x =x",
];
for pat_src in patterns {
let pat = crate::parser::parse_with_shapes(pat_src, &shapes).expect("parses");
for inp in inputs {
let bytes = inp.as_bytes();
let toks = crate::lexer::lex_with_shapes(bytes, &[], &shapes, 0);
let set = scan_tokens_from(&pat, bytes, &toks, 0);
if let Some(nfa) = crate::nfa::scan_nfa_over(&pat, bytes, &toks) {
assert_eq!(nfa, set, "engines differ on {pat_src:?} over {inp:?}");
}
assert_eq!(
crate::engine::scan_with_shapes(&pat, bytes, &shapes),
set,
"scan_with_shapes differs on {pat_src:?} over {inp:?}"
);
}
}
}
#[test]
fn the_device_gate_declines_a_custom_kind() {
use crate::custom::{Precedence, ShapeSet};
let mut shapes = ShapeSet::new();
shapes.declare("order = `[A-Z]{3}-[0-9]{4}`", Precedence::Before).expect("declares");
let pat = crate::parser::parse_with_shapes("\\{order}", &shapes).expect("parses");
assert!(!crate::gpu::gpu_eligible(&pat));
assert!(crate::gpu::gpu_eligible(&parse("\\W \\N").expect("parses")));
}
#[test]
fn a_shape_atom_is_unknown_without_its_set() {
assert!(parse("\\{order}").is_err());
}
#[test]
fn a_state_is_the_width_the_sweep_was_tuned_for() {
use std::mem::size_of;
assert_eq!(size_of::<Rank>(), 16, "the preference path: seven bytes inline, or a slice");
assert_eq!(size_of::<Env>(), 8, "the register map, a thin pointer behind an Rc");
assert_eq!(size_of::<Hist>(), 8, "the binding list, a thin pointer behind an Rc");
assert_eq!(size_of::<State>(), 40, "a position and those three");
}
#[test]
fn shape_precedence_decides_an_overlap_with_a_builtin() {
use crate::custom::{Precedence, ShapeSet};
let input = b"10.0.0.1";
let mut before = ShapeSet::new();
before.declare("quad = `[0-9.]{8}`", Precedence::Before).expect("declares");
let toks = crate::lexer::lex_with_shapes(input, &[], &before, 0);
assert_eq!(toks[0].kind, crate::token::TokenKind::Custom(0), "Before wins the overlap");
let mut after = ShapeSet::new();
after.declare("quad = `[0-9.]{8}`", Precedence::After).expect("declares");
let toks = crate::lexer::lex_with_shapes(input, &[], &after, 0);
assert_eq!(toks[0].kind, crate::token::TokenKind::Ip, "After leaves the IP reading");
}
#[test]
fn an_orbit_scope_makes_literals_compare_under_a_symmetry() {
assert_eq!(run("\"Cat\"", "Cat cat CAT").len(), 1);
assert_eq!(run("(?orbit:case \"Cat\")", "Cat cat CAT").len(), 3);
assert_eq!(run("(?orbit:case (\"cat\" | \"dog\"))", "CAT Dog bird").len(), 2);
assert_eq!(run("(?orbit:case \\N)", "12 ab").len(), 1);
}
#[test]
fn an_orbit_scope_reaches_into_a_token_class() {
assert_eq!(run("(?orbit:case [\"cat\" \"dog\"])", "CAT Dog bird").len(), 2);
}
#[test]
fn a_bad_orbit_modifier_is_a_parse_error() {
assert!(parse("(?bogus:case \"x\")").is_err());
assert!(parse("(?orbit:nosuch \"x\")").is_err());
assert!(parse("(?orbit \"x\")").is_err());
assert!(parse("(\"x\" | \"y\")").is_ok());
}
#[test]
fn lookahead_is_zero_width_and_both_polarities_work() {
let m = run("~(\\W) \\W", "cat 12 dog");
assert_eq!(m.len(), 2);
assert_eq!(&"cat 12 dog"[m[0].start..m[0].end], "cat");
let followed: Vec<_> = run("\\W ~(\\N)", "cat 12 dog 34 end")
.iter()
.map(|m| "cat 12 dog 34 end"[m.start..m.end].to_string())
.collect();
assert_eq!(followed, vec!["cat", "dog"]);
let unfollowed: Vec<_> = run("\\W !~(\\N)", "cat 12 dog 34 end")
.iter()
.map(|m| "cat 12 dog 34 end"[m.start..m.end].to_string())
.collect();
assert_eq!(unfollowed, vec!["end"]);
}
#[test]
fn lookbehind_reads_the_direction_the_matcher_never_exposed() {
let after_word: Vec<_> = run("~<(\\W) \\N", "cat 12 34 dog 56")
.iter()
.map(|m| "cat 12 34 dog 56"[m.start..m.end].to_string())
.collect();
assert_eq!(after_word, vec!["12", "56"]);
let not_after_word: Vec<_> = run("!~<(\\W) \\N", "cat 12 34 dog 56")
.iter()
.map(|m| "cat 12 34 dog 56"[m.start..m.end].to_string())
.collect();
assert_eq!(not_after_word, vec!["34"]);
}
#[test]
fn an_unbounded_backward_assertion_is_a_parse_error() {
assert!(parse("~<(\\W*) \\N").is_err());
assert!(parse("~<(\\W+) \\N").is_err());
assert!(parse("~<(\\W{2,}) \\N").is_err());
assert!(parse("~<(\\W{2,3}) \\N").is_ok());
}
#[test]
fn the_literal_guard_still_works_and_stays_forward() {
let present: Vec<_> = run(". ~\"END\"", "a b END c")
.iter()
.map(|m| "a b END c"[m.start..m.end].to_string())
.collect();
assert_eq!(present, vec!["a", "b"]);
let absent: Vec<_> = run(". !~\"END\"", "a b END c")
.iter()
.map(|m| "a b END c"[m.start..m.end].to_string())
.collect();
assert_eq!(absent, vec!["END", "c"]);
assert!(parse("~<\"END\"").is_err());
}
#[test]
fn a_proximity_window_asks_within_how_many_tokens() {
let hay = "alpha one two three END beta";
let near: Vec<_> = run("\\W ~>2(\"END\")", hay)
.iter()
.map(|m| hay[m.start..m.end].to_string())
.collect();
assert_eq!(near, vec!["two", "three"], "END is within two tokens after each");
let wider: Vec<_> = run("\\W ~>3(\"END\")", hay)
.iter()
.map(|m| hay[m.start..m.end].to_string())
.collect();
assert_eq!(wider, vec!["one", "two", "three"], "one more token of reach");
}
#[test]
fn a_bounded_gap_between_two_tokens_spans_both_of_them() {
let hay = "alpha p q beta gamma alpha r s t u beta";
let near: Vec<_> = run("\"alpha\" .{0,2} \"beta\"", hay)
.iter()
.map(|m| hay[m.start..m.end].to_string())
.collect();
assert_eq!(near, vec!["alpha p q beta"], "the far pair has four tokens between");
let wider: Vec<_> = run("\"alpha\" .{0,4} \"beta\"", hay)
.iter()
.map(|m| hay[m.start..m.end].to_string())
.collect();
assert_eq!(wider.len(), 2, "both pairs reach at four: {wider:?}");
}
#[test]
fn a_proximity_window_and_its_negation_partition_the_matches() {
let hay = "a b c END d e f";
let all = run("\\W", hay).len();
let inside = run("\\W ~>2(\"END\")", hay).len();
let outside = run("\\W !~>2(\"END\")", hay).len();
assert_eq!(inside + outside, all, "{inside} within and {outside} not, of {all}");
assert!(inside > 0 && outside > 0, "the corpus must exercise both sides");
}
#[test]
fn a_window_can_be_asked_how_many_and_not_only_whether() {
let hay = "a b c d e f 1 2 3";
let three: Vec<_> = run("\\W ~>4{3,}(\\N)", hay)
.iter()
.map(|m| hay[m.start..m.end].to_string())
.collect();
assert_eq!(three, vec!["e", "f"], "all three numbers are within four of each");
let two: Vec<_> = run("\\W ~>4{2,}(\\N)", hay)
.iter()
.map(|m| hay[m.start..m.end].to_string())
.collect();
assert_eq!(two, vec!["d", "e", "f"], "d reaches two of them");
let sparse: Vec<_> = run("\\W ~>4{0,1}(\\N)", hay)
.iter()
.map(|m| hay[m.start..m.end].to_string())
.collect();
assert_eq!(sparse, vec!["a", "b", "c"], "d is the first to see two");
}
#[test]
fn a_count_over_a_region_is_bounded_by_the_bracket_not_by_a_distance() {
let hay = "f(1, 2, 3) g(4, 5) h(6, 7, 8, 9)";
let full: Vec<_> = run("\\W ~#{3,}(\\N) \\B", hay)
.iter()
.map(|m| hay[m.start..m.end].to_string())
.collect();
assert_eq!(full, vec!["f(1, 2, 3)", "h(6, 7, 8, 9)"], "g holds two");
let spill = "f(1, 2) 3 4 5";
let by_region: Vec<_> = run("\\W ~#{3,}(\\N) \\B", spill)
.iter()
.map(|m| spill[m.start..m.end].to_string())
.collect();
assert!(by_region.is_empty(), "the region stops at `)`: {by_region:?}");
let by_window: Vec<_> = run("\\W ~>9{3,}(\\N) \\B", spill)
.iter()
.map(|m| spill[m.start..m.end].to_string())
.collect();
assert_eq!(by_window, vec!["f(1, 2)"], "nine tokens of reach cross the bracket");
}
#[test]
fn a_region_reaches_its_own_close_through_nesting() {
let hay = "f(a(1, 2), 3) g(b(1), 2)";
let three: Vec<_> = run("\\W ~#{3,}(\\N) \\B", hay)
.iter()
.map(|m| hay[m.start..m.end].to_string())
.collect();
assert_eq!(three, vec!["f(a(1, 2), 3)"], "only f's region holds three");
let two: Vec<_> = run("\\W ~#{2}(\\N) \\B", hay)
.iter()
.map(|m| hay[m.start..m.end].to_string())
.collect();
assert_eq!(two, vec!["a(1, 2)", "g(b(1), 2)"], "exactly two, counting through nesting");
}
#[test]
fn a_region_count_and_its_negation_partition_every_position() {
let hay = "f(1, 2) x y g(3)";
let all = run("\\W", hay).len();
let inside = run("\\W ~#{1,}(\\N)", hay).len();
let outside = run("\\W !~#{1,}(\\N)", hay).len();
assert_eq!(inside + outside, all, "{inside} over a region and {outside} not, of {all}");
assert_eq!(inside, 2, "f and g open a group holding a number");
assert_eq!(outside, 2, "x and y open no group at all");
}
#[test]
fn a_region_count_satisfied_by_absence_does_not_make_its_literal_required() {
let hay = "f(alpha) g(beta)";
let found: Vec<_> = run("\\W ~#{0,1}(\"zzzqqq\") \\B", hay)
.iter()
.map(|m| hay[m.start..m.end].to_string())
.collect();
assert_eq!(found, vec!["f(alpha)", "g(beta)"], "every region holds at most one: none");
assert!(run("\\W ~#{1,}(\"zzzqqq\") \\B", hay).is_empty(), "a floor above zero needs it");
let none = parse("\\W ~#{0,1}(\"zzzqqq\")").expect("parses");
let some = parse("\\W ~#{1,}(\"zzzqqq\")").expect("parses");
assert!(!crate::prefilter::requires_absent(&none, hay.as_bytes()));
assert!(crate::prefilter::requires_absent(&some, hay.as_bytes()));
}
#[test]
fn a_region_count_keeps_the_pattern_off_the_prefix_path() {
let region = parse("\\W ~#{1,}(\\N)").expect("parses");
assert!(region.has_assert(), "the reach is the input's, not the pattern's");
assert_eq!(region.widest_forward_window(), 0, "no token count describes it");
assert!(!crate::prefilter::settles_from_a_prefix(®ion));
let mut hay = String::new();
for i in 0..40_000 {
hay.push_str(&format!("key_{i} : {i} ;\n"));
}
hay.push_str("alpha ( 7 ) ;\n");
assert_eq!(
crate::find(®ion, hay.as_bytes()),
crate::scan(®ion, hay.as_bytes()).first().copied()
);
}
#[test]
fn a_counting_selector_refuses_what_it_cannot_mean() {
assert!(parse("\\W ~#{3,2}(\\N)").is_err(), "a top below its bottom");
assert!(parse("\\W ~#{,2}(\\N)").is_err(), "the bottom is not optional");
assert!(parse("\\W ~#{2,}\"END\"").is_err(), "the region form needs parentheses");
assert!(parse("\\W ~>3\"END\"").is_err(), "the window form needs parentheses");
assert!(parse("\\W ~\"END\"").is_ok(), "the plain guard is still the literal form");
}
#[test]
fn a_prefix_does_not_settle_a_pattern_that_reads_past_its_match() {
let bounded = parse("\\W ~>3(\\N)").expect("parses");
let unbounded = parse("\\W ~(\\N \\N)").expect("parses");
let guarded = parse("\\W ~\"END\"").expect("parses");
assert!(crate::prefilter::settles_from_a_prefix(&bounded));
assert!(!crate::prefilter::settles_from_a_prefix(&unbounded));
assert!(!crate::prefilter::settles_from_a_prefix(&guarded));
assert_eq!(bounded.widest_forward_window(), 4, "three positions and a one-token inner");
assert_eq!(unbounded.widest_forward_window(), 0, "no bounded window to reserve for");
let mut hay = String::new();
for i in 0..40_000 {
hay.push_str(&format!("key_{i} : {i} ;\n"));
}
hay.push_str("alpha 7 ;\n");
for p in [&bounded, &unbounded, &guarded] {
assert_eq!(crate::find(p, hay.as_bytes()), crate::scan(p, hay.as_bytes()).first().copied());
}
}
#[test]
fn a_count_satisfied_by_absence_does_not_make_its_literal_required() {
let hay = "alpha beta gamma delta";
let found = run("\\W ~>4{0,1}(\"zzzqqq\")", hay);
assert_eq!(found.len(), 4, "every word has at most one zzzqqq nearby: none");
assert!(run("\\W ~>4{1,}(\"zzzqqq\")", hay).is_empty());
let none = parse("\\W ~>4{0,1}(\"zzzqqq\")").expect("parses");
let some = parse("\\W ~>4{1,}(\"zzzqqq\")").expect("parses");
assert!(!crate::prefilter::requires_absent(&none, hay.as_bytes()));
assert!(crate::prefilter::requires_absent(&some, hay.as_bytes()));
}
#[test]
fn a_count_that_nothing_can_satisfy_is_refused() {
assert!(parse("\\W ~>4{3,2}(\\N)").is_err(), "a top below its bottom");
assert!(parse("\\W ~>2{5,}(\\N)").is_err(), "more occurrences than the window holds");
assert!(parse("\\W ~>4{,2}(\\N)").is_err(), "the bottom is not optional");
}
#[test]
fn a_window_of_zero_tokens_is_refused_rather_than_matched() {
assert!(parse("\\W ~>0(\"END\")").is_err());
assert!(parse("\\W ~>(\"END\")").is_err(), "the count is not optional");
}
#[test]
fn a_bounded_assertion_does_not_depend_on_the_whole_input() {
let bounded = parse("\\W ~>3(\"END\")").expect("parses");
let unbounded = parse("\\W ~(\"END\")").expect("parses");
assert!(!bounded.has_assert(), "a bounded assertion is not an unbounded one");
assert!(unbounded.has_assert());
assert!(!bounded.depends_on_whole_input(), "so it can be finalized from a chunk");
assert!(unbounded.depends_on_whole_input());
}
#[test]
fn an_assertion_binds_nothing_that_escapes_it() {
let p = parse("~(\\W:inner) \\W:outer").expect("parses");
assert_eq!(p.capture_names(), vec!["outer".to_string()]);
}
#[test]
fn token_classes_union_complement_intersect_and_subtract() {
assert_eq!(run("[\\N \\W]", "ab 12 , cd").len(), 3);
let not_num: Vec<_> = run("[^\\N]", "ab 12 cd")
.iter()
.map(|m| "ab 12 cd"[m.start..m.end].to_string())
.collect();
assert_eq!(not_num, vec!["ab", "cd"]);
let hex: Vec<_> = run("[\\W && \\h]", "deadbeef zzz cafe")
.iter()
.map(|m| "deadbeef zzz cafe"[m.start..m.end].to_string())
.collect();
assert_eq!(hex, vec!["deadbeef", "cafe"]);
let lower: Vec<_> = run("[\\W -- \\u]", "ABC def GHI jkl")
.iter()
.map(|m| "ABC def GHI jkl"[m.start..m.end].to_string())
.collect();
assert_eq!(lower, vec!["def", "jkl"]);
}
#[test]
fn a_class_composes_with_literals_and_byte_patterns() {
assert_eq!(run("[\"cat\" \"dog\"]", "cat bird dog").len(), 2);
assert_eq!(run("[`[a-z]+` \\N]", "abc DEF 12").len(), 2);
}
#[test]
fn malformed_classes_are_parse_errors() {
for bad in ["[", "[\\N", "[]", "[&& \\N]"] {
assert!(parse(bad).is_err(), "should reject: {bad}");
}
}
#[test]
fn a_balanced_square_group_still_parses_after_classes() {
assert_eq!(run("\\B[.*]", "x [a b] y").len(), 1);
}
#[test]
fn input_anchors_are_distinct_from_line_anchors() {
let two_lines = "alpha beta\ngamma delta\n";
assert_eq!(run("^ \\W", two_lines).len(), 2);
let at_start = run("\\A \\W", two_lines);
assert_eq!(at_start.len(), 1);
assert_eq!(&two_lines[at_start[0].start..at_start[0].end], "alpha");
assert_eq!(run("$ \\W", two_lines).len(), 2);
let at_end = run("\\z \\W", two_lines);
assert_eq!(at_end.len(), 1);
assert_eq!(&two_lines[at_end[0].start..at_end[0].end], "delta");
assert_eq!(run("^ \\W", "only line\n").len(), run("\\A \\W", "only line\n").len());
assert_eq!(run("$ \\W", "only line\n").len(), run("\\z \\W", "only line\n").len());
}
#[test]
fn resume_anchor_takes_a_contiguous_run_instead_of_finding_occurrences() {
let input = "1 2 3 stop 4 5";
assert_eq!(run("\\N", input).len(), 5, "a search finds all five");
let contiguous = run("\\G \\N", input);
assert_eq!(contiguous.len(), 3, "the run ends at the word");
assert_eq!(&input[contiguous[2].start..contiguous[2].end], "3");
assert!(run("\\G \\N", "stop 1 2 3").is_empty(), "no run to take");
assert_eq!(run("\\N", "stop 1 2 3").len(), 3, "and searching still finds them");
}
#[test]
fn resume_anchor_is_refused_where_it_could_only_fail() {
assert!(crate::parser::parse("\\G \\N").is_ok(), "the head is where it belongs");
for bad in ["\\N \\G", "\\N (\\G \\W)", "(\\W | \\G \\N)", "\\G \\N \\G"] {
let err = crate::parser::parse(bad).expect_err("refused: {bad}");
assert!(format!("{err:?}").contains("\\\\G"), "names the construct: {err:?}");
}
}
#[test]
fn reset_start_reports_only_what_follows_it() {
let input = "name: alice age: bob";
let got = run("\\W \":\" \\K \\W", input);
assert_eq!(got.len(), 2);
assert_eq!(&input[got[0].start..got[0].end], "alice");
assert_eq!(&input[got[1].start..got[1].end], "bob");
let whole = run("\\W \":\" \\W", input);
assert_eq!(&input[whole[0].start..whole[0].end], "name: alice");
assert_eq!(got.len(), whole.len(), "same matches, different spans");
}
#[test]
fn reset_start_is_refused_where_the_engine_cannot_carry_it() {
assert!(crate::parser::parse("\\W \":\" \\K \\W").is_ok());
for bad in ["\\B( \\K \\W )", "@seam \\K \\W", "\\K \\S", "~(\\W) \\K \\W"] {
assert!(crate::parser::parse(bad).is_err(), "refused: {bad}");
}
}
#[test]
fn an_atomic_group_keeps_only_the_length_its_body_preferred() {
let input = "a b c";
assert_eq!(run("\\W* \\W", input).len(), 1, "greedy hands one back");
assert!(run("(?>\\W*) \\W", input).is_empty(), "atomic does not");
assert!(run("\\W*+ \\W", input).is_empty(), "and the possessive form is the same");
assert_eq!(run("(?>\\N*) \\W", "1 2 end").len(), 1, "numbers stop at the word");
assert_eq!(run("(?>\\W) \\W", input).len(), 1);
}
#[test]
fn possessive_quantifiers_read_as_the_atomic_form() {
let atomic = crate::parser::parse("(?>\\W*)").expect("parses");
let possessive = crate::parser::parse("\\W*+").expect("parses");
assert_eq!(atomic, possessive, "the spellings agree");
for (poss, group) in
[("\\N++", "(?>\\N+)"), ("\\N?+", "(?>\\N?)"), ("\\N{2,4}+", "(?>\\N{2,4})")]
{
assert_eq!(
crate::parser::parse(poss).expect("parses"),
crate::parser::parse(group).expect("parses"),
"{poss} is {group}"
);
}
let spaced = crate::parser::parse("\\W+ \"+\"").expect("parses");
assert_ne!(spaced, crate::parser::parse("\\W++").expect("parses"));
}
#[test]
fn an_atom_can_be_conditioned_on_the_construct_containing_it() {
let input = "x = 41\nf(42)\ny = 43\n";
assert_eq!(run("\\N", input).len(), 3, "three numbers, taken plainly");
let assigned = run("@super:assign \\N", input);
assert_eq!(assigned.len(), 2, "two of them are values in a binding");
assert_eq!(&input[assigned[0].start..assigned[0].end], "41");
assert_eq!(&input[assigned[1].start..assigned[1].end], "43");
let argument = run("@super:numeric \\N", input);
assert_eq!(argument.len(), 1, "and one stands alone as an argument");
assert_eq!(&input[argument[0].start..argument[0].end], "42");
let callee = run("@super:call \\W", input);
assert_eq!(callee.len(), 1);
assert_eq!(&input[callee[0].start..callee[0].end], "f");
}
#[test]
fn the_construct_boundary_is_an_anchor_of_its_own() {
let input = "k: v\na, b, c\n";
let heads = run("@super \\W", input);
assert_eq!(heads.len(), 2, "one head per construct");
for (got, want) in heads.iter().zip(["k", "a"]) {
assert_eq!(&input[got.start..got.end], want);
}
assert_eq!(run("\\W", input).len(), 5, "five words, two of them heads");
}
#[test]
fn a_seam_names_which_stream_has_to_stop_predicting_itself() {
let input = "let x = 1 ; let y = 2 ; print x ; print y ;";
let by_byte = run("@seam:byte \\W", input);
let by_token = run("@seam:token \\W", input);
let by_super = run("@seam:super \\W", input);
assert_eq!(
run("@seam \\W", input).iter().map(|m| m.start).collect::<Vec<_>>(),
by_token.iter().map(|m| m.start).collect::<Vec<_>>(),
"an unqualified seam reads the token stream"
);
assert!(!by_token.is_empty(), "the token stream is segmented");
assert_ne!(
by_byte.iter().map(|m| m.start).collect::<Vec<_>>(),
by_token.iter().map(|m| m.start).collect::<Vec<_>>(),
"byte and token grain disagree about where the breaks are"
);
let units = crate::supertoken::supertokens_from(&crate::lexer::lex(input.as_bytes()), input.as_bytes());
let heads: Vec<usize> = units.iter().map(|u| u.start).collect();
assert!(
by_super.iter().all(|m| heads.contains(&m.start)),
"a construct-grain seam keys to construct starts"
);
}
#[test]
fn only_the_grains_a_pattern_names_are_segmented() {
use crate::ast::Grain;
let p = crate::parser::parse("@seam:token \\W").expect("parses");
assert!(p.has_seam_at(Grain::Token));
assert!(!p.has_seam_at(Grain::Byte), "the byte stream is not segmented for this");
assert!(!p.has_seam_at(Grain::Super));
let plain = crate::parser::parse("@seam \\W").expect("parses");
assert!(plain.has_seam_at(Grain::Token));
assert!(!plain.has_seam_at(Grain::Byte), "the bytes are segmented only when named");
let bytes = crate::parser::parse("@seam:byte \\W").expect("parses");
assert!(bytes.has_seam_at(Grain::Byte));
assert!(!bytes.has_seam_at(Grain::Token));
}
#[test]
fn the_observation_axis_takes_a_grain_the_same_way() {
use crate::ast::Grain;
let p = crate::parser::parse("@ambiguous:token \\W").expect("parses");
assert!(p.has_observation_at(Grain::Token));
assert!(!p.has_observation_at(Grain::Byte), "only the named stream is read");
let plain = crate::parser::parse("@ambiguous \\W").expect("parses");
assert!(plain.has_observation_at(Grain::Byte));
assert!(!plain.has_observation_at(Grain::Token));
let input = "let x = 1 ; print x ; let yy = 22 ; print yy ;";
let all = run("\\W", input).len();
for pat in ["@ambiguous \\W", "@ambiguous:token \\W", "@ambiguous:super \\W"] {
assert!(run(pat, input).len() <= all, "{pat} selects from the words");
}
assert!(crate::parser::parse("@ambiguous:nonesuch \\W").is_err());
}
#[test]
fn an_unknown_grain_is_refused() {
assert!(crate::parser::parse("@seam:token \\W").is_ok());
assert!(crate::parser::parse("@seam:super \\W").is_ok());
assert!(crate::parser::parse("@seam:byte \\W").is_ok());
let err = crate::parser::parse("@seam:nonesuch \\W").expect_err("refused");
assert!(format!("{err:?}").contains("nonesuch"), "names it: {err:?}");
}
#[test]
fn an_unknown_supertoken_role_is_refused() {
assert!(crate::parser::parse("@super:call \\N").is_ok());
assert!(crate::parser::parse("@super").is_ok());
let err = crate::parser::parse("@super:nonesuch \\N").expect_err("refused");
assert!(format!("{err:?}").contains("nonesuch"), "names it: {err:?}");
}
#[test]
fn uuid_is_spelled_out_and_g_is_the_anchor() {
let id = "550e8400-e29b-41d4-a716-446655440000";
assert_eq!(run("\\{uuid}", id).len(), 1, "the long spelling reads a uuid");
assert!(crate::parser::parse("\\G").is_ok());
}
#[test]
fn lazy_quantifiers_prefer_the_shorter_match() {
let input = "a q b q";
let greedy = run(".* \"q\"", input);
let lazy = run(".*? \"q\"", input);
assert_eq!(greedy.len(), 1, "greedy runs to the last terminator");
assert_eq!(&input[greedy[0].start..greedy[0].end], "a q b q");
assert_eq!(lazy.len(), 2, "lazy stops at the first, then resumes");
assert_eq!(&input[lazy[0].start..lazy[0].end], "a q");
assert_eq!(&input[lazy[1].start..lazy[1].end], "b q");
let opt_in = "a b";
let g_opt = run("\\W? \\W", opt_in);
let l_opt = run("\\W?? \\W", opt_in);
assert_eq!(&opt_in[g_opt[0].start..g_opt[0].end], "a b");
assert_eq!(&opt_in[l_opt[0].start..l_opt[0].end], "a");
}
#[test]
fn a_lazy_quantifier_over_a_branching_body_still_prefers_shorter() {
let input = "a 1 q b 2 q";
let greedy = run("(\\W | \\N)* \"q\"", input);
let lazy = run("(\\W | \\N)*? \"q\"", input);
assert_eq!(greedy.len(), 1);
assert_eq!(&input[greedy[0].start..greedy[0].end], "a 1 q b 2 q");
assert_eq!(lazy.len(), 2);
assert_eq!(&input[lazy[0].start..lazy[0].end], "a 1 q");
assert_eq!(&input[lazy[1].start..lazy[1].end], "b 2 q");
}
#[test]
fn lazy_and_greedy_accept_the_same_inputs() {
for input in ["a b c 1", "1", "a 1 b 2", "no number here", ""] {
assert_eq!(
run("\\W* \\N", input).is_empty(),
run("\\W*? \\N", input).is_empty(),
"greedy and lazy must agree on acceptance for {input:?}"
);
}
}
#[test]
fn anchors_route_by_what_they_read() {
for src in ["\\A \\W", "\\z \\W", "^ \\W", "$ \\W", "^ $ \\W"] {
let p = parse(src).expect("parses");
assert!(
crate::nfa::scan_nfa(&p, b"a b\nc d\n").is_some(),
"the single-pass engine must handle {src:?}"
);
}
for src in ["@seam \\W", "@nested>1 \\W", "@novel \\W", "@echoed \\W"] {
let p = parse(src).expect("parses");
assert!(
crate::nfa::scan_nfa(&p, b"a b\nc d\n").is_none(),
"the single-pass engine must decline {src:?}"
);
}
}
#[test]
fn both_engines_agree_on_positional_anchors() {
for src in ["\\A \\W", "\\z \\W", "^ \\W", "$ \\W", "^ $ \\W", "^ \\W | \\z \\N"] {
let p = parse(src).expect("parses");
for inp in [
"",
"a",
"alpha beta\ngamma delta\n",
" indented\nplain\n",
"solo\n",
"a b c\n\n d e\n",
"1\nx 2\n",
] {
let bytes = inp.as_bytes();
let nfa = crate::nfa::scan_nfa(&p, bytes).expect("the linear engine handles it");
let set = scan_set_reachability(&p, bytes);
assert_eq!(nfa, set, "engines differ on {src:?} over {inp:?}");
}
}
}
#[test]
fn an_input_anchor_ignores_an_enclosing_group() {
assert_eq!(run("\\A \\B(\\W)", "(a) x").len(), 1);
assert!(run("\\A \\B(\\W)", "x (a)").is_empty());
assert!(run("\\B(\\A \\W)", "(a) x").is_empty());
}
#[test]
fn mac_keeps_its_named_atom_after_a_lost_its_letter() {
let m = run("\\{mac}", "nic 01:23:45:67:89:ab up");
assert_eq!(m.len(), 1);
assert_eq!(&"nic 01:23:45:67:89:ab up"[m[0].start..m[0].end], "01:23:45:67:89:ab");
}
#[test]
fn the_three_alternation_modes_differ_as_documented() {
assert_eq!(run("(\"a\" | \"a\" \"b\") \"c\"", "a b c").len(), 1);
assert_eq!(run("\\W | \\W \\W", "a b").len(), 2);
let longest = run("\\W || \\W \\W", "a b");
assert_eq!(longest.len(), 1);
assert_eq!(&"a b"[longest[0].start..longest[0].end], "a b");
assert!(run("(\"a\" |> \"a\" \"b\") \"c\"", "a b c").is_empty());
assert_eq!(run("(\"a\" \"b\" |> \"a\") \"c\"", "a b c").len(), 1);
}
#[test]
fn alternation_agrees_across_the_router_inside_a_balanced_group() {
assert_eq!(run("\\B(\\W | \\W \\W)", "(a b)").len(), 1);
assert_eq!(run("\\B((\"a\" | \"a\" \"b\") \"c\")", "(a b c)").len(), 1);
assert!(run("\\B(\\W |> \\W \\W)", "(a b)").is_empty());
}
#[test]
fn mixing_alternation_kinds_at_one_level_is_a_parse_error() {
let e = parse("\\N | \\W || \\Q").unwrap_err();
assert!(e.msg.contains("mixed alternation kinds"), "got {:?}", e.msg);
assert!(parse("\\N | (\\W || \\Q)").is_ok());
assert!(parse("(\\N | \\W) || \\Q").is_ok());
}
#[test]
fn a_bare_slash_is_still_a_literal() {
let hay = "<div>hi</div>";
let m = run("<\\W:t>.*</=t>", hay);
assert_eq!(m.len(), 1);
assert_eq!(m[0].group("t", hay.as_bytes()), Some(b"div" as &[u8]));
}
#[test]
fn explicit_whitespace_atoms_match() {
assert_eq!(run(r"\W \S \W", "a b").len(), 1, "\\S between atoms");
assert_eq!(run(r"\W \s \W", "a b").len(), 1, "\\s between atoms");
assert_eq!(run(r"\W \S \W \S \W", "a b").len(), 0);
}
#[test]
fn lowercase_byte_class_atoms() {
let hex: Vec<_> = run("\\h", "deadbeef 123 xyz").iter().map(|m| m.end - m.start).collect();
assert_eq!(hex.len(), 2, "deadbeef and 123 are all-hex; xyz is not");
assert_eq!(run("\\u", "ABC def GHI").len(), 2);
assert_eq!(run("\\l", "ABC def GHI").len(), 1);
assert_eq!(run("\\a", "abc d3f ghi").iter().map(|m| m.end - m.start).sum::<usize>(), 6);
}
#[test]
fn spectral_atom_parses_every_predicate_form() {
for ok in ["\\F{entropy>0.8}", "\\F{entropy<0.3}", "\\F{period=4}", "\\F{period:line}", "\\F{texture:code}", "\\F{texture:prose}", "\\F{onset}"] {
assert!(parse(ok).is_ok(), "should parse: {ok}");
}
for bad in ["\\F{bogus}", "\\F{entropy}", "\\F{texture:nope}", "\\F{period=x}"] {
assert!(parse(bad).is_err(), "should reject: {bad}");
}
}
#[test]
fn spectral_texture_atom_discriminates_code_from_prose() {
let code = "fn add(a,b){let c=a+b;return c*2;} impl P{fn n(&self){self.x*self.x+self.y*self.y}}";
let prose = "the quick brown fox jumps over the lazy dog near the old stone bridge in the cool air";
let code_hits = run("\\F{texture:code}", code).len();
let prose_hits = run("\\F{texture:code}", prose).len();
assert!(code_hits > prose_hits, "code {code_hits} should exceed prose {prose_hits}");
assert!(code_hits > 0, "code texture should match in code");
}
#[test]
fn matches_number_then_word() {
let m = run("\\N \\W", "weight 12 items here");
assert_eq!(m.len(), 1);
assert_eq!(&"weight 12 items here"[m[0].start..m[0].end], "12 items");
assert!(run("\\N \\W", "weight 12 kg here").is_empty(), "`12 kg` is one quantity token");
}
#[test]
fn matched_tag_binds_and_checks() {
let hay = "<div>hi</div>";
let ok = run("<\\W:t>.*</=t>", hay);
assert_eq!(ok.len(), 1);
assert_eq!(&hay[ok[0].start..ok[0].end], "<div>hi</div>");
assert_eq!(ok[0].group("t", hay.as_bytes()), Some(b"div" as &[u8]));
let bad = run("<\\W:t>.*</=t>", "<div>hi</span>");
assert!(bad.is_empty(), "mismatched tag must not match");
}
#[test]
fn atom_bind_through_set_engine_captures() {
let hay = "tag (5) rest";
let m = run("\\W:t \\B(\\N)", hay);
assert_eq!(m.len(), 1);
assert_eq!(&hay[m[0].start..m[0].end], "tag (5)");
assert_eq!(m[0].group("t", hay.as_bytes()), Some(b"tag" as &[u8]));
let hay2 = "alpha beta (7)";
let m2 = run("\\W:t \\W:u \\B(\\N)", hay2);
assert_eq!(m2.len(), 1);
assert_eq!(m2[0].group("t", hay2.as_bytes()), Some(b"alpha" as &[u8]));
assert_eq!(m2[0].group("u", hay2.as_bytes()), Some(b"beta" as &[u8]));
}
#[test]
fn repeated_token_matches_only_a_repeat() {
let ok = run("\\W:x =x", "the the cat");
assert_eq!(ok.len(), 1);
assert_eq!(&"the the cat"[ok[0].start..ok[0].end], "the the");
let none = run("\\W:x =x", "the cat sat");
assert!(none.is_empty());
}
#[test]
fn scoped_binding_does_not_leak_out_of_its_group() {
let global = run("\\B(\\W:x) =x", "(cat) cat");
assert_eq!(global.len(), 1);
assert_eq!(&"(cat) cat"[global[0].start..global[0].end], "(cat) cat");
assert!(
run("\\B(\\W::x) =x", "(cat) cat").is_empty(),
"a scoped bind must not leak out of its group"
);
assert_eq!(run("\\B(\\W::x =x)", "(the the)").len(), 1);
assert!(run("\\B(\\W::x =x)", "(the cat)").is_empty());
}
#[test]
fn balanced_group_handles_nesting() {
let m = run("\\W\\B(.*)", "call f(g(x)) end");
assert_eq!(m.len(), 1);
assert_eq!(&"call f(g(x)) end"[m[0].start..m[0].end], "f(g(x))");
let none = run("\\W\\B(.*)", "bare word");
assert!(none.is_empty());
}
#[test]
fn ordered_choice_matches_either() {
let a = run("\\N | \\W", "12");
assert_eq!(a.len(), 1);
let b = run("\\N | \\W", "hi");
assert_eq!(b.len(), 1);
}
#[test]
fn ordered_choice_is_committed() {
let m = run("\\W | \\W \\W", "a b");
assert_eq!(m.len(), 2);
assert_eq!(&"a b"[m[0].start..m[0].end], "a");
}
#[test]
fn guard_requires_forward_literal() {
let hit = run(". ~\"END\"", "begin END");
assert!(!hit.is_empty());
let miss = run(". ~\"END\"", "begin only");
assert!(miss.is_empty());
}
#[test]
fn parallel_scan_over_large_input_is_correct() {
let input = vec!["x"; 2000].join(" ");
let m = run("\\W:p =p", &input);
assert_eq!(m.len(), 1000);
assert_eq!(&input[m[0].start..m[0].end], "x x");
}
#[test]
fn ambiguous_anchor_fires_at_contested_points() {
let src = "the old man the boats";
let garden = run("@ambiguous .", src);
assert!(!garden.is_empty(), "a garden-path sentence has contested points");
let all = run(".", src).len();
assert!(garden.len() < all, "@ambiguous is selective, not every token");
}
#[test]
fn nested_anchor_matches_by_depth() {
let deep = run("@nested>1 .", "a f(g(x)) b");
let got: Vec<&str> = deep.iter().map(|h| &"a f(g(x)) b"[h.start..h.end]).collect();
assert_eq!(got, vec!["x"]);
let one = run("@nested>=1 .", "a (b c) d");
let g1: Vec<&str> = one.iter().map(|h| &"a (b c) d"[h.start..h.end]).collect();
assert_eq!(g1, vec!["b", "c"]);
}
#[test]
fn seam_anchor_is_selective() {
let src = "the the the the cat sat";
let all = run(".", src).len();
let seams = run("@seam .", src).len();
assert!(all >= 5, "the bare-dot baseline should match every token");
assert!(
seams >= 1 && seams < all,
"@seam should be selective: {seams} of {all} token(s), not none and not all"
);
}
#[test]
fn a_strain_anchor_takes_the_units_above_its_percentile() {
let src = format!("{}zqxj {}", "abcd ".repeat(400), "abcd ".repeat(400));
let all = run(".", &src).len();
let high = run("@strain:byte>99.5 .", &src);
let got: Vec<&str> = high.iter().map(|m| &src[m.start..m.end]).collect();
assert!(got.contains(&"zqxj"), "the unseen word is among the most strained: {got:?}");
assert!(high.len() < all / 20, "a high percentile is selective: {} of {all}", high.len());
assert!(run("@strain:byte>1000b .", &src).is_empty(), "no byte is strained a thousand bits");
}
#[test]
fn a_bound_anchor_takes_the_weakest_cuts() {
let src = format!("{}{}", "ab ab ".repeat(300), "xy xy ".repeat(300));
let first_x = src.find('x').expect("the second run");
let weakest = run("@bound:byte<=0.5 .", &src);
assert!(
weakest.iter().any(|m| m.start == first_x),
"the seam between the runs is among the weakest cuts: {:?}",
weakest.iter().map(|m| m.start).collect::<Vec<_>>()
);
let all = run(".", &src).len();
assert!(weakest.len() < all / 10, "a low percentile is selective: {} of {all}", weakest.len());
}
#[test]
fn a_kin_anchor_takes_its_example_and_what_the_field_places_with_it() {
let src = "alpha beta; gamma(delta)\n".repeat(200);
let a_words = run("@kin:byte(\"a\") .", &src);
let got: Vec<&str> = a_words.iter().map(|m| &src[m.start..m.end]).collect();
assert!(got.contains(&"alpha"), "a token opening with the example's byte is kin to it: {got:?}");
assert!(run("@kin:byte(\"~\") .", &src).is_empty(), "an example the input never holds matches nothing");
}
#[test]
fn a_kin_reference_takes_a_token_the_field_places_with_the_bound_one() {
use crate::ast::{Atom, Grain, Pattern};
assert_eq!(crate::parse("=kin a").expect("parses"), Pattern::Atom(Atom::RegisterKin("a".into(), Grain::Token)));
assert_eq!(
crate::parse("=kin:super a").expect("parses"),
Pattern::Atom(Atom::RegisterKin("a".into(), Grain::Super))
);
assert!(
matches!(crate::parse("=kin").expect("parses"), Pattern::Atom(Atom::RegisterEq(name, _)) if name == "kin"),
"with no register after it, kin is a register's name"
);
assert_eq!(run("(\\W):a =kin a", "x y").len(), 1);
assert!(run("(\\W):a =kin a", "x ;").is_empty(), "a word and a semicolon are not kin");
}
#[test]
fn a_phase_anchor_names_a_period_by_length_or_by_rank() {
use crate::ast::{AnchorKind, Pattern, PeriodRef};
assert_eq!(crate::parse("@phase:2/6").expect("parses"), Pattern::Anchor(AnchorKind::PhaseIn(2, PeriodRef::Length(6))));
assert_eq!(crate::parse("@phase:1#2").expect("parses"), Pattern::Anchor(AnchorKind::PhaseIn(1, PeriodRef::Rank(2))));
assert!(crate::parse("@phase:6/6").is_err(), "a column lies below its period");
assert!(crate::parse("@phase:0/40").is_err(), "a period lies within the lags searched");
assert!(crate::parse("@phase:0#0").is_err(), "the strongest period is #1");
assert!(crate::parse("@phase:40").is_err(), "no column lies past the longest period");
let src = format!("{}{}", "k = 1 ;\n".repeat(150), "k = 1 , 2 ;\n".repeat(150));
assert!(!run("@phase:0/4 .", &src).is_empty(), "column 0 of the 4-token period holds");
assert!(!run("@phase:0/6 .", &src).is_empty(), "column 0 of the 6-token period holds");
assert!(run("@phase:0/5 .", &src).is_empty(), "no 5-token period lives here");
assert_eq!(
run("@phase:0#1 .", &src).len(),
run("@phase:0 .", &src).len(),
"#1 is the strongest period, the one plain @phase counts in"
);
}
#[test]
fn the_gravity_anchors_parse_their_thresholds_and_refuse_what_names_nothing() {
use crate::ast::{AnchorKind, Cmp, Grain, GravityReading, Level, Pattern, Real};
let p = crate::parse("@strain>90").expect("a percentile");
assert_eq!(
p,
Pattern::Anchor(AnchorKind::Gravity(
GravityReading::Strain,
Grain::Token,
Cmp::Gt,
Level::Percentile(Real::new(90.0))
))
);
let p = crate::parse("@bound:super<=-1.5b").expect("a negative value in bits");
assert_eq!(
p,
Pattern::Anchor(AnchorKind::Gravity(GravityReading::Bound, Grain::Super, Cmp::Le, Level::Bits(Real::new(-1.5))))
);
assert!(crate::parse("@strain").is_err(), "a comparison is required");
assert!(crate::parse("@strain>150").is_err(), "a percentile runs 0 to 100");
assert!(crate::parse("@strain>150b").is_ok(), "bits are not a percentile");
assert!(crate::parse("@strain:bytes>1").is_err(), "an unknown grain is refused");
assert!(crate::parse("@kin:byte(\"ab\")").is_err(), "a byte-grain example is one byte");
assert!(crate::parse("@kin(\" \")").is_err(), "an example of whitespace names no token");
}
#[test]
fn silhouette_matches_by_structural_form() {
let src = "foo(a,b) and bar(x,y) but baz(1) and qux(p,q,r)";
let m = run("#\"W(W,W)\"", src);
let got: Vec<&str> = m.iter().map(|h| &src[h.start..h.end]).collect();
assert_eq!(got, vec!["foo(a,b)", "bar(x,y)"]);
}
#[test]
fn negative_guard_requires_absence() {
let src = "run 7 fail 9 pass";
let neg = run("\\N !~\"fail\"", src);
let got: Vec<&str> = neg.iter().map(|h| &src[h.start..h.end]).collect();
assert_eq!(got, vec!["9"], "only the number with no 'fail' after it");
let pos = run("\\N ~\"fail\"", src);
let gotp: Vec<&str> = pos.iter().map(|h| &src[h.start..h.end]).collect();
assert_eq!(gotp, vec!["7"]);
}
#[test]
fn shape_backreference_is_a_fuzzy_repeat() {
let src = "cat dog and pin bad the sky";
let m = run("\\W:x =shape x", src);
let got: Vec<&str> = m.iter().map(|h| &src[h.start..h.end]).collect();
assert_eq!(got, vec!["cat dog", "pin bad"]);
let exact = run("\\W:x =x", src);
assert!(exact.is_empty(), "no adjacent exact word repeat in {src:?}");
}
#[test]
fn case_backreference_matches_across_case() {
let src = "Foo foo bar BAZ baz";
let m = run("\\W:x =case x", src);
let got: Vec<&str> = m.iter().map(|h| &src[h.start..h.end]).collect();
assert_eq!(got, vec!["Foo foo", "BAZ baz"]);
}
#[test]
fn magnitude_atom_parses_every_predicate_form() {
for ok in ["\\M{>6}", "\\M{>=6}", "\\M{<3}", "\\M{<=3}", "\\M{mag>6}", "\\M{ mag >= 9 }"] {
assert!(parse(ok).is_ok(), "should parse: {ok}");
}
for bad in ["\\M{}", "\\M{6}", "\\M{>x}", "\\M{bogus}"] {
assert!(parse(bad).is_err(), "should reject: {bad}");
}
}
#[test]
fn magnitude_matches_numbers_by_scale() {
let src = "a 5 b 5000000000 c 42 d 999999999999";
let m = run("\\M{>6}", src);
let got: Vec<&str> = m.iter().map(|h| &src[h.start..h.end]).collect();
assert_eq!(got, vec!["5000000000", "999999999999"]);
}
#[test]
fn kind_magnitude_intersects_kind_and_scale() {
let longword = "a".repeat(70);
let src = format!("5000000000 42 {longword}");
let n: Vec<String> =
run("\\N{mag>6}", &src).iter().map(|h| src[h.start..h.end].to_string()).collect();
assert_eq!(n, vec!["5000000000".to_string()]);
let m: Vec<String> =
run("\\M{>6}", &src).iter().map(|h| src[h.start..h.end].to_string()).collect();
assert!(m.contains(&longword), "\\M{{>6}} is kind-blind and takes the long word");
let rep = run("\\N{2}", "a 1 2 3 b");
assert_eq!(&"a 1 2 3 b"[rep[0].start..rep[0].end], "1 2");
}
#[test]
fn magnitude_le_matches_small_tokens() {
let m = run("\\M{<3}", "tiny 5 huge 5000000000 mid 900");
assert!(m.iter().all(|h| &"tiny 5 huge 5000000000 mid 900"[h.start..h.end] != "5000000000"));
assert!(m.iter().any(|h| &"tiny 5 huge 5000000000 mid 900"[h.start..h.end] == "900"));
}
#[test]
fn kv_lens_matches_both_separators() {
let m = run("@kv", "name: value x=5");
let got: Vec<&str> = m.iter().map(|h| &"name: value x=5"[h.start..h.end]).collect();
assert_eq!(got, vec!["name:", "x="]);
}
#[test]
fn flag_lens_matches_short_and_long() {
let m = run("@flag", "run -x --verbose -5 end");
let got: Vec<&str> = m.iter().map(|h| &"run -x --verbose -5 end"[h.start..h.end]).collect();
assert_eq!(got, vec!["-x", "--verbose"]);
}
#[test]
fn list_lens_requires_a_comma() {
let m = run("@list", "items a, b, c but lone stands");
assert_eq!(m.len(), 1);
assert_eq!(&"items a, b, c but lone stands"[m[0].start..m[0].end], "a, b, c");
}
#[test]
fn range_lens_joins_numbers_not_clocks() {
let src = "span 1..10 and 3-7 and 2:9 but 12:30 clock";
let m = run("@range", src);
let got: Vec<&str> = m.iter().map(|h| &src[h.start..h.end]).collect();
assert_eq!(got, vec!["1..10", "3-7", "2:9"]);
}
#[test]
fn field_addressing_anchors_to_csv_field() {
let m = run("@3 \"ERROR\"", "a, b, ERROR");
assert_eq!(m.len(), 1);
assert_eq!(&"a, b, ERROR"[m[0].start..m[0].end], "ERROR");
assert!(run("@3 \"ERROR\"", "a, b, OK").is_empty());
let m2 = run("@2 \\W", "a, hello, c");
assert_eq!(m2.len(), 1);
assert_eq!(&"a, hello, c"[m2[0].start..m2[0].end], "hello");
}
#[test]
fn line_start_anchor_selects_only_line_leading_tokens() {
let input = "cat file\nrun cat\ncat again";
let m = run(r#"^ "cat""#, input);
assert_eq!(m.len(), 2, "two lines LEAD with cat, one merely mentions it");
assert_eq!(m[0].start, 0);
assert_eq!(&input[m[1].start..m[1].end], "cat");
assert!(m[1].start > input.find("run").unwrap(), "the third line's cat");
assert_eq!(run(r#"^ "cat""#, " \n\t cat x").len(), 1);
assert_eq!(run(r#""cat""#, input).len(), 3);
assert_eq!(run(r#"^ "cat""#, "run cat here").len(), 0);
}
#[test]
fn line_end_anchor_selects_only_line_trailing_tokens() {
let input = "run cat\ncat file\nx cat";
let m = run(r#"$ "cat""#, input);
assert_eq!(m.len(), 2, "two lines END with cat");
assert_eq!(run(r#"$ "cat""#, "cat trailing spaces ").len(), 0);
assert_eq!(run(r#"$ "cat""#, "ends with cat ").len(), 1, "trailing space still ends the line");
}
#[test]
fn anchors_compose_with_alternation_into_a_position_class() {
let pat = r#"(^ | "|" | ";") "cat""#;
assert_eq!(run(pat, "cat x").len(), 1, "start of input");
assert_eq!(run(pat, "a | cat x").len(), 1, "after a pipe");
assert_eq!(run(pat, "a ; cat x").len(), 1, "after a separator");
assert_eq!(run(pat, "echo the cat sat").len(), 0, "mid-line mention");
}
#[test]
fn a_token_alone_on_its_line_both_leads_and_ends_it() {
assert_eq!(run(r#"^ $ "cat""#, "x\ncat\ny").len(), 1);
assert_eq!(run(r#"^ $ "cat""#, "x\ncat y\nz").len(), 0);
}
fn byte_spans(ms: &[Span]) -> Vec<(usize, usize)> {
ms.iter().map(|m| (m.start(), m.end())).collect()
}
#[test]
fn a_greedy_loop_over_a_lazy_body_keeps_the_preferred_derivation() {
let p = parse("(.+?)+").unwrap();
let input = "956 116 bar";
let set = scan(&p, input.as_bytes());
let single = crate::nfa::scan_nfa(&p, input.as_bytes())
.expect("the single-pass engine takes this pattern");
assert_eq!(byte_spans(&set), byte_spans(&single), "the two engines rank this alike");
assert_eq!(set.len(), 1, "one match spanning the input, not one per token");
}
#[test]
fn a_greedy_option_prefers_a_nullable_body_matching_empty() {
let p = parse(". (.{0,2}?)?").unwrap();
let input = "488 foo 786 qux ";
let set = scan(&p, input.as_bytes());
let single = crate::nfa::scan_nfa(&p, input.as_bytes())
.expect("the single-pass engine takes this pattern");
assert_eq!(byte_spans(&set), byte_spans(&single), "the two engines rank this alike");
assert_eq!(set.len(), 4, "one match per token, not one per two");
}
}