use crate::{
ast::*,
parser::*,
span::{SourceSpan, Spanned}
};
use pretty_assertions::assert_eq;
#[test]
fn test_primary()
{
for (input, expected_str, expected_ast) in [
(
"(1 + 2)",
"(1 + 2)",
Expression::Group(Group {
expression: Box::new(Expression::Arithmetic(
ArithmeticExpression::Add(Add {
left: Box::new(Expression::Constant(Constant {
value: 1,
span: SourceSpan::default()
})),
right: Box::new(Expression::Constant(Constant {
value: 2,
span: SourceSpan::default()
})),
span: SourceSpan::default()
})
)),
span: SourceSpan::default()
})
),
(
"{x}",
"{x}",
Expression::Variable(Variable {
name: "x".into(),
span: SourceSpan::default()
})
),
(
"[1:10]",
"[1:10]",
Expression::Range(Range {
start: Box::new(Expression::Constant(Constant {
value: 1,
span: SourceSpan::default()
})),
end: Box::new(Expression::Constant(Constant {
value: 10,
span: SourceSpan::default()
})),
span: SourceSpan::default()
})
),
(
"3d6",
"3D6",
Expression::Dice(DiceExpression::Standard(StandardDice {
count: Box::new(Expression::Constant(Constant {
value: 3,
span: SourceSpan::default()
})),
faces: Box::new(Expression::Constant(Constant {
value: 6,
span: SourceSpan::default()
})),
span: SourceSpan::default()
}))
),
(
"42",
"42",
Expression::Constant(Constant {
value: 42,
span: SourceSpan::default()
})
),
(
"-42",
"-42",
Expression::Constant(Constant {
value: -42,
span: SourceSpan::default()
})
)
]
{
let span = Span::new(input);
match primary(span)
{
Ok((residue, result)) =>
{
assert!(
residue.fragment().is_empty(),
"Residue not empty for input: {}",
input
);
assert_eq!(
result.to_string(),
expected_str,
"Failed for input: {}",
input
);
assert_eq!(
result.untethered(),
expected_ast.untethered(),
"AST mismatch for input: {}",
input
);
},
Err(e) => panic!("Parsing failed for input: {}: {}", input, e)
}
}
for input in ["", " ", "+", "*", "/", "^", "3+4"]
{
let span = Span::new(input);
let result = primary(span);
assert!(
result.is_err() || !result.unwrap().0.fragment().is_empty(),
"Failed to reject invalid input: {}",
input
);
}
}
#[test]
fn test_group()
{
for (input, expected_str, expected_ast) in [
(
"(1)",
"(1)",
Group {
expression: Box::new(Expression::Constant(Constant {
value: 1,
span: SourceSpan::default()
})),
span: SourceSpan::default()
}
),
(
"(1 + 2)",
"(1 + 2)",
Group {
expression: Box::new(Expression::Arithmetic(
ArithmeticExpression::Add(Add {
left: Box::new(Expression::Constant(Constant {
value: 1,
span: SourceSpan::default()
})),
right: Box::new(Expression::Constant(Constant {
value: 2,
span: SourceSpan::default()
})),
span: SourceSpan::default()
})
)),
span: SourceSpan::default()
}
),
(
"((1 + 2) * 3)",
"((1 + 2) * 3)",
Group {
expression: Box::new(Expression::Arithmetic(
ArithmeticExpression::Mul(Mul {
left: Box::new(Expression::Group(Group {
expression: Box::new(Expression::Arithmetic(
ArithmeticExpression::Add(Add {
left: Box::new(Expression::Constant(
Constant {
value: 1,
span: SourceSpan::default()
}
)),
right: Box::new(Expression::Constant(
Constant {
value: 2,
span: SourceSpan::default()
}
)),
span: SourceSpan::default()
})
)),
span: SourceSpan::default()
})),
right: Box::new(Expression::Constant(Constant {
value: 3,
span: SourceSpan::default()
})),
span: SourceSpan::default()
})
)),
span: SourceSpan::default()
}
),
(
"({x})",
"({x})",
Group {
expression: Box::new(Expression::Variable(Variable {
name: "x".into(),
span: SourceSpan::default()
})),
span: SourceSpan::default()
}
),
(
"(3d6)",
"(3D6)",
Group {
expression: Box::new(Expression::Dice(
DiceExpression::Standard(StandardDice {
count: Box::new(Expression::Constant(Constant {
value: 3,
span: SourceSpan::default()
})),
faces: Box::new(Expression::Constant(Constant {
value: 6,
span: SourceSpan::default()
})),
span: SourceSpan::default()
})
)),
span: SourceSpan::default()
}
),
(
"(1d20 + 5)",
"(1D20 + 5)",
Group {
expression: Box::new(Expression::Arithmetic(
ArithmeticExpression::Add(Add {
left: Box::new(Expression::Dice(
DiceExpression::Standard(StandardDice {
count: Box::new(Expression::Constant(
Constant {
value: 1,
span: SourceSpan::default()
}
)),
faces: Box::new(Expression::Constant(
Constant {
value: 20,
span: SourceSpan::default()
}
)),
span: SourceSpan::default()
})
)),
right: Box::new(Expression::Constant(Constant {
value: 5,
span: SourceSpan::default()
})),
span: SourceSpan::default()
})
)),
span: SourceSpan::default()
}
),
(
"( 1 )",
"(1)",
Group {
expression: Box::new(Expression::Constant(Constant {
value: 1,
span: SourceSpan::default()
})),
span: SourceSpan::default()
}
),
(
"( 1 + 2 )",
"(1 + 2)",
Group {
expression: Box::new(Expression::Arithmetic(
ArithmeticExpression::Add(Add {
left: Box::new(Expression::Constant(Constant {
value: 1,
span: SourceSpan::default()
})),
right: Box::new(Expression::Constant(Constant {
value: 2,
span: SourceSpan::default()
})),
span: SourceSpan::default()
})
)),
span: SourceSpan::default()
}
)
]
{
let span = Span::new(input);
match group(span)
{
Ok((residue, result)) =>
{
assert!(
residue.fragment().is_empty(),
"Residue not empty for input: {}",
input
);
assert_eq!(
result.to_string(),
expected_str,
"Failed for input: {}",
input
);
assert_eq!(
result.untethered(),
expected_ast.untethered(),
"AST mismatch for input: {}",
input
);
},
Err(e) => panic!("Parsing failed for input: {}: {}", input, e)
}
}
for input in ["", " ", "(", ")", "1", "(1", "1)", "(1)2", "((1)"]
{
let span = Span::new(input);
let result = group(span);
assert!(
result.is_err() || !result.unwrap().0.fragment().is_empty(),
"Failed to reject invalid input: {}",
input
);
}
}
#[test]
fn test_variable()
{
for (input, expected) in [
("{x}", "{x}"),
("{variable}", "{variable}"),
("{long_variable_name}", "{long_variable_name}"),
("{hello-world}", "{hello-world}"),
("{こんにちは}", "{こんにちは}"),
("{Здравствуй_мир}", "{Здравствуй_мир}"),
("{Γειά-σου-κόσμε}", "{Γειά-σου-κόσμε}"),
("{x1}", "{x1}"),
("{x_1}", "{x_1}"),
("{x-1}", "{x-1}"),
("{hello world}", "{hello world}"),
("{hello world }", "{hello world}"),
("{an external variable}", "{an external variable}"),
("{foo.bar}", "{foo.bar}"),
("{1}", "{1}"),
("{-x}", "{-x}"),
("{ a b }", "{a b}"),
("{a\tb}", "{a b}"),
("{a\n b}", "{a b}"),
("{\u{00A0}a\u{2028}b\u{00A0}}", "{a b}"),
("{a + b}", "{a + b}"),
("{x@(1)}", "{x@(1)}"),
("{नमस्ते दुनिया}", "{नमस्ते दुनिया}")
]
{
let span = Span::new(input);
match variable(span)
{
Ok((residue, result)) =>
{
assert!(
residue.fragment().is_empty(),
"Residue not empty for input: {}",
input
);
assert_eq!(
result.to_string(),
expected,
"Failed for input: {}",
input
);
},
Err(e) => panic!("Parsing failed for input: {}: {}", input, e)
}
}
for input in [
"",
" ",
"x",
"{}",
"{ }",
"{{x}}",
"{\u{00A0}}",
"{a\u{0}b}",
"{a\u{202E}b}",
"x{}",
"{x}y"
]
{
let span = Span::new(input);
let result = variable(span);
assert!(
result.is_err() || !result.unwrap().0.fragment().is_empty(),
"Failed to reject invalid input: {}",
input
);
}
}
#[test]
fn test_range()
{
for (input, expected_str, expected_ast) in [
(
"[1:10]",
"[1:10]",
Range {
start: Box::new(Expression::Constant(Constant {
value: 1,
span: SourceSpan::default()
})),
end: Box::new(Expression::Constant(Constant {
value: 10,
span: SourceSpan::default()
})),
span: SourceSpan::default()
}
),
(
"[-10:10]",
"[-10:10]",
Range {
start: Box::new(Expression::Constant(Constant {
value: -10,
span: SourceSpan::default()
})),
end: Box::new(Expression::Constant(Constant {
value: 10,
span: SourceSpan::default()
})),
span: SourceSpan::default()
}
),
(
"[{min}:{max}]",
"[{min}:{max}]",
Range {
start: Box::new(Expression::Variable(Variable {
name: "min".into(),
span: SourceSpan::default()
})),
end: Box::new(Expression::Variable(Variable {
name: "max".into(),
span: SourceSpan::default()
})),
span: SourceSpan::default()
}
),
(
"[1+2:3*4]",
"[1 + 2:3 * 4]",
Range {
start: Box::new(Expression::Arithmetic(
ArithmeticExpression::Add(Add {
left: Box::new(Expression::Constant(Constant {
value: 1,
span: SourceSpan::default()
})),
right: Box::new(Expression::Constant(Constant {
value: 2,
span: SourceSpan::default()
})),
span: SourceSpan::default()
})
)),
end: Box::new(Expression::Arithmetic(
ArithmeticExpression::Mul(Mul {
left: Box::new(Expression::Constant(Constant {
value: 3,
span: SourceSpan::default()
})),
right: Box::new(Expression::Constant(Constant {
value: 4,
span: SourceSpan::default()
})),
span: SourceSpan::default()
})
)),
span: SourceSpan::default()
}
),
(
"[(1+2):(3*4)]",
"[(1 + 2):(3 * 4)]",
Range {
start: Box::new(Expression::Group(Group {
expression: Box::new(Expression::Arithmetic(
ArithmeticExpression::Add(Add {
left: Box::new(Expression::Constant(Constant {
value: 1,
span: SourceSpan::default()
})),
right: Box::new(Expression::Constant(Constant {
value: 2,
span: SourceSpan::default()
})),
span: SourceSpan::default()
})
)),
span: SourceSpan::default()
})),
end: Box::new(Expression::Group(Group {
expression: Box::new(Expression::Arithmetic(
ArithmeticExpression::Mul(Mul {
left: Box::new(Expression::Constant(Constant {
value: 3,
span: SourceSpan::default()
})),
right: Box::new(Expression::Constant(Constant {
value: 4,
span: SourceSpan::default()
})),
span: SourceSpan::default()
})
)),
span: SourceSpan::default()
})),
span: SourceSpan::default()
}
),
(
"[1d6:2d8]",
"[1D6:2D8]",
Range {
start: Box::new(Expression::Dice(DiceExpression::Standard(
StandardDice {
count: Box::new(Expression::Constant(Constant {
value: 1,
span: SourceSpan::default()
})),
faces: Box::new(Expression::Constant(Constant {
value: 6,
span: SourceSpan::default()
})),
span: SourceSpan::default()
}
))),
end: Box::new(Expression::Dice(DiceExpression::Standard(
StandardDice {
count: Box::new(Expression::Constant(Constant {
value: 2,
span: SourceSpan::default()
})),
faces: Box::new(Expression::Constant(Constant {
value: 8,
span: SourceSpan::default()
})),
span: SourceSpan::default()
}
))),
span: SourceSpan::default()
}
)
]
{
let span = Span::new(input);
match range(span)
{
Ok((residue, result)) =>
{
assert!(
residue.fragment().is_empty(),
"Residue not empty for input: {}",
input
);
assert_eq!(
result.to_string(),
expected_str,
"Failed for input: {}",
input
);
assert_eq!(
result.untethered(),
expected_ast.untethered(),
"AST mismatch for input: {}",
input
);
},
Err(e) => panic!("Parsing failed for input: {}: {}", input, e)
}
}
for input in ["", " ", "1:10", "[1]", "[1:]", "[:10]", "[1,10]", "1..10"]
{
let span = Span::new(input);
let result = range(span);
assert!(
result.is_err() || !result.unwrap().0.fragment().is_empty(),
"Failed to reject invalid input: {}",
input
);
}
}
#[test]
fn test_binding()
{
for (input, expected) in [
("{x}@(3D6)", "{x}@(3D6)"),
("{long_name}@(1D20)", "{long_name}@(1D20)"),
("{x-y}@(1D4)", "{x-y}@(1D4)"),
("{x.y.z}@(4D8)", "{x.y.z}@(4D8)"),
("{a new id}@(2D4)", "{a new id}@(2D4)"),
("{x} @ (3D6)", "{x}@(3D6)"),
("{x}@ ( 3D6 )", "{x}@(3D6)"),
("{x}@(1 + 2)", "{x}@(1 + 2)"),
("{x}@(3D6 + 2)", "{x}@(3D6 + 2)"),
("{x}@(1D6 drop lowest)", "{x}@(1D6 drop lowest)"),
("{a}@({b}@(1D4) + {b})", "{a}@({b}@(1D4) + {b})"),
("{x}@([1:6])", "{x}@([1:6])"),
("{x}@(-3)", "{x}@(-3)"),
("{3}@(1)", "{3}@(1)"),
("{a(b}@(1)", "{a(b}@(1)"),
("{x@(1)}@(2)", "{x@(1)}@(2)")
]
{
let span = Span::new(input);
match binding(span)
{
Ok((residue, result)) =>
{
assert!(
residue.fragment().is_empty(),
"Residue not empty for input: {}",
input
);
assert_eq!(
result.to_string(),
expected,
"Rendering mismatch for input: {}",
input
);
},
Err(e) => panic!("Parsing failed for input: {}: {}", input, e)
}
}
for input in [
"{x}",
"{xyz}",
"{}@(1)",
"{x}(1D6)",
"{x}@1D6",
"{x}@()",
"@{x}(1D6)",
"x@(1)",
""
]
{
let span = Span::new(input);
let result = binding(span);
assert!(
result.is_err() || !result.unwrap().0.fragment().is_empty(),
"Failed to reject invalid input: {:?}",
input
);
}
}
#[test]
fn test_binding_in_restricted_positions()
{
let (residue, expr) = dice_count(Span::new("{x}@(2+3)")).unwrap();
assert!(residue.fragment().is_empty());
assert!(matches!(expr, Expression::Binding(_)));
let (residue, expr) = standard_faces(Span::new("{f}@(6)")).unwrap();
assert!(residue.fragment().is_empty());
assert!(matches!(expr, Expression::Binding(_)));
let (residue, expr) = drop_expression(Span::new("{k}@(2)")).unwrap();
assert!(residue.fragment().is_empty());
assert!(matches!(expr, Expression::Binding(_)));
let (residue, expr) = expression(Span::new("1 + {x}@(3D6)")).unwrap();
assert!(residue.fragment().is_empty());
let rendered = expr.to_string();
assert_eq!(rendered, "1 + {x}@(3D6)");
}
#[test]
fn test_binding_rewinds_on_no_at_sign()
{
let input = "hello";
let span = Span::new(input);
let result = binding(span);
assert!(result.is_err(), "binding should reject `{}`", input);
if let Err(nom::Err::Error(e)) = result
{
assert_eq!(
e.errors[0].0.location_offset(),
0,
"binding error should rewind to the start of the input, not \
point past the consumed identifier"
);
}
else
{
panic!("expected nom::Err::Error, got: {:?}", result);
}
}