geam 0.1.1

Experimental Rust-embedded execution runtime for typed Gleam programs
use super::CaptureSubstitution;
use crate::plan::{CallArg, CustomConstructor, Expr, ValueShape, ValueType};
use crate::planner::context::PlanContext;
use crate::planner::error::{
    InvalidCallShapeReason, InvalidExpressionType, InvalidTypedAstReason, PlanError,
};
use gleam_core::ast::{CallArg as GleamCallArg, TypedExpr};

pub(super) fn plan_instantiated_call_args(
    arguments: Vec<GleamCallArg<TypedExpr>>,
    instantiated_shapes: &[ValueShape],
    context: &mut PlanContext<'_>,
    capture: Option<&CaptureSubstitution>,
) -> Result<Vec<CallArg>, PlanError> {
    let mut args = Vec::with_capacity(arguments.len());
    for (argument, instantiated_shape) in arguments.into_iter().zip(instantiated_shapes) {
        let expression =
            plan_argument_value(argument, instantiated_shape.clone(), capture, context)?;
        let actual = expression.value_type();
        let expected = instantiated_shape.value_type();
        if actual != expected {
            return Err(call_arg_type_mismatch(expected, actual));
        }
        if !expression.shape().can_flow_to(instantiated_shape) {
            return Err(call_arg_shape_mismatch());
        }
        args.push(CallArg::new(expression));
    }
    Ok(args)
}

pub(super) fn plan_function_call_args(
    arguments: Vec<GleamCallArg<TypedExpr>>,
    params: &[ValueShape],
    context: &mut PlanContext<'_>,
    capture: Option<&CaptureSubstitution>,
) -> Result<Vec<CallArg>, PlanError> {
    let mut args = Vec::with_capacity(arguments.len());
    for (argument, shape) in arguments.into_iter().zip(params) {
        let expression = plan_argument_value(argument, shape.clone(), capture, context)?;
        let actual = expression.value_type();
        let expected = shape.value_type();
        if actual == expected && !expression.shape().can_flow_to(shape) {
            return Err(call_arg_shape_mismatch());
        }
        if actual != expected {
            return Err(call_arg_type_mismatch(expected, actual));
        }
        args.push(CallArg::new(expression));
    }
    Ok(args)
}

pub(super) fn plan_custom_constructor_args(
    arguments: Vec<GleamCallArg<TypedExpr>>,
    constructor: &CustomConstructor,
    context: &mut PlanContext<'_>,
    capture: Option<&CaptureSubstitution>,
) -> Result<Vec<Expr>, PlanError> {
    let actual = arguments.len();
    arguments
        .into_iter()
        .enumerate()
        .map(|(index, argument)| {
            let Some(field) = constructor.fields().get(index) else {
                return Err(PlanError::InvalidTypedAst {
                    reason: InvalidTypedAstReason::CallShape {
                        reason: InvalidCallShapeReason::RecordConstructorExtraArguments {
                            expected: constructor.fields().len(),
                            actual,
                        },
                    },
                });
            };
            if let Some(label) = &argument.label
                && field.label() != Some(label)
            {
                return Err(PlanError::InvalidTypedAst {
                    reason: InvalidTypedAstReason::CallShape {
                        reason: InvalidCallShapeReason::LabelledArguments,
                    },
                });
            }
            let expression = plan_argument_value(
                argument,
                ValueShape::from_value_type(field.type_().clone()),
                capture,
                context,
            )?;
            if expression.value_type() != *field.type_() {
                return Err(call_arg_type_mismatch(
                    field.type_().clone(),
                    expression.value_type(),
                ));
            }
            Ok(expression)
        })
        .collect()
}

fn plan_argument_value(
    argument: GleamCallArg<TypedExpr>,
    expected: ValueShape,
    capture: Option<&CaptureSubstitution>,
    context: &mut PlanContext<'_>,
) -> Result<Expr, PlanError> {
    if let Some(capture) = capture
        && super::is_capture_local(&argument.value, &capture.name)
    {
        return Ok(capture.value.clone());
    }

    super::super::plan_expr_with_expected_source_stop_shape(argument.value, expected, context)
}

fn call_arg_type_mismatch(expected: ValueType, actual: ValueType) -> PlanError {
    if matches!(expected, ValueType::Function(_)) && matches!(actual, ValueType::Function(_)) {
        PlanError::InvalidTypedAst {
            reason: InvalidTypedAstReason::CallShape {
                reason: InvalidCallShapeReason::FunctionCallArgumentTypeMismatch,
            },
        }
    } else {
        PlanError::InvalidTypedAst {
            reason: InvalidTypedAstReason::ExpressionType {
                expected: InvalidExpressionType::from_value_type(expected),
                actual: InvalidExpressionType::from_value_type(actual),
            },
        }
    }
}

fn call_arg_shape_mismatch() -> PlanError {
    PlanError::InvalidTypedAst {
        reason: InvalidTypedAstReason::CallShape {
            reason: InvalidCallShapeReason::FunctionCallArgumentTypeMismatch,
        },
    }
}

#[cfg(test)]
#[allow(clippy::arc_with_non_send_sync)]
mod tests {
    use super::{plan_function_call_args, plan_instantiated_call_args};
    use crate::plan::{
        CustomConstructorDefinition, CustomConstructorRefinement, CustomType, CustomTypeDefinition,
        CustomTypeName, CustomTypePublicity, CustomValueShape, ValueShape,
    };
    use crate::planner::context::{AnonymousFunctions, PlanContext};
    use crate::planner::plan_module;
    use crate::planner::support::{compile, expect_plan_error};
    use crate::planner::{
        InvalidCallShapeReason, InvalidTypedAstReason, PlanError, UnsupportedBitArraySegmentReason,
    };
    use ecow::EcoString;
    use gleam_core::ast::{CallArg as GleamCallArg, Statement, TypedExpr, TypedModule};
    use gleam_core::type_::Type;
    use std::collections::HashMap;
    use std::sync::Arc;

    #[test]
    fn custom_constructor_argument_planning_errors_are_preserved() {
        assert_eq!(
            expect_plan_error(
                "pub type Boxed { Boxed(Int) } pub fn main() { Boxed({ <<1:native>> 1 }) }",
            ),
            PlanError::UnsupportedBitArraySegment {
                reason: UnsupportedBitArraySegmentReason::NativeEndianness,
            },
        );
    }

    #[test]
    fn call_arguments_reject_incompatible_constructor_refinements() {
        let expected = PlanError::InvalidTypedAst {
            reason: InvalidTypedAstReason::CallShape {
                reason: InvalidCallShapeReason::FunctionCallArgumentTypeMismatch,
            },
        };
        let source = r#"
pub type Choice {
  First
  Second
}

fn expect_first(value: Choice) {
  Nil
}

pub fn main() {
  expect_first(Second)
}
"#;
        let mut direct = compile(source);
        set_first_param_constructor(&mut direct, "expect_first", 0);
        assert_eq!(plan_module(direct), Err(expected.clone()));

        let mut function_value = compile(
            r#"
pub type Choice {
  First
  Second
}

fn expect_first(value: Choice) {
  Nil
}

pub fn main() {
  let function = expect_first
  function(Second)
}
"#,
        );
        set_first_param_constructor(&mut function_value, "expect_first", 0);
        assert_eq!(
            plan_module(function_value),
            Err(PlanError::InvalidTypedAst {
                reason: InvalidTypedAstReason::ModuleReference {
                    module: "main".into(),
                    name: "expect_first".into(),
                    reason: crate::planner::InvalidModuleReferenceReason::FunctionReferenceShape,
                },
            }),
        );
    }

    #[test]
    fn reject_margin_instantiated_call_argument_constructor_refinement() {
        let module = compile(
            r#"
pub type Choice {
  First
  Second
}

fn consume(value: Choice) {
  Nil
}

pub fn main() {
  consume(Second)
}
"#,
        );
        let arguments = main_call_arguments(&module);
        let module_name = EcoString::from("main");
        let functions = HashMap::new();
        let custom_types = vec![choice_definition()];
        let mut anonymous = AnonymousFunctions::default();
        let mut context = PlanContext::new_with_custom_types(
            &module_name,
            &functions,
            &custom_types,
            &mut anonymous,
        );
        let expected = ValueShape::Custom(CustomValueShape::new(
            choice_type().type_name().clone(),
            Vec::new(),
            CustomConstructorRefinement::Exact(0),
        ));
        assert_eq!(
            plan_instantiated_call_args(arguments, &[expected], &mut context, None,),
            Err(PlanError::InvalidTypedAst {
                reason: InvalidTypedAstReason::CallShape {
                    reason: InvalidCallShapeReason::FunctionCallArgumentTypeMismatch,
                },
            }),
        );
    }

    #[test]
    fn reject_margin_function_value_arguments_reject_incompatible_constructor_refinements() {
        let module = compile(
            r#"
pub type Choice {
  First
  Second
}

fn consume(value: Choice) {
  Nil
}

pub fn main() {
  consume(Second)
}
"#,
        );
        let arguments = main_call_arguments(&module);
        let module_name = EcoString::from("main");
        let functions = HashMap::new();
        let custom_types = vec![choice_definition()];
        let mut anonymous = AnonymousFunctions::default();
        let mut context = PlanContext::new_with_custom_types(
            &module_name,
            &functions,
            &custom_types,
            &mut anonymous,
        );
        let expected = ValueShape::Custom(CustomValueShape::new(
            choice_type().type_name().clone(),
            Vec::new(),
            CustomConstructorRefinement::Exact(0),
        ));

        assert_eq!(
            plan_function_call_args(arguments, &[expected], &mut context, None),
            Err(PlanError::InvalidTypedAst {
                reason: InvalidTypedAstReason::CallShape {
                    reason: InvalidCallShapeReason::FunctionCallArgumentTypeMismatch,
                },
            }),
        );
    }

    #[test]
    #[should_panic(expected = "test parameter should have a named custom type")]
    fn custom_parameter_fixture_guard_rejects_tuple_parameter() {
        let mut module = compile("fn identity(value: #(Int)) { value } pub fn main() { 1 }");

        set_first_param_constructor(&mut module, "identity", 0);
    }

    #[test]
    #[should_panic(expected = "test main body should be a call")]
    fn main_call_arguments_fixture_guard_rejects_non_call() {
        let module = compile("pub fn main() { Nil }");

        let _ = main_call_arguments(&module);
    }

    fn set_first_param_constructor(module: &mut TypedModule, function_name: &str, index: u16) {
        let function = module
            .definitions
            .functions
            .iter_mut()
            .find(|function| {
                function
                    .name
                    .as_ref()
                    .is_some_and(|name| name.1 == function_name)
            })
            .expect("test function should exist");
        let Type::Named {
            publicity,
            package,
            module,
            name,
            arguments,
            ..
        } = function.arguments[0].type_.as_ref()
        else {
            panic!("test parameter should have a named custom type");
        };
        function.arguments[0].type_ = Arc::new(Type::Named {
            publicity: *publicity,
            package: package.clone(),
            module: module.clone(),
            name: name.clone(),
            arguments: arguments.clone(),
            inferred_variant: Some(index),
        });
    }

    fn main_call_arguments(module: &TypedModule) -> Vec<GleamCallArg<TypedExpr>> {
        let main = module
            .definitions
            .functions
            .iter()
            .find(|function| function.name.as_ref().is_some_and(|name| name.1 == "main"))
            .expect("test main function should exist");
        let Statement::Expression(TypedExpr::Call { arguments, .. }) = &main.body[0] else {
            panic!("test main body should be a call");
        };
        arguments.clone()
    }

    fn choice_type() -> CustomType {
        CustomType::new(
            CustomTypeName::new("geam".into(), "main".into(), "Choice".into()),
            Vec::new(),
        )
    }

    fn choice_definition() -> CustomTypeDefinition {
        CustomTypeDefinition::new(
            choice_type().type_name().clone(),
            CustomTypePublicity::Public,
            false,
            Vec::new(),
            vec![
                CustomConstructorDefinition::new("First".into(), 0, Vec::new()),
                CustomConstructorDefinition::new("Second".into(), 1, Vec::new()),
            ],
        )
    }
}