use crate::common::{mock_lower, parse_with_math};
use oxdock_parser::ast::{Expr, StepKind, Value};
fn parse_assign_expr(script: &str) -> Expr {
let steps = parse_with_math(script, mock_lower).expect("parse arithmetic");
assert_eq!(steps.len(), 1, "expected one step, got {steps:?}");
match steps.into_iter().next().unwrap().kind {
StepKind::Assign { expr, .. } => expr,
other => panic!("expected Assign, got {other:?}"),
}
}
#[test]
#[allow(clippy::approx_constant)]
fn int_and_float_literals_bind() {
assert_eq!(
parse_assign_expr("LET $a: INT = 42\n"),
Expr::Literal(Value::int(42))
);
assert_eq!(
parse_assign_expr("LET $f: FLOAT = 3.14\n"),
Expr::Literal(Value::float(3.14))
);
assert_eq!(
parse_assign_expr("LET $f: FLOAT = 2.0\n"),
Expr::Literal(Value::float(2.0))
);
}
#[test]
fn bare_word_boundaries_keep_literal_reading() {
for word in ["30s", "100ms", "123/456", "1.0.0", "-f"] {
assert_eq!(
parse_assign_expr(&format!("LET $x: STRING = {word}\n")),
Expr::Literal(Value::string(word.to_string())),
"bare word {word:?} must stay a string"
);
}
}
#[test]
fn precedence_folds_constants() {
assert_eq!(
parse_assign_expr("LET $x: INT = 2 + 3 * 4\n"),
Expr::Literal(Value::int(14))
);
assert_eq!(
parse_assign_expr("LET $x: BOOL = 2+3*4==14\n"),
Expr::Literal(Value::bool(true))
);
assert_eq!(
parse_assign_expr("LET $x: INT = (2 + 3) * 4\n"),
Expr::Literal(Value::int(20))
);
}
#[test]
fn nesting_folds_at_any_depth() {
assert_eq!(
parse_assign_expr("LET $x: INT = 2 * (2 * (2 + 3)) * 4\n"),
Expr::Literal(Value::int(80))
);
assert_eq!(
parse_assign_expr("LET $x: INT = ((2 + 3) * (4 - 1))\n"),
Expr::Literal(Value::int(15))
);
assert_eq!(
parse_assign_expr("LET $x: INT = (1 + 2) * (3 + 4) - (10 / (2 + 3))\n"),
Expr::Literal(Value::int(19))
);
}
#[test]
fn additive_and_multiplicative_chains_are_left_associative() {
assert_eq!(
parse_assign_expr("LET $x: INT = 100 - 30 - 5\n"),
Expr::Literal(Value::int(65))
);
assert_eq!(
parse_assign_expr("LET $x: INT = 100 / 10 / 2\n"),
Expr::Literal(Value::int(5))
);
assert_eq!(
parse_assign_expr("LET $x: INT = 2 + 3 * 4 - 10 / 2\n"),
Expr::Literal(Value::int(9))
);
assert_eq!(
parse_assign_expr("LET $x: INT = 2 * (3 + 4)\n"),
Expr::Literal(Value::int(14))
);
}
#[test]
fn unary_minus_binds_tighter_than_mul() {
assert_eq!(
parse_assign_expr("LET $x: INT = 2 * -3\n"),
Expr::Literal(Value::int(-6))
);
assert_eq!(
parse_assign_expr("LET $x: INT = -2 * 3\n"),
Expr::Literal(Value::int(-6))
);
assert_eq!(
parse_assign_expr("LET $x: INT = -(2 + 3)\n"),
Expr::Literal(Value::int(-5))
);
assert_eq!(
parse_assign_expr("LET $x: INT = -9223372036854775808 + 1\n"),
Expr::Literal(Value::int(i64::MIN + 1))
);
}
#[test]
fn comparisons_fold_over_arithmetic() {
assert_eq!(
parse_assign_expr("LET $x: BOOL = 2 + 3 > 4\n"),
Expr::Literal(Value::bool(true))
);
assert_eq!(
parse_assign_expr("LET $x: BOOL = 2 * 3 <= 6\n"),
Expr::Literal(Value::bool(true))
);
assert_eq!(
parse_assign_expr("LET $x: BOOL = 10 - 4 >= 7\n"),
Expr::Literal(Value::bool(false))
);
assert_eq!(
parse_assign_expr("LET $x: BOOL = 1 + 1 == 2\n"),
Expr::Literal(Value::bool(true))
);
}
#[test]
fn float_chains_and_int_division() {
assert_eq!(
parse_assign_expr("LET $x: FLOAT = 0.5 * 4 + 1.5\n"),
Expr::Literal(Value::float(3.5))
);
assert_eq!(
parse_assign_expr("LET $x: FLOAT = 10.0 / 4\n"),
Expr::Literal(Value::float(2.5))
);
assert_eq!(
parse_assign_expr("LET $x: INT = 7 / 2\n"),
Expr::Literal(Value::int(3))
);
}
#[test]
fn deep_dynamic_subtrees_compile_to_single_rpn() {
match parse_assign_expr("LET $x: INT = $a * ($b + $c) - $d / $e\n") {
Expr::CompiledMath(ops) => {
assert_eq!(ops.len(), 9, "got {ops:?}");
assert!(matches!(ops[8], oxdock_parser::ast::MathOp::Sub));
}
other => panic!("expected CompiledMath, got {other:?}"),
}
match parse_assign_expr("LET $x: INT = $a * ($b * ($c + $d))\n") {
Expr::CompiledMath(ops) => {
assert_eq!(ops.len(), 7, "got {ops:?}");
}
other => panic!("expected CompiledMath, got {other:?}"),
}
}
#[test]
fn dynamic_subtrees_compile_to_rpn() {
match parse_assign_expr("LET $t: INT = $total + INT($size_str)\n") {
Expr::CompiledMath(ops) => {
assert_eq!(ops.len(), 4, "two loads + call + add, got {ops:?}");
assert!(matches!(&ops[3], oxdock_parser::ast::MathOp::Add));
}
other => panic!("expected CompiledMath, got {other:?}"),
}
}
#[test]
#[allow(clippy::approx_constant)]
fn unary_minus_folds_literals() {
assert_eq!(
parse_assign_expr("LET $x: INT = -5\n"),
Expr::Literal(Value::int(-5))
);
assert_eq!(
parse_assign_expr("LET $x: INT = --5\n"),
Expr::Literal(Value::int(5))
);
assert_eq!(
parse_assign_expr("LET $x: FLOAT = -3.14\n"),
Expr::Literal(Value::float(-3.14))
);
assert_eq!(
parse_assign_expr("LET $x: INT = -9223372036854775808\n"),
Expr::Literal(Value::int(i64::MIN))
);
match parse_assign_expr("LET $x: INT = -$v\n") {
Expr::CompiledMath(ops) => {
assert!(matches!(
ops.as_slice(),
[
oxdock_parser::ast::MathOp::LoadVar(_),
oxdock_parser::ast::MathOp::Neg
]
));
}
other => panic!("expected CompiledMath Neg, got {other:?}"),
}
}
#[test]
fn integer_boundary_overflow_bails() {
for script in [
"LET $x: INT = 9223372036854775808\n",
"LET $x: INT = 9223372036854775808 + 1\n",
"LET $x: INT = 99999999999999999999999\n",
] {
let err = parse_with_math(script, mock_lower).expect_err("literal must overflow");
assert!(
err.to_string().contains("integer overflow"),
"wrong error for {script:?}, got: {err}"
);
}
}
#[test]
fn promotion_and_ordering_fold() {
assert_eq!(
parse_assign_expr("LET $x: FLOAT = 1 + 2.5\n"),
Expr::Literal(Value::float(3.5))
);
assert_eq!(
parse_assign_expr("LET $x: BOOL = 3 < 4.5\n"),
Expr::Literal(Value::bool(true))
);
assert_eq!(
parse_assign_expr("LET $x: BOOL = 2 >= 2\n"),
Expr::Literal(Value::bool(true))
);
assert_eq!(
parse_assign_expr("LET $x: BOOL = 1 == 1.0\n"),
Expr::Literal(Value::bool(true))
);
assert_eq!(
parse_assign_expr("LET $x: BOOL = 0.5 + 0.25 == 0.75\n"),
Expr::Literal(Value::bool(true))
);
assert_eq!(
parse_assign_expr("LET $x: BOOL = 0.1 + 0.2 == 0.3\n"),
Expr::Literal(Value::bool(false))
);
}
#[test]
fn runtime_errors_stay_unfolded() {
for script in [
"LET $x: INT = 1 / 0\n",
"LET $x: INT = 9223372036854775807 + 1\n",
"LET $x: FLOAT = 1.0 / 0.0\n",
] {
match parse_assign_expr(script) {
Expr::CompiledMath(_) => {}
other => panic!("expected CompiledMath for {script:?}, got {other:?}"),
}
}
}
#[test]
fn int_float_conversions_parse() {
assert!(matches!(
parse_assign_expr("LET $n: INT = INT($s)\n"),
Expr::CompiledMath(_) | Expr::Call { .. }
));
assert!(matches!(
parse_assign_expr("LET $f: FLOAT = FLOAT($s)\n"),
Expr::CompiledMath(_) | Expr::Call { .. }
));
}
#[test]
fn call_arg_order_pushes_left_to_right() {
match parse_assign_expr("LET $x: STRING = FOO($a, $b)\n") {
Expr::CompiledMath(ops) => {
assert!(matches!(
ops.as_slice(),
[
oxdock_parser::ast::MathOp::LoadVar(a),
oxdock_parser::ast::MathOp::LoadVar(b),
oxdock_parser::ast::MathOp::Call { name, arity: 2 }
] if a == "a" && b == "b" && name == "MATH::FOO"
));
}
Expr::Call { name, args } => {
assert_eq!(name, "MATH::FOO");
assert_eq!(args.len(), 2);
}
other => panic!("expected call RPN, got {other:?}"),
}
}
#[test]
fn inspect_compiles_to_name_preserving_op() {
match parse_assign_expr("LET $ok: BOOL = INSPECT($p) == INSPECT($p)\n") {
Expr::CompiledMath(ops) => {
assert!(matches!(
ops.as_slice(),
[
oxdock_parser::ast::MathOp::Inspect(a),
oxdock_parser::ast::MathOp::Inspect(b),
oxdock_parser::ast::MathOp::Eq
] if a == "p" && b == "p"
));
}
other => panic!("expected Inspect RPN, got {other:?}"),
}
}
#[test]
fn compiled_math_display_round_trips() {
for script in [
"LET $t: INT = $total + INT($size_str)\n",
"LET $x: INT = 2 * -$v + 1\n",
"LET $x: BOOL = $a < $b\n",
"LET $x: INT = $a * ($b * ($c + $d))\n",
"LET $x: INT = 2 * (2 * (2 + 3)) * 4\n",
] {
let expr = parse_assign_expr(script);
let rendered = expr.to_string();
let again = parse_assign_expr(&format!("LET $t: INT = {rendered}\n"));
assert_eq!(expr, again, "round-trip failed for {script:?}");
}
}
#[test]
fn right_nested_division_keeps_operand_order() {
assert_eq!(
parse_assign_expr("LET $x: INT = 100 / (10 / (4 / 2))\n"),
Expr::Literal(Value::int(20))
);
assert_eq!(
parse_assign_expr("LET $x: INT = (100 / 10) / (4 / 2)\n"),
Expr::Literal(Value::int(5))
);
}
#[test]
fn unary_negation_interleaved_with_nested_parens() {
assert_eq!(
parse_assign_expr("LET $x: INT = -2 * -(3 + -(4 * 5))\n"),
Expr::Literal(Value::int(-34))
);
}
#[test]
fn mixed_promotion_across_subtrees() {
assert_eq!(
parse_assign_expr("LET $x: FLOAT = 1 + (2 * (3.5 - (4 / 2)))\n"),
Expr::Literal(Value::float(4.0))
);
}
#[test]
fn complex_subtrees_on_both_sides_of_ordering() {
assert_eq!(
parse_assign_expr("LET $x: BOOL = (2 * (3 + 1)) <= (10 - (1 * 2))\n"),
Expr::Literal(Value::bool(true))
);
}
#[test]
fn function_call_embedded_in_nested_arithmetic() {
match parse_assign_expr("LET $x: INT = 10 + (3 * (INT(\" 12 \") + 2))\n") {
Expr::CompiledMath(ops) => {
assert_eq!(ops.len(), 8, "got {ops:?}");
assert!(matches!(ops[7], oxdock_parser::ast::MathOp::Add));
}
other => panic!("expected CompiledMath, got {other:?}"),
}
}
#[test]
fn boundary_negation_across_parens() {
assert_eq!(
parse_assign_expr("LET $x: INT = -(-9223372036854775808 + 1)\n"),
Expr::Literal(Value::int(i64::MAX))
);
}
#[test]
fn chained_comparisons_are_rejected() {
for script in [
"LET $x: BOOL = $a < $b < $c\n",
"LET $x: BOOL = $a <= $b >= $c\n",
"LET $x: BOOL = $a == $b == $c\n",
"LET $x: BOOL = $a != $b != $c\n",
] {
parse_with_math(script, mock_lower).expect_err("chained comparison must fail");
}
match parse_assign_expr("LET $x: BOOL = $a < $b && $b < $c\n") {
Expr::Logical { op, left, right } => {
assert_eq!(op, oxdock_parser::ast::LogicalOp::And);
assert!(matches!(left.as_ref(), Expr::CompiledMath(_)));
assert!(matches!(right.as_ref(), Expr::CompiledMath(_)));
}
other => panic!("expected conjunction, got {other:?}"),
}
}
#[test]
fn chained_logical_operators_with_spaces() {
assert_eq!(
parse_assign_expr("LET $x: BOOL = false || false || true\n"),
Expr::Logical {
op: oxdock_parser::ast::LogicalOp::Or,
left: Box::new(Expr::Logical {
op: oxdock_parser::ast::LogicalOp::Or,
left: Box::new(Expr::Literal(Value::bool(false))),
right: Box::new(Expr::Literal(Value::bool(false))),
}),
right: Box::new(Expr::Literal(Value::bool(true))),
}
);
assert_eq!(
parse_assign_expr("LET $x: BOOL = true && true && false\n"),
Expr::Logical {
op: oxdock_parser::ast::LogicalOp::And,
left: Box::new(Expr::Logical {
op: oxdock_parser::ast::LogicalOp::And,
left: Box::new(Expr::Literal(Value::bool(true))),
right: Box::new(Expr::Literal(Value::bool(true))),
}),
right: Box::new(Expr::Literal(Value::bool(false))),
}
);
}