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": []
|
||||
}
|
||||
},
|
||||
|
||||
@@ -119,14 +119,42 @@ fn every_contract_names_a_resolvable_ratifying_test() {
|
||||
for row in &contracts {
|
||||
// columns: id | consumer/lifetime | ratifying-test | link
|
||||
let rt = &row[2];
|
||||
// take the path token (before any " (" note)
|
||||
let path = rt.split(" (").next().unwrap_or(rt).trim();
|
||||
// strip a trailing " (...)" note, then resolve every
|
||||
// " + "-separated path segment. A dual ratifier such as
|
||||
// "uniqueness.rs + linearity.rs (in-source mod tests)" names
|
||||
// two real files — both must resolve, mirroring the
|
||||
// dual-link handling in `link_target_exists` (clause-1).
|
||||
// The second segment is a bare leafname relative to the
|
||||
// first segment's directory.
|
||||
let body = rt.split(" (").next().unwrap_or(rt).trim();
|
||||
let segments: Vec<&str> = body.split(" + ").map(str::trim).collect();
|
||||
let first = segments[0];
|
||||
assert!(
|
||||
root().join(path).exists(),
|
||||
root().join(first).exists(),
|
||||
"ratifying-test does not resolve to a real file: {:?} (contract {:?})",
|
||||
path,
|
||||
first,
|
||||
row[0]
|
||||
);
|
||||
let base_dir = std::path::Path::new(first)
|
||||
.parent()
|
||||
.map(|p| p.to_path_buf())
|
||||
.unwrap_or_default();
|
||||
for seg in &segments[1..] {
|
||||
// a later segment may be a full repo-relative path or a
|
||||
// bare leafname rooted at the first segment's directory.
|
||||
let resolved = if root().join(seg).exists() {
|
||||
root().join(seg)
|
||||
} else {
|
||||
root().join(base_dir.join(seg))
|
||||
};
|
||||
assert!(
|
||||
resolved.exists(),
|
||||
"ratifying-test dual segment does not resolve to a real \
|
||||
file: {:?} (contract {:?})",
|
||||
seg,
|
||||
row[0]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ fn design_md_anchors_every_term_variant() {
|
||||
r#""t": "letrec""#,
|
||||
Term::LetRec {
|
||||
name: "f".into(),
|
||||
ty: Type::fn_implicit(vec![], Type::int(), vec![]),
|
||||
ty: Type::fn_owned(vec![], Type::int(), vec![]),
|
||||
params: vec![],
|
||||
body: Box::new(Term::Lit { lit: Literal::Int { value: 0 } }),
|
||||
in_term: Box::new(Term::Var { name: "f".into() }),
|
||||
@@ -248,7 +248,7 @@ fn design_md_anchors_every_pattern_variant() {
|
||||
fn design_md_anchors_every_type_variant() {
|
||||
let exemplars: Vec<(&str, Type)> = vec![
|
||||
(r#""k": "con""#, Type::int()),
|
||||
(r#""k": "fn""#, Type::fn_implicit(vec![], Type::unit(), vec![])),
|
||||
(r#""k": "fn""#, Type::fn_owned(vec![], Type::unit(), vec![])),
|
||||
(r#""k": "var""#, Type::Var { name: "a".into() }),
|
||||
(
|
||||
r#""k": "forall""#,
|
||||
@@ -313,7 +313,7 @@ fn design_md_anchors_every_def_kind() {
|
||||
name: "f".into(),
|
||||
doc: None,
|
||||
suppress: vec![],
|
||||
ty: Type::fn_implicit(vec![], Type::int(), vec![]),
|
||||
ty: Type::fn_owned(vec![], Type::int(), vec![]),
|
||||
params: vec![],
|
||||
body: Term::Lit { lit: Literal::Int { value: 0 } },
|
||||
export: None,
|
||||
@@ -338,7 +338,7 @@ fn design_md_anchors_every_def_kind() {
|
||||
superclass: None,
|
||||
methods: vec![ClassMethod {
|
||||
name: "show".into(),
|
||||
ty: Type::fn_implicit(vec![Type::Var { name: "a".into() }], Type::str_(), vec![]),
|
||||
ty: Type::fn_owned(vec![Type::Var { name: "a".into() }], Type::str_(), vec![]),
|
||||
default: None,
|
||||
}],
|
||||
doc: None,
|
||||
@@ -378,18 +378,16 @@ fn design_md_anchors_every_def_kind() {
|
||||
|
||||
/// Every `ParamMode` variant must have its serialized string form present
|
||||
/// in design/contracts/0002-data-model.md. The mode annotations are load-bearing for
|
||||
/// ownership checking; an LLM author must know all three forms.
|
||||
/// ownership checking; an LLM author must know both forms.
|
||||
#[test]
|
||||
fn design_md_anchors_every_parammode_variant() {
|
||||
let exemplars: Vec<(&str, ParamMode)> = vec![
|
||||
(r#""implicit""#, ParamMode::Implicit),
|
||||
(r#""own""#, ParamMode::Own),
|
||||
(r#""borrow""#, ParamMode::Borrow),
|
||||
];
|
||||
|
||||
for (anchor, mode) in exemplars {
|
||||
let _: &'static str = match mode {
|
||||
ParamMode::Implicit => "implicit",
|
||||
ParamMode::Own => "own",
|
||||
ParamMode::Borrow => "borrow",
|
||||
};
|
||||
@@ -525,7 +523,7 @@ fn design_md_anchors_nested_struct_keys() {
|
||||
let _ = Suppress { code: "x".into(), because: "y".into() };
|
||||
let _ = ClassMethod {
|
||||
name: "m".into(),
|
||||
ty: Type::fn_implicit(vec![], Type::int(), vec![]),
|
||||
ty: Type::fn_owned(vec![], Type::int(), vec![]),
|
||||
default: None,
|
||||
};
|
||||
let _ = InstanceMethod {
|
||||
|
||||
@@ -21,7 +21,7 @@ fn fn_without_export_hash_is_unchanged() {
|
||||
// equal the value captured before the field existed.
|
||||
let def = Def::Fn(FnDef {
|
||||
name: "f".into(),
|
||||
ty: Type::fn_implicit(vec![Type::int()], Type::int(), vec![]),
|
||||
ty: Type::fn_owned(vec![Type::int()], Type::int(), vec![]),
|
||||
params: vec!["x".into()],
|
||||
body: Term::Var { name: "x".into() },
|
||||
doc: None,
|
||||
@@ -29,7 +29,7 @@ fn fn_without_export_hash_is_unchanged() {
|
||||
export: None,
|
||||
});
|
||||
// GOLDEN: captured pre-field (Step 2) from `def_hash` on this shape.
|
||||
let golden = "b4662aa70839f60b";
|
||||
let golden = "8ce080ee897b3f80";
|
||||
assert_eq!(def_hash(&def), golden,
|
||||
"pre-M1 fn hash drifted — additive-field invariant violated");
|
||||
}
|
||||
|
||||
@@ -31,8 +31,8 @@ fn sample_fn() -> Def {
|
||||
params: vec![Type::int(), Type::int()],
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
param_modes: vec![ParamMode::Own, ParamMode::Own],
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
params: vec!["a".into(), "b".into()],
|
||||
body: Term::App {
|
||||
@@ -86,7 +86,7 @@ fn iter13a_schema_extension_preserves_pre_13a_hashes() {
|
||||
let sum_mod = ailang_surface::load_module(&examples.join("sum.ail"))
|
||||
.expect("examples/sum.ail loads");
|
||||
let sum_def = sum_mod.defs.iter().find(|d| d.name() == "sum").unwrap();
|
||||
assert_eq!(def_hash(sum_def), "25343a2e5927a257");
|
||||
assert_eq!(def_hash(sum_def), "19920ec4123d35d6");
|
||||
|
||||
let list_mod = ailang_surface::load_module(&examples.join("list.ail"))
|
||||
.expect("examples/list.ail loads");
|
||||
@@ -108,7 +108,7 @@ fn loop_recur_schema_extension_preserves_pre_loop_recur_hashes() {
|
||||
let sum_mod = ailang_surface::load_module(&examples.join("sum.ail"))
|
||||
.expect("examples/sum.ail loads");
|
||||
let sum_def = sum_mod.defs.iter().find(|d| d.name() == "sum").unwrap();
|
||||
assert_eq!(def_hash(sum_def), "25343a2e5927a257");
|
||||
assert_eq!(def_hash(sum_def), "19920ec4123d35d6");
|
||||
|
||||
let list_mod = ailang_surface::load_module(&examples.join("list.ail"))
|
||||
.expect("examples/list.ail loads");
|
||||
@@ -162,7 +162,7 @@ fn iter19b_schema_extension_preserves_pre_19b_hashes() {
|
||||
let sum_mod = ailang_surface::load_module(&examples.join("sum.ail"))
|
||||
.expect("examples/sum.ail loads");
|
||||
let sum_def = sum_mod.defs.iter().find(|d| d.name() == "sum").unwrap();
|
||||
assert_eq!(def_hash(sum_def), "25343a2e5927a257");
|
||||
assert_eq!(def_hash(sum_def), "19920ec4123d35d6");
|
||||
}
|
||||
|
||||
/// adding `Def::Class` and `Def::Instance` must
|
||||
@@ -177,7 +177,7 @@ fn iter22b1_schema_extension_preserves_pre_22b_hashes() {
|
||||
let sum_mod = ailang_surface::load_module(&examples.join("sum.ail"))
|
||||
.expect("examples/sum.ail loads");
|
||||
let sum_def = sum_mod.defs.iter().find(|d| d.name() == "sum").unwrap();
|
||||
assert_eq!(def_hash(sum_def), "25343a2e5927a257");
|
||||
assert_eq!(def_hash(sum_def), "19920ec4123d35d6");
|
||||
|
||||
let list_mod = ailang_surface::load_module(&examples.join("list.ail"))
|
||||
.expect("examples/list.ail loads");
|
||||
@@ -220,7 +220,7 @@ fn forall_without_constraints_hashes_bit_identical_to_pre_22b2() {
|
||||
let t = Type::Forall {
|
||||
vars: vec!["a".into()],
|
||||
constraints: vec![],
|
||||
body: Box::new(Type::fn_implicit(
|
||||
body: Box::new(Type::fn_owned(
|
||||
vec![Type::Var { name: "a".into() }],
|
||||
Type::Var { name: "a".into() },
|
||||
vec![],
|
||||
@@ -254,7 +254,7 @@ fn ct4_migrated_fixtures_have_canonical_form_hashes() {
|
||||
// io/print_str no longer adds a trailing newline.
|
||||
assert_eq!(
|
||||
def_hash(main_def),
|
||||
"8ed47b4062ce00f5",
|
||||
"602d7a6d6ba72bc4",
|
||||
"ordering_match::main canonical hash must match captured post-fputs-swap value"
|
||||
);
|
||||
|
||||
@@ -272,7 +272,7 @@ fn ct4_migrated_fixtures_have_canonical_form_hashes() {
|
||||
assert_eq!(dup_classmod_mod.defs.len(), 1, "test_22b1_dup_classmod expected to have exactly 1 def (class TShow)");
|
||||
assert_eq!(
|
||||
def_hash(&dup_classmod_mod.defs[0]),
|
||||
"b8bca96c2d09ed93",
|
||||
"4ca71c7d8212c96a",
|
||||
"test_22b1_dup_classmod class TShow canonical hash; post-24.2 captured value"
|
||||
);
|
||||
}
|
||||
@@ -286,7 +286,7 @@ fn ct4_unmigrated_fixtures_remain_bit_identical() {
|
||||
let sum_mod = ailang_surface::load_module(&examples.join("sum.ail"))
|
||||
.expect("examples/sum.ail loads");
|
||||
let sum_def = sum_mod.defs.iter().find(|d| d.name() == "sum").unwrap();
|
||||
assert_eq!(def_hash(sum_def), "25343a2e5927a257",
|
||||
assert_eq!(def_hash(sum_def), "19920ec4123d35d6",
|
||||
"sum.sum hash drifted across canonical-form tightening — unexpected");
|
||||
|
||||
let list_mod = ailang_surface::load_module(&examples.join("list.ail"))
|
||||
|
||||
@@ -67,7 +67,6 @@ enum VariantTag {
|
||||
TypeVar,
|
||||
TypeForall,
|
||||
// ParamMode
|
||||
ParamModeImplicit,
|
||||
ParamModeOwn,
|
||||
ParamModeBorrow,
|
||||
}
|
||||
@@ -111,7 +110,6 @@ const EXPECTED_VARIANTS: &[VariantTag] = &[
|
||||
VariantTag::TypeFn,
|
||||
VariantTag::TypeVar,
|
||||
VariantTag::TypeForall,
|
||||
VariantTag::ParamModeImplicit,
|
||||
VariantTag::ParamModeOwn,
|
||||
VariantTag::ParamModeBorrow,
|
||||
];
|
||||
@@ -341,9 +339,6 @@ fn visit_type(t: &Type, observed: &mut HashSet<VariantTag>) {
|
||||
|
||||
fn visit_param_mode(m: &ParamMode, observed: &mut HashSet<VariantTag>) {
|
||||
match m {
|
||||
ParamMode::Implicit => {
|
||||
observed.insert(VariantTag::ParamModeImplicit);
|
||||
}
|
||||
ParamMode::Own => {
|
||||
observed.insert(VariantTag::ParamModeOwn);
|
||||
}
|
||||
@@ -364,6 +359,17 @@ fn examples_dir() -> PathBuf {
|
||||
crate_dir.parent().unwrap().parent().unwrap().join("examples")
|
||||
}
|
||||
|
||||
/// Fixtures that intentionally do NOT parse — the `#55` cutover
|
||||
/// reject corpus. Negative fixtures whose whole point is that the
|
||||
/// parser rejects them, so they have no AST to scan for variant
|
||||
/// coverage and the scan must skip them. Mirrors the same list in
|
||||
/// `crates/ail/tests/roundtrip_cli.rs`.
|
||||
///
|
||||
/// - `bare_slot_reject.ail`: a bare fn-type slot with no mode. The
|
||||
/// binary-ParamMode parser rejects it ("fn-type slot requires a
|
||||
/// mode: write (own T) or (borrow T)").
|
||||
const NON_PARSEABLE_FIXTURES: &[&str] = &["bare_slot_reject.ail"];
|
||||
|
||||
fn list_ail_fixtures() -> Vec<PathBuf> {
|
||||
let dir = examples_dir();
|
||||
let mut paths: Vec<PathBuf> = std::fs::read_dir(&dir)
|
||||
@@ -373,7 +379,7 @@ fn list_ail_fixtures() -> Vec<PathBuf> {
|
||||
.filter(|p| {
|
||||
p.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.map(|n| n.ends_with(".ail"))
|
||||
.map(|n| n.ends_with(".ail") && !NON_PARSEABLE_FIXTURES.contains(&n))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -49,7 +49,7 @@ fn spec_mentions_every_term_variant() {
|
||||
"(let-rec",
|
||||
Term::LetRec {
|
||||
name: "f".into(),
|
||||
ty: Type::fn_implicit(vec![], Type::int(), vec![]),
|
||||
ty: Type::fn_owned(vec![], Type::int(), vec![]),
|
||||
params: vec![],
|
||||
body: Box::new(Term::Lit { lit: Literal::Int { value: 0 } }),
|
||||
in_term: Box::new(Term::Var { name: "f".into() }),
|
||||
@@ -207,7 +207,7 @@ fn spec_mentions_every_type_variant() {
|
||||
("(con ", Type::int()),
|
||||
(
|
||||
"(fn-type",
|
||||
Type::fn_implicit(vec![], Type::unit(), vec![]),
|
||||
Type::fn_owned(vec![], Type::unit(), vec![]),
|
||||
),
|
||||
(
|
||||
"TYVAR-NAME",
|
||||
@@ -271,7 +271,7 @@ fn spec_mentions_every_def_kind() {
|
||||
name: "f".into(),
|
||||
doc: None,
|
||||
suppress: vec![],
|
||||
ty: Type::fn_implicit(vec![], Type::int(), vec![]),
|
||||
ty: Type::fn_owned(vec![], Type::int(), vec![]),
|
||||
params: vec![],
|
||||
body: Term::Lit { lit: Literal::Int { value: 0 } },
|
||||
export: None,
|
||||
|
||||
@@ -48,7 +48,7 @@ fn kernel_tier_module_auto_imports_without_explicit_import() {
|
||||
"(module bridge\n",
|
||||
" (import k_mod)\n",
|
||||
" (fn anchor\n",
|
||||
" (type (forall (vars a) (fn-type (params (con KT a)) (ret (con Unit)))))\n",
|
||||
" (type (forall (vars a) (fn-type (params (own (con KT a))) (ret (own (con Unit))))))\n",
|
||||
" (params k) (body unit)))\n",
|
||||
),
|
||||
);
|
||||
@@ -61,7 +61,7 @@ fn kernel_tier_module_auto_imports_without_explicit_import() {
|
||||
"(module consumer\n",
|
||||
" (import bridge)\n",
|
||||
" (fn use_kt\n",
|
||||
" (type (forall (vars a) (fn-type (params (con KT a)) (ret (con Unit)))))\n",
|
||||
" (type (forall (vars a) (fn-type (params (own (con KT a))) (ret (own (con Unit))))))\n",
|
||||
" (params k) (body unit)))\n",
|
||||
),
|
||||
);
|
||||
@@ -106,7 +106,7 @@ fn two_kernel_tier_modules_coload() {
|
||||
" (import k_a)\n",
|
||||
" (import k_b)\n",
|
||||
" (fn anchor\n",
|
||||
" (type (fn-type (params (con A) (con B)) (ret (con Unit))))\n",
|
||||
" (type (fn-type (params (own (con A)) (own (con B))) (ret (own (con Unit)))))\n",
|
||||
" (params a b) (body unit)))\n",
|
||||
),
|
||||
);
|
||||
@@ -117,7 +117,7 @@ fn two_kernel_tier_modules_coload() {
|
||||
"(module consumer\n",
|
||||
" (import bridge)\n",
|
||||
" (fn use_ab\n",
|
||||
" (type (fn-type (params (con A) (con B)) (ret (con Unit))))\n",
|
||||
" (type (fn-type (params (own (con A)) (own (con B))) (ret (own (con Unit)))))\n",
|
||||
" (params a b) (body unit)))\n",
|
||||
),
|
||||
);
|
||||
@@ -151,7 +151,7 @@ fn explicit_import_takes_precedence_over_auto_import() {
|
||||
"(module consumer\n",
|
||||
" (import k_mod)\n",
|
||||
" (fn use_kt\n",
|
||||
" (type (fn-type (params (con KT)) (ret (con Unit))))\n",
|
||||
" (type (fn-type (params (own (con KT))) (ret (own (con Unit)))))\n",
|
||||
" (params k) (body unit)))\n",
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user