hamelin_eval 0.10.13

Expression evaluation for Hamelin query language
Documentation
use crate::eval::environment::Environment;
use crate::eval::error::EvalError;
use crate::eval::evaluator::eval;
use crate::registry::EvalRegistry;
use hamelin_lib::tree::options::ExpressionTypeCheckOptions;
use hamelin_lib::tree::typed_ast::expression::TypedExpressionKind;
use hamelin_lib::type_check_expression;
use std::sync::Arc;

use super::test_helpers::TestContext;

#[test]
fn test_function_evaluation_error_types() {
    // Test that the error types format correctly
    let eval_error = EvalError::NoEvalImplementation {
        function_name: "test_function".to_string(),
    };
    match &eval_error {
        EvalError::NoEvalImplementation { function_name } => {
            assert_eq!(function_name, "test_function");
        }
        _ => panic!("Expected NoEvalImplementation error"),
    }
    assert_eq!(
        eval_error.to_string(),
        "Function 'test_function' has no evaluation implementation"
    );
}

#[test]
fn test_eval_error_display() {
    // Test different EvalError variants display correctly
    let no_impl_error = EvalError::NoEvalImplementation {
        function_name: "missing_func".to_string(),
    };
    assert_eq!(
        no_impl_error.to_string(),
        "Function 'missing_func' has no evaluation implementation"
    );

    let execution_error = EvalError::ExecutionError {
        message: "Division by zero".to_string(),
        source: None,
    };
    assert_eq!(execution_error.to_string(), "Division by zero");
}

#[test]
fn test_error_expression_evaluation() {
    use hamelin_lib::tree::builder::{field_ref, ExpressionBuilder};

    let ctx = TestContext::new();
    let env = Environment::new();

    // Building an expression with an undefined variable should result in an error expression
    let expr = type_check_expression(
        field_ref("undefined").build(),
        ExpressionTypeCheckOptions::builder()
            .bindings(Arc::new(ctx.translation_env.clone()))
            .build(),
    )
    .output;

    // The expression should be an error expression since the variable is not in the translation context
    assert!(matches!(expr.kind, TypedExpressionKind::Error(_)));

    // Attempting to evaluate an error expression should return an error
    let result = eval(&expr, &env);
    assert!(result.is_err());

    let error = result.unwrap_err();
    assert!(matches!(error, EvalError::ExecutionError { .. }));
}

#[test]
fn test_error_propagation_in_operations() {
    use hamelin_lib::tree::builder::{add, field_ref, ExpressionBuilder};

    let ctx = TestContext::new();
    let env = Environment::new();

    // Create an expression with an undefined variable in an arithmetic operation
    let expr = type_check_expression(
        add(field_ref("undefined"), 42).build(),
        ExpressionTypeCheckOptions::builder()
            .bindings(Arc::new(ctx.translation_env.clone()))
            .build(),
    )
    .output;

    // The expression should be an error expression
    assert!(matches!(expr.kind, TypedExpressionKind::Error(_)));

    // Evaluating should return an error
    let result = eval(&expr, &env);
    assert!(result.is_err());
}

#[test]
fn test_multiple_error_sources() {
    use hamelin_lib::tree::builder::{add, field_ref, ExpressionBuilder};

    let ctx = TestContext::new();
    let env = Environment::new();

    // Create an expression with multiple undefined variables
    let expr = type_check_expression(
        add(field_ref("undefined1"), field_ref("undefined2")).build(),
        ExpressionTypeCheckOptions::builder()
            .bindings(Arc::new(ctx.translation_env.clone()))
            .build(),
    )
    .output;

    // The expression might be an error expression or might have error subexpressions
    // Either way, evaluating should return an error
    let result = eval(&expr, &env);
    assert!(result.is_err());

    // Verify it's an execution error
    let error = result.unwrap_err();
    assert!(matches!(error, EvalError::ExecutionError { .. }));
}

#[test]
fn test_error_in_nested_expressions() {
    use hamelin_lib::tree::builder::{add, field_ref, multiply, ExpressionBuilder};

    let ctx = TestContext::new();
    let env = Environment::new();

    // Create a nested expression with an error in the middle: (5 + undefined) * 3
    let expr = type_check_expression(
        multiply(add(5, field_ref("undefined")), 3).build(),
        ExpressionTypeCheckOptions::builder()
            .bindings(Arc::new(ctx.translation_env.clone()))
            .build(),
    )
    .output;

    // Evaluating should return an error due to the undefined variable
    let result = eval(&expr, &env);
    assert!(result.is_err());

    // Verify it's an execution error
    let error = result.unwrap_err();
    assert!(matches!(error, EvalError::ExecutionError { .. }));
}

#[test]
fn test_runtime_error_handling() {
    use hamelin_lib::tree::builder::{divide, ExpressionBuilder};

    let env = Environment::new();

    // Create a division by zero expression
    let expr =
        type_check_expression(divide(10, 0).build(), ExpressionTypeCheckOptions::default()).output;
    let result = eval(&expr, &env);

    // Should return a runtime error
    assert!(result.is_err());
    let error = result.unwrap_err();
    assert!(matches!(error, EvalError::ExecutionError { .. }));
}

#[test]
fn test_error_message_quality() {
    let execution_error = EvalError::ExecutionError {
        message: "Array index 5 out of bounds for array of length 3".to_string(),
        source: None,
    };

    let error_msg = execution_error.to_string();
    assert!(error_msg.contains("out of bounds"));
    assert!(error_msg.contains("5"));
    assert!(error_msg.contains("3"));
}

#[test]
fn test_eval_error_variants() {
    // Test EvalError::NoEvalImplementation
    let no_impl = EvalError::NoEvalImplementation {
        function_name: "custom_func".to_string(),
    };
    assert!(no_impl.to_string().contains("custom_func"));
    assert!(no_impl.to_string().contains("no evaluation implementation"));

    // Test EvalError::ExecutionError with source
    let exec_error = EvalError::execution_with_source(
        "Operation failed",
        std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied"),
    );
    assert!(exec_error.to_string().contains("Operation failed"));
}

#[test]
fn test_no_eval_implementation_error_from_registry() {
    use hamelin_lib::tree::builder::{add, ExpressionBuilder};

    // Create an environment with an EMPTY registry (no eval implementations registered)
    let empty_registry = Arc::new(EvalRegistry::new());
    let env = Environment::with_registry(empty_registry);

    // Create a valid typed expression (addition of two integers)
    // This will type-check successfully, but when we try to eval it,
    // the registry won't have an implementation for IntPlusInt
    let expr =
        type_check_expression(add(1, 2).build(), ExpressionTypeCheckOptions::default()).output;

    // Evaluating should fail with NoEvalImplementation
    let result = eval(&expr, &env);
    assert!(result.is_err());

    let error = result.unwrap_err();
    match error {
        EvalError::NoEvalImplementation { function_name } => {
            // The function name should indicate it's an integer addition
            assert!(
                function_name.contains('+') || function_name.contains("plus"),
                "Expected function name to indicate addition, got: {}",
                function_name
            );
        }
        other => panic!("Expected NoEvalImplementation error, got: {:?}", other),
    }
}