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();
|
||||
|
||||
Reference in New Issue
Block a user