#![cfg(feature = "js-runtime")]
use pmcp_code_mode::eval::{evaluate_array_method_with_scope, evaluate_with_scope};
use pmcp_code_mode::executor::{
ArrayMethodCall, BinaryOperator, ObjectField, PlanCompiler, PlanStep, UnaryOperator, ValueExpr,
};
use serde_json::{json, Value as JsonValue};
use std::collections::HashMap;
fn empty_globals() -> HashMap<String, JsonValue> {
HashMap::new()
}
fn empty_locals() -> HashMap<String, JsonValue> {
HashMap::new()
}
fn eval_isolated(expr: &ValueExpr) -> JsonValue {
evaluate_with_scope(expr, &empty_globals(), &empty_locals())
.expect("expression should evaluate without error")
}
fn compile_return_expr(js_source: &str) -> ValueExpr {
let wrapped = format!("return {};", js_source);
let mut compiler = PlanCompiler::new();
let plan = compiler
.compile_code(&wrapped)
.unwrap_or_else(|err| panic!("compile failed for `{}`: {:?}", js_source, err));
for step in plan.steps {
if let PlanStep::Return { value } = step {
return value;
}
}
panic!("no Return step found for `{}`", js_source);
}
#[test]
fn variant_literal_passthrough() {
let expr = ValueExpr::Literal(json!(42));
assert_eq!(eval_isolated(&expr), json!(42));
}
#[test]
fn variant_literal_string() {
let expr = ValueExpr::Literal(json!("hello"));
assert_eq!(eval_isolated(&expr), json!("hello"));
}
#[test]
fn variant_variable_local_scope_wins_over_global() {
let mut globals = empty_globals();
globals.insert("x".into(), json!(1));
let mut locals = empty_locals();
locals.insert("x".into(), json!(99));
let expr = ValueExpr::Variable("x".into());
let result =
evaluate_with_scope(&expr, &globals, &locals).expect("variable lookup should succeed");
assert_eq!(result, json!(99));
}
#[test]
fn variant_variable_global_fallback() {
let mut globals = empty_globals();
globals.insert("answer".into(), json!(42));
let expr = ValueExpr::Variable("answer".into());
let result = evaluate_with_scope(&expr, &globals, &empty_locals())
.expect("global variable lookup should succeed");
assert_eq!(result, json!(42));
}
#[test]
fn variant_variable_undefined_builtin_returns_null() {
let expr = ValueExpr::Variable("undefined".into());
assert_eq!(eval_isolated(&expr), JsonValue::Null);
}
#[test]
fn variant_property_access_returns_value() {
let expr = ValueExpr::PropertyAccess {
object: Box::new(ValueExpr::Literal(json!({"a": 1, "b": 2}))),
property: "b".into(),
};
assert_eq!(eval_isolated(&expr), json!(2));
}
#[test]
fn variant_property_access_missing_returns_null() {
let expr = ValueExpr::PropertyAccess {
object: Box::new(ValueExpr::Literal(json!({"a": 1}))),
property: "missing".into(),
};
assert_eq!(eval_isolated(&expr), JsonValue::Null);
}
#[test]
fn variant_array_index_in_bounds() {
let expr = ValueExpr::ArrayIndex {
array: Box::new(ValueExpr::Literal(json!([10, 20, 30]))),
index: Box::new(ValueExpr::Literal(json!(1))),
};
assert_eq!(eval_isolated(&expr), json!(20));
}
#[test]
fn variant_object_literal_evaluates_fields() {
let expr = ValueExpr::ObjectLiteral {
fields: vec![
ObjectField::KeyValue {
key: "n".into(),
value: ValueExpr::Literal(json!(5)),
},
ObjectField::KeyValue {
key: "msg".into(),
value: ValueExpr::Literal(json!("hi")),
},
],
};
assert_eq!(eval_isolated(&expr), json!({"n": 5, "msg": "hi"}));
}
#[test]
fn variant_array_literal_evaluates_each_item() {
let expr = ValueExpr::ArrayLiteral {
items: vec![
ValueExpr::Literal(json!(1)),
ValueExpr::Literal(json!(2)),
ValueExpr::Literal(json!(3)),
],
};
assert_eq!(eval_isolated(&expr), json!([1, 2, 3]));
}
#[test]
fn variant_binop_addition_numeric() {
let expr = ValueExpr::BinaryOp {
left: Box::new(ValueExpr::Literal(json!(2))),
op: BinaryOperator::Add,
right: Box::new(ValueExpr::Literal(json!(3))),
};
assert_eq!(eval_isolated(&expr), json!(5.0));
}
#[test]
fn variant_binop_multiplication() {
let expr = ValueExpr::BinaryOp {
left: Box::new(ValueExpr::Literal(json!(4))),
op: BinaryOperator::Mul,
right: Box::new(ValueExpr::Literal(json!(7))),
};
assert_eq!(eval_isolated(&expr), json!(28.0));
}
#[test]
fn variant_binop_strict_equality_true() {
let expr = ValueExpr::BinaryOp {
left: Box::new(ValueExpr::Literal(json!(5))),
op: BinaryOperator::StrictEq,
right: Box::new(ValueExpr::Literal(json!(5))),
};
assert_eq!(eval_isolated(&expr), json!(true));
}
#[test]
fn variant_unary_not() {
let expr = ValueExpr::UnaryOp {
op: UnaryOperator::Not,
operand: Box::new(ValueExpr::Literal(json!(false))),
};
assert_eq!(eval_isolated(&expr), json!(true));
}
#[test]
fn variant_unary_neg() {
let expr = ValueExpr::UnaryOp {
op: UnaryOperator::Neg,
operand: Box::new(ValueExpr::Literal(json!(7))),
};
assert_eq!(eval_isolated(&expr), json!(-7.0));
}
#[test]
fn variant_ternary_truthy_branch() {
let expr = ValueExpr::Ternary {
condition: Box::new(ValueExpr::Literal(json!(true))),
consequent: Box::new(ValueExpr::Literal(json!("yes"))),
alternate: Box::new(ValueExpr::Literal(json!("no"))),
};
assert_eq!(eval_isolated(&expr), json!("yes"));
}
#[test]
fn variant_ternary_falsy_branch() {
let expr = ValueExpr::Ternary {
condition: Box::new(ValueExpr::Literal(json!(0))),
consequent: Box::new(ValueExpr::Literal(json!("yes"))),
alternate: Box::new(ValueExpr::Literal(json!("no"))),
};
assert_eq!(eval_isolated(&expr), json!("no"));
}
#[test]
fn variant_optional_chain_on_object_returns_value() {
let expr = ValueExpr::OptionalChain {
object: Box::new(ValueExpr::Literal(json!({"a": 5}))),
property: "a".into(),
};
assert_eq!(eval_isolated(&expr), json!(5));
}
#[test]
fn variant_optional_chain_on_null_returns_null() {
let expr = ValueExpr::OptionalChain {
object: Box::new(ValueExpr::Literal(JsonValue::Null)),
property: "anything".into(),
};
assert_eq!(eval_isolated(&expr), JsonValue::Null);
}
#[test]
fn variant_nullish_coalesce_left_is_null() {
let expr = ValueExpr::NullishCoalesce {
left: Box::new(ValueExpr::Literal(JsonValue::Null)),
right: Box::new(ValueExpr::Literal(json!(42))),
};
assert_eq!(eval_isolated(&expr), json!(42));
}
#[test]
fn variant_nullish_coalesce_left_is_zero_keeps_zero() {
let expr = ValueExpr::NullishCoalesce {
left: Box::new(ValueExpr::Literal(json!(0))),
right: Box::new(ValueExpr::Literal(json!(99))),
};
assert_eq!(eval_isolated(&expr), json!(0));
}
#[test]
fn variant_block_with_local_bindings() {
let expr = ValueExpr::Block {
bindings: vec![
("a".into(), ValueExpr::Literal(json!(2))),
("b".into(), ValueExpr::Literal(json!(3))),
],
result: Box::new(ValueExpr::BinaryOp {
left: Box::new(ValueExpr::Variable("a".into())),
op: BinaryOperator::Add,
right: Box::new(ValueExpr::Variable("b".into())),
}),
};
assert_eq!(eval_isolated(&expr), json!(5.0));
}
fn eval_method_isolated(arr: JsonValue, method: ArrayMethodCall) -> JsonValue {
let globals = empty_globals();
let mut locals = empty_locals();
evaluate_array_method_with_scope(&arr, &method, &globals, &mut locals)
.expect("array method should evaluate without error")
}
#[test]
fn array_method_length() {
let result = eval_method_isolated(json!([1, 2, 3, 4]), ArrayMethodCall::Length);
assert_eq!(result, json!(4));
}
#[test]
fn array_method_map_doubles_each_element() {
let method = ArrayMethodCall::Map {
item_var: "x".into(),
body: Box::new(ValueExpr::BinaryOp {
left: Box::new(ValueExpr::Variable("x".into())),
op: BinaryOperator::Mul,
right: Box::new(ValueExpr::Literal(json!(2))),
}),
};
let result = eval_method_isolated(json!([1, 2, 3]), method);
assert_eq!(result, json!([2.0, 4.0, 6.0]));
}
#[test]
fn array_method_filter_keeps_evens() {
let method = ArrayMethodCall::Filter {
item_var: "n".into(),
predicate: Box::new(ValueExpr::BinaryOp {
left: Box::new(ValueExpr::BinaryOp {
left: Box::new(ValueExpr::Variable("n".into())),
op: BinaryOperator::Mod,
right: Box::new(ValueExpr::Literal(json!(2))),
}),
op: BinaryOperator::StrictEq,
right: Box::new(ValueExpr::Literal(json!(0))),
}),
};
let result = eval_method_isolated(json!([1, 2, 3, 4, 5]), method);
assert_eq!(result, json!([2, 4]));
}
#[test]
fn array_method_find_returns_first_match() {
let method = ArrayMethodCall::Find {
item_var: "x".into(),
predicate: Box::new(ValueExpr::BinaryOp {
left: Box::new(ValueExpr::Variable("x".into())),
op: BinaryOperator::Gt,
right: Box::new(ValueExpr::Literal(json!(2))),
}),
};
let result = eval_method_isolated(json!([1, 2, 3, 4]), method);
assert_eq!(result, json!(3));
}
#[test]
fn array_method_some_true_when_any_match() {
let method = ArrayMethodCall::Some {
item_var: "x".into(),
predicate: Box::new(ValueExpr::BinaryOp {
left: Box::new(ValueExpr::Variable("x".into())),
op: BinaryOperator::Gt,
right: Box::new(ValueExpr::Literal(json!(10))),
}),
};
let result = eval_method_isolated(json!([1, 2, 11]), method);
assert_eq!(result, json!(true));
}
#[test]
fn array_method_every_false_when_one_fails() {
let method = ArrayMethodCall::Every {
item_var: "x".into(),
predicate: Box::new(ValueExpr::BinaryOp {
left: Box::new(ValueExpr::Variable("x".into())),
op: BinaryOperator::Gt,
right: Box::new(ValueExpr::Literal(json!(0))),
}),
};
let result = eval_method_isolated(json!([1, 2, -1, 3]), method);
assert_eq!(result, json!(false));
}
#[test]
fn array_method_reduce_sums_elements() {
let method = ArrayMethodCall::Reduce {
acc_var: "acc".into(),
item_var: "x".into(),
body: Box::new(ValueExpr::BinaryOp {
left: Box::new(ValueExpr::Variable("acc".into())),
op: BinaryOperator::Add,
right: Box::new(ValueExpr::Variable("x".into())),
}),
initial: Box::new(ValueExpr::Literal(json!(0))),
};
let result = eval_method_isolated(json!([1, 2, 3, 4]), method);
assert_eq!(result, json!(10.0));
}
#[test]
fn array_method_includes_returns_true() {
let method = ArrayMethodCall::Includes {
item: Box::new(ValueExpr::Literal(json!(3))),
};
let result = eval_method_isolated(json!([1, 2, 3]), method);
assert_eq!(result, json!(true));
}
#[test]
fn array_method_join_default_separator() {
let method = ArrayMethodCall::Join { separator: None };
let result = eval_method_isolated(json!(["a", "b", "c"]), method);
assert_eq!(result, json!("a,b,c"));
}
#[test]
fn array_method_concat_appends_other() {
let method = ArrayMethodCall::Concat {
other: Box::new(ValueExpr::Literal(json!([4, 5]))),
};
let result = eval_method_isolated(json!([1, 2, 3]), method);
assert_eq!(result, json!([1, 2, 3, 4, 5]));
}
const CORPUS: &[(&str, &str)] = &[
("1 + 2", "3.0"),
("10 - 3", "7.0"),
("4 * 5", "20.0"),
("9 / 2", "4.5"),
("11 % 3", "2.0"),
("-7", "-7.0"),
("3 === 3", "true"),
("3 === 4", "false"),
("5 > 3", "true"),
("(1 + 1) === 2", "true"),
(r#""hello" + " " + "world""#, r#""hello world""#),
(r#""abc".toUpperCase()"#, r#""ABC""#),
(r#"" trim ".trim()"#, r#""trim""#),
(r#""a,b,c".split(",")"#, r#"["a", "b", "c"]"#),
("[1, 2, 3].length", "3"),
("[1, 2, 3].map(x => x * 2)", "[2.0, 4.0, 6.0]"),
("[1, 2, 3, 4].filter(n => n % 2 === 0)", "[2, 4]"),
("[1, 2, 3].reduce((a, x) => a + x, 0)", "6.0"),
("[1, 2, 3].includes(2)", "true"),
("[1, 2, 3].slice(0, 2)", "[1, 2]"),
("[1, [2, [3]]].flat()", "[1, 2, [3]]"),
("({ a: 1, b: 2 }).b", "2"),
("({ x: 1, y: 2 })", "{\"x\": 1, \"y\": 2}"),
("true ? 1 : 2", "1"),
("false ? 1 : 2", "2"),
("Math.abs(-5)", "5.0"),
("Math.max(1, 2, 3)", "3.0"),
("Object.keys({ a: 1, b: 2 })", "[\"a\", \"b\"]"),
("parseFloat(\"3.14\")", "3.14"),
];
#[test]
fn corpus_evaluator_semantic_baseline() {
let mut failures: Vec<String> = Vec::new();
for (input, expected_str) in CORPUS {
let expr = compile_return_expr(input);
let actual = match evaluate_with_scope(&expr, &empty_globals(), &empty_locals()) {
Ok(v) => v,
Err(err) => {
failures.push(format!("eval failed for `{}`: {:?}", input, err));
continue;
},
};
let expected: JsonValue = serde_json::from_str(expected_str)
.unwrap_or_else(|_| panic!("expected payload not valid JSON: `{}`", expected_str));
if actual != expected {
failures.push(format!(
"mismatch for `{}`:\n expected: {}\n actual: {}",
input, expected, actual
));
}
}
assert!(
failures.is_empty(),
"{} corpus entries diverged from expected output:\n\n{}",
failures.len(),
failures.join("\n\n")
);
}
#[test]
fn corpus_size_meets_minimum() {
assert!(
CORPUS.len() >= 20,
"corpus shrank below the Wave 0 minimum (≥20 programs); found {}",
CORPUS.len()
);
}