Files
AILang/crates/ailang-check/tests/subst_preserves_modes.rs
T
Brummel 449df13c9c fix(check): preserve fn-type modes through Subst::apply
`Subst::apply` rebuilt every `Type::Fn` with `param_modes: vec![]`
and `ret_mode: Implicit`, discarding the modes the surrounding type
carried. That was harmless while nothing downstream of inference read
fn-type modes — but it is a latent under-specification: substitution
is meant to be type-preserving, and a fn type's modes are part of its
identity (an `(own T) -> (own U)` contract is not the same contract as
its borrow-returning sibling). Modes are positional over `params`, and
substitution remaps each param type element-wise without changing the
arity, so the mode lists stay aligned — preserving them is a clone, not
a re-derivation.

The typed-MIR boundary (milestone 0060) makes this load-bearing: a node
type synthesised by `lower_to_mir::synth_pure` ends with `subst.apply`,
and the codegen drop path reads `ret_mode == Own` off a call's callee
node to decide RC trackability. With the modes stripped here, an
Own-returning call read back `Implicit`, nothing was trackable, and a
heap-allocated binder leaked at scope close. Preserving the modes
restores that signal at the source rather than re-deriving it in codegen
(which is exactly the re-derivation the milestone exists to delete).

Safe by construction: `unify`'s Fn arm destructures modes with `..` and
compares only params/ret/effects, so carrying modes through `apply`
cannot change unification; and `Type`'s custom `PartialEq` already
treats `Implicit` and `Own` as interchangeable (only `Borrow` is
distinct), so equality outcomes are unaffected except where preserving
a `Borrow` is the more correct answer anyway. Whole `ailang-check`
suite stays green.

RED pin: crates/ailang-check/tests/subst_preserves_modes.rs.
2026-05-31 18:01:24 +02:00

57 lines
2.1 KiB
Rust

//! mir.1b producer pin: `Subst::apply` must preserve fn-type modes.
//!
//! The typed-MIR boundary carries the modes `check` proved. The
//! codegen drop path reads `ret_mode == Own` off a call's callee node
//! to decide RC trackability — an Own-returning call hands a fresh heap
//! allocation to the caller's frame, so the binder needs an
//! `ailang_rc_dec` at scope close. `Subst::apply` is the last transform
//! `lower_to_mir::synth_pure` runs on every node type, so an `apply`
//! that rebuilds `Type::Fn` with empty `param_modes` / `Implicit`
//! `ret_mode` silently erases the Own signal: the callee node reads
//! back `Implicit`, nothing is trackable, and a heap-`Str` binder leaks
//! (`frees=0`). This pins that `apply` is mode-preserving — a
//! type-preserving substitution must not drop the modes that are part
//! of a fn type's identity.
use ailang_core::ast::ParamMode;
use ailang_core::Type;
use ailang_check::Subst;
#[test]
fn subst_apply_preserves_fn_ret_and_param_modes() {
// `(borrow Int) -> (own Str)` — an identity substitution must
// round-trip both the borrow param-mode and the Own ret-mode
// unchanged. (`Own` is the mode the drop path actually reads;
// `Borrow` is the one the custom `Type` equality keeps distinct,
// so it doubles as a witness that a stripped result is observable.)
let fn_ty = Type::Fn {
params: vec![Type::int()],
param_modes: vec![ParamMode::Borrow],
ret: Box::new(Type::str_()),
ret_mode: ParamMode::Own,
effects: vec![],
};
let applied = Subst::default().apply(&fn_ty);
let Type::Fn {
param_modes,
ret_mode,
..
} = applied
else {
panic!("apply of a Fn type is a Fn type");
};
assert!(
matches!(ret_mode, ParamMode::Own),
"ret_mode Own must survive Subst::apply (the drop path reads it \
for RC trackability), got {ret_mode:?}"
);
assert_eq!(
param_modes.first(),
Some(&ParamMode::Borrow),
"param_modes must survive Subst::apply, got {param_modes:?}"
);
}