use super::*;
use crate::types::{
effect_compatible, effect_paths, project_runtime_type_with_vars, runtime_core_type,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct HandlerBindingInfo {
pub(super) consumes: Vec<typed_ir::Path>,
pub(super) principal_input_effect: Option<typed_ir::Type>,
pub(super) principal_output_effect: Option<typed_ir::Type>,
pub(super) residual_before_known: bool,
pub(super) residual_after_known: bool,
pub(super) pure: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct HandlerCallBoundary {
pub(super) consumes: Vec<typed_ir::Path>,
pub(super) input_effect: Option<typed_ir::Type>,
pub(super) output_effect: Option<typed_ir::Type>,
pub(super) consumes_input_effect: bool,
pub(super) preserves_output_effect: bool,
pub(super) pure: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct HandlerAdapterPlan {
pub(super) consumes: Vec<typed_ir::Path>,
pub(super) residual_before: Option<typed_ir::Type>,
pub(super) residual_after: Option<typed_ir::Type>,
pub(super) return_wrapper_effect: Option<typed_ir::Type>,
pub(super) return_value: Option<typed_ir::Type>,
}
pub(super) fn handler_binding_info(binding: &Binding) -> Option<HandlerBindingInfo> {
let mut info = expr_handler_info(&binding.body)?;
if let Some(effect) = binding_parent_effect_path(&binding.name) {
if info.consumes.iter().any(|path| path.segments.len() == 1) {
info.consumes.retain(|path| path.segments.len() != 1);
}
if info.consumes.is_empty()
|| !info
.consumes
.iter()
.any(|existing| effect_paths_match(existing, &effect))
{
info.consumes.push(effect);
}
}
if let Some((input_effect, output_effect)) = principal_handler_effects(&binding.scheme.body) {
info.principal_input_effect = Some(input_effect);
info.principal_output_effect = Some(output_effect);
}
Some(info)
}
fn binding_parent_effect_path(target: &typed_ir::Path) -> Option<typed_ir::Path> {
if target.segments.len() < 2 {
return None;
}
Some(typed_ir::Path {
segments: target
.segments
.iter()
.take(target.segments.len() - 1)
.cloned()
.collect(),
})
}
pub(super) fn handler_call_boundary(
info: &HandlerBindingInfo,
args: &[&Expr],
result_ty: &RuntimeType,
) -> HandlerCallBoundary {
let input_effect = args
.iter()
.filter_map(|arg| handler_argument_effect(arg))
.reduce(merge_handler_residual_effects)
.filter(|effect| !effect_is_empty(effect));
let output_effect = runtime_thunk_effect(result_ty).filter(|effect| !effect_is_empty(effect));
let structural_input_consumes = info
.principal_input_effect
.as_ref()
.is_some_and(|effect| effect_contains_any(effect, &info.consumes));
let curried_handler_input_consumes =
input_effect.is_none() && args.len() > 1 && !info.consumes.is_empty();
HandlerCallBoundary {
consumes: info.consumes.clone(),
consumes_input_effect: input_effect
.as_ref()
.is_some_and(|effect| effect_contains_any(effect, &info.consumes))
|| structural_input_consumes
|| curried_handler_input_consumes,
preserves_output_effect: output_effect
.as_ref()
.is_some_and(|effect| !effect_is_empty(effect)),
input_effect,
output_effect,
pure: info.pure,
}
}
fn handler_argument_effect(arg: &Expr) -> Option<typed_ir::Type> {
let mut effect = runtime_thunk_effect(&arg.ty).filter(|effect| !effect_is_empty(effect));
collect_expr_thunk_effects(arg, &mut effect);
effect
}
fn collect_expr_thunk_effects(expr: &Expr, out: &mut Option<typed_ir::Type>) {
if let RuntimeType::Thunk { effect, .. } = &expr.ty
&& let Some(effect) = precise_handler_effect(effect)
{
merge_optional_handler_effect(out, effect);
if !matches!(expr.kind, ExprKind::Thunk { .. }) {
return;
}
}
match &expr.kind {
ExprKind::Lambda { body, .. } | ExprKind::BindHere { expr: body } => {
collect_expr_thunk_effects(body, out);
}
ExprKind::Apply { callee, arg, .. } => {
collect_expr_thunk_effects(callee, out);
collect_expr_thunk_effects(arg, out);
}
ExprKind::If {
cond,
then_branch,
else_branch,
..
} => {
collect_expr_thunk_effects(cond, out);
collect_expr_thunk_effects(then_branch, out);
collect_expr_thunk_effects(else_branch, out);
}
ExprKind::Tuple(items) => {
for item in items {
collect_expr_thunk_effects(item, out);
}
}
ExprKind::Record { fields, spread } => {
for field in fields {
collect_expr_thunk_effects(&field.value, out);
}
match spread {
Some(RecordSpreadExpr::Head(expr)) | Some(RecordSpreadExpr::Tail(expr)) => {
collect_expr_thunk_effects(expr, out);
}
None => {}
}
}
ExprKind::Variant { value, .. } => {
if let Some(value) = value {
collect_expr_thunk_effects(value, out);
}
}
ExprKind::Select { base, .. } => collect_expr_thunk_effects(base, out),
ExprKind::Match {
scrutinee, arms, ..
} => {
collect_expr_thunk_effects(scrutinee, out);
for arm in arms {
if let Some(guard) = &arm.guard {
collect_expr_thunk_effects(guard, out);
}
collect_expr_thunk_effects(&arm.body, out);
}
}
ExprKind::Block { stmts, tail } => {
for stmt in stmts {
collect_stmt_thunk_effects(stmt, out);
}
if let Some(tail) = tail {
collect_expr_thunk_effects(tail, out);
}
}
ExprKind::Handle { body, arms, .. } => {
collect_expr_thunk_effects(body, out);
for arm in arms {
if let Some(guard) = &arm.guard {
collect_expr_thunk_effects(guard, out);
}
collect_expr_thunk_effects(&arm.body, out);
}
}
ExprKind::Thunk { expr, .. }
| ExprKind::LocalPushId { body: expr, .. }
| ExprKind::AddId { thunk: expr, .. }
| ExprKind::Coerce { expr, .. }
| ExprKind::Pack { expr, .. } => collect_expr_thunk_effects(expr, out),
ExprKind::Var(_)
| ExprKind::EffectOp(_)
| ExprKind::PrimitiveOp(_)
| ExprKind::Lit(_)
| ExprKind::PeekId
| ExprKind::FindId { .. } => {}
}
}
fn collect_stmt_thunk_effects(stmt: &Stmt, out: &mut Option<typed_ir::Type>) {
match stmt {
Stmt::Let { value, .. } | Stmt::Expr(value) | Stmt::Module { body: value, .. } => {
collect_expr_thunk_effects(value, out);
}
}
}
fn merge_optional_handler_effect(out: &mut Option<typed_ir::Type>, effect: typed_ir::Type) {
*out = match out.take() {
Some(existing) => Some(merge_handler_residual_effects(existing, effect)),
None => Some(effect),
};
}
fn precise_handler_effect(effect: &typed_ir::Type) -> Option<typed_ir::Type> {
let paths = effect_paths(effect);
(!paths.is_empty()).then(|| normalize_effect(effect.clone()))
}
pub(super) fn handler_adapter_plan(
info: &HandlerBindingInfo,
boundary: &HandlerCallBoundary,
) -> HandlerAdapterPlan {
let mut residual_before = handler_residual_before(info, boundary);
let mut residual_after = residual_before
.as_ref()
.map(|effect| remove_consumed_effects(effect, &info.consumes));
let mut return_wrapper_effect =
handler_return_wrapper_effect(info, boundary, residual_after.as_ref());
if boundary.consumes_input_effect
&& boundary.preserves_output_effect
&& let Some(output_effect) = boundary
.output_effect
.as_ref()
.filter(|effect| !effect_is_empty(effect))
{
residual_before = Some(
boundary
.input_effect
.as_ref()
.map(|effect| select_consumed_effects(effect, &info.consumes))
.filter(|effect| !effect_is_empty(effect))
.unwrap_or_else(|| effect_row_from_paths(info.consumes.clone())),
);
residual_after = Some(typed_ir::Type::Never);
return_wrapper_effect = Some(output_effect.clone());
}
HandlerAdapterPlan {
consumes: info.consumes.clone(),
residual_before,
residual_after,
return_wrapper_effect,
return_value: None,
}
}
pub(super) fn substitute_handler_adapter_plan(
plan: &HandlerAdapterPlan,
substitutions: &BTreeMap<typed_ir::TypeVar, typed_ir::Type>,
) -> HandlerAdapterPlan {
HandlerAdapterPlan {
consumes: plan.consumes.clone(),
residual_before: plan
.residual_before
.as_ref()
.map(|effect| normalize_effect(substitute_type(effect, substitutions))),
residual_after: plan
.residual_after
.as_ref()
.map(|effect| normalize_effect(substitute_type(effect, substitutions))),
return_wrapper_effect: plan
.return_wrapper_effect
.as_ref()
.map(|effect| normalize_effect(substitute_type(effect, substitutions))),
return_value: plan
.return_value
.as_ref()
.map(|value| substitute_type(value, substitutions)),
}
}
pub(super) fn refine_handler_adapter_plan_from_signature(
mut plan: HandlerAdapterPlan,
params: &[(typed_ir::Type, typed_ir::Type)],
ret: &typed_ir::Type,
consumes: &[typed_ir::Path],
) -> HandlerAdapterPlan {
if !effect_contains_unknown_or_var(ret) {
plan.return_value = Some(ret.clone());
}
let Some(consumed) = params
.iter()
.map(|(_, effect)| select_consumed_effects(effect, consumes))
.filter(|effect| {
!effect_is_empty(effect)
&& !effect_contains_unknown_or_var(effect)
&& !effect_paths(effect).is_empty()
})
.reduce(merge_handler_residual_effects)
else {
return plan;
};
plan.residual_before = Some(
merge_effects(Some(consumed), plan.residual_before.clone())
.unwrap_or(typed_ir::Type::Never),
);
plan
}
pub(super) fn handler_preserved_residual_effect(
effect: &typed_ir::Type,
consumed: &[typed_ir::Path],
) -> Option<typed_ir::Type> {
let residual = normalize_effect(remove_consumed_effects(effect, consumed));
(!effect_is_empty(&residual)).then_some(residual)
}
pub(super) fn merge_handler_residual_effects(
left: typed_ir::Type,
right: typed_ir::Type,
) -> typed_ir::Type {
normalize_effect(merge_effect_rows(left, right))
}
pub(super) fn apply_handler_adapter_plan_to_binding(
mut binding: Binding,
plan: &HandlerAdapterPlan,
) -> Binding {
let mut next_effect_id = next_effect_id_after_expr(&binding.body);
binding.body = apply_handler_adapter_plan_to_expr(binding.body, plan, &mut next_effect_id);
binding.scheme.body =
project_runtime_type_with_vars(&runtime_core_type(&binding.body.ty), &BTreeSet::new());
binding
}
fn runtime_thunk_effect(ty: &RuntimeType) -> Option<typed_ir::Type> {
match ty {
RuntimeType::Thunk { effect, .. } => precise_handler_effect(effect),
RuntimeType::Unknown | RuntimeType::Core(_) | RuntimeType::Fun { .. } => None,
}
}
fn handler_residual_before(
info: &HandlerBindingInfo,
boundary: &HandlerCallBoundary,
) -> Option<typed_ir::Type> {
let has_call_site_consumed_effect = boundary
.input_effect
.as_ref()
.map(|effect| select_consumed_effects(effect, &info.consumes))
.is_some_and(|effect| !effect_is_empty(&effect));
let structural = info
.principal_input_effect
.as_ref()
.filter(|effect| effect_contains_any(effect, &info.consumes))
.cloned()
.or_else(|| {
(!has_call_site_consumed_effect && !info.consumes.is_empty())
.then(|| effect_row_from_paths(info.consumes.clone()))
});
merge_effects(structural, boundary.input_effect.clone())
}
fn handler_return_wrapper_effect(
info: &HandlerBindingInfo,
boundary: &HandlerCallBoundary,
residual_after: Option<&typed_ir::Type>,
) -> Option<typed_ir::Type> {
if boundary.consumes_input_effect && boundary.output_effect.is_none() {
let residual = residual_after
.cloned()
.filter(|effect| !effect_is_empty(effect));
if residual.is_some() {
return residual;
}
}
if !boundary.pure || boundary.output_effect.is_some() {
return None;
}
if std::env::var_os("YULANG_PRINCIPAL_ELABORATE_HANDLER_RETURN").is_none() {
return None;
}
info.consumes
.first()
.cloned()
.map(|path| typed_ir::Type::Named {
path,
args: Vec::new(),
})
}
fn merge_effects(
left: Option<typed_ir::Type>,
right: Option<typed_ir::Type>,
) -> Option<typed_ir::Type> {
match (left, right) {
(Some(left), Some(right)) => Some(merge_effect_rows(left, right)),
(Some(effect), None) | (None, Some(effect)) => Some(effect),
(None, None) => None,
}
}
fn merge_effect_rows(left: typed_ir::Type, right: typed_ir::Type) -> typed_ir::Type {
if effect_is_empty(&left) {
return right;
}
if effect_is_empty(&right) || left == right {
return left;
}
let (mut items, left_tail) = effect_row_parts(left);
let (right_items, right_tail) = effect_row_parts(right);
for item in right_items {
push_unique_effect_item(&mut items, item);
}
let tail = merge_effect_tails(left_tail, right_tail);
effect_row(items, tail)
}
fn effect_row_parts(effect: typed_ir::Type) -> (Vec<typed_ir::Type>, typed_ir::Type) {
match effect {
typed_ir::Type::Row { items, tail } => (items, *tail),
other => (vec![other], typed_ir::Type::Never),
}
}
fn merge_effect_tails(left: typed_ir::Type, right: typed_ir::Type) -> typed_ir::Type {
if effect_is_empty(&left) {
return right;
}
if effect_is_empty(&right) || left == right {
return left;
}
typed_ir::Type::Union(vec![left, right])
}
fn push_unique_effect_item(items: &mut Vec<typed_ir::Type>, item: typed_ir::Type) {
if effect_is_empty(&item) || items.iter().any(|existing| existing == &item) {
return;
}
for existing in items.iter_mut() {
let Some(ordering) = same_path_effect_item_precision_ordering(&item, existing) else {
continue;
};
match ordering {
std::cmp::Ordering::Greater => {
*existing = item;
return;
}
std::cmp::Ordering::Less => return,
std::cmp::Ordering::Equal if same_path_effect_item_is_precise(&item) => {}
std::cmp::Ordering::Equal => return,
}
}
items.push(item);
}
fn same_path_effect_item_precision_ordering(
candidate: &typed_ir::Type,
existing: &typed_ir::Type,
) -> Option<std::cmp::Ordering> {
let (candidate_path, candidate_args) = named_effect_item(candidate)?;
let (existing_path, existing_args) = named_effect_item(existing)?;
effect_paths_match(candidate_path, existing_path).then(|| {
effect_item_payload_precision(candidate_args)
.cmp(&effect_item_payload_precision(existing_args))
})
}
fn same_path_effect_item_is_precise(effect: &typed_ir::Type) -> bool {
named_effect_item(effect).is_some_and(|(_, args)| {
effect_item_payload_precision(args) == EffectPayloadPrecision::Precise
})
}
fn named_effect_item(effect: &typed_ir::Type) -> Option<(&typed_ir::Path, &[typed_ir::TypeArg])> {
match effect {
typed_ir::Type::Named { path, args } => Some((path, args)),
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum EffectPayloadPrecision {
Unknown,
Empty,
Precise,
}
fn effect_item_payload_precision(args: &[typed_ir::TypeArg]) -> EffectPayloadPrecision {
if args.iter().any(type_arg_contains_unknown_or_var) {
EffectPayloadPrecision::Unknown
} else if args.is_empty() {
EffectPayloadPrecision::Empty
} else {
EffectPayloadPrecision::Precise
}
}
fn effect_row(items: Vec<typed_ir::Type>, tail: typed_ir::Type) -> typed_ir::Type {
let items = items
.into_iter()
.filter(|item| !effect_is_empty(item) && item != &tail)
.collect::<Vec<_>>();
if items.is_empty() {
return tail;
}
typed_ir::Type::Row {
items,
tail: Box::new(tail),
}
}
fn effect_row_from_paths(paths: Vec<typed_ir::Path>) -> typed_ir::Type {
effect_row(
paths
.into_iter()
.map(|path| typed_ir::Type::Named {
path,
args: Vec::new(),
})
.collect(),
typed_ir::Type::Never,
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ConsumedEffectFilter {
Remove,
Select,
}
fn remove_consumed_effects(effect: &typed_ir::Type, consumed: &[typed_ir::Path]) -> typed_ir::Type {
filter_consumed_effects(effect, consumed, ConsumedEffectFilter::Remove)
}
fn select_consumed_effects(effect: &typed_ir::Type, consumed: &[typed_ir::Path]) -> typed_ir::Type {
filter_consumed_effects(effect, consumed, ConsumedEffectFilter::Select)
}
fn filter_consumed_effects(
effect: &typed_ir::Type,
consumed: &[typed_ir::Path],
mode: ConsumedEffectFilter,
) -> typed_ir::Type {
match effect {
typed_ir::Type::Named { path, .. }
if consumed
.iter()
.any(|consumed| effect_paths_match(consumed, path)) =>
{
match mode {
ConsumedEffectFilter::Remove => typed_ir::Type::Never,
ConsumedEffectFilter::Select => effect.clone(),
}
}
typed_ir::Type::Row { items, tail } => {
let items = items
.iter()
.map(|item| filter_consumed_effects(item, consumed, mode))
.filter(|item| !effect_is_empty(item))
.collect();
let tail = filter_consumed_effects(tail, consumed, mode);
effect_row(items, tail)
}
typed_ir::Type::Union(items) | typed_ir::Type::Inter(items) => effect_row(
items
.iter()
.map(|item| filter_consumed_effects(item, consumed, mode))
.filter(|item| !effect_is_empty(item))
.collect(),
typed_ir::Type::Never,
),
typed_ir::Type::Recursive { var, body } => typed_ir::Type::Recursive {
var: var.clone(),
body: Box::new(filter_consumed_effects(body, consumed, mode)),
},
other => match mode {
ConsumedEffectFilter::Remove => other.clone(),
ConsumedEffectFilter::Select => typed_ir::Type::Never,
},
}
}
fn effect_contains_unknown_or_var(effect: &typed_ir::Type) -> bool {
match effect {
typed_ir::Type::Unknown | typed_ir::Type::Any | typed_ir::Type::Var(_) => true,
typed_ir::Type::Never => false,
typed_ir::Type::Tuple(items) => items.iter().any(effect_contains_unknown_or_var),
typed_ir::Type::Row { items, tail } => {
items.iter().any(effect_contains_unknown_or_var) || effect_contains_unknown_or_var(tail)
}
typed_ir::Type::Union(items) | typed_ir::Type::Inter(items) => {
items.iter().any(effect_contains_unknown_or_var)
}
typed_ir::Type::Recursive { body, .. } => effect_contains_unknown_or_var(body),
typed_ir::Type::Named { args, .. } => args.iter().any(type_arg_contains_unknown_or_var),
typed_ir::Type::Fun {
param,
param_effect,
ret_effect,
ret,
} => {
effect_contains_unknown_or_var(param)
|| effect_contains_unknown_or_var(param_effect)
|| effect_contains_unknown_or_var(ret_effect)
|| effect_contains_unknown_or_var(ret)
}
typed_ir::Type::Record(record) => {
record
.fields
.iter()
.any(|field| effect_contains_unknown_or_var(&field.value))
|| record.spread.as_ref().is_some_and(|spread| match spread {
typed_ir::RecordSpread::Head(spread) | typed_ir::RecordSpread::Tail(spread) => {
effect_contains_unknown_or_var(spread.as_ref())
}
})
}
typed_ir::Type::Variant(variant) => {
variant
.cases
.iter()
.any(|case| case.payloads.iter().any(effect_contains_unknown_or_var))
|| variant
.tail
.as_ref()
.is_some_and(|tail| effect_contains_unknown_or_var(tail.as_ref()))
}
}
}
fn type_arg_contains_unknown_or_var(arg: &typed_ir::TypeArg) -> bool {
match arg {
typed_ir::TypeArg::Type(ty) => effect_contains_unknown_or_var(ty),
typed_ir::TypeArg::Bounds(bounds) => {
bounds
.lower
.as_ref()
.is_some_and(|ty| effect_contains_unknown_or_var(ty.as_ref()))
|| bounds
.upper
.as_ref()
.is_some_and(|ty| effect_contains_unknown_or_var(ty.as_ref()))
}
}
}
fn normalize_effect(effect: typed_ir::Type) -> typed_ir::Type {
match effect {
typed_ir::Type::Row { items, tail } => {
let items = items
.into_iter()
.map(normalize_effect)
.filter(|item| !effect_is_empty(item))
.collect();
effect_row(items, normalize_effect(*tail))
}
typed_ir::Type::Union(items) | typed_ir::Type::Inter(items) => effect_row(
items
.into_iter()
.map(normalize_effect)
.filter(|item| !effect_is_empty(item))
.collect(),
typed_ir::Type::Never,
),
typed_ir::Type::Recursive { var, body } => typed_ir::Type::Recursive {
var,
body: Box::new(normalize_effect(*body)),
},
other => other,
}
}
fn apply_handler_adapter_plan_to_expr(
expr: Expr,
plan: &HandlerAdapterPlan,
next_effect_id: &mut usize,
) -> Expr {
let Expr { ty, kind } = expr;
match kind {
ExprKind::Lambda {
param,
param_effect_annotation,
param_function_allowed_effects,
body,
} => {
let body = if matches!(body.kind, ExprKind::Lambda { .. }) {
apply_handler_adapter_plan_to_expr(*body, plan, next_effect_id)
} else {
let body = rewrite_first_handle(*body, plan);
wrap_first_handler_returns(body, plan, next_effect_id)
};
let body_is_handler_boundary = expr_contains_handle_before_lambda(&body);
let ty = update_lambda_runtime_type(ty, &body.ty, body_is_handler_boundary, plan);
Expr::typed(
ExprKind::Lambda {
param,
param_effect_annotation,
param_function_allowed_effects,
body: Box::new(body),
},
ty,
)
}
other => {
let expr = Expr::typed(other, ty);
let expr = rewrite_first_handle(expr, plan);
wrap_first_handler_returns(expr, plan, next_effect_id)
}
}
}
fn update_lambda_runtime_type(
ty: RuntimeType,
body_ty: &RuntimeType,
body_is_handler_boundary: bool,
plan: &HandlerAdapterPlan,
) -> RuntimeType {
match ty {
RuntimeType::Fun { param, .. } => {
let param = if body_is_handler_boundary {
handler_boundary_param_type(*param, plan)
} else {
*param
};
RuntimeType::fun(param, body_ty.clone())
}
other => other,
}
}
fn update_lambda_return_type(ty: RuntimeType, body_ty: &RuntimeType) -> RuntimeType {
match ty {
RuntimeType::Fun { param, .. } => RuntimeType::fun(*param, body_ty.clone()),
other => other,
}
}
fn handler_boundary_param_type(param: RuntimeType, plan: &HandlerAdapterPlan) -> RuntimeType {
let Some(effect) = plan
.residual_before
.clone()
.filter(|effect| !effect_is_empty(effect))
else {
return param;
};
match param {
RuntimeType::Thunk { value, .. } => RuntimeType::thunk(effect, *value),
other => RuntimeType::thunk(effect, other),
}
}
fn expr_contains_handle_before_lambda(expr: &Expr) -> bool {
match &expr.kind {
ExprKind::Handle { .. } => true,
ExprKind::Block {
stmts,
tail: Some(tail),
} => {
stmts.iter().any(stmt_contains_handle_before_lambda)
|| expr_contains_handle_before_lambda(tail)
}
ExprKind::BindHere { expr }
| ExprKind::Thunk { expr, .. }
| ExprKind::Coerce { expr, .. }
| ExprKind::Pack { expr, .. } => expr_contains_handle_before_lambda(expr),
ExprKind::LocalPushId { body, .. } => expr_contains_handle_before_lambda(body),
ExprKind::AddId { thunk, .. } => expr_contains_handle_before_lambda(thunk),
ExprKind::If {
cond,
then_branch,
else_branch,
..
} => {
expr_contains_handle_before_lambda(cond)
|| expr_contains_handle_before_lambda(then_branch)
|| expr_contains_handle_before_lambda(else_branch)
}
ExprKind::Tuple(items) => items.iter().any(expr_contains_handle_before_lambda),
ExprKind::Record { fields, spread } => {
fields
.iter()
.any(|field| expr_contains_handle_before_lambda(&field.value))
|| spread.as_ref().is_some_and(|spread| match spread {
RecordSpreadExpr::Head(expr) | RecordSpreadExpr::Tail(expr) => {
expr_contains_handle_before_lambda(expr)
}
})
}
ExprKind::Variant { value, .. } => value
.as_deref()
.is_some_and(expr_contains_handle_before_lambda),
ExprKind::Select { base, .. } => expr_contains_handle_before_lambda(base),
ExprKind::Match {
scrutinee, arms, ..
} => {
expr_contains_handle_before_lambda(scrutinee)
|| arms.iter().any(|arm| {
arm.guard
.as_ref()
.is_some_and(expr_contains_handle_before_lambda)
|| expr_contains_handle_before_lambda(&arm.body)
})
}
ExprKind::Lambda { .. } => false,
_ => false,
}
}
fn stmt_contains_handle_before_lambda(stmt: &Stmt) -> bool {
match stmt {
Stmt::Let { value, .. } | Stmt::Expr(value) | Stmt::Module { body: value, .. } => {
expr_contains_handle_before_lambda(value)
}
}
}
fn rewrite_first_handle(expr: Expr, plan: &HandlerAdapterPlan) -> Expr {
let Expr { ty, kind } = expr;
match kind {
ExprKind::Handle {
body,
arms,
evidence,
mut handler,
} => {
handler.consumes = plan.consumes.clone();
if plan.residual_before.is_some() {
handler.residual_before = plan.residual_before.clone();
}
if plan.residual_after.is_some() {
handler.residual_after = plan.residual_after.clone();
}
let body = ensure_consuming_handle_body_thunk(*body, plan);
Expr::typed(
ExprKind::Handle {
body: Box::new(body),
arms,
evidence,
handler,
},
ty,
)
}
ExprKind::Lambda {
param,
param_effect_annotation,
param_function_allowed_effects,
body,
} => Expr::typed(
ExprKind::Lambda {
param,
param_effect_annotation,
param_function_allowed_effects,
body: Box::new(rewrite_first_handle(*body, plan)),
},
ty,
),
ExprKind::BindHere { expr } => Expr::typed(
ExprKind::BindHere {
expr: Box::new(rewrite_first_handle(*expr, plan)),
},
ty,
),
ExprKind::LocalPushId { id, body } => Expr::typed(
ExprKind::LocalPushId {
id,
body: Box::new(rewrite_first_handle(*body, plan)),
},
ty,
),
ExprKind::AddId {
id,
allowed,
active,
thunk,
} => Expr::typed(
ExprKind::AddId {
id,
allowed,
active,
thunk: Box::new(rewrite_first_handle(*thunk, plan)),
},
ty,
),
other => Expr::typed(other, ty),
}
}
fn ensure_consuming_handle_body_thunk(body: Expr, plan: &HandlerAdapterPlan) -> Expr {
if plan.consumes.is_empty() || matches!(body.ty, RuntimeType::Thunk { .. }) {
return body;
}
let effect = plan
.residual_before
.clone()
.filter(|effect| !effect_is_empty(effect))
.unwrap_or_else(|| effect_row_from_paths(plan.consumes.clone()));
let value = body.ty.clone();
Expr::typed(
ExprKind::Thunk {
effect: effect.clone(),
value: value.clone(),
expr: Box::new(body),
},
RuntimeType::thunk(effect, value),
)
}
fn wrap_first_handler_returns(
body: Expr,
plan: &HandlerAdapterPlan,
next_effect_id: &mut usize,
) -> Expr {
let Some(effect) = plan.return_wrapper_effect.clone() else {
return body;
};
if effect_is_empty(&effect) {
return body;
}
wrap_first_handle_returns_with_effect(body, effect, plan.return_value.as_ref(), next_effect_id)
}
fn wrap_first_handle_returns_with_effect(
expr: Expr,
effect: typed_ir::Type,
expected_value: Option<&typed_ir::Type>,
next_effect_id: &mut usize,
) -> Expr {
let Expr { ty, kind } = expr;
match kind {
ExprKind::Handle {
body,
arms,
evidence,
handler,
} => {
let arms = arms
.into_iter()
.map(|arm| HandleArm {
body: wrap_value_in_thunk(arm.body, &effect, expected_value),
..arm
})
.collect::<Vec<_>>();
let value = expected_value.cloned().map(RuntimeType::core).unwrap_or(ty);
let thunk_ty = RuntimeType::thunk(effect, value);
Expr::typed(
ExprKind::Handle {
body,
arms,
evidence,
handler,
},
thunk_ty,
)
}
ExprKind::Lambda {
param,
param_effect_annotation,
param_function_allowed_effects,
body,
} => {
let body = wrap_first_handle_returns_with_effect(
*body,
effect,
expected_value,
next_effect_id,
);
let ty = update_lambda_return_type(ty, &body.ty);
Expr::typed(
ExprKind::Lambda {
param,
param_effect_annotation,
param_function_allowed_effects,
body: Box::new(body),
},
ty,
)
}
ExprKind::BindHere { expr } => {
let expr = wrap_first_handle_returns_with_effect(
*expr,
effect,
expected_value,
next_effect_id,
);
let ty = match &expr.ty {
RuntimeType::Thunk { value, .. } => value.as_ref().clone(),
_ => ty,
};
Expr::typed(
ExprKind::BindHere {
expr: Box::new(expr),
},
ty,
)
}
ExprKind::LocalPushId { id, body } => {
let body = wrap_first_handle_returns_with_effect(
*body,
effect,
expected_value,
next_effect_id,
);
let ty = body.ty.clone();
Expr::typed(
ExprKind::LocalPushId {
id,
body: Box::new(body),
},
ty,
)
}
ExprKind::AddId {
id,
allowed,
active,
thunk,
} => {
let thunk = wrap_first_handle_returns_with_effect(
*thunk,
effect,
expected_value,
next_effect_id,
);
let ty = thunk.ty.clone();
Expr::typed(
ExprKind::AddId {
id,
allowed,
active,
thunk: Box::new(thunk),
},
ty,
)
}
other => {
let expr = Expr::typed(other, ty);
wrap_handler_return(expr, &effect, next_effect_id)
}
}
}
fn wrap_value_in_thunk(
body: Expr,
effect: &typed_ir::Type,
expected_value: Option<&typed_ir::Type>,
) -> Expr {
if let RuntimeType::Thunk {
effect: existing,
value: _,
} = &body.ty
&& (existing == effect
|| (effect_compatible(existing, effect) && effect_compatible(effect, existing)))
{
return body;
}
let value = expected_value
.cloned()
.map(RuntimeType::core)
.unwrap_or_else(|| body.ty.clone());
let thunk_ty = RuntimeType::thunk(effect.clone(), value.clone());
Expr::typed(
ExprKind::Thunk {
effect: effect.clone(),
value,
expr: Box::new(body),
},
thunk_ty,
)
}
fn wrap_handler_return(body: Expr, effect: &typed_ir::Type, next_effect_id: &mut usize) -> Expr {
if matches!(body.kind, ExprKind::LocalPushId { .. }) || !matches!(body.ty, RuntimeType::Core(_))
{
return body;
}
let value_ty = body.ty.clone();
let thunk_ty = RuntimeType::thunk(effect.clone(), value_ty.clone());
let thunk = Expr::typed(
ExprKind::Thunk {
effect: effect.clone(),
value: value_ty.clone(),
expr: Box::new(body),
},
thunk_ty.clone(),
);
let add_id = Expr::typed(
ExprKind::AddId {
id: crate::ir::EffectIdRef::Peek,
allowed: effect.clone(),
active: false,
thunk: Box::new(thunk),
},
thunk_ty,
);
let forced = Expr::typed(
ExprKind::BindHere {
expr: Box::new(add_id),
},
value_ty.clone(),
);
let id = crate::ir::EffectIdVar(*next_effect_id);
*next_effect_id += 1;
Expr::typed(
ExprKind::LocalPushId {
id,
body: Box::new(forced),
},
value_ty,
)
}
fn next_effect_id_after_expr(expr: &Expr) -> usize {
max_effect_id_expr(expr).map_or(0, |id| id + 1)
}
fn max_effect_id_expr(expr: &Expr) -> Option<usize> {
let mut max = None;
match &expr.kind {
ExprKind::Lambda { body, .. }
| ExprKind::BindHere { expr: body }
| ExprKind::Thunk { expr: body, .. }
| ExprKind::LocalPushId { body, .. }
| ExprKind::Coerce { expr: body, .. }
| ExprKind::Pack { expr: body, .. } => update_max(&mut max, max_effect_id_expr(body)),
ExprKind::Apply { callee, arg, .. } => {
update_max(&mut max, max_effect_id_expr(callee));
update_max(&mut max, max_effect_id_expr(arg));
}
ExprKind::If {
cond,
then_branch,
else_branch,
..
} => {
update_max(&mut max, max_effect_id_expr(cond));
update_max(&mut max, max_effect_id_expr(then_branch));
update_max(&mut max, max_effect_id_expr(else_branch));
}
ExprKind::Tuple(items) => {
for item in items {
update_max(&mut max, max_effect_id_expr(item));
}
}
ExprKind::Record { fields, spread } => {
for field in fields {
update_max(&mut max, max_effect_id_expr(&field.value));
}
if let Some(spread) = spread {
match spread {
RecordSpreadExpr::Head(expr) | RecordSpreadExpr::Tail(expr) => {
update_max(&mut max, max_effect_id_expr(expr));
}
}
}
}
ExprKind::Variant { value, .. } => {
if let Some(value) = value {
update_max(&mut max, max_effect_id_expr(value));
}
}
ExprKind::Select { base, .. } => update_max(&mut max, max_effect_id_expr(base)),
ExprKind::Match {
scrutinee, arms, ..
} => {
update_max(&mut max, max_effect_id_expr(scrutinee));
for arm in arms {
update_max(&mut max, max_effect_id_pattern(&arm.pattern));
if let Some(guard) = &arm.guard {
update_max(&mut max, max_effect_id_expr(guard));
}
update_max(&mut max, max_effect_id_expr(&arm.body));
}
}
ExprKind::Block { stmts, tail } => {
for stmt in stmts {
update_max(&mut max, max_effect_id_stmt(stmt));
}
if let Some(tail) = tail {
update_max(&mut max, max_effect_id_expr(tail));
}
}
ExprKind::Handle { body, arms, .. } => {
update_max(&mut max, max_effect_id_expr(body));
for arm in arms {
update_max(&mut max, max_effect_id_pattern(&arm.payload));
if let Some(guard) = &arm.guard {
update_max(&mut max, max_effect_id_expr(guard));
}
update_max(&mut max, max_effect_id_expr(&arm.body));
}
}
ExprKind::AddId { id, thunk, .. } => {
update_max(&mut max, max_effect_id_ref(*id));
update_max(&mut max, max_effect_id_expr(thunk));
}
ExprKind::FindId { id } => update_max(&mut max, max_effect_id_ref(*id)),
ExprKind::Var(_)
| ExprKind::EffectOp(_)
| ExprKind::PrimitiveOp(_)
| ExprKind::Lit(_)
| ExprKind::PeekId => {}
}
max
}
fn max_effect_id_stmt(stmt: &Stmt) -> Option<usize> {
match stmt {
Stmt::Let { pattern, value } => {
max_effect_id_pattern(pattern).max(max_effect_id_expr(value))
}
Stmt::Expr(expr) | Stmt::Module { body: expr, .. } => max_effect_id_expr(expr),
}
}
fn max_effect_id_pattern(pattern: &Pattern) -> Option<usize> {
match pattern {
Pattern::Tuple { items, .. } => items.iter().filter_map(max_effect_id_pattern).max(),
Pattern::List {
prefix,
spread,
suffix,
..
} => prefix
.iter()
.chain(suffix)
.filter_map(max_effect_id_pattern)
.chain(
spread
.iter()
.filter_map(|pattern| max_effect_id_pattern(pattern)),
)
.max(),
Pattern::Record { fields, spread, .. } => {
let mut max = None;
for field in fields {
update_max(&mut max, max_effect_id_pattern(&field.pattern));
if let Some(default) = &field.default {
update_max(&mut max, max_effect_id_expr(default));
}
}
if let Some(spread) = spread {
match spread {
RecordSpreadPattern::Head(pattern) | RecordSpreadPattern::Tail(pattern) => {
update_max(&mut max, max_effect_id_pattern(pattern));
}
}
}
max
}
Pattern::Variant { value, .. } => value.as_deref().and_then(max_effect_id_pattern),
Pattern::Or { left, right, .. } => {
max_effect_id_pattern(left).max(max_effect_id_pattern(right))
}
Pattern::As { pattern, .. } => max_effect_id_pattern(pattern),
Pattern::Wildcard { .. } | Pattern::Bind { .. } | Pattern::Lit { .. } => None,
}
}
fn max_effect_id_ref(id: crate::ir::EffectIdRef) -> Option<usize> {
match id {
crate::ir::EffectIdRef::Var(id) => Some(id.0),
crate::ir::EffectIdRef::Peek => None,
}
}
fn update_max(max: &mut Option<usize>, candidate: Option<usize>) {
if let Some(candidate) = candidate {
*max = Some(max.map_or(candidate, |max| max.max(candidate)));
}
}
fn effect_contains_any(effect: &typed_ir::Type, targets: &[typed_ir::Path]) -> bool {
if matches!(effect, typed_ir::Type::Any) && !targets.is_empty() {
return true;
}
let paths = effect_paths(effect);
paths.iter().any(|path| {
targets
.iter()
.any(|target| effect_paths_match(path, target))
})
}
fn expr_handler_info(expr: &Expr) -> Option<HandlerBindingInfo> {
match &expr.kind {
ExprKind::Handle {
body,
arms,
handler,
..
} => {
let consumes = if handler.consumes.is_empty() {
handle_arm_parent_effects(arms)
} else {
handler.consumes.clone()
};
Some(HandlerBindingInfo {
consumes,
principal_input_effect: None,
principal_output_effect: None,
residual_before_known: handler.residual_before.is_some(),
residual_after_known: handler.residual_after.is_some(),
pure: handler.residual_after.as_ref().is_some_and(effect_is_empty),
})
.or_else(|| expr_handler_info(body))
.or_else(|| arms.iter().find_map(handle_arm_handler_info))
}
ExprKind::Apply { callee, arg, .. } => {
expr_handler_info(callee).or_else(|| expr_handler_info(arg))
}
ExprKind::Lambda { body, .. }
| ExprKind::BindHere { expr: body }
| ExprKind::Thunk { expr: body, .. }
| ExprKind::LocalPushId { body, .. }
| ExprKind::AddId { thunk: body, .. }
| ExprKind::Coerce { expr: body, .. }
| ExprKind::Pack { expr: body, .. } => expr_handler_info(body),
ExprKind::If {
cond,
then_branch,
else_branch,
..
} => expr_handler_info(cond)
.or_else(|| expr_handler_info(then_branch))
.or_else(|| expr_handler_info(else_branch)),
ExprKind::Tuple(items) => items.iter().find_map(expr_handler_info),
ExprKind::Record { fields, spread } => fields
.iter()
.find_map(|field| expr_handler_info(&field.value))
.or_else(|| {
spread.as_ref().and_then(|spread| match spread {
RecordSpreadExpr::Head(expr) | RecordSpreadExpr::Tail(expr) => {
expr_handler_info(expr)
}
})
}),
ExprKind::Variant { value, .. } => {
value.as_ref().and_then(|value| expr_handler_info(value))
}
ExprKind::Select { base, .. } => expr_handler_info(base),
ExprKind::Match {
scrutinee, arms, ..
} => expr_handler_info(scrutinee).or_else(|| arms.iter().find_map(match_arm_handler_info)),
ExprKind::Block { stmts, tail } => stmts
.iter()
.find_map(stmt_handler_info)
.or_else(|| tail.as_ref().and_then(|tail| expr_handler_info(tail))),
ExprKind::Var(_)
| ExprKind::EffectOp(_)
| ExprKind::PrimitiveOp(_)
| ExprKind::Lit(_)
| ExprKind::PeekId
| ExprKind::FindId { .. } => None,
}
}
fn handle_arm_parent_effects(arms: &[HandleArm]) -> Vec<typed_ir::Path> {
let mut out = Vec::<typed_ir::Path>::new();
for arm in arms {
for path in [Some(arm.effect.clone()), parent_effect_path(&arm.effect)]
.into_iter()
.flatten()
{
if path.segments.is_empty() {
continue;
}
if !out
.iter()
.any(|existing| effect_paths_match(existing, &path))
{
out.push(path);
}
}
}
out
}
fn parent_effect_path(operation: &typed_ir::Path) -> Option<typed_ir::Path> {
if operation.segments.len() < 2 {
return None;
}
Some(typed_ir::Path {
segments: operation
.segments
.iter()
.take(operation.segments.len() - 1)
.cloned()
.collect(),
})
}
fn principal_handler_effects(ty: &typed_ir::Type) -> Option<(typed_ir::Type, typed_ir::Type)> {
match ty {
typed_ir::Type::Fun {
param_effect,
ret_effect,
..
} => Some((param_effect.as_ref().clone(), ret_effect.as_ref().clone())),
typed_ir::Type::Recursive { body, .. } => principal_handler_effects(body),
typed_ir::Type::Inter(items) | typed_ir::Type::Union(items) => {
items.iter().find_map(principal_handler_effects)
}
_ => None,
}
}
fn handle_arm_handler_info(arm: &HandleArm) -> Option<HandlerBindingInfo> {
arm.guard
.as_ref()
.and_then(expr_handler_info)
.or_else(|| expr_handler_info(&arm.body))
}
fn match_arm_handler_info(arm: &MatchArm) -> Option<HandlerBindingInfo> {
arm.guard
.as_ref()
.and_then(expr_handler_info)
.or_else(|| expr_handler_info(&arm.body))
}
fn stmt_handler_info(stmt: &Stmt) -> Option<HandlerBindingInfo> {
match stmt {
Stmt::Let { value, .. } | Stmt::Expr(value) | Stmt::Module { body: value, .. } => {
expr_handler_info(value)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn handler_binding_detection_is_structural() {
let mut binding = test_binding(fun(named("unit"), named("unit")));
assert!(handler_binding_info(&binding).is_none());
binding.body = Expr::typed(
ExprKind::Lambda {
param: typed_ir::Name("x".to_string()),
param_effect_annotation: None,
param_function_allowed_effects: None,
body: Box::new(Expr::typed(
ExprKind::Handle {
body: Box::new(Expr::typed(
ExprKind::Var(path("x")),
RuntimeType::core(named("unit")),
)),
arms: vec![HandleArm {
effect: path_segments(&["std", "effect"]),
payload: Pattern::Wildcard {
ty: RuntimeType::core(named("unit")),
},
resume: None,
guard: None,
body: Expr::typed(
ExprKind::Lit(typed_ir::Lit::Unit),
RuntimeType::core(named("unit")),
),
}],
evidence: crate::ir::JoinEvidence {
result: named("unit"),
},
handler: crate::ir::HandleEffect {
consumes: Vec::new(),
residual_before: None,
residual_after: None,
},
},
RuntimeType::core(named("unit")),
)),
},
RuntimeType::fun(
RuntimeType::core(named("unit")),
RuntimeType::core(named("unit")),
),
);
let info = handler_binding_info(&binding).expect("handler info");
assert!(info.consumes.contains(&path_segments(&["std", "effect"])));
assert_eq!(info.principal_input_effect, Some(typed_ir::Type::Never));
assert_eq!(info.principal_output_effect, Some(typed_ir::Type::Never));
assert!(!info.residual_before_known);
assert!(!info.residual_after_known);
assert!(!info.pure);
}
#[test]
fn handler_binding_detection_ignores_value_arm_sentinel() {
let mut binding = test_binding(fun(named("int"), named("int")));
binding.name = path("pass");
binding.body = Expr::typed(
ExprKind::Lambda {
param: typed_ir::Name("x".to_string()),
param_effect_annotation: None,
param_function_allowed_effects: None,
body: Box::new(Expr::typed(
ExprKind::Handle {
body: Box::new(Expr::typed(
ExprKind::Var(path("x")),
RuntimeType::core(named("int")),
)),
arms: vec![HandleArm {
effect: typed_ir::Path::default(),
payload: Pattern::Bind {
name: typed_ir::Name("v".to_string()),
ty: RuntimeType::core(named("int")),
},
resume: None,
guard: None,
body: Expr::typed(
ExprKind::Var(path("v")),
RuntimeType::core(named("int")),
),
}],
evidence: crate::ir::JoinEvidence {
result: named("int"),
},
handler: crate::ir::HandleEffect {
consumes: Vec::new(),
residual_before: Some(typed_ir::Type::Never),
residual_after: Some(typed_ir::Type::Never),
},
},
RuntimeType::core(named("int")),
)),
},
RuntimeType::fun(
RuntimeType::core(named("int")),
RuntimeType::core(named("int")),
),
);
let info = handler_binding_info(&binding).expect("handler info");
assert!(info.consumes.is_empty());
assert!(info.pure);
}
#[test]
fn handler_call_boundary_reports_consumed_and_preserved_effects() {
let consumes = path_segments(&["std", "flow", "sub"]);
let outer = path_segments(&["std", "junction", "junction"]);
let info = HandlerBindingInfo {
consumes: vec![consumes.clone()],
principal_input_effect: None,
principal_output_effect: None,
residual_before_known: true,
residual_after_known: true,
pure: true,
};
let arg = Expr::typed(
ExprKind::Var(path("x")),
RuntimeType::thunk(
effect_row(vec![effect(consumes), effect(outer.clone())]),
RuntimeType::core(named("int")),
),
);
let result_ty = RuntimeType::thunk(effect(outer), RuntimeType::core(named("int")));
let boundary = handler_call_boundary(&info, &[&arg], &result_ty);
assert!(boundary.consumes_input_effect);
assert!(boundary.preserves_output_effect);
assert!(boundary.pure);
}
#[test]
fn handler_adapter_plan_combines_structural_and_call_site_effects() {
let consumes = path_segments(&["std", "flow", "sub"]);
let outer = path_segments(&["std", "junction", "junction"]);
let info = HandlerBindingInfo {
consumes: vec![consumes.clone()],
principal_input_effect: Some(effect(consumes.clone())),
principal_output_effect: Some(typed_ir::Type::Never),
residual_before_known: true,
residual_after_known: true,
pure: true,
};
let boundary = HandlerCallBoundary {
consumes: vec![consumes.clone()],
input_effect: Some(effect(outer.clone())),
output_effect: None,
consumes_input_effect: false,
preserves_output_effect: false,
pure: true,
};
let plan = handler_adapter_plan(&info, &boundary);
assert_eq!(
plan.residual_before,
Some(effect_row(vec![
effect(consumes.clone()),
effect(outer.clone())
]))
);
assert_eq!(plan.residual_after, Some(effect_row(vec![effect(outer)])));
assert_eq!(plan.return_wrapper_effect, None);
}
#[test]
fn handler_adapter_plan_preserves_consumed_effect_payload_from_signature() {
let consumes = path_segments(&["std", "flow", "sub"]);
let outer = path_segments(&["std", "undet", "undet"]);
let info = HandlerBindingInfo {
consumes: vec![consumes.clone()],
principal_input_effect: None,
principal_output_effect: None,
residual_before_known: true,
residual_after_known: true,
pure: true,
};
let boundary = HandlerCallBoundary {
consumes: vec![consumes.clone()],
input_effect: Some(effect_row(vec![
effect_with_arg(consumes.clone(), typed_ir::Type::Unknown),
effect(outer.clone()),
])),
output_effect: Some(effect(outer.clone())),
consumes_input_effect: true,
preserves_output_effect: true,
pure: true,
};
let plan = handler_adapter_plan(&info, &boundary);
let plan = refine_handler_adapter_plan_from_signature(
plan,
&[(
named("int"),
effect_with_arg(consumes.clone(), named("int")),
)],
&named("int"),
&[consumes.clone()],
);
assert_eq!(
plan.residual_before,
Some(effect_row(vec![effect_with_arg(consumes, named("int"))]))
);
assert_eq!(plan.residual_after, Some(typed_ir::Type::Never));
assert_eq!(plan.return_wrapper_effect, Some(effect(outer)));
assert_eq!(plan.return_value, Some(named("int")));
}
fn test_binding(scheme_body: typed_ir::Type) -> Binding {
Binding {
name: path_segments(&["std", "prelude", "&impl#0", "method"]),
type_params: Vec::new(),
scheme: typed_ir::Scheme {
requirements: Vec::new(),
body: scheme_body.clone(),
},
body: Expr::typed(ExprKind::Var(path("body")), RuntimeType::core(scheme_body)),
}
}
fn fun(param: typed_ir::Type, ret: typed_ir::Type) -> typed_ir::Type {
typed_ir::Type::Fun {
param: Box::new(param),
param_effect: Box::new(typed_ir::Type::Never),
ret_effect: Box::new(typed_ir::Type::Never),
ret: Box::new(ret),
}
}
fn named(name: &str) -> typed_ir::Type {
typed_ir::Type::Named {
path: path(name),
args: Vec::new(),
}
}
fn effect(path: typed_ir::Path) -> typed_ir::Type {
typed_ir::Type::Named {
path,
args: Vec::new(),
}
}
fn effect_with_arg(path: typed_ir::Path, arg: typed_ir::Type) -> typed_ir::Type {
typed_ir::Type::Named {
path,
args: vec![typed_ir::TypeArg::Type(arg)],
}
}
fn effect_row(items: Vec<typed_ir::Type>) -> typed_ir::Type {
typed_ir::Type::Row {
items,
tail: Box::new(typed_ir::Type::Never),
}
}
fn path(name: &str) -> typed_ir::Path {
typed_ir::Path::from_name(typed_ir::Name(name.to_string()))
}
fn path_segments(segments: &[&str]) -> typed_ir::Path {
typed_ir::Path {
segments: segments
.iter()
.map(|segment| typed_ir::Name((*segment).to_string()))
.collect(),
}
}
}