blob: 17bcbf0a2bf075aa1288ac47f8f8fa4d9f8f94a9 [file] [edit]
//! Permutation-based tests.
//!
//! This attempts to generate exhaustive coverage of the grammar by using
//! permutations of all of the possible inputs to the grammar. This includes
//! both valid and invalid inputs (particularly those that are truncated).
//!
//! It generates representative inputs for some of the expressions. For
//! example, a repetition generates an output that includes 0, 1, or 2
//! repetitions of the expression. Or something like "Identifier" just does a
//! few representative values like "a", "ab", and "abb" (with the assumption
//! that the Identifier grammar is already correct).
//!
//! This uses a state machine and is driven using the `Iterator` API to fetch
//! each new input to test.
//!
//! This is incomplete, and I'm not entirely happy with the design. This
//! misses some inputs, particularly invalid ones with unexpected inputs. I
//! intended to spend more time reading
//! https://www.fuzzingbook.org/html/Grammars.html to think of better
//! strategies to stress the parser.
//!
//! However, a coverage-based fuzzer wouldn't necessarily give all the input
//! that I would want because the point of this tool is to compare against
//! rustc. The fuzzer would be fuzzing the Reference grammar, not the rustc
//! parser. We want to get full coverage of *both* those parsers. A fuzzer
//! based on just the Reference coverage wouldn't ensure that the Reference
//! isn't missing something.
//!
//! Known issues:
//!
//! - Permute didn't find that pm2 doesn't error on `prefix'x'` because it was
//! only generating `prefix'`. Any ideas on how to generate tests that
//! exercise this?
use grammar::{Expression, ExpressionKind, Grammar, RangeLimit};
use std::collections::HashMap;
pub struct PermutationIterator<'g> {
pub grammar: &'g Grammar,
name_context: HashMap<String, usize>,
state: IteratorState<'g>,
}
enum IteratorState<'g> {
Terminal {
value: String,
done: bool,
},
Seq {
exprs: Vec<&'g Expression>,
/// Current active length; counts down from exprs.len() to 1 to emit truncated sequences.
truncated_len: usize,
iterators: Vec<PermutationIterator<'g>>,
current_values: Vec<String>,
initialized: bool,
exhausted: bool,
},
SeqWithNamedRanges {
exprs: Vec<&'g Expression>,
named_range_indices: Vec<(usize, String, usize, usize)>, // (index, name, min, max)
current_named_values: HashMap<String, usize>,
iterators: Vec<PermutationIterator<'g>>,
current_values: Vec<String>,
exhausted: bool,
},
Alt {
iterators: Vec<PermutationIterator<'g>>,
current_index: usize,
},
Optional {
iterator: Box<PermutationIterator<'g>>,
emitted_empty: bool,
},
Repeat {
expr: &'g Expression,
include_empty: bool,
current_stage: usize, // 0 = empty (if include_empty), 1 = single, 2 = double
iterator: Option<Box<PermutationIterator<'g>>>,
},
RepeatRange {
expr: &'g Expression,
max: usize,
current_count: usize,
iterator: Option<Box<PermutationIterator<'g>>>,
pending_repeat: Option<(String, usize)>, // (value, times_left_to_emit)
},
}
impl<'g> PermutationIterator<'g> {
pub fn new(grammar: &'g Grammar, expression: &'g Expression) -> PermutationIterator<'g> {
Self::new_with_context(grammar, expression, HashMap::new())
}
fn new_with_context(
grammar: &'g Grammar,
expression: &'g Expression,
name_context: HashMap<String, usize>,
) -> PermutationIterator<'g> {
let state = match &expression.kind {
ExpressionKind::Alt(exprs) => {
let iterators: Vec<_> = exprs
.iter()
.map(|e| Self::new_with_context(grammar, e, name_context.clone()))
.collect();
IteratorState::Alt {
iterators,
current_index: 0,
}
}
ExpressionKind::Grouped(expr) => {
return Self::new_with_context(grammar, expr, name_context);
}
ExpressionKind::Sequence(exprs) => {
if exprs.is_empty() {
IteratorState::Terminal {
value: String::new(),
done: false,
}
} else {
let filtered_exprs: Vec<&Expression> = exprs
.iter()
.filter(|e| {
!matches!(
e.kind,
ExpressionKind::Break(_) | ExpressionKind::Comment(_)
)
})
.collect();
// Check if any expressions are named repeat ranges
let mut named_range_indices = Vec::new();
for (idx, expr) in filtered_exprs.iter().enumerate() {
if let ExpressionKind::RepeatRange {
name: Some(name),
min,
max,
limit,
..
} = &expr.kind
{
let min_count = min.unwrap_or(0) as usize;
let max_count = match max {
Some(m) => match limit {
RangeLimit::HalfOpen => *m as usize,
RangeLimit::Closed => (*m + 1) as usize,
},
None => min_count + 3,
};
named_range_indices.push((idx, name.clone(), min_count, max_count));
}
}
if named_range_indices.is_empty() {
// No named ranges, use regular Seq
let n = filtered_exprs.len();
IteratorState::Seq {
exprs: filtered_exprs,
truncated_len: n,
iterators: Vec::new(),
current_values: Vec::new(),
initialized: false,
exhausted: false,
}
} else {
// Has named ranges, use special handling
let current_named_values: HashMap<String, usize> = named_range_indices
.iter()
.map(|(_, name, min, _)| (name.clone(), *min))
.collect();
IteratorState::SeqWithNamedRanges {
exprs: filtered_exprs,
named_range_indices,
current_named_values,
iterators: Vec::new(),
current_values: Vec::new(),
exhausted: false,
}
}
}
}
ExpressionKind::Optional(expr) => {
let iterator =
Box::new(Self::new_with_context(grammar, expr, name_context.clone()));
IteratorState::Optional {
iterator,
emitted_empty: false,
}
}
ExpressionKind::NegativeLookahead(expr) => {
let iterator =
Box::new(Self::new_with_context(grammar, expr, name_context.clone()));
IteratorState::Optional {
iterator,
emitted_empty: false,
}
}
ExpressionKind::Repeat(expr) | ExpressionKind::RepeatPlus(expr) => {
IteratorState::Repeat {
expr,
include_empty: true,
current_stage: 0,
iterator: None,
}
}
ExpressionKind::RepeatRange {
expr,
name,
min,
max,
limit,
} => {
// If this has a name and it's in the context, use that specific count
if let Some(name) = name {
if let Some(&count) = name_context.get(name) {
// Use the specified count from context
IteratorState::RepeatRange {
expr,
max: count + 1,
current_count: count,
iterator: None,
pending_repeat: None,
}
} else {
// Name not in context yet, this shouldn't happen in SeqWithNamedRanges
// but handle it anyway
let min_count = min.unwrap_or(0) as usize;
let max_count = match max {
Some(m) => match limit {
RangeLimit::HalfOpen => *m as usize,
RangeLimit::Closed => (*m + 1) as usize,
},
None => min_count + 3,
};
let start_count = if min_count == 0 { 0 } else { min_count };
IteratorState::RepeatRange {
expr,
max: max_count,
current_count: start_count,
iterator: None,
pending_repeat: None,
}
}
} else {
// No name, normal behavior
let min_count = min.unwrap_or(0) as usize;
let max_count = match max {
Some(m) => match limit {
RangeLimit::HalfOpen => *m as usize,
RangeLimit::Closed => (*m + 1) as usize,
},
None => min_count + 3,
};
let start_count = if min_count == 0 { 0 } else { min_count };
IteratorState::RepeatRange {
expr,
max: max_count,
current_count: start_count,
iterator: None,
pending_repeat: None,
}
}
}
ExpressionKind::RepeatRangeNamed(expr, name) => {
// Look up the count from the context
let count = name_context.get(name).copied().unwrap_or(1);
IteratorState::RepeatRange {
expr,
max: count + 1,
current_count: count,
iterator: None,
pending_repeat: None,
}
}
ExpressionKind::Nt(name) => {
let prod = grammar.productions.get(name).unwrap();
return Self::new_with_context(grammar, &prod.expression, name_context);
}
ExpressionKind::Terminal(s) => IteratorState::Terminal {
value: s.clone(),
done: false,
},
ExpressionKind::Prose(prose) => match prose.as_str() {
"`XID_Start` defined by Unicode" => IteratorState::Terminal {
value: "a".to_string(),
done: false,
},
"`XID_Continue` defined by Unicode" => IteratorState::Terminal {
value: "b".to_string(),
done: false,
},
_ => panic!("prose {prose} not supported"),
},
ExpressionKind::Break(_) => unreachable!(),
ExpressionKind::Comment(_) => unreachable!(),
ExpressionKind::Charset(chars) => {
let iterators: Vec<_> = chars
.iter()
.map(|e| Self::new_with_context(grammar, e, name_context.clone()))
.collect();
IteratorState::Alt {
iterators,
current_index: 0,
}
}
ExpressionKind::CharacterRange(start, end) => {
// Behave like Alt of start and end characters
let mut iterators = Vec::new();
let start_ch = start.get_ch();
let end_ch = end.get_ch();
iterators.push(PermutationIterator {
grammar,
name_context: name_context.clone(),
state: IteratorState::Terminal {
value: start_ch.to_string(),
done: false,
},
});
iterators.push(PermutationIterator {
grammar,
name_context: name_context.clone(),
state: IteratorState::Terminal {
value: end_ch.to_string(),
done: false,
},
});
IteratorState::Alt {
iterators,
current_index: 0,
}
}
ExpressionKind::NegExpression(_expr) => IteratorState::Terminal {
value: String::from("a"), // TODO: Comment here why this choice.
done: false,
},
ExpressionKind::Cut(expr) => {
return Self::new_with_context(grammar, expr, name_context);
}
ExpressionKind::Unicode((ch, _)) => IteratorState::Terminal {
value: ch.to_string(),
done: false,
},
};
PermutationIterator {
grammar,
name_context,
state,
}
}
}
impl<'g> Iterator for PermutationIterator<'g> {
type Item = String;
fn next(&mut self) -> Option<Self::Item> {
// Capture grammar reference before mutably borrowing state
let grammar = self.grammar;
match &mut self.state {
IteratorState::Terminal { value, done } => {
if *done {
None
} else {
*done = true;
Some(value.clone())
}
}
IteratorState::Alt {
iterators,
current_index,
} => {
while *current_index < iterators.len() {
if let Some(val) = iterators[*current_index].next() {
return Some(val);
}
*current_index += 1;
}
None
}
IteratorState::Optional {
iterator,
emitted_empty,
} => {
if !*emitted_empty {
*emitted_empty = true;
return Some(String::new());
}
iterator.next()
}
IteratorState::Repeat {
expr,
include_empty,
current_stage,
iterator,
} => {
// Stage 0: emit empty string (only for Repeat, not RepeatPlus)
if *current_stage == 0 && *include_empty {
*current_stage = 1;
return Some(String::new());
}
// Stage 1: emit single permutations
if *current_stage == 1 {
if iterator.is_none() {
*iterator = Some(Box::new(Self::new_with_context(
grammar,
expr,
self.name_context.clone(),
)));
}
if let Some(iter) = iterator {
if let Some(result) = iter.next() {
return Some(result);
}
}
// Stage 1 complete, move to stage 2
*current_stage = 2;
*iterator = Some(Box::new(Self::new_with_context(
grammar,
expr,
self.name_context.clone(),
)));
}
// Stage 2: emit double permutations (each element repeated twice)
if *current_stage == 2 {
// Get next single value and prepare to emit it twice
if let Some(iter) = iterator {
if let Some(val) = iter.next() {
let doubled = format!("{val}{val}");
return Some(doubled);
}
}
}
None
}
IteratorState::RepeatRange {
expr,
max,
current_count,
iterator,
pending_repeat,
} => {
// If we're at count 0 (min was 0), emit empty string
if *current_count == 0 {
*current_count = 1;
if *current_count >= *max {
return None;
}
return Some(String::new());
}
loop {
// If we haven't reached max count yet
if *current_count >= *max {
return None;
}
// Check if we have a pending repeat to emit
if let Some((val, times_left)) = pending_repeat {
if *times_left > 1 {
*times_left -= 1;
return Some(val.clone());
} else {
// Emit last repetition and clear pending
let result = val.clone();
*pending_repeat = None;
return Some(result);
}
}
// Initialize iterator for current count if needed
if iterator.is_none() {
*iterator = Some(Box::new(Self::new_with_context(
grammar,
expr,
self.name_context.clone(),
)));
}
// Try to get next value from iterator
if let Some(iter) = iterator {
if let Some(val) = iter.next() {
let result = val.repeat(*current_count);
return Some(result);
}
}
// Current count exhausted, move to next
*current_count += 1;
*iterator = None;
}
}
IteratorState::Seq {
exprs,
truncated_len,
iterators,
current_values,
initialized,
exhausted,
} => {
if *exhausted {
return None;
}
loop {
// (Re)initialize iterators for the current truncated_len.
if !*initialized {
let tlen = *truncated_len;
*iterators = exprs[..tlen]
.iter()
.map(|e| Self::new_with_context(grammar, e, self.name_context.clone()))
.collect();
*current_values = vec![String::new(); tlen];
// Get first value from each iterator.
let mut ok = true;
for (i, iter) in iterators.iter_mut().enumerate() {
if let Some(val) = iter.next() {
current_values[i] = val;
} else {
ok = false;
break;
}
}
if ok {
*initialized = true;
return Some(current_values.concat());
} else {
// Empty iterator at this length; try shorter.
if *truncated_len > 1 {
*truncated_len -= 1;
continue;
} else {
*exhausted = true;
return None;
}
}
}
// Try to advance the rightmost iterator.
let mut pos = iterators.len() - 1;
let mut advanced = false;
loop {
if let Some(val) = iterators[pos].next() {
current_values[pos] = val;
advanced = true;
break;
} else {
// This iterator is exhausted; reset it and move left.
if pos == 0 {
// All iterators for this truncated_len are exhausted.
break;
}
iterators[pos] = Self::new_with_context(
grammar,
&exprs[pos],
self.name_context.clone(),
);
if let Some(val) = iterators[pos].next() {
current_values[pos] = val;
}
pos -= 1;
}
}
if advanced {
return Some(current_values.concat());
}
// Current length exhausted; move to the next shorter truncation.
if *truncated_len > 1 {
*truncated_len -= 1;
*initialized = false;
} else {
*exhausted = true;
return None;
}
}
}
IteratorState::SeqWithNamedRanges {
exprs,
named_range_indices,
current_named_values,
iterators,
current_values,
exhausted,
} => {
if *exhausted {
return None;
}
loop {
// Initialize iterators if needed
if iterators.is_empty() {
for expr in exprs.iter() {
iterators.push(Self::new_with_context(
grammar,
expr,
current_named_values.clone(),
));
}
*current_values = iterators.iter().map(|_| String::new()).collect();
// Get first value from each iterator
for (i, iter) in iterators.iter_mut().enumerate() {
if let Some(val) = iter.next() {
current_values[i] = val;
} else {
// Empty iterator, try next name values
break;
}
}
if current_values.iter().all(|v| !v.is_empty()) {
return Some(current_values.concat());
}
}
// Try to advance rightmost iterator
let mut pos = iterators.len().checked_sub(1)?;
loop {
if let Some(val) = iterators[pos].next() {
current_values[pos] = val;
return Some(current_values.concat());
} else {
// This iterator exhausted
if pos == 0 {
// All iterators for this name combo exhausted
// Try to increment named values (only min and max, not values in between)
let mut incremented = false;
for (_idx, name, min, max) in named_range_indices.iter().rev() {
let current_val =
current_named_values.get(name).copied().unwrap_or(*min);
// Only generate for min and max values
if current_val == *min && *min + 1 < *max {
// Jump from min to max-1 (which is the actual max value since max is exclusive)
current_named_values.insert(name.clone(), *max - 1);
incremented = true;
break;
} else {
// Reset to min
current_named_values.insert(name.clone(), *min);
}
}
if !incremented {
*exhausted = true;
return None;
}
// Reset all iterators with new named values
iterators.clear();
current_values.clear();
break; // Go back to initialization
}
// Reset this iterator and move left
iterators[pos] = Self::new_with_context(
grammar,
&exprs[pos],
current_named_values.clone(),
);
if let Some(val) = iterators[pos].next() {
current_values[pos] = val;
}
pos -= 1;
}
}
}
}
}
}
}
/// Generates all permutations of one, two, or three character long strings.
pub struct ThreeIterator {
/// Current string length being generated (1, 2, or 3).
len: u8,
/// Character indices for each position (values 0..=0x7F).
indices: [u8; 3],
/// Set when all lengths are exhausted.
done: bool,
}
impl ThreeIterator {
pub fn new() -> ThreeIterator {
ThreeIterator {
len: 1,
indices: [0; 3],
done: false,
}
}
}
impl Iterator for ThreeIterator {
type Item = String;
fn next(&mut self) -> Option<Self::Item> {
if self.done {
return None;
}
let len = self.len as usize;
// Build the current string from the active indices.
let result: String = self.indices[..len]
.iter()
.map(|&i| char::from_u32(i as u32).unwrap())
.collect();
// Advance indices right-to-left, carrying into higher positions.
let mut carry = true;
for i in (0..len).rev() {
if carry {
if self.indices[i] < 0x7F {
self.indices[i] += 1;
carry = false;
} else {
self.indices[i] = 0;
// carry remains true; propagate left
}
}
}
if carry {
// All positions overflowed — this length is exhausted.
if self.len < 3 {
self.len += 1;
self.indices = [0; 3];
} else {
self.done = true;
}
}
Some(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_permutations(grammar: &str, expected: &[&str]) {
let g = Grammar::grammar_from_str(grammar, "cat").unwrap();
let e = &g.productions.get("P").unwrap().expression;
let ps: Vec<_> = PermutationIterator::new(&g, e).collect();
assert_eq!(ps, expected);
}
#[test]
fn seq_and_alt() {
// Full sequence, then truncations (length 2, then length 1).
assert_permutations(
"P -> `A` (`B` | (`C1` | `C2`) | `D`) `E`",
&["ABE", "AC1E", "AC2E", "ADE", "AB", "AC1", "AC2", "AD", "A"],
);
}
#[test]
fn optional() {
// Full sequence, then truncations.
assert_permutations(
"P -> `A` (`B` | `C`)? `D`",
&["AD", "ABD", "ACD", "A", "AB", "AC", "A"],
);
}
#[test]
fn seq_truncated() {
// A single sequence with no alternatives: ABC, then AB, then A.
assert_permutations("P -> `A` `B` `C`", &["ABC", "AB", "A"]);
}
#[test]
fn seq_truncated_with_alts() {
// Each position has alternatives; verify all combos per length, then shorter lengths.
assert_permutations(
"P -> (`A` | `X`) (`B` | `Y`)",
&["AB", "AY", "XB", "XY", "A", "X"],
);
}
#[test]
fn repeat() {
assert_permutations("P -> (`A` | `B`)*", &["", "A", "B", "AA", "BB"]);
}
#[test]
fn repeat_plus() {
assert_permutations("P -> (`A` | `B`)+", &["", "A", "B", "AA", "BB"]);
}
#[test]
fn repeat_range() {
assert_permutations("P -> (`A` | `B`){0..}", &["", "A", "B", "AA", "BB"]);
assert_permutations("P -> (`A` | `B`){1..3}", &["A", "B", "AA", "BB"]);
assert_permutations("P -> (`A` | `B`){2..=3}", &["AA", "BB", "AAA", "BBB"]);
}
#[test]
fn charset() {
// Test with Terminal and Range
assert_permutations("P -> [`A` `X`-`Z`]", &["A", "X", "Z"]);
// Test with just Range
assert_permutations("P -> [`a`-`c`]", &["a", "c"]);
}
#[test]
fn named_repeat_range() {
// Test named repeat ranges are synchronized (only min and max values)
assert_permutations("P -> `A`{n:1..=5} `B` `C`{n}", &["ABC", "AAAAABCCCCC"]);
}
#[test]
fn negative_lookahead() {
// NegativeLookahead emits empty string first, then all permutations of the expression.
assert_permutations("P -> !`A`", &["", "A"]);
assert_permutations("P -> !(`A` | `B`)", &["", "A", "B"]);
// In a sequence: empty lookahead plus the rest, then lookahead expr plus the rest,
// then truncated-length permutations (just the lookahead expression alone).
assert_permutations("P -> !`X` `Y`", &["Y", "XY", "", "X"]);
}
}