use std::collections::{HashMap, HashSet};
use std::hash::{Hash, Hasher};
use pest::error::{Error, ErrorVariant};
use super::{is_non_failing, is_non_progressing, to_hash_map};
use crate::parser::{ParserExpr, ParserNode, ParserRule, Rule};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
enum StackBase {
Empty,
Blank,
Any,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct AbsStack {
base: StackBase,
len: u8,
blank: u8,
base_intact: bool,
}
impl AbsStack {
const MAX_LEN: u8 = 8;
const UNKNOWN: AbsStack = AbsStack {
base: StackBase::Any,
len: 0,
blank: 0,
base_intact: false,
};
fn mask(len: u8) -> u8 {
((1u16 << len) - 1) as u8
}
fn join(self, other: AbsStack) -> AbsStack {
if self == other {
return self;
}
let base = match (self.base, other.base) {
(a, b) if a == b => a,
(StackBase::Any, _) | (_, StackBase::Any) => StackBase::Any,
_ => StackBase::Blank,
};
if self.len == other.len {
AbsStack {
base,
len: self.len,
blank: self.blank & other.blank,
base_intact: self.base_intact && other.base_intact,
}
} else if base != StackBase::Any && self.all_blank() && other.all_blank() {
AbsStack {
base: StackBase::Blank,
len: 0,
blank: 0,
base_intact: false,
}
} else {
AbsStack::UNKNOWN
}
}
fn push(self, blank: bool) -> AbsStack {
if self.len < AbsStack::MAX_LEN {
AbsStack {
len: self.len + 1,
blank: self.blank | (u8::from(blank) << self.len),
..self
}
} else {
AbsStack::UNKNOWN
}
}
fn all_blank(self) -> bool {
self.base != StackBase::Any && self.blank == AbsStack::mask(self.len)
}
fn is_empty(self) -> bool {
self.base == StackBase::Empty && self.len == 0
}
fn non_empty(self) -> bool {
self.len > 0
}
fn top_blank(self) -> bool {
self.len > 0 && self.blank >> (self.len - 1) & 1 == 1
}
fn popped_all(self) -> AbsStack {
AbsStack {
base: StackBase::Empty,
len: 0,
blank: 0,
base_intact: self.base_intact && self.base == StackBase::Empty,
}
}
fn popped_one(self) -> AbsStack {
if self.len > 0 {
AbsStack {
len: self.len - 1,
blank: self.blank & AbsStack::mask(self.len - 1),
..self
}
} else {
match self.base {
StackBase::Empty => self,
StackBase::Blank => AbsStack {
base: StackBase::Blank,
len: 0,
blank: 0,
base_intact: false,
},
StackBase::Any => AbsStack::UNKNOWN,
}
}
}
fn same_top(self, other: AbsStack) -> bool {
self.len == other.len && self.blank == other.blank
}
}
type Maybe = Option<AbsStack>;
fn join(a: Maybe, b: Maybe) -> Maybe {
match (a, b) {
(Some(a), Some(b)) => Some(a.join(b)),
(a, None) => a,
(None, b) => b,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Outcome {
ok: Maybe,
err: Maybe,
empty: bool,
}
impl Outcome {
const UNKNOWN: Outcome = Outcome {
ok: Some(AbsStack::UNKNOWN),
err: Some(AbsStack::UNKNOWN),
empty: false,
};
fn succeeds(s: AbsStack, empty: bool) -> Outcome {
Outcome {
ok: Some(s),
err: None,
empty,
}
}
fn fails(s: AbsStack) -> Outcome {
Outcome {
ok: None,
err: Some(s),
empty: false,
}
}
fn either(s: AbsStack) -> Outcome {
Outcome {
ok: Some(s),
err: Some(s),
empty: false,
}
}
fn exact(self) -> Maybe {
if self.err.is_none() && self.empty {
self.ok
} else {
None
}
}
}
pub(super) fn validate_stack_repetition<'a, 'i: 'a>(
rules: &'a [ParserRule<'i>],
) -> Vec<Error<Rule>> {
let map = to_hash_map(rules);
let mut seen = HashSet::new();
if ["WHITESPACE", "COMMENT"]
.iter()
.any(|name| rule_modifies_stack(name, &map, &mut seen))
{
return vec![];
}
let mut analysis = StackAnalysis {
rules: &map,
skips: map.contains_key("WHITESPACE") || map.contains_key("COMMENT"),
memo: HashMap::new(),
active: HashSet::new(),
rule_memo: HashMap::new(),
rule_active: HashSet::new(),
errors: vec![],
reported: HashSet::new(),
walking: false,
walked: HashSet::new(),
};
let entry = AbsStack {
base: StackBase::Any,
len: 0,
blank: 0,
base_intact: true,
};
for rule in rules {
analysis.walk(&rule.node, entry);
}
analysis.errors
}
fn rule_modifies_stack(
name: &str,
rules: &HashMap<String, &ParserNode<'_>>,
seen: &mut HashSet<String>,
) -> bool {
match name {
"POP" | "DROP" | "POP_ALL" => true,
_ => match rules.get(name) {
Some(node) if seen.insert(name.to_owned()) => {
expr_modifies_stack(&node.expr, rules, seen)
}
_ => false,
},
}
}
fn expr_modifies_stack(
expr: &ParserExpr<'_>,
rules: &HashMap<String, &ParserNode<'_>>,
seen: &mut HashSet<String>,
) -> bool {
match expr {
ParserExpr::Push(_) => true,
#[cfg(feature = "grammar-extras")]
ParserExpr::PushLiteral(_) => true,
ParserExpr::Ident(name) => rule_modifies_stack(name, rules, seen),
ParserExpr::Str(_)
| ParserExpr::Insens(_)
| ParserExpr::Range(_, _)
| ParserExpr::PeekSlice(_, _) => false,
ParserExpr::PosPred(_) | ParserExpr::NegPred(_) => false,
ParserExpr::Seq(lhs, rhs) | ParserExpr::Choice(lhs, rhs) => {
expr_modifies_stack(&lhs.expr, rules, seen)
|| expr_modifies_stack(&rhs.expr, rules, seen)
}
ParserExpr::Opt(inner)
| ParserExpr::Rep(inner)
| ParserExpr::RepOnce(inner)
| ParserExpr::RepExact(inner, _)
| ParserExpr::RepMin(inner, _)
| ParserExpr::RepMax(inner, _)
| ParserExpr::RepMinMax(inner, _, _) => expr_modifies_stack(&inner.expr, rules, seen),
#[cfg(feature = "grammar-extras")]
ParserExpr::NodeTag(inner, _) => expr_modifies_stack(&inner.expr, rules, seen),
}
}
struct ByAddr<'a, T>(&'a T);
impl<T> Clone for ByAddr<'_, T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for ByAddr<'_, T> {}
impl<T> PartialEq for ByAddr<'_, T> {
fn eq(&self, other: &Self) -> bool {
std::ptr::eq(self.0, other.0)
}
}
impl<T> Eq for ByAddr<'_, T> {}
impl<T> Hash for ByAddr<'_, T> {
fn hash<H: Hasher>(&self, state: &mut H) {
std::ptr::hash(self.0, state)
}
}
struct StackAnalysis<'a, 'i> {
rules: &'a HashMap<String, &'a ParserNode<'i>>,
skips: bool,
memo: HashMap<(ByAddr<'a, ParserExpr<'i>>, AbsStack), Outcome>,
active: HashSet<(ByAddr<'a, ParserExpr<'i>>, AbsStack)>,
rule_memo: HashMap<(String, AbsStack), Outcome>,
rule_active: HashSet<(String, AbsStack)>,
errors: Vec<Error<Rule>>,
reported: HashSet<ByAddr<'a, ParserNode<'i>>>,
walking: bool,
walked: HashSet<(ByAddr<'a, ParserExpr<'i>>, AbsStack)>,
}
impl<'a, 'i> StackAnalysis<'a, 'i> {
fn walk(&mut self, node: &'a ParserNode<'i>, s: AbsStack) {
self.walking = true;
let _ = self.visit(node, s);
self.walking = false;
}
fn visit(&mut self, node: &'a ParserNode<'i>, s: AbsStack) -> Outcome {
if self.walking && self.walked.insert((ByAddr(&node.expr), s)) {
self.step(node, s)
} else {
self.outcome(node, s)
}
}
fn outcome(&mut self, node: &'a ParserNode<'i>, s: AbsStack) -> Outcome {
let key = (ByAddr(&node.expr), s);
if let Some(&o) = self.memo.get(&key) {
return o;
}
if !self.active.insert(key) {
return Outcome::UNKNOWN;
}
let walking = std::mem::replace(&mut self.walking, false);
let o = self.step(node, s);
self.walking = walking;
let _ = self.active.remove(&key);
let _ = self.memo.insert(key, o);
o
}
fn step(&mut self, node: &'a ParserNode<'i>, s: AbsStack) -> Outcome {
match &node.expr {
ParserExpr::Str(string) | ParserExpr::Insens(string) => {
if string.is_empty() {
Outcome::succeeds(s, true)
} else {
Outcome::either(s)
}
}
ParserExpr::Range(_, _) => Outcome::either(s),
ParserExpr::Ident(name) => self.ident(name, s),
ParserExpr::PeekSlice(0, None) => self.ident("PEEK_ALL", s),
ParserExpr::PeekSlice(0, Some(0)) => Outcome::succeeds(s, true),
ParserExpr::PeekSlice(_, _) => Outcome::either(s),
ParserExpr::PosPred(inner) => {
let o = self.visit(inner, s);
Outcome {
ok: o.ok.map(|_| s),
err: o.err.map(|_| s),
empty: true,
}
}
ParserExpr::NegPred(inner) => {
let o = self.visit(inner, s);
Outcome {
ok: o.err.map(|_| s),
err: o.ok.map(|_| s),
empty: true,
}
}
ParserExpr::Seq(lhs, rhs) => {
let l = self.visit(lhs, s);
let r = match l.ok {
Some(mid) => self.visit(rhs, mid),
None => Outcome {
ok: None,
err: None,
empty: true,
},
};
Outcome {
ok: r.ok,
err: if l.err.is_some() || r.err.is_some() {
Some(s)
} else {
None
},
empty: l.empty && r.empty && !self.skips,
}
}
ParserExpr::Choice(lhs, rhs) => {
let l = self.visit(lhs, s);
let r = match self.failed(l.err, s) {
Some(failed) => self.visit(rhs, failed),
None => Outcome {
ok: None,
err: None,
empty: true,
},
};
Outcome {
ok: join(l.ok, r.ok),
err: self.failed(r.err, s),
empty: (l.ok.is_none() || l.empty) && (r.ok.is_none() || r.empty),
}
}
ParserExpr::Opt(inner) => {
let o = self.visit(inner, s);
Outcome {
ok: join(o.ok, self.failed(o.err, s)),
err: None,
empty: o.ok.is_none() || o.empty,
}
}
ParserExpr::Rep(inner) => self.repeat(node, inner, s, 0, None),
ParserExpr::RepOnce(inner) => self.repeat(node, inner, s, 1, None),
ParserExpr::RepMin(inner, min) => self.repeat(node, inner, s, *min, None),
ParserExpr::RepExact(inner, n) => self.repeat(node, inner, s, *n, Some(*n)),
ParserExpr::RepMax(inner, max) => self.repeat(node, inner, s, 0, Some(*max)),
ParserExpr::RepMinMax(inner, min, max) => {
self.repeat(node, inner, s, (*min).min(*max), Some(*max))
}
ParserExpr::Push(inner) => {
let o = self.visit(inner, s);
Outcome {
ok: o.ok.map(|after| after.push(o.empty)),
err: o.err,
empty: o.empty,
}
}
#[cfg(feature = "grammar-extras")]
ParserExpr::PushLiteral(string) => Outcome::succeeds(s.push(string.is_empty()), true),
#[cfg(feature = "grammar-extras")]
ParserExpr::NodeTag(inner, _) => self.visit(inner, s),
}
}
fn ident(&mut self, name: &str, s: AbsStack) -> Outcome {
match name {
"PEEK_ALL" => {
if s.all_blank() {
Outcome::succeeds(s, true)
} else {
Outcome::either(s)
}
}
"POP_ALL" => {
if s.all_blank() {
Outcome::succeeds(s.popped_all(), true)
} else {
Outcome {
ok: Some(s.popped_all()),
err: Some(AbsStack::UNKNOWN),
empty: false,
}
}
}
"PEEK" | "POP" => {
if s.is_empty() {
Outcome {
ok: None,
err: None,
empty: true,
}
} else {
let after = if name == "POP" { s.popped_one() } else { s };
if s.top_blank() {
Outcome::succeeds(after, true)
} else {
Outcome {
ok: Some(after),
err: Some(after),
empty: false,
}
}
}
}
"DROP" => {
if s.is_empty() {
Outcome::fails(s)
} else if s.non_empty() {
Outcome::succeeds(s.popped_one(), true)
} else {
Outcome {
ok: Some(s.popped_one()),
err: Some(s),
empty: true,
}
}
}
_ => match self.rules.get(name) {
Some(&node) => self.rule(name, node, s),
None => Outcome::either(s),
},
}
}
fn rule(&mut self, name: &str, node: &'a ParserNode<'i>, s: AbsStack) -> Outcome {
let key = (name.to_owned(), s);
if let Some(&o) = self.rule_memo.get(&key) {
return o;
}
if !self.rule_active.insert(key.clone()) {
return Outcome::UNKNOWN;
}
let walking = std::mem::replace(&mut self.walking, false);
let o = self.outcome(node, s);
self.walking = walking;
let _ = self.rule_active.remove(&key);
let _ = self.rule_memo.insert(key, o);
o
}
fn failed(&mut self, err: Maybe, s: AbsStack) -> Maybe {
err.map(|err| err.join(s))
}
fn repeat(
&mut self,
node: &'a ParserNode<'i>,
inner: &'a ParserNode<'i>,
s: AbsStack,
min: u32,
max: Option<u32>,
) -> Outcome {
if max.is_none() && self.walking && self.loops_forever(inner, s) {
self.report(node, inner);
}
let mut empty = !self.skips || max.is_some_and(|m| m <= 1);
let (mid, fails) = self.steps(inner, s, u64::from(min), true, &mut empty);
let err = fails.then_some(s);
let Some(mid) = mid else {
return Outcome {
ok: None,
err,
empty: true,
};
};
let ok = match max {
Some(m) => {
let optional = u64::from(m.saturating_sub(min));
self.steps(inner, mid, optional, false, &mut empty).0
}
None => self.greedy(inner, mid, &mut empty),
};
Outcome { ok, err, empty }
}
fn steps(
&mut self,
inner: &'a ParserNode<'i>,
s: AbsStack,
count: u64,
required: bool,
empty: &mut bool,
) -> (Maybe, bool) {
let mut trace: Vec<AbsStack> = vec![];
let mut seen: HashMap<AbsStack, usize> = HashMap::new();
let mut at = s;
let mut fails = false;
let mut t: u64 = 0;
while t < count {
if let Some(&j) = seen.get(&at) {
let period = t - j as u64;
let end = trace[j + ((count - t) % period) as usize];
return (Some(end), fails);
}
let _ = seen.insert(at, trace.len());
trace.push(at);
let o = self.visit(inner, at);
if o.ok.is_some() {
*empty &= o.empty;
}
t += 1;
let next = if required {
fails |= o.err.is_some();
o.ok
} else {
join(o.ok, self.failed(o.err, at))
};
match next {
Some(n) => at = n,
None => return (None, fails),
}
}
(Some(at), fails)
}
fn greedy(&mut self, inner: &'a ParserNode<'i>, s: AbsStack, empty: &mut bool) -> Maybe {
let mut seen = HashSet::new();
let mut ok = None;
let mut at = s;
let mut first = true;
while seen.insert(at) {
let o = self.visit(inner, at);
if o.ok.is_some() {
*empty &= o.empty;
}
if o.err.is_some() {
let restored = if first { None } else { Some(at) };
ok = join(ok, join(self.failed(o.err, at), restored));
}
first = false;
match o.ok {
Some(n) => at = n,
None => break,
}
}
ok
}
fn loops_forever(&mut self, body: &'a ParserNode<'i>, s: AbsStack) -> bool {
const MAX_STEPS: usize = 64;
let fresh = |st: AbsStack| AbsStack {
base_intact: true,
..st
};
let mut cur = fresh(s);
let mut seen = vec![cur];
for _ in 0..MAX_STEPS {
let Some(out) = self.outcome(body, cur).exact() else {
return false;
};
if out.base_intact {
if seen.iter().any(|earlier| out.same_top(*earlier)) {
return true;
}
} else {
seen.clear();
}
cur = fresh(out);
seen.push(cur);
}
false
}
fn report(&mut self, node: &'a ParserNode<'i>, inner: &'a ParserNode<'i>) {
if is_non_failing(&inner.expr, self.rules, &mut vec![])
|| is_non_progressing(&inner.expr, self.rules, &mut vec![])
{
return;
}
if self.reported.insert(ByAddr(node)) {
self.errors.push(Error::new_from_span(
ErrorVariant::CustomError {
message: "expression inside repetition is non-progressing and will repeat \
infinitely"
.to_owned(),
},
node.span,
));
}
}
}
#[cfg(test)]
mod tests {
use pest::Parser;
use pest::Span;
use crate::ast::RuleType;
use crate::parser::{consume_rules, ParserExpr, ParserNode, ParserRule, PestParser, Rule};
use crate::unwrap_or_report;
fn stack_loop_errors(input: &str) -> Vec<String> {
match consume_rules(PestParser::parse(Rule::grammar_rules, input).unwrap()) {
Ok(_) => vec![],
Err(errors) => errors
.iter()
.map(|e| format!("{:?} {}", e.line_col, e.variant.message()))
.collect(),
}
}
#[test]
#[should_panic(expected = "grammar error
--> 1:17
|
1 | a = { POP_ALL ~ PEEK_ALL* }
| ^-------^
|
= expression inside repetition is non-progressing and will repeat infinitely")]
fn peek_all_repetition_after_pop_all() {
let input = "a = { POP_ALL ~ PEEK_ALL* }";
unwrap_or_report(consume_rules(
PestParser::parse(Rule::grammar_rules, input).unwrap(),
));
}
#[test]
#[should_panic(expected = "grammar error
--> 1:7
|
1 | a = { (PUSH(\"\") ~ POP)* }
| ^---------------^
|
= expression inside repetition is non-progressing and will repeat infinitely")]
fn push_empty_then_pop_repetition() {
let input = "a = { (PUSH(\"\") ~ POP)* }";
unwrap_or_report(consume_rules(
PestParser::parse(Rule::grammar_rules, input).unwrap(),
));
}
#[test]
fn stack_repetitions_that_loop() {
for input in [
"a = { POP_ALL ~ PEEK_ALL* }",
"a = { POP_ALL ~ POP_ALL+ }",
"a = { POP_ALL ~ PEEK[..]* }",
"a = { POP_ALL ~ (PEEK_ALL ~ POP_ALL){2,} }",
"e = { PEEK_ALL } a = { POP_ALL ~ e* }",
"a = { (PUSH(\"\") ~ POP)* }",
"a = { (PUSH(\"\") ~ DROP)* }",
"a = { (PUSH(\"\") ~ PEEK ~ POP)* }",
"a = { (PUSH(\"\") ~ PUSH(\"\") ~ DROP ~ DROP)* }",
"a = { POP_ALL ~ PUSH(\"\") ~ PEEK_ALL* }",
"a = { POP_ALL ~ (DROP | PEEK_ALL*) }",
"a = { PEEK[0..0]* }",
"a = { POP_ALL ~ (DROP | (PUSH(\"\") ~ PEEK))* }",
"WHITESPACE = _{ \" \" } a = { POP_ALL ~ PEEK_ALL* }",
] {
let errors = stack_loop_errors(input);
assert_eq!(errors.len(), 1, "{input}: {errors:?}");
assert!(
errors[0].contains("non-progressing and will repeat infinitely"),
"{input}: {errors:?}"
);
}
}
#[test]
fn stack_repetitions_that_may_end() {
for input in [
"a = { PEEK_ALL* }",
"a = { POP_ALL* }",
"a = { PEEK[..]* }",
"a = { PEEK_ALL* } b = { PUSH(\"x\") ~ a }",
"a = { PUSH(ANY*) ~ PEEK_ALL* }",
"a = { POP_ALL ~ PUSH(ASCII_DIGIT+) ~ PEEK_ALL* }",
"a = { DROP* }",
"a = { POP_ALL ~ (PUSH(\"\") ~ DROP ~ DROP)* }",
"a = { POP_ALL ~ PEEK[0..1]* }",
"a = { POP_ALL ~ (PEEK_ALL ~ \"x\")* }",
"a = { (PUSH(\"x\") ~ POP)* }",
"a = { POP_ALL ~ (!\"x\" ~ PEEK_ALL)* }",
"a = { POP_ALL ~ PEEK* }",
"a = { POP_ALL ~ (PEEK_ALL | PEEK_ALL*) }",
"WHITESPACE = _{ \" \" } a = { POP_ALL ~ ((PEEK_ALL ~ PEEK_ALL) | PEEK_ALL*) }",
"a = { POP_ALL ~ (DROP ~ PEEK_ALL*)? }",
"a = { POP_ALL ~ (DROP ~ PUSH(\"\") ~ PEEK*)* }",
"a = { POP_ALL ~ PUSH(\"a\") ~ DROP ~ DROP ~ POP_ALL ~ PEEK_ALL* }",
"a = { POP_ALL ~ ((DROP ~ PUSH(\"\") ~ PEEK*) | PUSH(\"\")){,1} }",
"a = { (POP_ALL ~ PUSH(\"\"){8} ~ DROP{9} ~ PEEK_ALL*)? }",
"a = { POP_ALL ~ PUSH(\"x\"){8} ~ ((&DROP ~ PEEK_ALL* ~ DROP) | PUSH(\"\")){9} }",
"a = { POP_ALL ~ PUSH(\"x\"){8} ~ ((&DROP ~ PEEK_ALL* ~ DROP) | PUSH(\"\")){10,9} }",
"a = { PUSH(\"\") ~ (POP_ALL | PEEK*) } b = { PUSH(\"y\") ~ PUSH(\"x\") ~ a }",
"a = { POP_ALL ~ PUSH(\"x\") ~ (POP | PEEK_ALL*) }",
"a = { POP_ALL ~ PUSH(\"x\") ~ POP? ~ PEEK_ALL* }",
"a = { POP_ALL ~ PUSH(\"x\") ~ PUSH(\"\") ~ (!PUSH(\"\") | POP_ALL | (DROP ~ DROP ~ PEEK_ALL*)) }",
"WHITESPACE = _{ \" \" } a = { PUSH(\"\"{2}) ~ PEEK* }",
"WHITESPACE = _{ \" \" } b = !{ PEEK_ALL{2} } a = @{ POP_ALL ~ PUSH(b) ~ PEEK_ALL* }",
"WHITESPACE = _{ \" \" } a = { (PUSH(\"\" ~ \"\") ~ POP)* }",
"WHITESPACE = _{ \" \" } b = !{ \"\" ~ \"\" } a = @{ POP_ALL ~ PUSH(b) ~ PEEK_ALL* }",
] {
assert_eq!(stack_loop_errors(input), Vec::<String>::new(), "{input}");
}
}
#[test]
fn stack_repetitions_that_loop_but_are_not_detected() {
for input in [
"a = { (PUSH(\"\") ~ PEEK)* }",
"WHITESPACE = _{ \" \" } a = { (PUSH(\"\") ~ POP)* }",
] {
assert_eq!(stack_loop_errors(input), Vec::<String>::new(), "{input}");
}
}
#[test]
fn stack_of_unknown_blank_height() {
let blanks = "POP_ALL ~ ((&\"x\" ~ PUSH(\"\")) | (PUSH(\"\") ~ PUSH(\"\")))";
let errors = stack_loop_errors(&format!("a = {{ {blanks} ~ PEEK_ALL* }}"));
assert_eq!(errors.len(), 1, "{errors:?}");
let errors = stack_loop_errors(&format!("a = {{ {blanks} ~ DROP ~ PEEK_ALL* }}"));
assert_eq!(errors.len(), 1, "{errors:?}");
let errors = stack_loop_errors(&format!("a = {{ {blanks} ~ (DROP ~ DROP)* }}"));
assert_eq!(errors, Vec::<String>::new());
}
#[test]
fn stack_of_empty_strings_replaced_by_an_empty_stack() {
let blanks = "POP_ALL ~ ((&\"x\" ~ PUSH(\"\")) | (PUSH(\"\") ~ PUSH(\"\")))";
for body in ["POP_ALL ~ PUSH(\"\")", "PUSH(\"\") ~ POP_ALL"] {
let errors = stack_loop_errors(&format!("a = {{ {blanks} ~ ({body})* }}"));
assert_eq!(errors.len(), 1, "{body}: {errors:?}");
}
for choice in ["(&\"x\" ~ POP_ALL) | \"y\"", "\"y\" | (&\"x\" ~ POP_ALL)"] {
let errors = stack_loop_errors(&format!("a = {{ {blanks} ~ ({choice}) ~ PEEK_ALL* }}"));
assert_eq!(errors.len(), 1, "{choice}: {errors:?}");
let errors =
stack_loop_errors(&format!("a = {{ {blanks} ~ ({choice}) ~ (DROP ~ DROP)* }}"));
assert_eq!(errors, Vec::<String>::new(), "{choice}");
}
}
#[test]
fn stack_repetition_is_not_reported_when_whitespace_changes_the_stack() {
let input = "WHITESPACE = _{ PUSH(\" \") } a = { POP_ALL ~ PEEK_ALL* }";
assert_eq!(stack_loop_errors(input), Vec::<String>::new());
}
#[test]
fn stack_repetition_errors_once() {
for input in ["a = { \"\"* }", "a = { (POP_ALL ~ \"\")* }"] {
let errors = stack_loop_errors(input);
assert!(errors.len() <= 1, "{input}: {errors:?}");
}
assert_eq!(stack_loop_errors("a = { \"\"* }").len(), 1);
}
#[test]
fn unreachable_repetitions_are_reported_like_validate_repetition() {
assert_eq!(
stack_loop_errors("a = { \"x\" | (\"x\" ~ \"\"*) }").len(),
1
);
assert_eq!(
stack_loop_errors("a = { POP_ALL ~ (\"x\" | (\"x\" ~ PEEK_ALL*)) }").len(),
1
);
}
#[test]
fn large_repetition_bounds_are_cheap() {
let start = std::time::Instant::now();
assert_eq!(stack_loop_errors("a = { \"\"{1000000000,} }").len(), 1);
assert_eq!(
stack_loop_errors("a = { (PUSH(\"\") ~ POP){4294967295,} }").len(),
1
);
assert_eq!(
stack_loop_errors("a = { POP_ALL ~ (PUSH(\"\") ~ DROP){4294967295} ~ \"x\" }").len(),
0
);
assert!(start.elapsed().as_secs() < 2, "{:?}", start.elapsed());
}
#[test]
fn nested_bounded_repetitions_are_linear() {
let mut body = "(DROP | PUSH(\"\"))".to_owned();
for _ in 0..24 {
body = format!("({body}){{3}}");
}
let input = format!("a = {{ POP_ALL ~ {body} }}");
let start = std::time::Instant::now();
assert_eq!(stack_loop_errors(&input), Vec::<String>::new());
assert!(start.elapsed().as_secs() < 2, "{:?}", start.elapsed());
}
#[test]
fn long_sequences_are_linear() {
let span = Span::new("a", 0, 1).unwrap();
let node = |expr| ParserNode { expr, span };
let mut seq = node(ParserExpr::Ident("POP_ALL".to_owned()));
for _ in 0..4096 {
seq = node(ParserExpr::Seq(
Box::new(seq),
Box::new(node(ParserExpr::Str("a".to_owned()))),
));
}
let peek_all = node(ParserExpr::Ident("PEEK_ALL".to_owned()));
let body = node(ParserExpr::Seq(
Box::new(seq),
Box::new(node(ParserExpr::Rep(Box::new(peek_all)))),
));
let rules = vec![ParserRule {
name: "a".to_owned(),
span,
ty: RuleType::Normal,
node: body,
}];
let elapsed = std::thread::Builder::new()
.stack_size(512 * 1024 * 1024)
.spawn(move || {
let start = std::time::Instant::now();
assert_eq!(super::validate_stack_repetition(&rules).len(), 1);
start.elapsed()
})
.unwrap()
.join()
.unwrap();
assert!(elapsed.as_secs() < 2, "{elapsed:?}");
}
}