hamelin_eval 0.10.13

Expression evaluation for Hamelin query language
Documentation
//! Evaluator tests for `FieldAccess::Broadcast{Struct,Tuple,Variant}Field`.
//!
//! These variants are produced by the typechecker when it sees
//! `value.field` with `value: Array<Struct|Tuple|Variant>`. They reach
//! `eval_field_lookup` through the freeze/constant-folding pass
//! (`hamelin_translation::ir::freeze`), which walks constant subtrees
//! and tries to pre-evaluate them — so the evaluator needs to produce
//! results that match the declared typed-AST shape exactly, or the
//! folded value will disagree with the type and downstream emission
//! breaks.

use crate::eval::environment::Environment;
use crate::eval::evaluator::eval;
use crate::value::Value;
use hamelin_lib::tree::ast::identifier::SimpleIdentifier;
use hamelin_lib::tree::builder::{
    array, field, field_ref, struct_literal, tuple, ExpressionBuilder,
};
use hamelin_lib::tree::options::ExpressionTypeCheckOptions;
use hamelin_lib::tree::typed_ast::environment::TypeEnvironment;
use hamelin_lib::type_check_expression;
use hamelin_lib::types::{
    array::Array, struct_type::Struct, tuple::Tuple, Type, INT, STRING, VARIANT,
};
use std::sync::Arc;

use super::test_helpers::{test_context, TestContext};

// -----------------------------------------------------------------------------
// BroadcastStructField
// -----------------------------------------------------------------------------

#[test]
fn broadcast_struct_field_simple() {
    let ctx = test_context();

    // [{name: "alice", age: 30}, {name: "bob", age: 25}].name -> ["alice", "bob"]
    let expr = field(
        array()
            .element(struct_literal().field("name", "alice").field("age", 30))
            .element(struct_literal().field("name", "bob").field("age", 25)),
        "name",
    );

    let result = ctx.eval_expr(&expr);
    assert_eq!(
        result,
        Value::Array(vec![
            Value::String("alice".to_string()),
            Value::String("bob".to_string()),
        ])
    );
}

#[test]
fn broadcast_struct_field_nested_struct_in_struct() {
    let ctx = test_context();

    // [{outer: {inner: 1}}, {outer: {inner: 2}}].outer is Array<Struct{inner: Int}>;
    // then .inner broadcasts again to produce Array<Int>.
    let expr = field(
        field(
            array()
                .element(struct_literal().field("outer", struct_literal().field("inner", 1)))
                .element(struct_literal().field("outer", struct_literal().field("inner", 2))),
            "outer",
        ),
        "inner",
    );

    let result = ctx.eval_expr(&expr);
    assert_eq!(result, Value::Array(vec![Value::Int(1), Value::Int(2)]));
}

#[test]
fn broadcast_struct_field_array_valued_field_is_not_flattened() {
    let ctx = test_context();

    // [{nums: [1,2]}, {nums: [3,4]}, {nums: [5]}].nums
    //   typed node's result is Array<Array<Int>> — broadcasting never
    //   flattens (per SIM-4169). Each inner array is preserved verbatim.
    let expr = field(
        array()
            .element(struct_literal().field("nums", array().element(1).element(2)))
            .element(struct_literal().field("nums", array().element(3).element(4)))
            .element(struct_literal().field("nums", array().element(5))),
        "nums",
    );

    let result = ctx.eval_expr(&expr);
    assert_eq!(
        result,
        Value::Array(vec![
            Value::Array(vec![Value::Int(1), Value::Int(2)]),
            Value::Array(vec![Value::Int(3), Value::Int(4)]),
            Value::Array(vec![Value::Int(5)]),
        ])
    );
}

#[test]
fn broadcast_struct_field_null_outer_array() {
    // arr: Array<Struct{name: string}> bound to Value::Null — .name returns Null.
    let mut ctx = TestContext::default();
    ctx.set(
        "arr",
        Value::Null,
        Array::new(Struct::default().with_str("name", STRING).into()).into(),
    );

    let expr = field(field_ref("arr"), "name");

    let result = ctx.eval_expr(&expr);
    assert_eq!(result, Value::Null);
}

#[test]
fn broadcast_struct_field_null_element_preserved() {
    // Array of structs with a null element: null elements produce null in the output.
    let elem_type: Type = Struct::default().with_str("name", STRING).into();
    let mut ctx = TestContext::default();
    ctx.set(
        "arr",
        Value::Array(vec![
            Value::Struct({
                let mut m = ordermap::OrderMap::new();
                m.insert(
                    SimpleIdentifier::new("name"),
                    Value::String("alice".to_string()),
                );
                m
            }),
            Value::Null,
            Value::Struct({
                let mut m = ordermap::OrderMap::new();
                m.insert(
                    SimpleIdentifier::new("name"),
                    Value::String("charlie".to_string()),
                );
                m
            }),
        ]),
        Array::new(elem_type).into(),
    );

    let expr = field(field_ref("arr"), "name");

    let result = ctx.eval_expr(&expr);
    assert_eq!(
        result,
        Value::Array(vec![
            Value::String("alice".to_string()),
            Value::Null,
            Value::String("charlie".to_string()),
        ])
    );
}

// -----------------------------------------------------------------------------
// BroadcastTupleElement
// -----------------------------------------------------------------------------

#[test]
fn broadcast_tuple_element_first() {
    let ctx = test_context();

    // [(1, "a"), (2, "b"), (3, "c")].f0 -> [1, 2, 3]
    let expr = field(
        array()
            .element(tuple().element(1).element("a"))
            .element(tuple().element(2).element("b"))
            .element(tuple().element(3).element("c")),
        "f0",
    );

    let result = ctx.eval_expr(&expr);
    assert_eq!(
        result,
        Value::Array(vec![Value::Int(1), Value::Int(2), Value::Int(3)])
    );
}

#[test]
fn broadcast_tuple_element_second() {
    let ctx = test_context();

    let expr = field(
        array()
            .element(tuple().element(1).element("a"))
            .element(tuple().element(2).element("b")),
        "f1",
    );

    let result = ctx.eval_expr(&expr);
    assert_eq!(
        result,
        Value::Array(vec![
            Value::String("a".to_string()),
            Value::String("b".to_string()),
        ])
    );
}

#[test]
fn broadcast_tuple_element_array_valued_is_not_flattened() {
    let ctx = test_context();

    // [([1,2], "a"), ([3], "b")].f0
    //   f0 is Array<Array<Int>> — broadcasting never flattens.
    let expr = field(
        array()
            .element(tuple().element(array().element(1).element(2)).element("a"))
            .element(tuple().element(array().element(3)).element("b")),
        "f0",
    );

    let result = ctx.eval_expr(&expr);
    assert_eq!(
        result,
        Value::Array(vec![
            Value::Array(vec![Value::Int(1), Value::Int(2)]),
            Value::Array(vec![Value::Int(3)]),
        ])
    );
}

#[test]
fn broadcast_tuple_element_null_outer_array() {
    let mut ctx = TestContext::default();
    ctx.set(
        "arr",
        Value::Null,
        Array::new(Tuple::new(vec![INT, STRING]).into()).into(),
    );

    let expr = field(field_ref("arr"), "f0");

    let result = ctx.eval_expr(&expr);
    assert_eq!(result, Value::Null);
}

// -----------------------------------------------------------------------------
// BroadcastVariantField
// -----------------------------------------------------------------------------

#[test]
fn broadcast_variant_field_simple() {
    // arr: Array<Variant> bound to two JSON objects; .foo returns Array<Variant>.
    let mut env = Environment::new();
    env.bind(
        SimpleIdentifier::new("arr"),
        Value::Array(vec![
            Value::Variant(serde_json::json!({"foo": 1})),
            Value::Variant(serde_json::json!({"foo": 2})),
        ]),
    );

    let mut trans_env = TypeEnvironment::default();
    trans_env.bind_str("arr", Array::new(VARIANT).into());

    let expr = type_check_expression(
        field(field_ref("arr"), "foo").build(),
        ExpressionTypeCheckOptions::builder()
            .bindings(Arc::new(trans_env))
            .build(),
    )
    .output;

    let result = eval(&expr, &env).unwrap();
    assert_eq!(
        result,
        Value::Array(vec![
            Value::Variant(serde_json::json!(1)),
            Value::Variant(serde_json::json!(2)),
        ])
    );
}

#[test]
fn broadcast_variant_field_missing_field_is_null_variant() {
    let mut env = Environment::new();
    env.bind(
        SimpleIdentifier::new("arr"),
        Value::Array(vec![
            Value::Variant(serde_json::json!({"foo": 1})),
            Value::Variant(serde_json::json!({"bar": 2})),
        ]),
    );

    let mut trans_env = TypeEnvironment::default();
    trans_env.bind_str("arr", Array::new(VARIANT).into());

    let expr = type_check_expression(
        field(field_ref("arr"), "foo").build(),
        ExpressionTypeCheckOptions::builder()
            .bindings(Arc::new(trans_env))
            .build(),
    )
    .output;

    let result = eval(&expr, &env).unwrap();
    assert_eq!(
        result,
        Value::Array(vec![
            Value::Variant(serde_json::json!(1)),
            Value::Variant(serde_json::Value::Null),
        ])
    );
}

#[test]
fn broadcast_variant_field_null_outer_array() {
    let mut ctx = TestContext::default();
    ctx.set("arr", Value::Null, Array::new(VARIANT).into());

    let expr = field(field_ref("arr"), "foo");

    let result = ctx.eval_expr(&expr);
    assert_eq!(result, Value::Null);
}

// -----------------------------------------------------------------------------
// Cross-cutting: broadcast field access inside a transform() lambda body.
//
// When a lambda parameter is itself typed as `Array<Struct|Tuple|Variant>`,
// `x.field` inside the body produces a Broadcast field-access variant — so
// `invoke_closure` has to route through the same new eval arms as direct
// field access. This anchors that the broadcast-eval path works inside the
// closure/transform pipeline too.
// -----------------------------------------------------------------------------

#[test]
fn transform_lambda_with_broadcast_struct_field_in_body() {
    use hamelin_lib::tree::builder::{call, lambda1};

    let ctx = test_context();

    // transform(
    //   [[{name:"a"},{name:"b"}], [{name:"c"}]],
    //   x -> x.name
    // )
    //   = [["a","b"], ["c"]]
    // Here `x: Array<Struct{name:string}>` so `x.name` is a BroadcastStructField
    // inside the lambda body, then transform wraps the per-row results back
    // into an outer array.
    let expr = call("transform")
        .arg(
            array()
                .element(
                    array()
                        .element(struct_literal().field("name", "a"))
                        .element(struct_literal().field("name", "b")),
                )
                .element(array().element(struct_literal().field("name", "c"))),
        )
        .arg(lambda1("x").body(field(field_ref("x"), "name")));

    let result = ctx.eval_expr(&expr);
    assert_eq!(
        result,
        Value::Array(vec![
            Value::Array(vec![
                Value::String("a".to_string()),
                Value::String("b".to_string()),
            ]),
            Value::Array(vec![Value::String("c".to_string())]),
        ])
    );
}

#[test]
fn transform_lambda_with_scalar_struct_field_in_body_still_works() {
    use hamelin_lib::tree::builder::{call, lambda1};

    let ctx = test_context();

    // transform([{name:"a"}, {name:"b"}], x -> x.name) = ["a", "b"]
    // Here `x: Struct{name:string}` (scalar), so `x.name` is a plain
    // `FieldAccess::StructField` — confirms the non-broadcast path is
    // intact inside lambdas.
    let expr = call("transform")
        .arg(
            array()
                .element(struct_literal().field("name", "a"))
                .element(struct_literal().field("name", "b")),
        )
        .arg(lambda1("x").body(field(field_ref("x"), "name")));

    let result = ctx.eval_expr(&expr);
    assert_eq!(
        result,
        Value::Array(vec![
            Value::String("a".to_string()),
            Value::String("b".to_string()),
        ])
    );
}