feat(lang): eliminate the Implicit ownership default — totality + the drop-soundness it demasks (#55)
Deletes `ParamMode::Implicit`. `ParamMode` is now `{Own, Borrow}`:
every fn-type slot on every signature carries an explicit `own` or
`borrow`, no defaulted position survives anywhere (model 0008 §2,
spec 0062). The parser rejects a bare fn-type slot; `borrow-return`
and `borrow-over-value` reject at the signature; the corpus is
migrated to minimal-ownership modes (consumed ⇒ own, read-only-heap
⇒ borrow, value ⇒ trivial-own). The documented `Implicit`-ret-mode
leak is fixed: an owned heap return now drops exactly once (live=0,
acceptance criterion 5).
This was the easy half. Removing the default ACTIVATED a family of
drop paths that `Implicit` had silently skipped — the pre-cutover
language was leaking (and in places mis-dropping) here rather than
crashing, because an Implicit scrutinee turned the drop off. Making
the modes explicit (Own) turned those paths on and exposed two
latent-bug clusters, all fixed RED-first as part of this cutover:
Drop-soundness family (four legs):
A. lit-sub-pattern double-free — the desugar re-matched the same
owned scrutinee in the lit fall-through; fixed by grouping
consecutive same-ctor arms into one match (bind fields once),
in ailang-core desugar.
B. Cons-husk leak on non-tail arm bodies — the lit-sub-pattern
desugar rebound the owned scrutinee via `Let $mp = xs`, which
bumped consume_count and suppressed the existing fn-return
partial_drop. Fixed by not rebinding a bare-Var scrutinee
(one husk-freeing mechanism, not two).
C. polymorphic `drop_<T>` rc_dec'd monomorphised value fields —
the per-ADT drop fn was emitted once from the polymorphic
TypeDef, defaulting type-var fields to ptr and rc_dec'ing
inline Ints (segfault). Fixed with per-monomorph drop
functions (new ailang-codegen::dropmono): the drop set is
collected from the lowered MIR, value-type fields are skipped,
heap fields still freed once; monomorphic-concrete ADTs keep
their byte-identical un-suffixed drop symbol.
D. static Str literal passed to an `(own Str)` param — the
literal lowers to a header-less rodata constant; the callee's
now-active rc_dec read its length field as a refcount and
freed a static address (segfault). Fixed with the missing
fourth StrRep::Static→Heap promotion in lower_to_mir's App arm,
gated on Own mode (borrow args stay static, no regression).
over-strict-mode lint over-fired: it suggested `(borrow V)` for
value-typed params (which `borrow-over-value` rejects — own is the
only legal mode there) and fired on `(intrinsic)` bodies (whose
consumption the linearity walk cannot observe). Tightened to skip
both; contract 0008 updated to the narrowed firing scope.
Irreversible step — canonical-form hash reset (model 0008 §6,
acceptance criterion 6). Every signature now carries explicit modes,
so the hashable canonical JSON changed for every module. RATIFY:
the corpus-wide hash-pin reset (hash_pin, prelude_module_hash_pin,
mono_hash_stability, eq_ord_e2e, embed_export_hash_stable, the
ct4/iter*/loop_recur schema-extension pins) and the list ir_snapshot
golden were regenerated once, deliberately, as the intended one-time
consequence of removing the mode elision from the canonical form —
not a regression. Each regenerated hash verified deterministic across
two runs.
Also fixes a pre-existing latent failure surfaced by the verification
gate, unrelated to this cutover: the `every_contract_names_a_resolvable_
ratifying_test` resolver (design_index_pin) could not resolve the
" + " dual-link ratifying-test form (`uniqueness.rs + linearity.rs`)
that the #57 audit-close (dfdc65f) introduced — it shipped red on that
commit. Resolver taught the dual-link form, mirroring its sibling.
Verification: cargo test --workspace = 731 passed, 0 failed (twice,
stable); e2e 102 passed, no binary exits non-zero (corpus crash-free);
grep-clean for Implicit/fn_implicit/mode_eq across crates; every drop
fix confirmed via emitted IR + AILANG_RC_STATS balance on the head==K,
head!=K, and Nil paths. Three BLOCKEDs en route (the unsound first
husk-dec attempt, the over-strict derivation premise, the leg-B fix
direction) were each treated as a real design/spec gap and rediagnosed,
not patched over.
Supersedes #54 (return-position-only leak patch). Precondition #57
(linearity hardening) was already met. Spec docs/specs/0062, plan
docs/plans/0121.
closes #55
This commit is contained in:
+17
-106
@@ -770,17 +770,14 @@ pub enum Type {
|
||||
/// `(own T)` wrappers from the surface form. They are metadata
|
||||
/// on `Type::Fn`, not new `Type` variants — so unification,
|
||||
/// occurs, apply, and every other `Type` match-arm keeps working
|
||||
/// unchanged. `param_modes` is omitted from canonical JSON when
|
||||
/// every entry is `Implicit`; `ret_mode` is omitted when it is
|
||||
/// `Implicit`, so pre-mode-annotation fixtures hash
|
||||
/// bit-identically. Full contract in
|
||||
/// unchanged. Both are always present (one mode per slot,
|
||||
/// `param_modes.len() == params.len()`); ownership has no default
|
||||
/// (spec 0062). Full contract in
|
||||
/// `design/contracts/0008-memory-model.md`.
|
||||
Fn {
|
||||
params: Vec<Type>,
|
||||
#[serde(default, skip_serializing_if = "all_implicit")]
|
||||
param_modes: Vec<ParamMode>,
|
||||
ret: Box<Type>,
|
||||
#[serde(default, skip_serializing_if = "ParamMode::is_implicit")]
|
||||
ret_mode: ParamMode,
|
||||
#[serde(default)]
|
||||
effects: Vec<String>,
|
||||
@@ -829,123 +826,37 @@ impl Type {
|
||||
Type::Con { name: "Float".into(), args: vec![] }
|
||||
}
|
||||
|
||||
/// Build a `Type::Fn` with all parameter modes set to
|
||||
/// `ParamMode::Implicit` and `ret_mode` set to `Implicit`. This
|
||||
/// is the form every typechecker / desugar / codegen site that
|
||||
/// synthesises a fn-type should use, so that newly inferred
|
||||
/// fn-types retain pre-mode-annotation canonical-JSON bytes.
|
||||
pub fn fn_implicit(params: Vec<Type>, ret: Type, effects: Vec<String>) -> Type {
|
||||
/// Build a `Type::Fn` with every parameter mode and the return
|
||||
/// mode set to `ParamMode::Own`. The synthesis form for every
|
||||
/// typechecker / desugar / codegen site that builds a fn-type;
|
||||
/// `Own` is correct by construction (spec 0062 Data flow: the old
|
||||
/// typechecker made `Implicit ≡ Own`, so synthesised fn-types were
|
||||
/// already semantically `Own`).
|
||||
pub fn fn_owned(params: Vec<Type>, ret: Type, effects: Vec<String>) -> Type {
|
||||
let n = params.len();
|
||||
Type::Fn {
|
||||
params,
|
||||
param_modes: vec![ParamMode::Implicit; n],
|
||||
param_modes: vec![ParamMode::Own; n],
|
||||
ret: Box::new(ret),
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
effects,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Visit every `Type::Fn` in `m`, letting `f` rewrite its modes.
|
||||
/// `f(params_len, param_modes, ret_mode)`. Used by the throwaway
|
||||
/// `migrate-modes` tool (spec 0062); has no other caller and is
|
||||
/// removed if the migration machinery is retired.
|
||||
pub fn for_each_fn_type_mut(
|
||||
m: &mut Module,
|
||||
f: &mut impl FnMut(usize, &mut Vec<ParamMode>, &mut ParamMode),
|
||||
) {
|
||||
fn walk_ty(t: &mut Type, f: &mut impl FnMut(usize, &mut Vec<ParamMode>, &mut ParamMode)) {
|
||||
match t {
|
||||
Type::Fn { params, param_modes, ret, ret_mode, .. } => {
|
||||
let n = params.len();
|
||||
for p in params.iter_mut() { walk_ty(p, f); }
|
||||
walk_ty(ret, f);
|
||||
f(n, param_modes, ret_mode);
|
||||
}
|
||||
Type::Con { args, .. } => { for a in args.iter_mut() { walk_ty(a, f); } }
|
||||
Type::Forall { body, .. } => walk_ty(body, f),
|
||||
Type::Var { .. } => {}
|
||||
}
|
||||
}
|
||||
for def in m.defs.iter_mut() {
|
||||
if let Def::Fn(fd) = def {
|
||||
walk_ty(&mut fd.ty, f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-parameter / return mode marker on a [`Type::Fn`]. Full
|
||||
/// contract lives in `design/contracts/0008-memory-model.md`.
|
||||
///
|
||||
/// `Implicit` is the legacy state for fn-types that were constructed
|
||||
/// before the borrow/own surface annotations existed. Semantically,
|
||||
/// `Implicit ≡ Own`; the distinction exists only so pre-annotation
|
||||
/// JSON fixtures continue to serialize without a `"mode"` wrapper
|
||||
/// and therefore keep their canonical-JSON hash.
|
||||
///
|
||||
/// `Own` and `Borrow` are author-asserted: the surface form
|
||||
/// `(own T)` / `(borrow T)` round-trips through this enum.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
/// Ownership has no default: every fn-type slot carries an explicit
|
||||
/// `Own` or `Borrow` (spec 0062).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ParamMode {
|
||||
/// Unannotated / back-compat. Treated as `Own` by the typechecker.
|
||||
#[default]
|
||||
Implicit,
|
||||
/// `(own T)` — caller transfers ownership; callee consumes.
|
||||
Own,
|
||||
/// `(borrow T)` — caller retains ownership; callee may not consume.
|
||||
Borrow,
|
||||
}
|
||||
|
||||
impl ParamMode {
|
||||
/// Used by the `skip_serializing_if` predicate on
|
||||
/// [`Type::Fn::ret_mode`].
|
||||
pub fn is_implicit(&self) -> bool {
|
||||
matches!(self, ParamMode::Implicit)
|
||||
}
|
||||
}
|
||||
|
||||
/// Serde helper for [`Type::Fn::param_modes`]. Returns `true` when
|
||||
/// every entry is [`ParamMode::Implicit`] (or when the list is
|
||||
/// empty), so canonical JSON omits the field for any fn-type without
|
||||
/// explicit `(borrow)` / `(own)` annotations and pre-annotation
|
||||
/// fixtures keep bit-identical hashes.
|
||||
fn all_implicit(modes: &[ParamMode]) -> bool {
|
||||
modes.iter().all(|m| m.is_implicit())
|
||||
}
|
||||
|
||||
/// Equality of [`ParamMode`] for the purposes of `Type` equality.
|
||||
/// `Implicit` and `Own` are treated as the same mode; `Borrow` is
|
||||
/// distinct. This keeps pre-annotation fixtures (whose fn-types
|
||||
/// serialize `Implicit`) compatible with newly-written fixtures
|
||||
/// that mark the same fn-type explicitly with `(own T)`.
|
||||
fn mode_eq(a: &ParamMode, b: &ParamMode) -> bool {
|
||||
match (a, b) {
|
||||
(ParamMode::Borrow, ParamMode::Borrow) => true,
|
||||
(ParamMode::Borrow, _) | (_, ParamMode::Borrow) => false,
|
||||
// Implicit and Own are interchangeable.
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Equality of two `param_modes` slices, robust to the
|
||||
/// "elided when all-implicit" representation used by typechecker /
|
||||
/// desugar / codegen sites that construct fn-types with
|
||||
/// `param_modes: vec![]`. Both slices are normalised to "implicit
|
||||
/// padding to match the longer one"; equality then proceeds
|
||||
/// element-wise via [`mode_eq`].
|
||||
fn mode_slices_eq(a: &[ParamMode], b: &[ParamMode]) -> bool {
|
||||
let n = a.len().max(b.len());
|
||||
for i in 0..n {
|
||||
let x = a.get(i).copied().unwrap_or(ParamMode::Implicit);
|
||||
let y = b.get(i).copied().unwrap_or(ParamMode::Implicit);
|
||||
if !mode_eq(&x, &y) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
impl PartialEq for Type {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
@@ -971,8 +882,8 @@ impl PartialEq for Type {
|
||||
) => {
|
||||
ap == bp
|
||||
&& ar == br
|
||||
&& mode_slices_eq(apm, bpm)
|
||||
&& mode_eq(arm, brm)
|
||||
&& apm == bpm
|
||||
&& arm == brm
|
||||
&& {
|
||||
let mut a = ae.clone();
|
||||
let mut b = be.clone();
|
||||
|
||||
@@ -933,7 +933,7 @@ impl Desugarer {
|
||||
ret: ret.clone(),
|
||||
effects: effects.clone(),
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
}
|
||||
}
|
||||
Type::Forall { .. } => panic!(
|
||||
@@ -1115,8 +1115,6 @@ impl Desugarer {
|
||||
arms,
|
||||
};
|
||||
}
|
||||
let s = self.fresh();
|
||||
let s_var = Term::Var { name: s.clone() };
|
||||
// `default` is unreachable for valid programs (the
|
||||
// typechecker requires either a catch-all arm or exhaustive
|
||||
// ctor coverage). Use the polymorphic bottom builtin
|
||||
@@ -1125,6 +1123,28 @@ impl Desugarer {
|
||||
// `Unit`-typed `_` arm to dominate it. Codegen lowers the
|
||||
// var to LLVM `unreachable`.
|
||||
let default = Term::Var { name: "__unreachable__".into() };
|
||||
// When the scrutinee is ALREADY a bare `Term::Var` (a function
|
||||
// param or an existing let-binder), do NOT introduce a fresh
|
||||
// `Let $mp = <scrutinee>` rebind: reuse the binder directly as
|
||||
// the chain scrutinee. The rebind is spurious here and, post-#55,
|
||||
// harmful — walking the `Let` value in `Position::Consume` bumps
|
||||
// the param's `consume_count` to 1, which trips the fn-return
|
||||
// husk-free gate (`consume_count != 0` ⇒ skip) and relocates the
|
||||
// outer-cell ownership onto the internal `$mp` binder the gate
|
||||
// never inspects, leaking the moved-from outer cell. Reusing the
|
||||
// binder keeps `consume_count == 0` and populates `moved_slots`
|
||||
// exactly as the single-match case, so the existing gate fires
|
||||
// unchanged (refs #55, examples/lit_pat_ctor_tail_drop.ail,
|
||||
// examples/lit_pat_nil_scrutinee_drop.ail).
|
||||
//
|
||||
// A COMPOUND-expression scrutinee (not a bare Var) still needs
|
||||
// the `$mp` rebind so it is evaluated once and shared across all
|
||||
// chain arms rather than re-evaluated per arm.
|
||||
if matches!(scrutinee, Term::Var { .. }) {
|
||||
return self.build_chain(&scrutinee, &arms, &default);
|
||||
}
|
||||
let s = self.fresh();
|
||||
let s_var = Term::Var { name: s.clone() };
|
||||
let chain = self.build_chain(&s_var, &arms, &default);
|
||||
Term::Let {
|
||||
name: s,
|
||||
@@ -1136,16 +1156,109 @@ impl Desugarer {
|
||||
/// Recursively builds a chain of single-arm matches with a shared
|
||||
/// fall-through. Empty arms ⇒ `default`; otherwise the first arm
|
||||
/// is desugared with the rest of the chain as its fall-through.
|
||||
///
|
||||
/// Consecutive arms whose head pattern is the *same* outer ctor
|
||||
/// (same name + arity) are grouped into ONE [`Term::Match`] arm
|
||||
/// that binds the ctor fields exactly once, then branches the
|
||||
/// per-arm sub-patterns over those bound field vars. This is the
|
||||
/// single-ownership-scope invariant the arm-close drop accounting
|
||||
/// in `match_lower` relies on: a ctor's owned children are bound
|
||||
/// (and therefore dropped) under one match scope per ctor, never
|
||||
/// re-matched on the same scrutinee. Re-matching the same owned
|
||||
/// scrutinee under a second ctor scope (the pre-fix lowering of a
|
||||
/// lit sub-pattern's `else` branch) bound — and the post-#55
|
||||
/// `Own`-param arm-close dropped — the same heap child twice
|
||||
/// (double-free; refs #55, examples/lit_pat_ctor_tail_drop.ail).
|
||||
fn build_chain(&mut self, s_var: &Term, arms: &[Arm], default: &Term) -> Term {
|
||||
if arms.is_empty() {
|
||||
return default.clone();
|
||||
}
|
||||
let head = &arms[0];
|
||||
// Detect a maximal run of consecutive arms sharing the head
|
||||
// arm's outer ctor + arity. A run of length ≥ 1 of ctor arms
|
||||
// is lowered as a single bind-once ctor-match; everything else
|
||||
// (Wild / Var / Lit head) falls through to the per-arm path.
|
||||
if let Pattern::Ctor { ctor, fields } = &head.pat {
|
||||
let arity = fields.len();
|
||||
let group_len = arms
|
||||
.iter()
|
||||
.take_while(|a| match &a.pat {
|
||||
Pattern::Ctor {
|
||||
ctor: c,
|
||||
fields: f,
|
||||
} => c == ctor && f.len() == arity,
|
||||
_ => false,
|
||||
})
|
||||
.count();
|
||||
let group = &arms[..group_len];
|
||||
let rest = &arms[group_len..];
|
||||
let rest_chain = self.build_chain(s_var, rest, default);
|
||||
return self.build_ctor_group(s_var, ctor, arity, group, rest_chain);
|
||||
}
|
||||
let rest = &arms[1..];
|
||||
let fall_k = self.build_chain(s_var, rest, default);
|
||||
self.desugar_one_arm(s_var, head, fall_k)
|
||||
}
|
||||
|
||||
/// Lowers a run of consecutive arms that all match the same outer
|
||||
/// ctor `ctor`/`arity` into a single [`Term::Match`] that binds the
|
||||
/// ctor fields once. The bound field vars are threaded through each
|
||||
/// arm's sub-patterns (via [`wrap_sub`](Self::wrap_sub)); the arms'
|
||||
/// internal fall-throughs chain left-to-right, ending in
|
||||
/// `rest_chain` (the arms after the group, which by construction
|
||||
/// match a *different* ctor or are catch-alls). The single match's
|
||||
/// wildcard arm also routes to `rest_chain`. No arm in the group
|
||||
/// re-matches `s_var`, so the ctor's owned children live under
|
||||
/// exactly one match scope.
|
||||
fn build_ctor_group(
|
||||
&mut self,
|
||||
s_var: &Term,
|
||||
ctor: &str,
|
||||
arity: usize,
|
||||
group: &[Arm],
|
||||
rest_chain: Term,
|
||||
) -> Term {
|
||||
let fresh_vars: Vec<String> = (0..arity).map(|_| self.fresh()).collect();
|
||||
let flat_fields: Vec<Pattern> = fresh_vars
|
||||
.iter()
|
||||
.map(|n| Pattern::Var { name: n.clone() })
|
||||
.collect();
|
||||
// Build the group body inside-out: the last arm falls to
|
||||
// `rest_chain`; each earlier arm falls to the next arm's body.
|
||||
// Within an arm, fold the field sub-patterns right-to-left so
|
||||
// the deepest field is matched first (matches the single-arm
|
||||
// `desugar_one_arm` ordering).
|
||||
let mut inner = rest_chain.clone();
|
||||
for arm in group.iter().rev() {
|
||||
let sub_fields = match &arm.pat {
|
||||
Pattern::Ctor { fields, .. } => fields,
|
||||
// `build_chain` only calls this with ctor arms.
|
||||
_ => unreachable!("build_ctor_group requires ctor arms"),
|
||||
};
|
||||
let mut arm_body = arm.body.clone();
|
||||
for (sub, fv) in sub_fields.iter().zip(fresh_vars.iter()).rev() {
|
||||
arm_body = self.wrap_sub(fv, sub, arm_body, &inner);
|
||||
}
|
||||
inner = arm_body;
|
||||
}
|
||||
Term::Match {
|
||||
scrutinee: Box::new(s_var.clone()),
|
||||
arms: vec![
|
||||
Arm {
|
||||
pat: Pattern::Ctor {
|
||||
ctor: ctor.to_string(),
|
||||
fields: flat_fields,
|
||||
},
|
||||
body: inner,
|
||||
},
|
||||
Arm {
|
||||
pat: Pattern::Wild,
|
||||
body: rest_chain,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/// Lowers one arm into a term. Wild/Var arms drop the chain (the
|
||||
/// arm matches everything); Lit and Ctor arms emit a `Term::Match`
|
||||
/// with the desugared head pattern as the first arm and a
|
||||
@@ -1988,7 +2101,7 @@ mod tests {
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
params: vec!["xs".into()],
|
||||
body: body_match,
|
||||
@@ -2046,7 +2159,7 @@ mod tests {
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
params: vec!["xs".into()],
|
||||
body: original.clone(),
|
||||
@@ -2138,7 +2251,7 @@ mod tests {
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
params: vec!["n".into()],
|
||||
body: Box::new(Term::Var { name: "n".into() }),
|
||||
@@ -2164,7 +2277,7 @@ mod tests {
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
params: vec![],
|
||||
body: fact_letrec_term(),
|
||||
@@ -2228,7 +2341,7 @@ mod tests {
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
params: vec!["n".into()],
|
||||
// (let-rec helper (params x) (type Int -> Int)
|
||||
@@ -2241,7 +2354,7 @@ mod tests {
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
params: vec!["x".into()],
|
||||
body: Box::new(Term::App {
|
||||
@@ -2278,7 +2391,7 @@ mod tests {
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
"lifted fn type should have capture appended; got {:?}",
|
||||
lifted.ty
|
||||
@@ -2348,7 +2461,7 @@ mod tests {
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
params: vec!["x".into()],
|
||||
body: Box::new(Term::App {
|
||||
@@ -2377,7 +2490,7 @@ mod tests {
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
params: vec![],
|
||||
body: Term::Let {
|
||||
@@ -2451,7 +2564,7 @@ mod tests {
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
params: vec!["z".into()],
|
||||
body: Box::new(Term::App {
|
||||
@@ -2498,7 +2611,7 @@ mod tests {
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
params: vec!["p".into()],
|
||||
body: outer_body,
|
||||
@@ -2548,7 +2661,7 @@ mod tests {
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
params: vec!["x".into()],
|
||||
body: Box::new(Term::Let {
|
||||
@@ -2578,7 +2691,7 @@ mod tests {
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
params: vec![],
|
||||
body: letrec,
|
||||
@@ -2611,7 +2724,7 @@ mod tests {
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
params: vec!["x".into()],
|
||||
body: Box::new(Term::App {
|
||||
@@ -2641,7 +2754,7 @@ mod tests {
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
params: vec![],
|
||||
body: letrec,
|
||||
@@ -2733,7 +2846,7 @@ mod tests {
|
||||
ret: Box::new(Type::Var { name: "a".into() }),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
params: vec!["k".into()],
|
||||
// 2026-05-21 operator-routing-eq-ord: keep `==` here (vs
|
||||
@@ -2786,7 +2899,7 @@ mod tests {
|
||||
ret: Box::new(Type::Var { name: "a".into() }),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
}),
|
||||
},
|
||||
params: vec!["x".into()],
|
||||
@@ -2862,7 +2975,7 @@ mod tests {
|
||||
ret: Box::new(Type::Var { name: "a".into() }),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
params: vec!["k".into()],
|
||||
body: Box::new(Term::Var { name: "x".into() }),
|
||||
@@ -2891,7 +3004,7 @@ mod tests {
|
||||
ret: Box::new(Type::Var { name: "a".into() }),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
}),
|
||||
},
|
||||
params: vec!["x".into()],
|
||||
@@ -2932,7 +3045,7 @@ mod tests {
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
params: vec!["j".into()],
|
||||
body: Box::new(Term::App {
|
||||
@@ -2953,7 +3066,7 @@ mod tests {
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
params: vec!["i".into()],
|
||||
body: Box::new(inner),
|
||||
@@ -2975,7 +3088,7 @@ mod tests {
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
params: vec![],
|
||||
body: outer,
|
||||
@@ -3011,7 +3124,7 @@ mod tests {
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
params: vec!["j".into()],
|
||||
body: Box::new(Term::App {
|
||||
@@ -3035,7 +3148,7 @@ mod tests {
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
params: vec!["i".into()],
|
||||
body: Box::new(inner),
|
||||
@@ -3057,7 +3170,7 @@ mod tests {
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
params: vec![],
|
||||
body: outer,
|
||||
@@ -3189,7 +3302,7 @@ mod tests {
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
params: vec!["n".into()],
|
||||
body: body_match,
|
||||
@@ -3207,15 +3320,15 @@ mod tests {
|
||||
!any_lit_pattern(body),
|
||||
"desugarer must remove every Pattern::Lit from the term tree; got: {body:#?}"
|
||||
);
|
||||
// The desugar wraps the match in `let $mp_N = scrutinee in <chain>`,
|
||||
// and the chain head must be a `Term::If` (the lit arm).
|
||||
let chain = match body {
|
||||
Term::Let { body, .. } => body.as_ref(),
|
||||
other => panic!("expected outer Let from chain machinery, got {other:?}"),
|
||||
};
|
||||
// The scrutinee is a bare `Term::Var` (the param `n`), so the
|
||||
// chain machinery reuses the binder directly without a spurious
|
||||
// `let $mp_N = n` rebind (the rebind would bump the param's
|
||||
// `consume_count` and break the fn-return husk-free gate; refs
|
||||
// #55). The chain head is therefore the `Term::If` (the lit arm)
|
||||
// sitting directly at the function body.
|
||||
assert!(
|
||||
matches!(chain, Term::If { .. }),
|
||||
"expected Term::If at the chain head, got {chain:?}"
|
||||
matches!(body, Term::If { .. }),
|
||||
"expected Term::If at the chain head, got {body:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3264,7 +3377,7 @@ mod tests {
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
params: vec!["xs".into()],
|
||||
body: body_match,
|
||||
@@ -3347,7 +3460,7 @@ mod tests {
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
params: vec!["n".into()],
|
||||
body: body_match,
|
||||
@@ -3361,13 +3474,11 @@ mod tests {
|
||||
Def::Fn(f) => &f.body,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
// Outer is `let $mp_N = scrutinee in <chain>`. The chain is
|
||||
// `if (== sv 0) then 100 else if (== sv 1) then 200 else __unreachable__`.
|
||||
let chain = match body {
|
||||
Term::Let { body, .. } => body.as_ref(),
|
||||
other => panic!("expected outer Let from chain machinery, got {other:?}"),
|
||||
};
|
||||
let inner_else = match chain {
|
||||
// The scrutinee is a bare `Term::Var` (the param `n`), so there
|
||||
// is no `let $mp_N = n` rebind (it would break the fn-return
|
||||
// husk-free gate; refs #55). The body IS the chain directly:
|
||||
// `if (== n 0) then 100 else if (== n 1) then 200 else __unreachable__`.
|
||||
let inner_else = match body {
|
||||
Term::If { else_, .. } => else_.as_ref(),
|
||||
other => panic!("expected outer If, got {other:?}"),
|
||||
};
|
||||
|
||||
@@ -179,7 +179,7 @@ mod tests {
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
params: vec!["a".into(), "b".into()],
|
||||
body: Term::App {
|
||||
|
||||
@@ -1811,7 +1811,7 @@ mod tests {
|
||||
"defs": [{
|
||||
"kind": "fn",
|
||||
"name": "f",
|
||||
"type": { "k": "fn", "params": [], "ret": { "k": "con", "name": type_con }, "effects": [] },
|
||||
"type": { "k": "fn", "params": [], "param_modes": [], "ret": { "k": "con", "name": type_con }, "ret_mode": "own", "effects": [] },
|
||||
"params": [],
|
||||
"body": { "t": "lit", "lit": { "kind": "unit" } }
|
||||
}],
|
||||
@@ -1829,7 +1829,7 @@ mod tests {
|
||||
"defs": [
|
||||
{ "kind": "type", "name": type_name, "ctors": [] },
|
||||
{ "kind": "fn", "name": "f",
|
||||
"type": { "k": "fn", "params": [], "ret": { "k": "con", "name": type_name }, "effects": [] },
|
||||
"type": { "k": "fn", "params": [], "param_modes": [], "ret": { "k": "con", "name": type_name }, "ret_mode": "own", "effects": [] },
|
||||
"params": [],
|
||||
"body": { "t": "lit", "lit": { "kind": "unit" } } }
|
||||
],
|
||||
@@ -1847,7 +1847,7 @@ mod tests {
|
||||
"defs": [{
|
||||
"kind": "fn",
|
||||
"name": "f",
|
||||
"type": { "k": "fn", "params": [], "ret": { "k": "con", "name": type_con }, "effects": [] },
|
||||
"type": { "k": "fn", "params": [], "param_modes": [], "ret": { "k": "con", "name": type_con }, "ret_mode": "own", "effects": [] },
|
||||
"params": [],
|
||||
"body": { "t": "lit", "lit": { "kind": "unit" } }
|
||||
}],
|
||||
@@ -1982,7 +1982,7 @@ mod tests {
|
||||
"defs": [{
|
||||
"kind": "fn",
|
||||
"name": "f",
|
||||
"type": { "k": "fn", "params": [], "ret": { "k": "con", "name": "Unit" }, "effects": [] },
|
||||
"type": { "k": "fn", "params": [], "param_modes": [], "ret": { "k": "con", "name": "Unit" }, "ret_mode": "own", "effects": [] },
|
||||
"params": [],
|
||||
"body": {
|
||||
"t": "lam",
|
||||
@@ -2026,7 +2026,7 @@ mod tests {
|
||||
"defs": [{
|
||||
"kind": "fn",
|
||||
"name": "f",
|
||||
"type": { "k": "fn", "params": [], "ret": { "k": "con", "name": "Unit" }, "effects": [] },
|
||||
"type": { "k": "fn", "params": [], "param_modes": [], "ret": { "k": "con", "name": "Unit" }, "ret_mode": "own", "effects": [] },
|
||||
"params": [],
|
||||
"body": {
|
||||
"t": "ctor",
|
||||
@@ -2173,8 +2173,8 @@ mod tests {
|
||||
{ "class": "other.MyEq", "type": { "k": "var", "name": "a" } }
|
||||
],
|
||||
"body": {
|
||||
"k": "fn", "params": [{ "k": "var", "name": "a" }],
|
||||
"ret": { "k": "con", "name": "Unit" }, "effects": []
|
||||
"k": "fn", "params": [{ "k": "var", "name": "a" }], "param_modes": ["own"],
|
||||
"ret": { "k": "con", "name": "Unit" }, "ret_mode": "own", "effects": []
|
||||
}
|
||||
},
|
||||
"params": ["x"],
|
||||
@@ -2226,8 +2226,8 @@ mod tests {
|
||||
"methods": [
|
||||
{ "name": "tshow",
|
||||
"type": { "k": "fn",
|
||||
"params": [{ "k": "var", "name": "a" }],
|
||||
"ret": { "k": "con", "name": "Str" },
|
||||
"params": [{ "k": "var", "name": "a" }], "param_modes": ["own"],
|
||||
"ret": { "k": "con", "name": "Str" }, "ret_mode": "own",
|
||||
"effects": [] } }
|
||||
]
|
||||
},
|
||||
@@ -2326,9 +2326,9 @@ mod tests {
|
||||
}],
|
||||
body: Box::new(Type::Fn {
|
||||
params: vec![Type::Var { name: "a".to_string() }],
|
||||
param_modes: vec![],
|
||||
param_modes: vec![crate::ast::ParamMode::Own],
|
||||
ret: Box::new(Type::Var { name: "a".to_string() }),
|
||||
ret_mode: Default::default(),
|
||||
ret_mode: crate::ast::ParamMode::Own,
|
||||
effects: vec![],
|
||||
}),
|
||||
};
|
||||
@@ -2392,8 +2392,8 @@ mod tests {
|
||||
],
|
||||
"body": {
|
||||
"k": "fn",
|
||||
"params": [{ "k": "var", "name": "b" }],
|
||||
"ret": { "k": "con", "name": "Unit" },
|
||||
"params": [{ "k": "var", "name": "b" }], "param_modes": ["own"],
|
||||
"ret": { "k": "con", "name": "Unit" }, "ret_mode": "own",
|
||||
"effects": []
|
||||
}
|
||||
}
|
||||
@@ -2597,8 +2597,8 @@ mod tests {
|
||||
],
|
||||
"body": {
|
||||
"k": "fn",
|
||||
"params": [{ "k": "var", "name": "a" }],
|
||||
"ret": { "k": "con", "name": "Unit" },
|
||||
"params": [{ "k": "var", "name": "a" }], "param_modes": ["own"],
|
||||
"ret": { "k": "con", "name": "Unit" }, "ret_mode": "own",
|
||||
"effects": []
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user