mod return_body;
use self::return_body::function_return_expr;
use crate::plan::{
CaptureArg, FunctionTemplate, Param, ParamBinding, ReturnExpr, Step, ValueShape,
};
use crate::planner::context::{FunctionInfo, FunctionParam, PlanContext, PlannedCaptures};
use crate::planner::error::{
InvalidFunctionShapeReason, InvalidTypedAstReason, PlanError, UnsupportedFunctionReason,
};
use crate::planner::statement::plan_steps_and_return;
use ecow::EcoString;
use gleam_core::ast::{TypedFunction, TypedStatement};
use vec1::Vec1;
pub(super) struct PlannedFunctionBody {
pub(super) params: Vec<Param>,
captures: PlannedCaptures,
pub(super) steps: Vec<Step>,
pub(super) return_: ReturnExpr,
}
pub(super) fn plan_function(
info: FunctionInfo,
function: TypedFunction,
context: PlanContext<'_>,
) -> Result<FunctionTemplate, PlanError> {
let name = function_name(&function)?;
if function.external_erlang.is_some() || function.external_javascript.is_some() {
return Err(PlanError::UnsupportedFunction {
name,
reason: UnsupportedFunctionReason::External,
});
}
plan_selected_function(info, function, context, name)
}
pub(super) fn plan_selected_external_fallback(
name: EcoString,
info: FunctionInfo,
function: TypedFunction,
context: PlanContext<'_>,
) -> Result<FunctionTemplate, PlanError> {
plan_selected_function(info, function, context, name)
}
pub(super) fn function_name(function: &TypedFunction) -> Result<EcoString, PlanError> {
match &function.name {
Some((_, name)) => Ok(name.clone()),
None => Err(PlanError::InvalidTypedAst {
reason: InvalidTypedAstReason::FunctionShape {
name: "<anonymous>".into(),
reason: InvalidFunctionShapeReason::Anonymous,
},
}),
}
}
fn plan_selected_function(
info: FunctionInfo,
function: TypedFunction,
mut context: PlanContext<'_>,
name: EcoString,
) -> Result<FunctionTemplate, PlanError> {
context.set_current_function(name.clone());
context.set_type_parameters(info.type_parameters.clone());
let params = define_params(&info.params, &mut context)?;
let return_shape = info.return_shape();
let planned = plan_steps_and_return(
function.body,
&mut context,
PlanError::InvalidTypedAst {
reason: InvalidTypedAstReason::FunctionShape {
name: name.clone(),
reason: InvalidFunctionShapeReason::EmptyBody,
},
},
Some(&return_shape),
)?;
let return_ = function_return_expr(&name, &return_shape, planned.return_)?;
Ok(FunctionTemplate::from_signature(
info.signature,
name,
params,
Vec::new(),
planned.steps,
return_,
))
}
pub(super) fn plan_anonymous_function_body(
name: &EcoString,
return_shape: &ValueShape,
params: &[FunctionParam],
captures: Vec<crate::planner::context::CaptureBinding>,
body: Vec1<TypedStatement>,
context: &mut PlanContext<'_>,
) -> Result<PlannedFunctionBody, PlanError> {
let params = define_params(params, context)?;
let captures = context.define_captures(captures);
let planned = crate::planner::statement::plan_non_empty_steps_and_return(
body,
context,
Some(return_shape),
)?;
let return_ = function_return_expr(name, return_shape, planned.return_)?;
Ok(PlannedFunctionBody {
params,
captures,
steps: planned.steps,
return_,
})
}
pub(super) fn anonymous_function_plan(
info: FunctionInfo,
name: EcoString,
planned: PlannedFunctionBody,
) -> (FunctionTemplate, Vec<CaptureArg>) {
let (capture_slots, capture_sources) = planned.captures.into_parts();
(
FunctionTemplate::from_signature(
info.signature,
name,
planned.params,
capture_slots,
planned.steps,
planned.return_,
),
capture_sources,
)
}
fn define_params(
params: &[FunctionParam],
context: &mut PlanContext<'_>,
) -> Result<Vec<Param>, PlanError> {
params
.iter()
.map(|param| match ¶m.binding {
ParamBinding::Named(name) => {
context.define_existing_param(
name.clone(),
param.local(),
param.shape().clone(),
)?;
Ok(Param::named_shape(
param.local().clone(),
name.clone(),
param.shape().clone(),
))
}
ParamBinding::Discard => Ok(Param::discard_shape(
param.local().clone(),
param.shape().clone(),
)),
})
.collect()
}
#[cfg(test)]
mod tests {
use crate::plan::{
BoolLocalId, FunctionFunctionId, FunctionShape, FunctionTemplateId,
FunctionTemplateSignature, FunctionType, IntFunctionFunctionId, IntLocalId, LocalId,
NilLocalId, StringLocalId, TypeScheme, ValueType,
};
use crate::planner::context::{FunctionInfo, PlanContext};
use crate::planner::dsl::{
bool_, bool_arg, bool_function_ref, bool_function_return_block,
bool_function_return_bool_case, bool_function_return_expr, bool_function_return_int_case,
bool_function_return_string_case, bool_function_return_tail_call_at,
bool_return_tail_call_at, call_bool_at, call_int_at, call_int_function_at,
call_int_returning_function_at, function, function_function_ref,
function_function_return_block, function_function_return_expr,
function_function_return_int_case, function_function_return_string_case,
function_function_return_tail_call_at, host_call_site, host_call_site_in, int, int_arg,
int_function_arg, int_function_call_arg, int_function_closure, int_function_ref,
int_function_return_block, int_function_return_bool_case, int_function_return_expr,
int_function_return_int_case, int_function_return_string_case,
int_function_return_tail_call_at, int_return_block, int_return_bool_case, int_return_expr,
int_return_int_case, int_return_tail_call_at, let_int_function_step, let_int_step,
local_bool, local_int, local_int_function, local_nil, local_string, module,
module_with_anonymous, nil, nil_arg, nil_function_ref, nil_function_return_block,
nil_function_return_bool_case, nil_function_return_expr, nil_function_return_int_case,
nil_function_return_string_case, nil_function_return_tail_call_at, nil_return_tail_call_at,
return_bool_function, return_function_function, return_int_function, return_nil_function,
return_string_function, string, string_arg, string_function_ref,
string_function_return_block, string_function_return_bool_case,
string_function_return_expr, string_function_return_int_case,
string_function_return_string_case, string_function_return_tail_call_at,
string_return_tail_call_at,
};
use crate::planner::plan_module;
use crate::planner::support::{compile, compile_minimal_module, expect_plan_error};
use crate::planner::{
InvalidFunctionShapeReason, InvalidTypedAstReason, PlanError, UnsupportedFunctionReason,
};
#[test]
fn plan_final_direct_call_as_tail_call() {
let source = r#"
fn add(a: Int, b: Int) {
a + b
}
pub fn main() {
add(1, 2)
}
"#;
let actual = plan_module(compile(source)).expect("source should plan");
let expected = module(
"main",
function(
"main",
int_return_tail_call_at(
1,
[int_arg(int(1)), int_arg(int(2))],
host_call_site(source, "main", "add(1, 2)"),
),
),
[
function("add", local_int(0, "a").add_int(local_int(1, "b")))
.param_int(0, "a")
.param_int(1, "b"),
],
);
assert_eq!(actual, expected);
}
#[test]
fn plan_block_case_branches_preserve_tail_call() {
let source = r#"
fn count_down(n: Int, acc: Int) {
{
case n {
0 -> acc
_ -> count_down(n - 1, acc + 1)
}
}
}
pub fn main() {
count_down(1, 0)
}
"#;
let actual = plan_module(compile(source)).expect("source should plan");
let expected = module(
"main",
function(
"main",
int_return_tail_call_at(
1,
[int_arg(int(1)), int_arg(int(0))],
host_call_site(source, "main", "count_down(1, 0)"),
),
),
[function(
"count_down",
int_return_block(
[],
int_return_int_case(
local_int(0, "n"),
[(0, int_return_expr(local_int(1, "acc")))],
int_return_tail_call_at(
1,
[
int_arg(local_int(0, "n").sub_int(int(1))),
int_arg(local_int(1, "acc").add_int(int(1))),
],
host_call_site(source, "count_down", "count_down(n - 1, acc + 1)"),
),
),
),
)
.param_int(0, "n")
.param_int(1, "acc")],
);
assert_eq!(actual, expected);
}
#[test]
fn plan_function_return_family_block_int_case_fallbacks_as_tail_calls() {
let source = r#"
fn int_identity(value: Int) {
value
}
fn string_identity(value: String) {
value
}
fn bool_identity(value: Bool) {
value
}
fn nil_identity(value: Nil) {
value
}
fn get_int(n: Int) {
{
case n {
0 -> int_identity
_ -> get_int(n - 1)
}
}
}
fn get_string(n: Int) {
{
case n {
0 -> string_identity
_ -> get_string(n - 1)
}
}
}
fn get_bool(n: Int) {
{
case n {
0 -> bool_identity
_ -> get_bool(n - 1)
}
}
}
fn get_nil(n: Int) {
{
case n {
0 -> nil_identity
_ -> get_nil(n - 1)
}
}
}
fn get_getter(n: Int) {
{
case n {
0 -> get_int
_ -> get_getter(n - 1)
}
}
}
pub fn main() {
get_int(0)(1)
}
"#;
let actual = plan_module(compile(source)).expect("source should plan");
let int_to_int = function_type([ValueType::Int], ValueType::Int);
let string_to_string = function_type([ValueType::String], ValueType::String);
let bool_to_bool = function_type([ValueType::Bool], ValueType::Bool);
let nil_to_nil = function_type([ValueType::Nil], ValueType::Nil);
let int_to_int_function =
crate::plan::FunctionFunctionType::new(vec![ValueType::Int], int_to_int.clone());
let expected = module(
"main",
function(
"main",
call_int_function_at(
call_int_returning_function_at(
5,
[int_arg(int(0))],
int_to_int.clone(),
host_call_site_in(source, "main", "get_int(0)(1)", "get_int(0)"),
),
[int_function_call_arg(int(1))],
host_call_site(source, "main", "get_int(0)(1)"),
),
),
[
function("int_identity", local_int(0, "value")).param_int(0, "value"),
function("string_identity", local_string(0, "value")).param_string(0, "value"),
function("bool_identity", local_bool(0, "value")).param_bool(0, "value"),
function("nil_identity", local_nil(0, "value")).param_nil(0, "value"),
function(
"get_int",
return_int_function(
int_to_int.clone(),
int_function_return_block(
[],
int_function_return_int_case(
local_int(0, "n"),
[(
0,
int_function_return_expr(int_function_ref(
1,
[LocalId::Int(IntLocalId(0))],
)),
)],
int_function_return_tail_call_at(
5,
int_to_int.clone(),
[int_arg(local_int(0, "n").sub_int(int(1)))],
host_call_site(source, "get_int", "get_int(n - 1)"),
),
),
),
),
)
.param_int(0, "n"),
function(
"get_string",
return_string_function(
string_to_string.clone(),
string_function_return_block(
[],
string_function_return_int_case(
local_int(0, "n"),
[(
0,
string_function_return_expr(string_function_ref(
2,
[LocalId::String(StringLocalId(0))],
)),
)],
string_function_return_tail_call_at(
6,
string_to_string.clone(),
[int_arg(local_int(0, "n").sub_int(int(1)))],
host_call_site(source, "get_string", "get_string(n - 1)"),
),
),
),
),
)
.param_int(0, "n"),
function(
"get_bool",
return_bool_function(
bool_to_bool.clone(),
bool_function_return_block(
[],
bool_function_return_int_case(
local_int(0, "n"),
[(
0,
bool_function_return_expr(bool_function_ref(
3,
[LocalId::Bool(BoolLocalId(0))],
)),
)],
bool_function_return_tail_call_at(
7,
bool_to_bool.clone(),
[int_arg(local_int(0, "n").sub_int(int(1)))],
host_call_site(source, "get_bool", "get_bool(n - 1)"),
),
),
),
),
)
.param_int(0, "n"),
function(
"get_nil",
return_nil_function(
nil_to_nil.clone(),
nil_function_return_block(
[],
nil_function_return_int_case(
local_int(0, "n"),
[(
0,
nil_function_return_expr(nil_function_ref(
4,
[LocalId::Nil(NilLocalId(0))],
)),
)],
nil_function_return_tail_call_at(
8,
nil_to_nil.clone(),
[int_arg(local_int(0, "n").sub_int(int(1)))],
host_call_site(source, "get_nil", "get_nil(n - 1)"),
),
),
),
),
)
.param_int(0, "n"),
function(
"get_getter",
return_function_function(function_function_return_block(
[],
function_function_return_int_case(
local_int(0, "n"),
[(
0,
function_function_return_expr(function_function_ref(
FunctionFunctionId::Int(IntFunctionFunctionId(5)),
[LocalId::Int(IntLocalId(0))],
int_to_int.clone(),
)),
)],
function_function_return_tail_call_at(
9,
int_to_int_function.clone(),
[int_arg(local_int(0, "n").sub_int(int(1)))],
host_call_site(source, "get_getter", "get_getter(n - 1)"),
),
),
)),
)
.param_int(0, "n"),
],
);
assert_eq!(actual, expected);
}
#[test]
fn plan_function_return_family_bool_case_branches() {
let source = r#"
fn int_identity(value: Int) {
value
}
fn int_increment(value: Int) {
value + 1
}
fn string_identity(value: String) {
value
}
fn string_suffix(value: String) {
value <> "!"
}
fn bool_true(value: Bool) {
True
}
fn bool_false(value: Bool) {
False
}
fn nil_identity(value: Nil) {
value
}
fn nil_other(value: Nil) {
Nil
}
fn choose_int(flag: Bool) {
case flag {
True -> int_identity
False -> int_increment
}
}
fn choose_string(flag: Bool) {
case flag {
True -> string_identity
False -> string_suffix
}
}
fn choose_bool(flag: Bool) {
case flag {
True -> bool_true
False -> bool_false
}
}
fn choose_nil(flag: Bool) {
case flag {
True -> nil_identity
False -> nil_other
}
}
pub fn main() {
choose_int(True)(1)
}
"#;
let actual = plan_module(compile(source)).expect("source should plan");
let int_to_int = function_type([ValueType::Int], ValueType::Int);
let string_to_string = function_type([ValueType::String], ValueType::String);
let bool_to_bool = function_type([ValueType::Bool], ValueType::Bool);
let nil_to_nil = function_type([ValueType::Nil], ValueType::Nil);
let expected = module(
"main",
function(
"main",
call_int_function_at(
call_int_returning_function_at(
9,
[bool_arg(bool_(true))],
int_to_int.clone(),
host_call_site_in(
source,
"main",
"choose_int(True)(1)",
"choose_int(True)",
),
),
[int_function_call_arg(int(1))],
host_call_site(source, "main", "choose_int(True)(1)"),
),
),
[
function("int_identity", local_int(0, "value")).param_int(0, "value"),
function("int_increment", local_int(0, "value").add_int(int(1)))
.param_int(0, "value"),
function("string_identity", local_string(0, "value")).param_string(0, "value"),
function(
"string_suffix",
local_string(0, "value").concatenate(string("!")),
)
.param_string(0, "value"),
function("bool_true", bool_(true)).param_bool(0, "value"),
function("bool_false", bool_(false)).param_bool(0, "value"),
function("nil_identity", local_nil(0, "value")).param_nil(0, "value"),
function("nil_other", nil()).param_nil(0, "value"),
function(
"choose_int",
return_int_function(
int_to_int.clone(),
int_function_return_bool_case(
local_bool(0, "flag"),
int_function_return_expr(int_function_ref(
1,
[LocalId::Int(IntLocalId(0))],
)),
int_function_return_expr(int_function_ref(
2,
[LocalId::Int(IntLocalId(0))],
)),
),
),
)
.param_bool(0, "flag"),
function(
"choose_string",
return_string_function(
string_to_string,
string_function_return_bool_case(
local_bool(0, "flag"),
string_function_return_expr(string_function_ref(
3,
[LocalId::String(StringLocalId(0))],
)),
string_function_return_expr(string_function_ref(
4,
[LocalId::String(StringLocalId(0))],
)),
),
),
)
.param_bool(0, "flag"),
function(
"choose_bool",
return_bool_function(
bool_to_bool,
bool_function_return_bool_case(
local_bool(0, "flag"),
bool_function_return_expr(bool_function_ref(
5,
[LocalId::Bool(BoolLocalId(0))],
)),
bool_function_return_expr(bool_function_ref(
6,
[LocalId::Bool(BoolLocalId(0))],
)),
),
),
)
.param_bool(0, "flag"),
function(
"choose_nil",
return_nil_function(
nil_to_nil,
nil_function_return_bool_case(
local_bool(0, "flag"),
nil_function_return_expr(nil_function_ref(
7,
[LocalId::Nil(NilLocalId(0))],
)),
nil_function_return_expr(nil_function_ref(
8,
[LocalId::Nil(NilLocalId(0))],
)),
),
),
)
.param_bool(0, "flag"),
],
);
assert_eq!(actual, expected);
}
#[test]
fn plan_function_return_family_string_case_branches() {
let source = r#"
fn int_identity(value: Int) {
value
}
fn int_increment(value: Int) {
value + 1
}
fn string_identity(value: String) {
value
}
fn string_suffix(value: String) {
value <> "!"
}
fn bool_true(value: Bool) {
True
}
fn bool_false(value: Bool) {
False
}
fn nil_identity(value: Nil) {
value
}
fn nil_other(value: Nil) {
Nil
}
fn choose_int(key: String) {
case key {
"one" -> int_identity
_ -> int_increment
}
}
fn choose_string(key: String) {
case key {
"one" -> string_identity
_ -> string_suffix
}
}
fn choose_bool(key: String) {
case key {
"one" -> bool_true
_ -> bool_false
}
}
fn choose_nil(key: String) {
case key {
"one" -> nil_identity
_ -> nil_other
}
}
fn choose_increment(key: String) {
int_increment
}
fn choose_getter(key: String) {
case key {
"one" -> choose_int
_ -> choose_increment
}
}
pub fn main() {
choose_int("one")(1)
}
"#;
let actual = plan_module(compile(source)).expect("source should plan");
let int_to_int = function_type([ValueType::Int], ValueType::Int);
let string_to_string = function_type([ValueType::String], ValueType::String);
let bool_to_bool = function_type([ValueType::Bool], ValueType::Bool);
let nil_to_nil = function_type([ValueType::Nil], ValueType::Nil);
let expected = module(
"main",
function(
"main",
call_int_function_at(
call_int_returning_function_at(
9,
[string_arg(string("one"))],
int_to_int.clone(),
host_call_site_in(
source,
"main",
r#"choose_int("one")(1)"#,
r#"choose_int("one")"#,
),
),
[int_function_call_arg(int(1))],
host_call_site(source, "main", r#"choose_int("one")(1)"#),
),
),
[
function("int_identity", local_int(0, "value")).param_int(0, "value"),
function("int_increment", local_int(0, "value").add_int(int(1)))
.param_int(0, "value"),
function("string_identity", local_string(0, "value")).param_string(0, "value"),
function(
"string_suffix",
local_string(0, "value").concatenate(string("!")),
)
.param_string(0, "value"),
function("bool_true", bool_(true)).param_bool(0, "value"),
function("bool_false", bool_(false)).param_bool(0, "value"),
function("nil_identity", local_nil(0, "value")).param_nil(0, "value"),
function("nil_other", nil()).param_nil(0, "value"),
function(
"choose_int",
return_int_function(
int_to_int.clone(),
int_function_return_string_case(
local_string(0, "key"),
[(
"one",
int_function_return_expr(int_function_ref(
1,
[LocalId::Int(IntLocalId(0))],
)),
)],
int_function_return_expr(int_function_ref(
2,
[LocalId::Int(IntLocalId(0))],
)),
),
),
)
.param_string(0, "key"),
function(
"choose_string",
return_string_function(
string_to_string,
string_function_return_string_case(
local_string(0, "key"),
[(
"one",
string_function_return_expr(string_function_ref(
3,
[LocalId::String(StringLocalId(0))],
)),
)],
string_function_return_expr(string_function_ref(
4,
[LocalId::String(StringLocalId(0))],
)),
),
),
)
.param_string(0, "key"),
function(
"choose_bool",
return_bool_function(
bool_to_bool,
bool_function_return_string_case(
local_string(0, "key"),
[(
"one",
bool_function_return_expr(bool_function_ref(
5,
[LocalId::Bool(BoolLocalId(0))],
)),
)],
bool_function_return_expr(bool_function_ref(
6,
[LocalId::Bool(BoolLocalId(0))],
)),
),
),
)
.param_string(0, "key"),
function(
"choose_nil",
return_nil_function(
nil_to_nil,
nil_function_return_string_case(
local_string(0, "key"),
[(
"one",
nil_function_return_expr(nil_function_ref(
7,
[LocalId::Nil(NilLocalId(0))],
)),
)],
nil_function_return_expr(nil_function_ref(
8,
[LocalId::Nil(NilLocalId(0))],
)),
),
),
)
.param_string(0, "key"),
function(
"choose_increment",
return_int_function(
int_to_int.clone(),
int_function_return_expr(int_function_ref(
2,
[LocalId::Int(IntLocalId(0))],
)),
),
)
.param_string(0, "key"),
function(
"choose_getter",
return_function_function(function_function_return_string_case(
local_string(0, "key"),
[(
"one",
function_function_return_expr(function_function_ref(
FunctionFunctionId::Int(IntFunctionFunctionId(9)),
[LocalId::String(StringLocalId(0))],
int_to_int.clone(),
)),
)],
function_function_return_expr(function_function_ref(
FunctionFunctionId::Int(IntFunctionFunctionId(13)),
[LocalId::String(StringLocalId(0))],
int_to_int,
)),
)),
)
.param_string(0, "key"),
],
);
assert_eq!(actual, expected);
}
#[test]
fn plan_bool_case_branches_as_tail_calls() {
let source = r#"
fn positive(value: Int) {
value
}
fn negative(value: Int) {
0 - value
}
fn choose(flag: Bool) {
case flag {
True -> positive(1)
False -> negative(1)
}
}
pub fn main() {
choose(True)
}
"#;
let actual = plan_module(compile(source)).expect("source should plan");
let expected = module(
"main",
function(
"main",
int_return_tail_call_at(
3,
[bool_arg(bool_(true))],
host_call_site(source, "main", "choose(True)"),
),
),
[
function("positive", local_int(0, "value")).param_int(0, "value"),
function("negative", int(0).sub_int(local_int(0, "value"))).param_int(0, "value"),
function(
"choose",
int_return_bool_case(
local_bool(0, "flag"),
int_return_tail_call_at(
1,
[int_arg(int(1))],
host_call_site(source, "choose", "positive(1)"),
),
int_return_tail_call_at(
2,
[int_arg(int(1))],
host_call_site(source, "choose", "negative(1)"),
),
),
)
.param_bool(0, "flag"),
],
);
assert_eq!(actual, expected);
}
fn function_type(
arguments: impl IntoIterator<Item = ValueType>,
return_: ValueType,
) -> FunctionType {
FunctionType::new(arguments.into_iter().collect(), return_)
}
#[test]
fn plan_non_tail_direct_call_stays_expression_call() {
let source = r#"
fn add(a: Int, b: Int) {
a + b
}
pub fn main() {
add(1, 2) + 3
}
"#;
let actual = plan_module(compile(source)).expect("source should plan");
let expected = module(
"main",
function(
"main",
call_int_at(
1,
[int_arg(int(1)), int_arg(int(2))],
host_call_site(source, "main", "add(1, 2)"),
)
.add_int(int(3)),
),
[
function("add", local_int(0, "a").add_int(local_int(1, "b")))
.param_int(0, "a")
.param_int(1, "b"),
],
);
assert_eq!(actual, expected);
}
#[test]
fn plan_call_argument_direct_call_stays_expression_call() {
let source = r#"
fn add(a: Int, b: Int) {
a + b
}
fn identity(value: Int) {
value
}
pub fn main() {
identity(add(1, 2))
}
"#;
let actual = plan_module(compile(source)).expect("source should plan");
let expected = module(
"main",
function(
"main",
int_return_tail_call_at(
2,
[int_arg(call_int_at(
1,
[int_arg(int(1)), int_arg(int(2))],
host_call_site(source, "main", "add(1, 2)"),
))],
host_call_site(source, "main", "identity(add(1, 2))"),
),
),
[
function("add", local_int(0, "a").add_int(local_int(1, "b")))
.param_int(0, "a")
.param_int(1, "b"),
function("identity", local_int(0, "value")).param_int(0, "value"),
],
);
assert_eq!(actual, expected);
}
#[test]
fn plan_expression_statement_direct_call_stays_expression_call() {
let source = r#"
fn add(a: Int, b: Int) {
a + b
}
pub fn main() {
add(1, 2)
3
}
"#;
let actual = plan_module(compile(source)).expect("source should plan");
let expected = module(
"main",
function("main", int(3)).evaluate(call_int_at(
1,
[int_arg(int(1)), int_arg(int(2))],
host_call_site(source, "main", "add(1, 2)"),
)),
[
function("add", local_int(0, "a").add_int(local_int(1, "b")))
.param_int(0, "a")
.param_int(1, "b"),
],
);
assert_eq!(actual, expected);
}
#[test]
fn plan_let_value_direct_call_stays_expression_call() {
let source = r#"
fn add(a: Int, b: Int) {
a + b
}
pub fn main() {
let value = add(1, 2)
value
}
"#;
let actual = plan_module(compile(source)).expect("source should plan");
let expected = module(
"main",
function("main", local_int(0, "value")).let_int(
0,
"value",
call_int_at(
1,
[int_arg(int(1)), int_arg(int(2))],
host_call_site(source, "main", "add(1, 2)"),
),
),
[
function("add", local_int(0, "a").add_int(local_int(1, "b")))
.param_int(0, "a")
.param_int(1, "b"),
],
);
assert_eq!(actual, expected);
}
#[test]
fn plan_short_circuit_rhs_direct_call_stays_expression_call() {
let source = r#"
fn truth() {
True
}
pub fn main() {
False && truth()
}
"#;
let actual = plan_module(compile(source)).expect("source should plan");
let expected = module(
"main",
function(
"main",
bool_(false).and_bool(call_bool_at(
1,
[],
host_call_site(source, "main", "truth()"),
)),
),
[function("truth", bool_(true))],
);
assert_eq!(actual, expected);
}
#[test]
fn plan_final_function_value_call_stays_expression_call() {
let source = r#"
fn add(a: Int, b: Int) {
a + b
}
pub fn main() {
let f = add
f(1, 2)
}
"#;
let actual = plan_module(compile(source)).expect("source should plan");
let expected = module(
"main",
function(
"main",
call_int_function_at(
local_int_function(0, "f", [ValueType::Int, ValueType::Int]),
[int_function_call_arg(int(1)), int_function_call_arg(int(2))],
host_call_site(source, "main", "f(1, 2)"),
),
)
.step(let_int_function_step(
0,
"f",
int_function_ref(
1,
[LocalId::Int(IntLocalId(0)), LocalId::Int(IntLocalId(1))],
),
)),
[
function("add", local_int(0, "a").add_int(local_int(1, "b")))
.param_int(0, "a")
.param_int(1, "b"),
],
);
assert_eq!(actual, expected);
}
#[test]
fn plan_shadowed_current_function_local_call_stays_function_value_call() {
let source = r#"
pub fn main() {
let main = fn() { 0 }
main()
}
"#;
let actual = plan_module(compile(source)).expect("source should plan");
let expected = module_with_anonymous(
"main",
function(
"main",
call_int_function_at(
local_int_function(0, "main", Vec::<ValueType>::new()),
[],
host_call_site(source, "main", "main()"),
),
)
.step(let_int_function_step(
0,
"main",
int_function_closure(1, Vec::<LocalId>::new(), []),
)),
[],
[function("<anonymous:0>", int(0))],
);
assert_eq!(actual, expected);
}
#[test]
fn plan_shadowed_current_function_argument_call_stays_function_value_call() {
let source = r#"
fn one() {
1
}
fn run(run: fn() -> Int) {
run()
}
pub fn main() {
run(one)
}
"#;
let actual = plan_module(compile(source)).expect("source should plan");
let expected = module(
"main",
function(
"main",
int_return_tail_call_at(
2,
[int_function_arg(int_function_ref(1, Vec::<LocalId>::new()))],
host_call_site(source, "main", "run(one)"),
),
),
[
function("one", int(1)),
function(
"run",
call_int_function_at(
local_int_function(0, "run", Vec::<ValueType>::new()),
[],
host_call_site(source, "run", "run()"),
),
)
.param_int_function(0, "run", Vec::<ValueType>::new()),
],
);
assert_eq!(actual, expected);
}
#[test]
fn plan_final_pipeline_direct_call_preserves_tail_call() {
let source = r#"
fn count_down(n: Int, acc: Int) {
case n {
0 -> acc
_ -> count_down(n - 1, acc + 1)
}
}
pub fn main() {
1 |> count_down(0)
}
"#;
let actual = plan_module(compile(source)).expect("source should plan");
let expected = module(
"main",
function(
"main",
int_return_block(
[let_int_step(0, "_pipe", int(1))],
int_return_tail_call_at(
1,
[int_arg(local_int(0, "_pipe")), int_arg(int(0))],
host_call_site(source, "main", "count_down(0)"),
),
),
),
[function(
"count_down",
int_return_int_case(
local_int(0, "n"),
[(0, int_return_expr(local_int(1, "acc")))],
int_return_tail_call_at(
1,
[
int_arg(local_int(0, "n").sub_int(int(1))),
int_arg(local_int(1, "acc").add_int(int(1))),
],
host_call_site(source, "count_down", "count_down(n - 1, acc + 1)"),
),
),
)
.param_int(0, "n")
.param_int(1, "acc")],
);
assert_eq!(actual, expected);
}
#[test]
fn plan_main_returning_function_value() {
let actual = plan_module(compile(
r#"
fn identity(value: Int) {
value
}
pub fn main() {
identity
}
"#,
))
.expect("source should plan");
let expected = module(
"main",
function("main", int_function_ref(1, [LocalId::Int(IntLocalId(0))])),
[function("identity", local_int(0, "value")).param_int(0, "value")],
);
assert_eq!(actual, expected);
}
#[test]
fn plan_main_as_local_function_call() {
let source = r#"
pub fn main() {
1
}
pub fn helper() {
main()
}
"#;
let actual = plan_module(compile(source)).expect("source should plan");
let expected = module(
"main",
function("main", int(1)),
[function(
"helper",
int_return_tail_call_at(0, [], host_call_site(source, "helper", "main()")),
)],
);
assert_eq!(actual, expected);
}
#[test]
fn plan_typed_local_function_calls() {
let source = r#"
pub fn string_id(value: String) {
value
}
pub fn bool_id(value: Bool) {
value
}
pub fn nil_id(value: Nil) {
value
}
pub fn main() {
string_id("geam")
}
pub fn bool_main() {
bool_id(True)
}
pub fn nil_main() {
nil_id(Nil)
}
"#;
let actual = plan_module(compile(source)).expect("source should plan");
let expected = module(
"main",
function(
"main",
string_return_tail_call_at(
1,
[string_arg(string("geam"))],
host_call_site(source, "main", "string_id(\"geam\")"),
),
),
[
function("string_id", local_string(0, "value")).param_string(0, "value"),
function("bool_id", local_bool(0, "value")).param_bool(0, "value"),
function("nil_id", local_nil(0, "value")).param_nil(0, "value"),
function(
"bool_main",
bool_return_tail_call_at(
2,
[bool_arg(bool_(true))],
host_call_site(source, "bool_main", "bool_id(True)"),
),
),
function(
"nil_main",
nil_return_tail_call_at(
3,
[nil_arg(nil())],
host_call_site(source, "nil_main", "nil_id(Nil)"),
),
),
],
);
assert_eq!(actual, expected);
}
#[test]
fn plan_labelled_discard_argument_preserves_param_slot() {
let source = r#"
fn pick(ignored _: Int, value value: Int) {
value
}
pub fn main() {
pick(value: 2, ignored: 1)
}
"#;
let actual = plan_module(compile(source)).expect("source should plan");
let expected = module(
"main",
function(
"main",
int_return_tail_call_at(
1,
[int_arg(int(1)), int_arg(int(2))],
host_call_site(source, "main", "pick(value: 2, ignored: 1)"),
),
),
[function("pick", local_int(1, "value"))
.discard_int_param(0)
.param_int(1, "value")],
);
assert_eq!(actual, expected);
}
#[test]
fn reject_profile_external_function() {
assert_eq!(
expect_plan_error(
r#"
@external(erlang, "one", "two")
pub fn main() -> Int
"#,
),
PlanError::UnsupportedFunction {
name: "main".into(),
reason: UnsupportedFunctionReason::External,
},
);
}
#[test]
fn reject_margin_function_shapes() {
let mut empty_body = compile_minimal_module();
empty_body.definitions.functions[0].body = Vec::new();
assert_eq!(
plan_module(empty_body),
Err(PlanError::InvalidTypedAst {
reason: InvalidTypedAstReason::FunctionShape {
name: "main".into(),
reason: InvalidFunctionShapeReason::EmptyBody,
},
}),
);
let mut anonymous = compile_minimal_module();
anonymous.definitions.functions[0].name = None;
assert_eq!(
plan_module(anonymous),
Err(PlanError::InvalidTypedAst {
reason: InvalidTypedAstReason::FunctionShape {
name: "<anonymous>".into(),
reason: InvalidFunctionShapeReason::Anonymous,
},
}),
);
let mut return_type_mismatch = compile_minimal_module();
return_type_mismatch.definitions.functions[0].return_type = gleam_core::type_::bool();
assert_eq!(
plan_module(return_type_mismatch),
Err(PlanError::InvalidTypedAst {
reason: InvalidTypedAstReason::FunctionShape {
name: "main".into(),
reason: InvalidFunctionShapeReason::ReturnTypeMismatch,
},
}),
);
}
#[test]
fn reject_margin_plan_function_name_shape() {
let mut module = compile_minimal_module();
let mut function = module.definitions.functions.remove(0);
function.name = None;
let info = FunctionInfo {
signature: FunctionTemplateSignature::new(
FunctionTemplateId::new(0),
TypeScheme::new(0),
FunctionShape::new(Vec::new(), crate::plan::ValueShape::Int),
),
type_parameters: Default::default(),
return_shape: crate::plan::ValueShape::Int,
params: Vec::new(),
definition_span: crate::plan::SourceSpan::new(0, 0),
};
let mut anonymous = crate::planner::context::AnonymousFunctions::default();
let module_name = "main".into();
let functions = Default::default();
let context = PlanContext::new(&module_name, &functions, &mut anonymous);
assert_eq!(
super::plan_function(info, function, context),
Err(PlanError::InvalidTypedAst {
reason: InvalidTypedAstReason::FunctionShape {
name: "<anonymous>".into(),
reason: InvalidFunctionShapeReason::Anonymous,
},
}),
);
}
#[test]
fn function_owners_reject_mismatched_parameter_slots() {
let invalid_param = crate::planner::context::FunctionParam::new(
crate::plan::ParamLocal::int(IntLocalId(0)),
crate::plan::ValueShape::String,
crate::plan::ParamBinding::Named("value".into()),
None,
);
let expected = PlanError::InvalidTypedAst {
reason: InvalidTypedAstReason::ExpressionShape {
kind: crate::planner::InvalidExpressionShapeKind::Invalid,
},
};
let mut named_module = compile_minimal_module();
let named_function = named_module.definitions.functions.remove(0);
let info = FunctionInfo {
signature: FunctionTemplateSignature::new(
FunctionTemplateId::new(0),
TypeScheme::new(0),
FunctionShape::new(
vec![crate::plan::ValueShape::String],
crate::plan::ValueShape::Int,
),
),
type_parameters: Default::default(),
return_shape: crate::plan::ValueShape::Int,
params: vec![invalid_param.clone()],
definition_span: crate::plan::SourceSpan::new(0, 0),
};
let mut anonymous = crate::planner::context::AnonymousFunctions::default();
let module_name = "main".into();
let functions = Default::default();
let context = PlanContext::new(&module_name, &functions, &mut anonymous);
assert_eq!(
super::plan_function(info, named_function, context),
Err(expected.clone()),
);
let mut anonymous_module = compile_minimal_module();
let mut anonymous_body = anonymous_module.definitions.functions.remove(0).body;
let anonymous_body = vec1::Vec1::new(anonymous_body.remove(0));
let functions = Default::default();
let mut anonymous = crate::planner::context::AnonymousFunctions::default();
let module_name = "main".into();
let mut context =
crate::planner::context::PlanContext::new(&module_name, &functions, &mut anonymous);
assert_eq!(
super::plan_anonymous_function_body(
&"<anonymous:0>".into(),
&crate::plan::ValueShape::Int,
&[invalid_param],
Vec::new(),
anonymous_body,
&mut context,
)
.map(|_| ()),
Err(expected),
);
}
}