Iter 18a: (borrow T) / (own T) mode annotations on fn signatures

Adds form-A surface (borrow T) / (own T) wrappers in fn-type
params and ret slots. Internally these are not new Type variants
but per-position metadata on Type::Fn:

  Type::Fn { params, param_modes, ret, ret_mode, effects }
  enum ParamMode { Implicit, Own, Borrow }

This keeps Type itself unchanged, so unification, occurs, apply,
and ~190 other Type match-arms in the typechecker need no new
branch. Fields use serde skip-if-default predicates so canonical
JSON hashes for every pre-18a fixture stay bit-identical (sum,
list, hof, closure, list_map, etc.: zero diff under git).

Iter 18a is purely additive: typechecker treats modes as
transparent (mode-compat check deferred to 18c), no codegen
change (--alloc=gc / Boehm path unchanged), no linearity
enforcement. The annotation info flows into the JSON side-table
that 18c will consume.

Equality of mode slices is length-tolerant: a vec![] (the form
written by mechanically-updated construction sites that elide
modes) compares equal to vec![Implicit; n]. This kept the patch
from being a 100-site mechanical migration through the
typechecker. 18c will choose: keep the slack, or normalise to
full-length on construction.

New fixture examples/borrow_own_demo.{ailx,ail.json} exercises
both modes. list_length declares (borrow (con List)); sum_list
declares (own (con List)). Stdout: 3 then 6.

Documentation: DESIGN.md schema-additions block clarified that
modes are Type::Fn metadata (not Type variants); migration plan
flag rename --memory=rc → --alloc=rc to match the existing CLI
flag.

Verification:
- cargo build --workspace green
- cargo test --workspace green (154 tests, +1 vs baseline 153)
- git diff examples/{sum,list,hof,closure,list_map,...}.ail.json: empty
- borrow_own_demo runtime stdout: "3\n6\n"
- canonical JSON contains "param_modes":["borrow"] and ["own"]
This commit is contained in:
2026-05-08 02:07:07 +02:00
parent 52b129f558
commit b9ca8894a5
15 changed files with 606 additions and 41 deletions
+26 -2
View File
@@ -2676,6 +2676,8 @@ impl<'a> Emitter<'a> {
params: param_tys.clone(),
ret: ret_ty.clone(),
effects: effects.clone(),
param_modes: vec![],
ret_mode: ParamMode::Implicit,
}),
Term::App { callee, args, .. } => {
let cty = self.synth_with_extras(callee, extras)?;
@@ -2859,11 +2861,15 @@ fn builtin_ail_type(name: &str) -> Option<Type> {
params: vec![Type::int(), Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
};
let int_int_bool = || Type::Fn {
params: vec![Type::int(), Type::int()],
ret: Box::new(Type::bool_()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
};
Some(match name {
"+" | "-" | "*" | "/" | "%" => int_int_int(),
@@ -2882,12 +2888,16 @@ fn builtin_ail_type(name: &str) -> Option<Type> {
],
ret: Box::new(Type::bool_()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
}),
},
"not" => Type::Fn {
params: vec![Type::bool_()],
ret: Box::new(Type::bool_()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
// Iter 16d: `__unreachable__` is the polymorphic bottom value
// (`forall a. a`). Mirrors the typechecker's `builtins::install`.
@@ -3073,13 +3083,15 @@ fn qualify_local_types_codegen(
.collect(),
}
}
Type::Fn { params, ret, effects } => Type::Fn {
Type::Fn { params, ret, effects, .. } => Type::Fn {
params: params
.iter()
.map(|p| qualify_local_types_codegen(p, owner_module, owner_local_types))
.collect(),
ret: Box::new(qualify_local_types_codegen(ret, owner_module, owner_local_types)),
effects: effects.clone(),
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
Type::Forall { vars, body } => Type::Forall {
vars: vars.clone(),
@@ -3099,10 +3111,12 @@ fn apply_subst_to_type(t: &Type, subst: &BTreeMap<String, Type>) -> Type {
name: name.clone(),
args: args.iter().map(|a| apply_subst_to_type(a, subst)).collect(),
},
Type::Fn { params, ret, effects } => Type::Fn {
Type::Fn { params, ret, effects, .. } => Type::Fn {
params: params.iter().map(|p| apply_subst_to_type(p, subst)).collect(),
ret: Box::new(apply_subst_to_type(ret, subst)),
effects: effects.clone(),
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
Type::Forall { vars, body } => {
// Inner forall shadows: don't substitute re-bound names.
@@ -3312,6 +3326,8 @@ mod tests {
params: vec![Type::int(), Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec!["a".into(), "b".into()],
body: Term::App {
@@ -3332,6 +3348,8 @@ mod tests {
params: vec![],
ret: Box::new(Type::unit()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec![],
body: Term::Lit { lit: Literal::Unit },
@@ -3384,6 +3402,8 @@ mod tests {
params: vec![],
ret: Box::new(Type::unit()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec![],
body: Term::Let {
@@ -3425,6 +3445,8 @@ mod tests {
params: vec![],
ret: Box::new(Type::unit()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec![],
body: Term::Let {
@@ -3468,6 +3490,8 @@ mod tests {
params: vec![],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec![],
body: Term::Lit {