use pretty_assertions::assert_eq;
use super::ast::{DEPTH, Nesting, nest_function};
use crate::{
CompilationError, EvaluationError, Parser, SourceSpan, Validator, compile,
compile_unoptimized, evaluate, support::on_small_stack
};
#[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".into(),
first: SourceSpan { start: 1, end: 2 },
duplicate: SourceSpan { start: 6, end: 7 }
})
);
}
#[test]
fn validate_duplicate_multichar_parameter()
{
let ast = Parser::parse("{abc}, {abc}: 1").unwrap();
assert_eq!(
Validator::validate(&ast),
Err(CompilationError::DuplicateParameter {
name: "abc".into(),
first: SourceSpan { start: 1, end: 4 },
duplicate: SourceSpan { start: 8, end: 11 }
})
);
}
#[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".into(),
first: SourceSpan { start: 1, end: 2 },
duplicate: SourceSpan { start: 11, end: 12 }
})
);
}
#[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".into(),
first: SourceSpan { start: 1, end: 2 },
duplicate: SourceSpan { start: 6, end: 7 }
})
);
}
#[test]
fn validate_duplicate_with_whitespace()
{
let ast = Parser::parse("{x}, {x}: {x}").unwrap();
assert_eq!(
Validator::validate(&ast),
Err(CompilationError::DuplicateParameter {
name: "x".into(),
first: SourceSpan { start: 1, end: 2 },
duplicate: SourceSpan { start: 8, end: 9 }
})
);
}
#[test]
fn validate_duplicate_canonical_parameter()
{
let ast = Parser::parse("{a b}, {a\tb}: 1").unwrap();
assert_eq!(
Validator::validate(&ast),
Err(CompilationError::DuplicateParameter {
name: "a b".into(),
first: SourceSpan { start: 1, end: 4 },
duplicate: SourceSpan { start: 8, end: 11 }
})
);
}
#[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_referenced_canonically()
{
let ast = Parser::parse("{a b}@(3D6) + {a\n b} + { a b }").unwrap();
assert_eq!(Validator::validate(&ast), Ok(()));
let function = compile_unoptimized("{a b}@(3D6) + {a\n b}").unwrap();
assert!(function.externals.is_empty());
}
#[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("4D{f}@(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".into(),
parameter: SourceSpan { start: 1, end: 2 },
binding: SourceSpan { start: 6, end: 7 }
})
);
}
#[test]
fn validate_duplicate_binding()
{
let ast = Parser::parse("{x}@(3D6) + {x}@(1D4)").unwrap();
assert_eq!(
Validator::validate(&ast),
Err(CompilationError::DuplicateBinding {
name: "x".into(),
first: SourceSpan { start: 1, end: 2 },
duplicate: SourceSpan { start: 13, end: 14 }
})
);
}
#[test]
fn validate_nested_duplicate_binding()
{
for source in ["{x}@({x}@(1))", "{x}@({x}@(1)) + {x}"]
{
let ast = Parser::parse(source).unwrap();
assert_eq!(
Validator::validate(&ast),
Err(CompilationError::DuplicateBinding {
name: "x".into(),
first: SourceSpan { start: 1, end: 2 },
duplicate: SourceSpan { start: 6, end: 7 }
}),
"{}",
source
);
}
}
#[test]
fn validate_nested_distinct_bindings()
{
let ast = Parser::parse("{x}@({y}@(1) + 1) + {x} + {y}").unwrap();
assert_eq!(Validator::validate(&ast), Ok(()));
}
#[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".into(),
reference: SourceSpan { start: 0, end: 3 },
binding: SourceSpan { start: 7, end: 8 }
})
);
}
#[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".into(),
reference: SourceSpan { start: 9, end: 12 },
binding: SourceSpan { start: 1, end: 2 }
})
);
}
#[test]
fn compile_unoptimized_propagates_duplicate_parameter()
{
let result = compile_unoptimized("{x}, {x}: 1");
assert_eq!(
result.err(),
Some(CompilationError::DuplicateParameter {
name: "x".into(),
first: SourceSpan { start: 1, end: 2 },
duplicate: SourceSpan { start: 6, end: 7 }
})
);
}
#[test]
fn compile_propagates_duplicate_parameter()
{
let result = compile("{x}, {x}: 1");
assert_eq!(
result.err(),
Some(CompilationError::DuplicateParameter {
name: "x".into(),
first: SourceSpan { start: 1, end: 2 },
duplicate: SourceSpan { start: 6, end: 7 }
})
);
}
#[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".into(),
first: SourceSpan { start: 1, end: 2 },
duplicate: SourceSpan { start: 6, end: 7 }
})
);
}
#[test]
fn duplicate_parameter_display()
{
let err = CompilationError::DuplicateParameter {
name: "x".into(),
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".into(),
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()
{
let ast = Parser::parse("{x}, {x}: 1").unwrap();
let mut validator = Validator::new();
let result = ast.accept(&mut validator);
assert!(matches!(
result,
Err(CompilationError::DuplicateParameter { name, .. }) if name == "x"
));
}
#[test]
fn validator_visitor_arms_are_no_ops()
{
let ast = Parser::parse(
"{x}: [1:{x}] + (1D6 - 1D[1,2,3]) * 2D6 drop lowest / \
3D8 drop highest + -1 ^ 2 % {y}@(1)"
)
.unwrap();
let mut validator = Validator::new();
assert_eq!(ast.accept(&mut validator), Ok(()));
}
#[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".into(),
first: SourceSpan { start: 1, end: 4 },
duplicate: SourceSpan { start: 15, end: 18 }
})
);
}
#[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".into(),
first: SourceSpan { start: 1, end: 2 },
duplicate: SourceSpan { start: 11, end: 12 }
})
);
}
#[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".into(),
parameter: SourceSpan { start: 11, end: 12 },
binding: SourceSpan { start: 16, end: 17 }
})
);
}
#[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".into(),
reference: SourceSpan { start: 0, end: 3 },
binding: SourceSpan { start: 9, end: 10 }
})
);
}
#[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".into(),
reference: SourceSpan { start: 5, end: 8 },
binding: SourceSpan { start: 13, end: 14 }
})
);
}
#[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".into(),
parameter: SourceSpan { start: 1, end: 2 },
binding: SourceSpan { start: 6, end: 7 }
})
);
}
#[test]
fn compile_propagates_binding_collides_with_parameter()
{
let result = compile("{x}: {x}@(3D6) + {x}");
assert_eq!(
result.err(),
Some(CompilationError::BindingCollidesWithParameter {
name: "x".into(),
parameter: SourceSpan { start: 1, end: 2 },
binding: SourceSpan { start: 6, end: 7 }
})
);
}
#[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".into(),
parameter: SourceSpan { start: 1, end: 2 },
binding: SourceSpan { start: 6, end: 7 }
})
);
}
#[test]
fn compile_unoptimized_propagates_duplicate_binding()
{
let result = compile_unoptimized("{x}@(3D6) + {x}@(1D4)");
assert_eq!(
result.err(),
Some(CompilationError::DuplicateBinding {
name: "x".into(),
first: SourceSpan { start: 1, end: 2 },
duplicate: SourceSpan { start: 13, end: 14 }
})
);
}
#[test]
fn compile_propagates_duplicate_binding()
{
let result = compile("{x}@(3D6) + {x}@(1D4)");
assert_eq!(
result.err(),
Some(CompilationError::DuplicateBinding {
name: "x".into(),
first: SourceSpan { start: 1, end: 2 },
duplicate: SourceSpan { start: 13, end: 14 }
})
);
}
#[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".into(),
first: SourceSpan { start: 1, end: 2 },
duplicate: SourceSpan { start: 13, end: 14 }
})
);
}
#[test]
fn compile_unoptimized_propagates_use_before_bind()
{
let result = compile_unoptimized("{x} + {x}@(3D6)");
assert_eq!(
result.err(),
Some(CompilationError::UseBeforeBind {
name: "x".into(),
reference: SourceSpan { start: 0, end: 3 },
binding: SourceSpan { start: 7, end: 8 }
})
);
}
#[test]
fn compile_propagates_use_before_bind()
{
let result = compile("{x} + {x}@(3D6)");
assert_eq!(
result.err(),
Some(CompilationError::UseBeforeBind {
name: "x".into(),
reference: SourceSpan { start: 0, end: 3 },
binding: SourceSpan { start: 7, end: 8 }
})
);
}
#[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".into(),
reference: SourceSpan { start: 0, end: 3 },
binding: SourceSpan { start: 7, end: 8 }
})
);
}
#[test]
fn binding_collides_with_parameter_display()
{
let err = CompilationError::BindingCollidesWithParameter {
name: "x".into(),
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".into(),
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".into(),
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".into(),
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".into(),
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".into(),
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]
#[ignore = "stress: run with just stress"]
fn test_validate_deep()
{
on_small_stack(|| {
for nesting in Nesting::ROTATION.into_iter().chain([Nesting::Mixed])
{
let result = Validator::validate(&nest_function(nesting, DEPTH));
if nesting.binds()
{
assert!(
matches!(
&result,
Err(CompilationError::DuplicateBinding { name, .. })
if name == "a"
),
"{:?}: {:?}",
nesting,
result
);
}
else
{
assert_eq!(result, Ok(()), "{:?}", nesting);
}
}
});
}