mirror of
https://github.com/duhanbalci/dexpr.git
synced 2026-08-30 16:01:41 +00:00
0.4.1
This commit is contained in:
@@ -2069,3 +2069,138 @@ fn test_string_compare_in_condition() {
|
||||
"#;
|
||||
assert_eq!(run_and_get_result(code), Value::Number(dec!(80)));
|
||||
}
|
||||
|
||||
// ==================== SHORT-CIRCUIT / LIST AGGREGATE / LENGTH TESTS ====================
|
||||
|
||||
fn num_list(v: &[i64]) -> Value {
|
||||
Value::NumberList(Rc::new(v.iter().map(|n| rust_decimal::Decimal::from(*n)).collect()))
|
||||
}
|
||||
|
||||
fn obj(fields: Vec<(&str, Value)>) -> Value {
|
||||
let mut m = IndexMap::new();
|
||||
for (k, v) in fields {
|
||||
m.insert(SmolStr::new(k), v);
|
||||
}
|
||||
Value::Object(Rc::new(m))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_short_circuit_and_skips_failing_right() {
|
||||
// Right side would blow up (property on Null) if evaluated
|
||||
assert_eq!(
|
||||
run_expr_with_globals("x != null && x.a.b.c > 1", vec![("x", Value::Null)]),
|
||||
Value::Boolean(false)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_short_circuit_or_skips_failing_right() {
|
||||
assert_eq!(
|
||||
run_expr_with_globals("x == null || x.a.b.c > 1", vec![("x", Value::Null)]),
|
||||
Value::Boolean(true)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_short_circuit_left_must_be_boolean() {
|
||||
let err = run_expr_err("1 && true", vec![]);
|
||||
assert!(err.contains("Boolean"), "got: {err}");
|
||||
let err = run_expr_err("\"a\" || true", vec![]);
|
||||
assert!(err.contains("Boolean"), "got: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_short_circuit_right_value_passthrough() {
|
||||
// Right operand's value is the result when left doesn't short-circuit
|
||||
// (JS-like); using it in `if` still type-checks.
|
||||
assert_eq!(run_expr("true && 1"), Value::Number(dec!(1)));
|
||||
let err = run_expr_err("if true && 1 then 1 end", vec![]);
|
||||
assert!(err.contains("Boolean"), "got: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_short_circuit_register_pressure() {
|
||||
// Deep nesting: exercise register reuse in compile_logical_op
|
||||
let code = "a = 1\nb = 2\nc = 3\n(a < b && b < c && c > a) || (a > b && b > c) || (a == 1 && (b == 2 || c == 9) && !(a > c))";
|
||||
assert_eq!(run_expr(code), Value::Boolean(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_short_circuit_side_effect_not_run() {
|
||||
// Host fn records calls; must not be called when short-circuited
|
||||
let ast = parser::program("false && hit() == 1").expect("parse");
|
||||
let mut compiler = Compiler::new();
|
||||
let bytecode = compiler.compile(ast).expect("compile");
|
||||
let mut vm = VM::new(&bytecode);
|
||||
let called = Rc::new(std::cell::Cell::new(false));
|
||||
let c2 = called.clone();
|
||||
vm.register_function("hit", move |_| {
|
||||
c2.set(true);
|
||||
Ok(Value::Number(dec!(1)))
|
||||
});
|
||||
assert_eq!(vm.execute().unwrap(), Value::Boolean(false));
|
||||
assert!(!called.get(), "right side must not run");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_list_aggregates() {
|
||||
let el = Value::List(Rc::new(vec![]));
|
||||
assert_eq!(run_expr_with_globals("l.sum()", vec![("l", el.clone())]), Value::Number(dec!(0)));
|
||||
assert_eq!(run_expr_with_globals("l.avg()", vec![("l", el.clone())]), Value::Null);
|
||||
assert_eq!(run_expr_with_globals("l.min()", vec![("l", el.clone())]), Value::Null);
|
||||
assert_eq!(run_expr_with_globals("l.max()", vec![("l", el)]), Value::Null);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_projection_sum() {
|
||||
// items.amount on an empty items list must still sum to 0
|
||||
let items = Value::List(Rc::new(vec![]));
|
||||
assert_eq!(
|
||||
run_expr_with_globals("items.amount.sum()", vec![("items", items.clone())]),
|
||||
Value::Number(dec!(0))
|
||||
);
|
||||
assert_eq!(
|
||||
run_expr_with_globals("items.amount.length", vec![("items", items)]),
|
||||
Value::Number(dec!(0))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_numeric_list_aggregates() {
|
||||
let l = Value::List(Rc::new(vec![
|
||||
Value::Number(dec!(4)),
|
||||
Value::Number(dec!(1)),
|
||||
Value::Number(dec!(7)),
|
||||
]));
|
||||
assert_eq!(run_expr_with_globals("l.sum()", vec![("l", l.clone())]), Value::Number(dec!(12)));
|
||||
assert_eq!(run_expr_with_globals("l.avg()", vec![("l", l.clone())]), Value::Number(dec!(4)));
|
||||
assert_eq!(run_expr_with_globals("l.min()", vec![("l", l.clone())]), Value::Number(dec!(1)));
|
||||
assert_eq!(run_expr_with_globals("l.max()", vec![("l", l)]), Value::Number(dec!(7)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mixed_list_aggregate_errors() {
|
||||
let l = Value::List(Rc::new(vec![Value::Number(dec!(1)), Value::String("x".into())]));
|
||||
let err = run_expr_err("l.sum()", vec![("l", l)]);
|
||||
assert!(err.contains("list of numbers"), "got: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_length_property_on_lists() {
|
||||
assert_eq!(run_expr_with_globals("l.length", vec![("l", num_list(&[1, 2, 3]))]), Value::Number(dec!(3)));
|
||||
let sl = Value::StringList(Rc::new(vec!["a".into()]));
|
||||
assert_eq!(run_expr_with_globals("l.length", vec![("l", sl)]), Value::Number(dec!(1)));
|
||||
let ml = Value::List(Rc::new(vec![Value::Number(dec!(1)), Value::String("x".into())]));
|
||||
assert_eq!(run_expr_with_globals("l.length", vec![("l", ml)]), Value::Number(dec!(2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_length_property_projection_on_object_list() {
|
||||
// List of objects with a `length` field → projection, not count
|
||||
let l = Value::List(Rc::new(vec![
|
||||
obj(vec![("length", Value::Number(dec!(10)))]),
|
||||
obj(vec![("length", Value::Number(dec!(20)))]),
|
||||
]));
|
||||
assert_eq!(run_expr_with_globals("l.length", vec![("l", l.clone())]), num_list(&[10, 20]));
|
||||
assert_eq!(run_expr_with_globals("l.length()", vec![("l", l)]), Value::Number(dec!(2)));
|
||||
}
|
||||
|
||||
@@ -1458,5 +1458,326 @@
|
||||
"o": {"type": "object", "value": {"name": "ali"}}
|
||||
},
|
||||
"expected": { "type": "boolean", "value": true }
|
||||
},
|
||||
|
||||
{
|
||||
"name": "unary: not on property",
|
||||
"code": "!o.active",
|
||||
"globals": {
|
||||
"o": {"type": "object", "value": {"active": false}}
|
||||
},
|
||||
"expected": { "type": "boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"name": "unary: not on method call",
|
||||
"code": "!o.keys().isEmpty()",
|
||||
"globals": {
|
||||
"o": {"type": "object", "value": {"a": "1"}}
|
||||
},
|
||||
"expected": { "type": "boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"name": "unary: neg on property",
|
||||
"code": "-o.age",
|
||||
"globals": {
|
||||
"o": {"type": "object", "value": {"age": "30"}}
|
||||
},
|
||||
"expected": { "type": "number", "value": "-30" }
|
||||
},
|
||||
{
|
||||
"name": "unary: neg on method call",
|
||||
"code": "-\"5\".length()",
|
||||
"expected": { "type": "number", "value": "-1" }
|
||||
},
|
||||
{
|
||||
"name": "unary: double neg",
|
||||
"code": "- -1",
|
||||
"expected": { "type": "number", "value": "1" }
|
||||
},
|
||||
{
|
||||
"name": "unary: double not",
|
||||
"code": "!!true",
|
||||
"expected": { "type": "boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"name": "unary: neg binds looser than power",
|
||||
"code": "-2 ** 2",
|
||||
"expected": { "type": "number", "value": "-4" }
|
||||
},
|
||||
{
|
||||
"name": "unary: neg exponent",
|
||||
"code": "2 ** -1",
|
||||
"expected": { "type": "number", "value": "0.5" }
|
||||
},
|
||||
{
|
||||
"name": "unary: neg in arithmetic",
|
||||
"code": "3 - -2",
|
||||
"expected": { "type": "number", "value": "5" }
|
||||
},
|
||||
{
|
||||
"name": "unary: neg times",
|
||||
"code": "-2 * 3",
|
||||
"expected": { "type": "number", "value": "-6" }
|
||||
},
|
||||
{
|
||||
"name": "unary: not with and",
|
||||
"code": "!false && true",
|
||||
"expected": { "type": "boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"name": "unary: not with comparison",
|
||||
"code": "!(1 > 2)",
|
||||
"expected": { "type": "boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"name": "unary: not on property in if",
|
||||
"code": "if !o.iptal then \"ok\" else \"iptal\" end",
|
||||
"globals": {
|
||||
"o": {"type": "object", "value": {"iptal": false}}
|
||||
},
|
||||
"expected": { "type": "string", "value": "ok" }
|
||||
},
|
||||
{
|
||||
"name": "short-circuit: and skips right when left false",
|
||||
"code": "x != null && x.name == \"a\"",
|
||||
"globals": {
|
||||
"x": {"type": "null"}
|
||||
},
|
||||
"expected": { "type": "boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"name": "short-circuit: and evaluates right when left true",
|
||||
"code": "x != null && x.name == \"a\"",
|
||||
"globals": {
|
||||
"x": {"type": "object", "value": {"name": "a"}}
|
||||
},
|
||||
"expected": { "type": "boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"name": "short-circuit: or skips right when left true",
|
||||
"code": "x == null || x.name == \"a\"",
|
||||
"globals": {
|
||||
"x": {"type": "null"}
|
||||
},
|
||||
"expected": { "type": "boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"name": "short-circuit: or evaluates right when left false",
|
||||
"code": "x == null || x.name == \"a\"",
|
||||
"globals": {
|
||||
"x": {"type": "object", "value": {"name": "b"}}
|
||||
},
|
||||
"expected": { "type": "boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"name": "short-circuit: and true true",
|
||||
"code": "true && true",
|
||||
"expected": { "type": "boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"name": "short-circuit: and true false",
|
||||
"code": "true && false",
|
||||
"expected": { "type": "boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"name": "short-circuit: or false false",
|
||||
"code": "false || false",
|
||||
"expected": { "type": "boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"name": "short-circuit: or false true",
|
||||
"code": "false || true",
|
||||
"expected": { "type": "boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"name": "short-circuit: chained and",
|
||||
"code": "true && true && false",
|
||||
"expected": { "type": "boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"name": "short-circuit: chained or",
|
||||
"code": "false || false || true",
|
||||
"expected": { "type": "boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"name": "short-circuit: mixed precedence",
|
||||
"code": "true || false && false",
|
||||
"expected": { "type": "boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"name": "short-circuit: mixed precedence 2",
|
||||
"code": "false && true || true",
|
||||
"expected": { "type": "boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"name": "short-circuit: nested with comparisons",
|
||||
"code": "a > 1 && (b < 5 || c == 3)",
|
||||
"globals": {
|
||||
"a": {"type": "number", "value": "2"},
|
||||
"b": {"type": "number", "value": "9"},
|
||||
"c": {"type": "number", "value": "3"}
|
||||
},
|
||||
"expected": { "type": "boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"name": "short-circuit: result assigned",
|
||||
"code": "r = x != null && x.v > 1\nr",
|
||||
"globals": {
|
||||
"x": {"type": "null"}
|
||||
},
|
||||
"expected": { "type": "boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"name": "short-circuit: in if condition",
|
||||
"code": "if x != null && x.v > 1 then 1 else 2 end",
|
||||
"globals": {
|
||||
"x": {"type": "null"}
|
||||
},
|
||||
"expected": { "type": "number", "value": "2" }
|
||||
},
|
||||
{
|
||||
"name": "short-circuit: skips method on null",
|
||||
"code": "x != null && x.upper() == \"A\"",
|
||||
"globals": {
|
||||
"x": {"type": "null"}
|
||||
},
|
||||
"expected": { "type": "boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"name": "short-circuit: right side complex",
|
||||
"code": "1 == 1 && (2 + 3) * 2 == 10",
|
||||
"expected": { "type": "boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"name": "short-circuit: left side complex",
|
||||
"code": "(2 + 3) * 2 == 10 && 1 == 1",
|
||||
"expected": { "type": "boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"name": "short-circuit: string concat after",
|
||||
"code": "(true && false) + \"\"",
|
||||
"expected": { "type": "string", "value": "false" }
|
||||
},
|
||||
{
|
||||
"name": "round: half away from zero 2.5",
|
||||
"code": "round(2.5)",
|
||||
"expected": { "type": "number", "value": "3" }
|
||||
},
|
||||
{
|
||||
"name": "round: half away from zero 1.5",
|
||||
"code": "round(1.5)",
|
||||
"expected": { "type": "number", "value": "2" }
|
||||
},
|
||||
{
|
||||
"name": "round: half away from zero 0.5",
|
||||
"code": "round(0.5)",
|
||||
"expected": { "type": "number", "value": "1" }
|
||||
},
|
||||
{
|
||||
"name": "round: negative half",
|
||||
"code": "round(-2.5)",
|
||||
"expected": { "type": "number", "value": "-3" }
|
||||
},
|
||||
{
|
||||
"name": "round: dp half",
|
||||
"code": "round(0.125, 2)",
|
||||
"expected": { "type": "number", "value": "0.13" }
|
||||
},
|
||||
{
|
||||
"name": "round: dp",
|
||||
"code": "round(1.234, 2)",
|
||||
"expected": { "type": "number", "value": "1.23" }
|
||||
},
|
||||
{
|
||||
"name": "round: dp up",
|
||||
"code": "round(1.235, 2)",
|
||||
"expected": { "type": "number", "value": "1.24" }
|
||||
},
|
||||
{
|
||||
"name": "length prop: string",
|
||||
"code": "\"hello\".length",
|
||||
"expected": { "type": "number", "value": "5" }
|
||||
},
|
||||
{
|
||||
"name": "length prop: unicode string",
|
||||
"code": "\"üşi\".length",
|
||||
"expected": { "type": "number", "value": "3" }
|
||||
},
|
||||
{
|
||||
"name": "length method: unicode string",
|
||||
"code": "\"üşi\".length()",
|
||||
"expected": { "type": "number", "value": "3" }
|
||||
},
|
||||
{
|
||||
"name": "length prop: string global",
|
||||
"code": "s.length",
|
||||
"globals": {
|
||||
"s": {"type": "string", "value": "Hello World"}
|
||||
},
|
||||
"expected": { "type": "number", "value": "11" }
|
||||
},
|
||||
{
|
||||
"name": "length prop: string in expression",
|
||||
"code": "s.length > 5",
|
||||
"globals": {
|
||||
"s": {"type": "string", "value": "Hello World"}
|
||||
},
|
||||
"expected": { "type": "boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"name": "length prop: object field named length still works",
|
||||
"code": "o.length",
|
||||
"globals": {
|
||||
"o": {"type": "object", "value": {"length": "7"}}
|
||||
},
|
||||
"expected": { "type": "number", "value": "7" }
|
||||
},
|
||||
{
|
||||
"name": "length prop: object field string",
|
||||
"code": "o.name.length",
|
||||
"globals": {
|
||||
"o": {"type": "object", "value": {"name": "ali"}}
|
||||
},
|
||||
"expected": { "type": "number", "value": "3" }
|
||||
},
|
||||
{
|
||||
"name": "assoc: and binds tighter than or",
|
||||
"code": "false && true || true",
|
||||
"expected": { "type": "boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"name": "assoc: and binds tighter than or 2",
|
||||
"code": "true || true && false",
|
||||
"expected": { "type": "boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"name": "assoc: or chain left",
|
||||
"code": "false || false || true && false",
|
||||
"expected": { "type": "boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"name": "assoc: equality left assoc",
|
||||
"code": "1 == 1 == true",
|
||||
"expected": { "type": "boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"name": "assoc: subtraction left assoc",
|
||||
"code": "10 - 3 - 2",
|
||||
"expected": { "type": "number", "value": "5" }
|
||||
},
|
||||
{
|
||||
"name": "assoc: add sub mixed",
|
||||
"code": "10 - 3 + 2",
|
||||
"expected": { "type": "number", "value": "9" }
|
||||
},
|
||||
{
|
||||
"name": "assoc: comparison then and",
|
||||
"code": "1 < 2 && 3 >= 3 && \"a\" != \"b\"",
|
||||
"expected": { "type": "boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"name": "assoc: in with and",
|
||||
"code": "\"a\" in \"abc\" && \"z\" in \"abc\" == false",
|
||||
"expected": { "type": "boolean", "value": true }
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user