use std::{
collections::hash_map::DefaultHasher,
fmt::{self, Write},
hash::{Hash, Hasher}
};
use pretty_assertions::assert_eq;
use crate::{
Parser, SourceSpan, Spanned,
ast::*,
s_expr::{SExpressible, SExpressibleOptions},
support::{on_small_stack, read_compilation_test_cases}
};
#[test]
fn test_debug_matches_derived()
{
let test_cases = read_compilation_test_cases(include_str!(
"../../tests/test_ast_debug.txt"
));
assert!(!test_cases.is_empty());
for (source, expected) in test_cases
{
let function = Parser::parse(source).unwrap();
assert_eq!(format!("{:?}", function), expected, "{}", source);
}
}
#[test]
fn test_debug_pretty_matches_derived()
{
let test_cases = read_compilation_test_cases(include_str!(
"../../tests/test_ast_debug_pretty.txt"
));
assert!(!test_cases.is_empty());
for (source, expected) in test_cases
{
let function = Parser::parse(source).unwrap();
assert_eq!(format!("{:#?}", function), expected, "{}", source);
}
}
#[test]
fn test_debug_passes_flags_to_leaves()
{
let function = Parser::parse("{x}: {x}D[-1, 10] drop highest 12").unwrap();
assert_eq!(
format!("{:x?}", function.body),
"Dice(DropHighest(DropHighest { dice: Custom(CustomDice { count: \
Variable(Variable { name: \"x\", span: SourceSpan { start: 5, end: \
8 } }), faces: [ffffffff, a], span: SourceSpan { start: 5, end: 11 \
} }), drop: Some(Constant(Constant { value: c, span: SourceSpan { \
start: 1f, end: 21 } })), span: SourceSpan { start: 5, end: 21 } }))"
);
}
#[test]
fn test_debug_pretty_deep()
{
on_small_stack(|| {
let depth = 500;
let expression = nest(Nesting::Negation, depth, 1);
let actual = format!("{:#?}", expression);
assert!(actual == pretty_negations(depth, 1), "mismatch");
});
}
#[test]
fn test_clone_equals_original()
{
let test_cases = read_compilation_test_cases(include_str!(
"../../tests/test_ast_debug.txt"
));
for (source, _) in test_cases
{
let function = Parser::parse(source).unwrap();
let body = function.body.clone();
assert!(body == function.body, "{}", source);
assert_eq!(hash_of(&body), hash_of(&function.body), "{}", source);
assert_eq!(
format!("{:#?}", body),
format!("{:#?}", function.body),
"{}",
source
);
}
}
#[test]
fn test_equality_distinguishes()
{
let pairs = [
("1 + 2", "1 + 3"),
("1 + 2", "1 - 2"),
("1 + 2", "1 + 2"),
("(1)", "{a}@(1)"),
("{a}@(1)", "{b}@(1)"),
("[1:2]", "[1:3]"),
("3D[1, 2]", "3D[1, 3]"),
("4D6 drop lowest", "4D6 drop lowest 1"),
("4D6 drop lowest", "4D6 drop highest"),
(
"4D6 drop lowest drop lowest",
"4D6 drop lowest drop highest"
),
("{x}: {x}", "{y}: {y}"),
("-(1)", "-(2)")
];
for (left, right) in pairs
{
let left_body = Parser::parse(left).unwrap().body;
let right_body = Parser::parse(right).unwrap().body;
assert!(left_body != right_body, "{} == {}", left, right);
assert!(left_body == left_body.clone(), "{}", left);
}
}
#[test]
fn test_hash_agrees_with_equality()
{
let source =
"{x}: 8D8 drop lowest (2) drop highest {a}@({x}) + [1:6] ^ -(3)";
let first = Parser::parse(source).unwrap();
let second = Parser::parse(source).unwrap();
assert!(first == second);
assert_eq!(hash_of(&first), hash_of(&second));
assert_eq!(hash_of(&first.body), hash_of(&second.body));
}
pub(super) const DEPTH: usize = 1_000_000;
pub(super) const LEAF: i32 = 7_777_777;
#[test]
#[ignore = "stress: run with just stress"]
fn test_deep_group() { exercise_on_small_stack(Nesting::Group) }
#[test]
#[ignore = "stress: run with just stress"]
fn test_deep_binding() { exercise_on_small_stack(Nesting::Binding) }
#[test]
#[ignore = "stress: run with just stress"]
fn test_deep_range_start() { exercise_on_small_stack(Nesting::RangeStart) }
#[test]
#[ignore = "stress: run with just stress"]
fn test_deep_range_end() { exercise_on_small_stack(Nesting::RangeEnd) }
#[test]
#[ignore = "stress: run with just stress"]
fn test_deep_negation() { exercise_on_small_stack(Nesting::Negation) }
#[test]
#[ignore = "stress: run with just stress"]
fn test_deep_exponent() { exercise_on_small_stack(Nesting::Exponent) }
#[test]
#[ignore = "stress: run with just stress"]
fn test_deep_addition() { exercise_on_small_stack(Nesting::Addition) }
#[test]
#[ignore = "stress: run with just stress"]
fn test_deep_subtraction() { exercise_on_small_stack(Nesting::Subtraction) }
#[test]
#[ignore = "stress: run with just stress"]
fn test_deep_multiplication()
{
exercise_on_small_stack(Nesting::Multiplication)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_deep_division() { exercise_on_small_stack(Nesting::Division) }
#[test]
#[ignore = "stress: run with just stress"]
fn test_deep_modulo() { exercise_on_small_stack(Nesting::Modulo) }
#[test]
#[ignore = "stress: run with just stress"]
fn test_deep_dice_count() { exercise_on_small_stack(Nesting::DiceCount) }
#[test]
#[ignore = "stress: run with just stress"]
fn test_deep_dice_faces() { exercise_on_small_stack(Nesting::DiceFaces) }
#[test]
#[ignore = "stress: run with just stress"]
fn test_deep_custom_count() { exercise_on_small_stack(Nesting::CustomCount) }
#[test]
#[ignore = "stress: run with just stress"]
fn test_deep_drop_expression()
{
exercise_on_small_stack(Nesting::DropExpression)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_deep_drop_clauses() { exercise_on_small_stack(Nesting::DropClauses) }
#[test]
#[ignore = "stress: run with just stress"]
fn test_deep_mixed() { exercise_on_small_stack(Nesting::Mixed) }
#[test]
fn test_miri_mixed() { exercise(|leaf| nest(Nesting::Mixed, 60, leaf)) }
#[test]
fn test_miri_drop_clauses()
{
exercise(|leaf| nest(Nesting::DropClauses, 40, leaf))
}
#[test]
fn test_miri_standalone_drop_clauses()
{
let chain = drop_clauses(standard_dice(constant(1)), 40);
drop(chain.clone());
match &chain
{
DiceExpression::DropLowest(clause) => drop(clause.clone()),
DiceExpression::DropHighest(clause) => drop(clause.clone()),
_ => unreachable!()
}
drop(chain);
}
#[test]
fn test_miri_single_drop_clause()
{
let expression = Parser::parse("{x}: {x}D[1, 2] drop lowest (2D6)")
.unwrap()
.body;
drop(expression.clone());
drop(expression);
}
#[derive(Copy, Clone, Debug)]
pub(super) enum Nesting
{
Group,
Binding,
RangeStart,
RangeEnd,
Negation,
Exponent,
Addition,
Subtraction,
Multiplication,
Division,
Modulo,
DiceCount,
DiceFaces,
CustomCount,
DropExpression,
DropClauses,
Mixed
}
impl Nesting
{
pub(super) const ROTATION: [Nesting; 16] = [
Nesting::Group,
Nesting::Binding,
Nesting::RangeStart,
Nesting::RangeEnd,
Nesting::Negation,
Nesting::Exponent,
Nesting::Addition,
Nesting::Subtraction,
Nesting::Multiplication,
Nesting::Division,
Nesting::Modulo,
Nesting::DiceCount,
Nesting::DiceFaces,
Nesting::CustomCount,
Nesting::DropExpression,
Nesting::DropClauses
];
pub(super) const fn binds(self) -> bool
{
matches!(self, Nesting::Binding | Nesting::Mixed)
}
}
pub(super) fn nest(
nesting: Nesting,
depth: usize,
leaf: i32
) -> Expression<'static>
{
if let Nesting::DropClauses = nesting
{
return Expression::Dice(drop_clauses(
standard_dice(constant(leaf)),
depth
))
}
let mut expression = constant(leaf);
for level in 0..depth
{
let nesting = match nesting
{
Nesting::Mixed =>
{
Nesting::ROTATION[level % Nesting::ROTATION.len()]
},
nesting => nesting
};
expression = wrap(nesting, expression);
}
expression
}
pub(super) fn nest_function(nesting: Nesting, depth: usize)
-> Function<'static>
{
let span = SourceSpan::SYNTHETIC;
Function {
parameters: None,
body: Expression::Arithmetic(ArithmeticExpression::Add(Add {
left: Box::new(nest(nesting, depth, 1)),
right: Box::new(Expression::Variable(Variable {
name: "x".into(),
span
})),
span
})),
span
}
}
pub(super) fn nest_parsable(
nesting: Nesting,
depth: usize,
leaf: i32
) -> Expression<'static>
{
if let Nesting::DropClauses = nesting
{
return nest(nesting, depth, leaf)
}
let mut expression = constant(leaf);
for level in 0..depth
{
let nesting = match nesting
{
Nesting::Mixed =>
{
Nesting::ROTATION[level % Nesting::ROTATION.len()]
},
nesting => nesting
};
let atomic = matches!(
expression,
Expression::Constant(_)
| Expression::Variable(_)
| Expression::Binding(_)
| Expression::Group(_)
);
let needs_group = match nesting
{
Nesting::DiceCount
| Nesting::DiceFaces
| Nesting::CustomCount
| Nesting::DropExpression
| Nesting::DropClauses => !atomic,
Nesting::Negation => matches!(expression, Expression::Constant(_)),
Nesting::Multiplication | Nesting::Division | Nesting::Modulo =>
{
matches!(
expression,
Expression::Arithmetic(
ArithmeticExpression::Add(_)
| ArithmeticExpression::Sub(_)
)
)
},
Nesting::Exponent => matches!(
expression,
Expression::Arithmetic(
ArithmeticExpression::Add(_)
| ArithmeticExpression::Sub(_)
| ArithmeticExpression::Mul(_)
| ArithmeticExpression::Div(_)
| ArithmeticExpression::Mod(_)
)
),
_ => false
};
if needs_group
{
expression = Expression::Group(Group {
expression: Box::new(expression),
span: SourceSpan::SYNTHETIC
});
}
expression = wrap(nesting, expression);
}
expression
}
fn wrap(nesting: Nesting, inner: Expression<'static>) -> Expression<'static>
{
let span = SourceSpan::SYNTHETIC;
let inner = Box::new(inner);
match nesting
{
Nesting::Group => Expression::Group(Group {
expression: inner,
span
}),
Nesting::Binding => Expression::Binding(Binding {
name: "a".into(),
name_span: span,
expression: inner,
span
}),
Nesting::RangeStart => Expression::Range(Range {
start: inner,
end: Box::new(constant(1)),
span
}),
Nesting::RangeEnd => Expression::Range(Range {
start: Box::new(constant(1)),
end: inner,
span
}),
Nesting::Negation =>
{
Expression::Arithmetic(ArithmeticExpression::Neg(Neg {
operand: inner,
span
}))
},
Nesting::Exponent =>
{
Expression::Arithmetic(ArithmeticExpression::Exp(Exp {
left: Box::new(constant(2)),
right: inner,
span
}))
},
Nesting::Addition =>
{
Expression::Arithmetic(ArithmeticExpression::Add(Add {
left: inner,
right: Box::new(constant(1)),
span
}))
},
Nesting::Subtraction =>
{
Expression::Arithmetic(ArithmeticExpression::Sub(Sub {
left: inner,
right: Box::new(constant(1)),
span
}))
},
Nesting::Multiplication =>
{
Expression::Arithmetic(ArithmeticExpression::Mul(Mul {
left: inner,
right: Box::new(constant(1)),
span
}))
},
Nesting::Division =>
{
Expression::Arithmetic(ArithmeticExpression::Div(Div {
left: inner,
right: Box::new(constant(1)),
span
}))
},
Nesting::Modulo =>
{
Expression::Arithmetic(ArithmeticExpression::Mod(Mod {
left: inner,
right: Box::new(constant(1)),
span
}))
},
Nesting::DiceCount =>
{
Expression::Dice(DiceExpression::Standard(StandardDice {
count: inner,
faces: Box::new(constant(6)),
span
}))
},
Nesting::DiceFaces =>
{
Expression::Dice(DiceExpression::Standard(StandardDice {
count: Box::new(constant(1)),
faces: inner,
span
}))
},
Nesting::CustomCount =>
{
Expression::Dice(DiceExpression::Custom(CustomDice {
count: inner,
faces: vec![1, 2],
span
}))
},
Nesting::DropExpression =>
{
Expression::Dice(DiceExpression::DropLowest(DropLowest {
dice: Box::new(standard_dice(constant(1))),
drop: Some(inner),
span
}))
},
Nesting::DropClauses =>
{
Expression::Dice(DiceExpression::DropHighest(DropHighest {
dice: Box::new(DiceExpression::DropLowest(DropLowest {
dice: Box::new(standard_dice(constant(1))),
drop: Some(inner),
span
})),
drop: None,
span
}))
},
Nesting::Mixed => unreachable!()
}
}
fn drop_clauses(
dice: DiceExpression<'static>,
depth: usize
) -> DiceExpression<'static>
{
let span = SourceSpan::SYNTHETIC;
let mut dice = dice;
for level in 0..depth
{
let drop = (level % 3 == 0).then(|| Box::new(constant(1)));
let inner = Box::new(dice);
dice = if level % 2 == 0
{
DiceExpression::DropLowest(DropLowest {
dice: inner,
drop,
span
})
}
else
{
DiceExpression::DropHighest(DropHighest {
dice: inner,
drop,
span
})
};
}
dice
}
fn constant(value: i32) -> Expression<'static>
{
Expression::Constant(Constant {
value,
span: SourceSpan::SYNTHETIC
})
}
fn standard_dice(count: Expression<'static>) -> DiceExpression<'static>
{
DiceExpression::Standard(StandardDice {
count: Box::new(count),
faces: Box::new(constant(6)),
span: SourceSpan::SYNTHETIC
})
}
fn exercise_on_small_stack(nesting: Nesting)
{
on_small_stack(|| exercise(|leaf| nest(nesting, DEPTH, leaf)));
}
fn exercise(build: impl Fn(i32) -> Expression<'static>)
{
let original = build(1);
let different = build(2);
assert!(original != different, "differing leaves compared equal");
drop(different);
let copy = original.clone();
assert!(original == copy, "clone compared unequal");
assert_eq!(hash_of(&original), hash_of(©));
let length = debug_length(&original);
assert!(length > 0);
assert_eq!(length, debug_length(©));
let length = display_length(&original);
assert!(length > 0);
assert_eq!(length, display_length(©));
assert!(
original.untethered() == original,
"untethering changed spans"
);
for with_groups in [false, true]
{
let options =
SExpressibleOptions::new(0, 4, usize::MAX).with_groups(with_groups);
let size = original.size_s_expr(options);
assert!(size > 0);
assert_eq!(s_expr_length(&original, options), size);
}
}
fn hash_of(value: &impl Hash) -> u64
{
let mut hasher = DefaultHasher::new();
value.hash(&mut hasher);
hasher.finish()
}
struct Counter(usize);
impl Write for Counter
{
fn write_str(&mut self, s: &str) -> fmt::Result
{
self.0 += s.len();
Ok(())
}
}
fn debug_length(value: &impl fmt::Debug) -> usize
{
let mut counter = Counter(0);
write!(counter, "{:?}", value).unwrap();
counter.0
}
fn display_length(value: &impl fmt::Display) -> usize
{
let mut counter = Counter(0);
write!(counter, "{}", value).unwrap();
counter.0
}
fn s_expr_length(
value: &impl SExpressible,
options: SExpressibleOptions
) -> usize
{
let mut counter = Counter(0);
value
.write_s_expr(&mut counter, options.soft_limit, options)
.unwrap();
counter.0
}
fn pretty_negations(depth: usize, leaf: i32) -> String
{
let indent = |level: usize| " ".repeat(level);
let span = |level: usize| {
format!(
"SourceSpan {{\n{}start: 0,\n{}end: 0,\n{}}}",
indent(level + 1),
indent(level + 1),
indent(level)
)
};
let mut out = String::new();
for level in 0..depth
{
let base = 3 * level;
out.push_str("Arithmetic(\n");
out.push_str(&format!("{}Neg(\n", indent(base + 1)));
out.push_str(&format!("{}Neg {{\n", indent(base + 2)));
out.push_str(&format!("{}operand: ", indent(base + 3)));
}
let base = 3 * depth;
out.push_str(&format!(
"Constant(\n{}Constant {{\n{}value: {},\n{}span: {},\n{}}},\n{})",
indent(base + 1),
indent(base + 2),
leaf,
indent(base + 2),
span(base + 2),
indent(base + 1),
indent(base)
));
for level in (0..depth).rev()
{
let base = 3 * level;
out.push_str(&format!(
",\n{}span: {},\n{}}},\n{}),\n{})",
indent(base + 3),
span(base + 3),
indent(base + 2),
indent(base + 1),
indent(base)
));
}
out
}