use pretty_assertions::assert_eq;
use crate::{
CompilationError, EvaluationError, Parser, SourceSpan, Validator, compile,
compile_unoptimized, evaluate
};
#[test]
fn validate_no_parameters()
{
let ast = Parser::parse("1D6 + 2").unwrap();
assert_eq!(Validator::validate(&ast), Ok(()));
}
#[test]
fn validate_single_parameter()
{
let ast = Parser::parse("x: {x} + 1").unwrap();
assert_eq!(Validator::validate(&ast), Ok(()));
}
#[test]
fn validate_multiple_distinct_parameters()
{
let ast = Parser::parse("x, y, z: {x} + {y} + {z}").unwrap();
assert_eq!(Validator::validate(&ast), Ok(()));
}
#[test]
fn validate_repeated_character_not_duplicate()
{
let ast = Parser::parse("xx: {xx} + 1").unwrap();
assert_eq!(Validator::validate(&ast), Ok(()));
}
#[test]
fn validate_duplicate_single_char_parameter()
{
let ast = Parser::parse("x, x: {x} + 1").unwrap();
assert_eq!(
Validator::validate(&ast),
Err(CompilationError::DuplicateParameter {
name: "x",
first: SourceSpan { start: 0, end: 1 },
duplicate: SourceSpan { start: 3, end: 4 }
})
);
}
#[test]
fn validate_duplicate_multichar_parameter()
{
let ast = Parser::parse("abc, abc: 1").unwrap();
assert_eq!(
Validator::validate(&ast),
Err(CompilationError::DuplicateParameter {
name: "abc",
first: SourceSpan { start: 0, end: 3 },
duplicate: SourceSpan { start: 5, end: 8 }
})
);
}
#[test]
fn validate_duplicate_after_distinct_parameter()
{
let ast = Parser::parse("a, b, a: {a} + {b}").unwrap();
assert_eq!(
Validator::validate(&ast),
Err(CompilationError::DuplicateParameter {
name: "a",
first: SourceSpan { start: 0, end: 1 },
duplicate: SourceSpan { start: 6, end: 7 }
})
);
}
#[test]
fn validate_duplicate_reports_first_only()
{
let ast = Parser::parse("x, x, x: {x}").unwrap();
assert_eq!(
Validator::validate(&ast),
Err(CompilationError::DuplicateParameter {
name: "x",
first: SourceSpan { start: 0, end: 1 },
duplicate: SourceSpan { start: 3, end: 4 }
})
);
}
#[test]
fn validate_duplicate_with_whitespace()
{
let ast = Parser::parse("x, x: {x}").unwrap();
assert_eq!(
Validator::validate(&ast),
Err(CompilationError::DuplicateParameter {
name: "x",
first: SourceSpan { start: 0, end: 1 },
duplicate: SourceSpan { start: 5, end: 6 }
})
);
}
#[test]
fn validate_binding_single_use()
{
let ast = Parser::parse("x@(3D6) + {x}").unwrap();
assert_eq!(Validator::validate(&ast), Ok(()));
}
#[test]
fn validate_binding_multiple_uses()
{
let ast = Parser::parse("x@(3D6) + {x} + {x}").unwrap();
assert_eq!(Validator::validate(&ast), Ok(()));
}
#[test]
fn validate_two_distinct_bindings()
{
let ast = Parser::parse("a@(3D6) + b@(2D4) + {a} + {b}").unwrap();
assert_eq!(Validator::validate(&ast), Ok(()));
}
#[test]
fn validate_binding_references_earlier_binding_in_rhs()
{
let ast = Parser::parse("a@(b@(3D6) + {b}) + {a}").unwrap();
assert_eq!(Validator::validate(&ast), Ok(()));
}
#[test]
fn validate_binding_in_restricted_positions()
{
let ast_count = Parser::parse("n@(2+3)D6 + {n}").unwrap();
assert_eq!(Validator::validate(&ast_count), Ok(()));
let ast_faces = Parser::parse("4Df@(6) + {f}").unwrap();
assert_eq!(Validator::validate(&ast_faces), Ok(()));
let ast_drop = Parser::parse("4D6 drop lowest k@(2) + {k}").unwrap();
assert_eq!(Validator::validate(&ast_drop), Ok(()));
}
#[test]
fn validate_binding_collides_with_parameter()
{
let ast = Parser::parse("x: x@(3D6) + {x}").unwrap();
assert_eq!(
Validator::validate(&ast),
Err(CompilationError::BindingCollidesWithParameter {
name: "x",
parameter: SourceSpan { start: 0, end: 1 },
binding: SourceSpan { start: 3, end: 4 }
})
);
}
#[test]
fn validate_duplicate_binding()
{
let ast = Parser::parse("x@(3D6) + x@(1D4)").unwrap();
assert_eq!(
Validator::validate(&ast),
Err(CompilationError::DuplicateBinding {
name: "x",
first: SourceSpan { start: 0, end: 1 },
duplicate: SourceSpan { start: 10, end: 11 }
})
);
}
#[test]
fn validate_use_before_bind_simple()
{
let ast = Parser::parse("{x} + x@(3D6)").unwrap();
assert_eq!(
Validator::validate(&ast),
Err(CompilationError::UseBeforeBind {
name: "x",
reference: SourceSpan { start: 0, end: 3 },
binding: SourceSpan { start: 6, end: 7 }
})
);
}
#[test]
fn validate_self_reference_inside_binding()
{
let ast = Parser::parse("x@(1 + {x})").unwrap();
assert_eq!(
Validator::validate(&ast),
Err(CompilationError::UseBeforeBind {
name: "x",
reference: SourceSpan { start: 7, end: 10 },
binding: SourceSpan { start: 0, end: 1 }
})
);
}
#[test]
fn compile_unoptimized_propagates_duplicate_parameter()
{
let result = compile_unoptimized("x, x: 1");
assert_eq!(
result.err(),
Some(CompilationError::DuplicateParameter {
name: "x",
first: SourceSpan { start: 0, end: 1 },
duplicate: SourceSpan { start: 3, end: 4 }
})
);
}
#[test]
fn compile_propagates_duplicate_parameter()
{
let result = compile("x, x: 1");
assert_eq!(
result.err(),
Some(CompilationError::DuplicateParameter {
name: "x",
first: SourceSpan { start: 0, end: 1 },
duplicate: SourceSpan { start: 3, end: 4 }
})
);
}
#[test]
fn evaluate_propagates_duplicate_parameter()
{
let mut rng = rand::rng();
let result = evaluate("x, x: 1", vec![0, 0], vec![], &mut rng);
assert_eq!(
result.err(),
Some(EvaluationError::DuplicateParameter {
name: "x",
first: SourceSpan { start: 0, end: 1 },
duplicate: SourceSpan { start: 3, end: 4 }
})
);
}
#[test]
fn duplicate_parameter_display()
{
let err = CompilationError::DuplicateParameter {
name: "x",
first: SourceSpan { start: 0, end: 1 },
duplicate: SourceSpan { start: 3, end: 4 }
};
assert_eq!(
format!("{}", err),
"duplicate parameter 'x' at 3..4 (first declared at 0..1)"
);
}
#[test]
fn duplicate_parameter_display_on_evaluation_error()
{
let err = EvaluationError::DuplicateParameter {
name: "x",
first: SourceSpan { start: 0, end: 1 },
duplicate: SourceSpan { start: 3, end: 4 }
};
assert_eq!(
format!("{}", err),
"duplicate parameter 'x' at 3..4 (first declared at 0..1)"
);
}
#[test]
fn validator_can_be_driven_through_the_trait()
{
use crate::ast::ASTVisitor;
let ast = Parser::parse("x, x: 1").unwrap();
let mut validator = Validator::new();
let result = validator.visit_function(&ast);
assert!(matches!(
result,
Err(CompilationError::DuplicateParameter { name: "x", .. })
));
}
#[test]
fn validator_visitor_arms_are_no_ops()
{
use crate::ast::{
ASTVisitor, ArithmeticExpression, DiceExpression, Expression
};
let ast = Parser::parse(
"x: [1:{x}] + (1D6 - 1D[1,2,3]) * 2D6 drop lowest / \
3D8 drop highest + -1 ^ 2 % 1"
)
.unwrap();
let mut validator = Validator::new();
assert_eq!(validator.visit_function(&ast), Ok(()));
fn drive_expression<'src>(
validator: &mut Validator,
expr: &'src Expression<'src>
)
{
match expr
{
Expression::Group(g) =>
{
assert_eq!(validator.visit_group(g), Ok(()));
drive_expression(validator, &g.expression);
},
Expression::Constant(c) =>
{
assert_eq!(validator.visit_constant(c), Ok(()));
},
Expression::Variable(v) =>
{
assert_eq!(validator.visit_variable(v), Ok(()));
},
Expression::Binding(b) =>
{
assert_eq!(validator.visit_binding(b), Ok(()));
drive_expression(validator, &b.expression);
},
Expression::Range(r) =>
{
assert_eq!(validator.visit_range(r), Ok(()));
drive_expression(validator, &r.start);
drive_expression(validator, &r.end);
},
Expression::Dice(d) => drive_dice(validator, d),
Expression::Arithmetic(a) => drive_arithmetic(validator, a)
}
}
fn drive_dice<'src>(
validator: &mut Validator,
dice: &'src DiceExpression<'src>
)
{
match dice
{
DiceExpression::Standard(d) =>
{
assert_eq!(validator.visit_standard_dice(d), Ok(()));
drive_expression(validator, &d.count);
drive_expression(validator, &d.faces);
},
DiceExpression::Custom(d) =>
{
assert_eq!(validator.visit_custom_dice(d), Ok(()));
drive_expression(validator, &d.count);
},
DiceExpression::DropLowest(d) =>
{
assert_eq!(validator.visit_drop_lowest(d), Ok(()));
drive_dice(validator, &d.dice);
if let Some(drop) = &d.drop
{
drive_expression(validator, drop);
}
},
DiceExpression::DropHighest(d) =>
{
assert_eq!(validator.visit_drop_highest(d), Ok(()));
drive_dice(validator, &d.dice);
if let Some(drop) = &d.drop
{
drive_expression(validator, drop);
}
}
}
}
fn drive_arithmetic<'src>(
validator: &mut Validator,
arith: &'src ArithmeticExpression<'src>
)
{
match arith
{
ArithmeticExpression::Add(a) =>
{
assert_eq!(validator.visit_add(a), Ok(()));
drive_expression(validator, &a.left);
drive_expression(validator, &a.right);
},
ArithmeticExpression::Sub(s) =>
{
assert_eq!(validator.visit_sub(s), Ok(()));
drive_expression(validator, &s.left);
drive_expression(validator, &s.right);
},
ArithmeticExpression::Mul(m) =>
{
assert_eq!(validator.visit_mul(m), Ok(()));
drive_expression(validator, &m.left);
drive_expression(validator, &m.right);
},
ArithmeticExpression::Div(d) =>
{
assert_eq!(validator.visit_div(d), Ok(()));
drive_expression(validator, &d.left);
drive_expression(validator, &d.right);
},
ArithmeticExpression::Mod(m) =>
{
assert_eq!(validator.visit_mod(m), Ok(()));
drive_expression(validator, &m.left);
drive_expression(validator, &m.right);
},
ArithmeticExpression::Exp(e) =>
{
assert_eq!(validator.visit_exp(e), Ok(()));
drive_expression(validator, &e.left);
drive_expression(validator, &e.right);
},
ArithmeticExpression::Neg(n) =>
{
assert_eq!(validator.visit_neg(n), Ok(()));
drive_expression(validator, &n.operand);
}
}
}
drive_expression(&mut validator, &ast.body);
}
#[test]
fn validate_duplicate_binding_multichar()
{
let ast = Parser::parse("abc@(3D6) + abc@(1D4)").unwrap();
assert_eq!(
Validator::validate(&ast),
Err(CompilationError::DuplicateBinding {
name: "abc",
first: SourceSpan { start: 0, end: 3 },
duplicate: SourceSpan { start: 12, end: 15 }
})
);
}
#[test]
fn validate_duplicate_binding_reports_first_pair_only()
{
let ast = Parser::parse("x@(1) + x@(2) + x@(3)").unwrap();
assert_eq!(
Validator::validate(&ast),
Err(CompilationError::DuplicateBinding {
name: "x",
first: SourceSpan { start: 0, end: 1 },
duplicate: SourceSpan { start: 8, end: 9 }
})
);
}
#[test]
fn validate_binding_collides_with_later_parameter()
{
let ast = Parser::parse("a, b, x: x@(3D6) + {x}").unwrap();
assert_eq!(
Validator::validate(&ast),
Err(CompilationError::BindingCollidesWithParameter {
name: "x",
parameter: SourceSpan { start: 6, end: 7 },
binding: SourceSpan { start: 9, end: 10 }
})
);
}
#[test]
fn validate_use_before_bind_in_dice_count()
{
let ast = Parser::parse("{n}D6 + n@(3)").unwrap();
assert_eq!(
Validator::validate(&ast),
Err(CompilationError::UseBeforeBind {
name: "n",
reference: SourceSpan { start: 0, end: 3 },
binding: SourceSpan { start: 8, end: 9 }
})
);
}
#[test]
fn validate_use_before_bind_across_sibling_bindings()
{
let ast = Parser::parse("x@({y}) + y@(1)").unwrap();
assert_eq!(
Validator::validate(&ast),
Err(CompilationError::UseBeforeBind {
name: "y",
reference: SourceSpan { start: 3, end: 6 },
binding: SourceSpan { start: 10, end: 11 }
})
);
}
#[test]
fn validate_mixed_parameter_binding_external()
{
let ast = Parser::parse("p: x@(1D6) + {p} + {x} + {env}").unwrap();
assert_eq!(Validator::validate(&ast), Ok(()));
}
#[test]
fn compile_unoptimized_propagates_binding_collides_with_parameter()
{
let result = compile_unoptimized("x: x@(3D6) + {x}");
assert_eq!(
result.err(),
Some(CompilationError::BindingCollidesWithParameter {
name: "x",
parameter: SourceSpan { start: 0, end: 1 },
binding: SourceSpan { start: 3, end: 4 }
})
);
}
#[test]
fn compile_propagates_binding_collides_with_parameter()
{
let result = compile("x: x@(3D6) + {x}");
assert_eq!(
result.err(),
Some(CompilationError::BindingCollidesWithParameter {
name: "x",
parameter: SourceSpan { start: 0, end: 1 },
binding: SourceSpan { start: 3, end: 4 }
})
);
}
#[test]
fn evaluate_propagates_binding_collides_with_parameter()
{
let mut rng = rand::rng();
let result = evaluate("x: x@(3D6) + {x}", vec![0], vec![], &mut rng);
assert_eq!(
result.err(),
Some(EvaluationError::BindingCollidesWithParameter {
name: "x",
parameter: SourceSpan { start: 0, end: 1 },
binding: SourceSpan { start: 3, end: 4 }
})
);
}
#[test]
fn compile_unoptimized_propagates_duplicate_binding()
{
let result = compile_unoptimized("x@(3D6) + x@(1D4)");
assert_eq!(
result.err(),
Some(CompilationError::DuplicateBinding {
name: "x",
first: SourceSpan { start: 0, end: 1 },
duplicate: SourceSpan { start: 10, end: 11 }
})
);
}
#[test]
fn compile_propagates_duplicate_binding()
{
let result = compile("x@(3D6) + x@(1D4)");
assert_eq!(
result.err(),
Some(CompilationError::DuplicateBinding {
name: "x",
first: SourceSpan { start: 0, end: 1 },
duplicate: SourceSpan { start: 10, end: 11 }
})
);
}
#[test]
fn evaluate_propagates_duplicate_binding()
{
let mut rng = rand::rng();
let result = evaluate("x@(3D6) + x@(1D4)", vec![], vec![], &mut rng);
assert_eq!(
result.err(),
Some(EvaluationError::DuplicateBinding {
name: "x",
first: SourceSpan { start: 0, end: 1 },
duplicate: SourceSpan { start: 10, end: 11 }
})
);
}
#[test]
fn compile_unoptimized_propagates_use_before_bind()
{
let result = compile_unoptimized("{x} + x@(3D6)");
assert_eq!(
result.err(),
Some(CompilationError::UseBeforeBind {
name: "x",
reference: SourceSpan { start: 0, end: 3 },
binding: SourceSpan { start: 6, end: 7 }
})
);
}
#[test]
fn compile_propagates_use_before_bind()
{
let result = compile("{x} + x@(3D6)");
assert_eq!(
result.err(),
Some(CompilationError::UseBeforeBind {
name: "x",
reference: SourceSpan { start: 0, end: 3 },
binding: SourceSpan { start: 6, end: 7 }
})
);
}
#[test]
fn evaluate_propagates_use_before_bind()
{
let mut rng = rand::rng();
let result = evaluate("{x} + x@(3D6)", vec![], vec![], &mut rng);
assert_eq!(
result.err(),
Some(EvaluationError::UseBeforeBind {
name: "x",
reference: SourceSpan { start: 0, end: 3 },
binding: SourceSpan { start: 6, end: 7 }
})
);
}
#[test]
fn binding_collides_with_parameter_display()
{
let err = CompilationError::BindingCollidesWithParameter {
name: "x",
parameter: SourceSpan { start: 0, end: 1 },
binding: SourceSpan { start: 3, end: 4 }
};
assert_eq!(
format!("{}", err),
"local binding 'x' at 3..4 collides with formal parameter declared at 0..1"
);
}
#[test]
fn binding_collides_with_parameter_display_on_evaluation_error()
{
let err = EvaluationError::BindingCollidesWithParameter {
name: "x",
parameter: SourceSpan { start: 0, end: 1 },
binding: SourceSpan { start: 3, end: 4 }
};
assert_eq!(
format!("{}", err),
"local binding 'x' at 3..4 collides with formal parameter declared at 0..1"
);
}
#[test]
fn duplicate_binding_display()
{
let err = CompilationError::DuplicateBinding {
name: "x",
first: SourceSpan { start: 0, end: 1 },
duplicate: SourceSpan { start: 10, end: 11 }
};
assert_eq!(
format!("{}", err),
"duplicate local binding 'x' at 10..11 (first bound at 0..1)"
);
}
#[test]
fn duplicate_binding_display_on_evaluation_error()
{
let err = EvaluationError::DuplicateBinding {
name: "x",
first: SourceSpan { start: 0, end: 1 },
duplicate: SourceSpan { start: 10, end: 11 }
};
assert_eq!(
format!("{}", err),
"duplicate local binding 'x' at 10..11 (first bound at 0..1)"
);
}
#[test]
fn use_before_bind_display()
{
let err = CompilationError::UseBeforeBind {
name: "x",
reference: SourceSpan { start: 0, end: 3 },
binding: SourceSpan { start: 6, end: 7 }
};
assert_eq!(
format!("{}", err),
"reference to 'x' at 0..3 precedes its binding at 6..7"
);
}
#[test]
fn use_before_bind_display_on_evaluation_error()
{
let err = EvaluationError::UseBeforeBind {
name: "x",
reference: SourceSpan { start: 0, end: 3 },
binding: SourceSpan { start: 6, end: 7 }
};
assert_eq!(
format!("{}", err),
"reference to 'x' at 0..3 precedes its binding at 6..7"
);
}