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
+40
View File
@@ -1200,6 +1200,46 @@ fn unreachable_demo() {
assert_eq!(lines, vec!["4", "5"]);
}
/// Iter 18a: `(borrow T)` and `(own T)` mode annotations on
/// fn-type parameters. Properties protected:
/// (1) the parser accepts `(borrow ...)` and `(own ...)` only as
/// wrappers in fn-type param/ret slots and round-trips them
/// into [`ailang_core::ParamMode`] on `Type::Fn`;
/// (2) the canonical-JSON serialisation emits `param_modes` only
/// when at least one mode is non-Implicit (verified by reading
/// the file and confirming the substring is present), so
/// pre-18a fixtures continue to hash bit-identically;
/// (3) the typechecker treats modes as transparent — it accepts a
/// fn whose parameter is `(borrow (con List))` even when the
/// same value is later passed to a fn whose parameter is
/// `(own (con List))`, because Iter 18a does not enforce
/// mode compatibility (deferred to 18c);
/// (4) codegen ignores modes entirely: list_length and sum_list
/// compile to the same LLVM IR they would have without the
/// wrappers, and the binary prints `3` then `6`.
#[test]
fn borrow_own_demo_modes_are_metadata_only() {
// Sanity-check (2): the on-disk JSON contains `param_modes`
// tokens for both fns. If the schema regressed and the field
// were dropped, this would fail before the binary even runs.
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap();
let json_path = workspace.join("examples").join("borrow_own_demo.ail.json");
let json = std::fs::read_to_string(&json_path).expect("read borrow_own_demo.ail.json");
assert!(
json.contains("\"param_modes\":[\"borrow\"]"),
"expected `param_modes:[\"borrow\"]` in canonical JSON; the schema for `(borrow T)` regressed"
);
assert!(
json.contains("\"param_modes\":[\"own\"]"),
"expected `param_modes:[\"own\"]` in canonical JSON; the schema for `(own T)` regressed"
);
let stdout = build_and_run("borrow_own_demo.ail.json");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["3", "6"]);
}
/// Iter 16e: polymorphic `==`. Properties protected:
/// (1) `==` typechecks at Int / Bool / Str / Unit (the fixture
/// uses every supported case directly via `(app == ...)`);
+8
View File
@@ -52,11 +52,15 @@ pub fn install(env: &mut crate::Env) {
params: vec![Type::int(), Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ailang_core::ast::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: ailang_core::ast::ParamMode::Implicit,
};
for op in ["+", "-", "*", "/", "%"] {
env.globals.insert(op.into(), int_int_int.clone());
@@ -82,6 +86,8 @@ pub fn install(env: &mut crate::Env) {
],
ret: Box::new(Type::bool_()),
effects: vec![],
param_modes: vec![],
ret_mode: ailang_core::ast::ParamMode::Implicit,
}),
},
);
@@ -91,6 +97,8 @@ pub fn install(env: &mut crate::Env) {
params: vec![Type::bool_()],
ret: Box::new(Type::bool_()),
effects: vec![],
param_modes: vec![],
ret_mode: ailang_core::ast::ParamMode::Implicit,
},
);
+106 -10
View File
@@ -84,10 +84,12 @@ impl Subst {
name: name.clone(),
args: args.iter().map(|a| self.apply(a)).collect(),
},
Type::Fn { params, ret, effects } => Type::Fn {
Type::Fn { params, ret, effects, .. } => Type::Fn {
params: params.iter().map(|p| self.apply(p)).collect(),
ret: Box::new(self.apply(ret)),
effects: effects.clone(),
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
Type::Forall { vars, body } => Type::Forall {
vars: vars.clone(),
@@ -122,10 +124,12 @@ fn substitute_rigids(t: &Type, mapping: &BTreeMap<String, Type>) -> Type {
name: name.clone(),
args: args.iter().map(|a| substitute_rigids(a, mapping)).collect(),
},
Type::Fn { params, ret, effects } => Type::Fn {
Type::Fn { params, ret, effects, .. } => Type::Fn {
params: params.iter().map(|p| substitute_rigids(p, mapping)).collect(),
ret: Box::new(substitute_rigids(ret, mapping)),
effects: effects.clone(),
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
Type::Forall { vars, body } => {
// Inner forall shadows: only substitute vars not re-bound here.
@@ -205,8 +209,8 @@ fn unify(a: &Type, b: &Type, subst: &mut Subst) -> Result<()> {
}
// Function types: zip params, unify ret, effects must match as a set.
(
Type::Fn { params: ap, ret: ar, effects: ae },
Type::Fn { params: bp, ret: br, effects: be },
Type::Fn { params: ap, ret: ar, effects: ae, .. },
Type::Fn { params: bp, ret: br, effects: be, .. },
) => {
if ap.len() != bp.len() {
return Err(CheckError::TypeMismatch {
@@ -978,7 +982,7 @@ fn check_fn(f: &FnDef, env: &Env) -> Result<()> {
};
let (param_tys, ret_ty, declared_effs) = match &inner_ty {
Type::Fn { params, ret, effects } => {
Type::Fn { params, ret, effects, .. } => {
(params.clone(), (**ret).clone(), effects.clone())
}
other => {
@@ -1212,14 +1216,14 @@ pub(crate) fn synth(
let cty = synth(callee, env, locals, effects, in_def, subst, counter)?;
let cty = subst.apply(&cty);
let (params, ret, fx) = match &cty {
Type::Fn { params, ret, effects: fx } => {
Type::Fn { params, ret, effects: fx, .. } => {
(params.clone(), (**ret).clone(), fx.clone())
}
Type::Forall { vars, body } => {
// Defensive — Var should already have instantiated.
let (_, body) = instantiate(vars, body, counter);
match &body {
Type::Fn { params, ret, effects: fx } => {
Type::Fn { params, ret, effects: fx, .. } => {
(params.clone(), (**ret).clone(), fx.clone())
}
other => {
@@ -1519,6 +1523,8 @@ pub(crate) fn synth(
params: param_tys.clone(),
ret: Box::new((**ret_ty).clone()),
effects: lam_effects.clone(),
param_modes: vec![],
ret_mode: ParamMode::Implicit,
})
}
Term::LetRec { name, ty, params, body, in_term } => {
@@ -1543,7 +1549,7 @@ pub(crate) fn synth(
other => other.clone(),
};
let (param_tys, ret_ty, declared_effs) = match &inner_ty {
Type::Fn { params: ps, ret, effects } => {
Type::Fn { params: ps, ret, effects, .. } => {
(ps.clone(), (**ret).clone(), effects.clone())
}
other => {
@@ -1664,13 +1670,15 @@ fn qualify_local_types(t: &Type, owner_module: &str, local_types: &IndexMap<Stri
.collect(),
}
}
Type::Fn { params, ret, effects } => Type::Fn {
Type::Fn { params, ret, effects, .. } => Type::Fn {
params: params
.iter()
.map(|p| qualify_local_types(p, owner_module, local_types))
.collect(),
ret: Box::new(qualify_local_types(ret, owner_module, local_types)),
effects: effects.clone(),
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
Type::Forall { vars, body } => Type::Forall {
vars: vars.clone(),
@@ -1953,6 +1961,8 @@ mod tests {
params: vec![Type::int(), Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
vec!["a", "b"],
Term::App {
@@ -1980,6 +1990,8 @@ mod tests {
params: vec![],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
vec![],
Term::Lit {
@@ -2003,7 +2015,9 @@ mod tests {
Type::Fn {
params: vec![],
ret: Box::new(Type::unit()),
effects: vec![], // !IO missing
effects: vec![], // !IO missing,
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
vec![],
Term::Do {
@@ -2032,6 +2046,8 @@ mod tests {
params: vec![],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
vec![],
Term::Let {
@@ -2072,6 +2088,8 @@ mod tests {
params: vec![Type::Con { name: "Maybe".into(), args: vec![] }],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
vec!["m"],
Term::Match {
@@ -2120,6 +2138,8 @@ mod tests {
params: vec![Type::Con { name: "Maybe".into(), args: vec![] }],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
vec!["m"],
Term::Match {
@@ -2160,6 +2180,8 @@ mod tests {
params: vec![],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
vec![],
Term::If {
@@ -2194,6 +2216,8 @@ mod tests {
params: vec![Type::Var { name: "a".into() }],
ret: Box::new(Type::Var { name: "a".into() }),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
}),
},
vec!["x"],
@@ -2217,6 +2241,8 @@ mod tests {
params: vec![Type::Var { name: "a".into() }],
ret: Box::new(Type::Var { name: "a".into() }),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
}),
},
vec!["x"],
@@ -2229,6 +2255,8 @@ mod tests {
params: vec![],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
vec![],
Term::App {
@@ -2244,6 +2272,8 @@ mod tests {
params: vec![],
ret: Box::new(Type::bool_()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
vec![],
Term::App {
@@ -2274,6 +2304,8 @@ mod tests {
params: vec![Type::Var { name: "a".into() }],
ret: Box::new(Type::Var { name: "a".into() }),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
}),
},
vec!["x"],
@@ -2286,6 +2318,8 @@ mod tests {
params: vec![],
ret: Box::new(Type::bool_()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
vec![],
Term::App {
@@ -2320,11 +2354,15 @@ mod tests {
params: vec![Type::Var { name: "a".into() }],
ret: Box::new(Type::Var { name: "b".into() }),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
Type::Var { name: "a".into() },
],
ret: Box::new(Type::Var { name: "b".into() }),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
}),
},
vec!["f", "x"],
@@ -2341,6 +2379,8 @@ mod tests {
params: vec![Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
vec!["n"],
Term::App {
@@ -2358,6 +2398,8 @@ mod tests {
params: vec![],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
vec![],
Term::App {
@@ -2401,6 +2443,8 @@ mod tests {
args: vec![Type::int()],
}),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
vec![],
Term::Ctor {
@@ -2442,6 +2486,8 @@ mod tests {
}],
ret: Box::new(Type::Var { name: "a".into() }),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
}),
},
params: vec!["b".into()],
@@ -2465,6 +2511,8 @@ mod tests {
params: vec![],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
vec![],
Term::App {
@@ -2483,6 +2531,8 @@ mod tests {
params: vec![],
ret: Box::new(Type::bool_()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
vec![],
Term::App {
@@ -2526,6 +2576,8 @@ mod tests {
}],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
vec!["b"],
Term::Lit { lit: Literal::Int { value: 0 } },
@@ -2556,6 +2608,8 @@ mod tests {
params: vec![],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
vec![],
Term::Seq {
@@ -2605,6 +2659,8 @@ mod tests {
params: vec![Type::Con { name: "L".into(), args: vec![] }],
ret: Box::new(Type::Con { name: "L".into(), args: vec![] }),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
vec!["xs"],
// Body: C(0, tail-app loop xs) — the recursion is
@@ -2687,6 +2743,8 @@ mod tests {
}],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
vec!["b"],
Term::Lit { lit: Literal::Int { value: 0 } },
@@ -2714,6 +2772,8 @@ mod tests {
args: vec![Type::int()],
}),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
vec![],
Term::Ctor {
@@ -2746,6 +2806,8 @@ mod tests {
}],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
vec!["b"],
Term::Match {
@@ -2809,6 +2871,8 @@ mod tests {
}],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
vec!["t"],
Term::Match {
@@ -2902,6 +2966,8 @@ mod tests {
params: vec![],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
vec![],
Term::Match {
@@ -2971,6 +3037,8 @@ mod tests {
params: vec![Type::Con { name: "L".into(), args: vec![] }],
ret: Box::new(Type::unit()),
effects: vec!["IO".into()],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
vec!["xs"],
Term::Match {
@@ -3041,6 +3109,8 @@ mod tests {
params: vec![Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec!["i".into()],
body: Box::new(Term::If {
@@ -3083,6 +3153,8 @@ mod tests {
params: vec![Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
vec!["n"],
body,
@@ -3106,6 +3178,8 @@ mod tests {
params: vec![Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec!["x".into()],
body: Box::new(Term::Lit { lit: Literal::Bool { value: true } }),
@@ -3128,6 +3202,8 @@ mod tests {
params: vec![Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec!["x".into()],
body: Box::new(Term::Seq {
@@ -3161,6 +3237,8 @@ mod tests {
params: vec![],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
vec![],
body,
@@ -3194,6 +3272,8 @@ mod tests {
params: vec![],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
vec![],
Term::Let {
@@ -3205,6 +3285,8 @@ mod tests {
params: vec![Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec!["x".into()],
body: Box::new(Term::App {
@@ -3247,6 +3329,8 @@ mod tests {
params: vec![Type::int(), Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
"lifted ty should have capture appended; got {:?}",
synth.ty
@@ -3314,6 +3398,8 @@ mod tests {
params: vec![Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec!["z".into()],
body: Box::new(Term::App {
@@ -3358,6 +3444,8 @@ mod tests {
}],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
vec!["p"],
outer_body,
@@ -3388,6 +3476,8 @@ mod tests {
params: vec![Type::int(), Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
"lifted ty should have the match-arm-capture type Int appended; got {:?}",
synth.ty
@@ -3439,6 +3529,8 @@ mod tests {
params: vec![Type::Var { name: "a".into() }],
ret: Box::new(Type::Var { name: "a".into() }),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
}),
},
vec!["x".into()],
@@ -3451,6 +3543,8 @@ mod tests {
params: vec![Type::int()],
ret: Box::new(Type::Var { name: "a".into() }),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec!["k".into()],
body: Box::new(Term::If {
@@ -3571,6 +3665,8 @@ mod tests {
params: vec![],
ret: Box::new(Type::bool_()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
vec![],
body,
+7 -3
View File
@@ -513,7 +513,7 @@ impl<'a> Lifter<'a> {
// capture types uniformly because they're all `Type::Fn`
// params at the AST level.
let inner_augmented_ty = match ty {
Type::Fn { params: ps, ret, effects } => {
Type::Fn { params: ps, ret, effects, .. } => {
let mut new_ps = ps.clone();
for (_, t) in &capture_types {
new_ps.push(t.clone());
@@ -522,6 +522,8 @@ impl<'a> Lifter<'a> {
params: new_ps,
ret: ret.clone(),
effects: effects.clone(),
param_modes: vec![],
ret_mode: ParamMode::Implicit,
}
}
Type::Forall { .. } => panic!(
@@ -569,7 +571,7 @@ impl<'a> Lifter<'a> {
// LetRec's declared type to mirror them on the eta-Lam.
let (orig_param_tys, orig_ret_ty, lr_effects): (Vec<Type>, Type, Vec<String>) =
match ty {
Type::Fn { params: ps, ret, effects } => {
Type::Fn { params: ps, ret, effects, .. } => {
(ps.clone(), (**ret).clone(), effects.clone())
}
Type::Forall { .. } => unreachable!("rejected above"),
@@ -832,13 +834,15 @@ fn substitute_rigids_local(t: &Type, mapping: &BTreeMap<String, Type>) -> Type {
name: name.clone(),
args: args.iter().map(|a| substitute_rigids_local(a, mapping)).collect(),
},
Type::Fn { params, ret, effects } => Type::Fn {
Type::Fn { params, ret, effects, .. } => Type::Fn {
params: params
.iter()
.map(|p| substitute_rigids_local(p, mapping))
.collect(),
ret: Box::new(substitute_rigids_local(ret, mapping)),
effects: effects.clone(),
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
Type::Forall { vars, body } => {
let inner: BTreeMap<String, Type> = mapping
+4
View File
@@ -117,6 +117,8 @@ fn body_errors_accumulate_across_defs() {
params: vec![],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec![],
body: Term::App {
@@ -136,6 +138,8 @@ fn body_errors_accumulate_across_defs() {
params: vec![],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec![],
body: Term::Var {
+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 {
+132 -9
View File
@@ -368,9 +368,22 @@ pub enum Type {
/// Function type with optional effect annotation. `effects` is a
/// set; equality compares it modulo order (see the [`PartialEq`]
/// impl below).
///
/// Iter 18a (Decision 10): `param_modes` and `ret_mode` carry the
/// `(borrow T)` / `(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`. Pre-18a fixtures therefore hash
/// bit-identically.
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>,
},
@@ -406,6 +419,100 @@ impl Type {
pub fn str_() -> Type {
Type::Con { name: "Str".into(), args: vec![] }
}
/// Iter 18a: 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-18a canonical-JSON bytes.
pub fn fn_implicit(params: Vec<Type>, ret: Type, effects: Vec<String>) -> Type {
let n = params.len();
Type::Fn {
params,
param_modes: vec![ParamMode::Implicit; n],
ret: Box::new(ret),
ret_mode: ParamMode::Implicit,
effects,
}
}
}
/// Iter 18a (Decision 10): per-parameter / return mode marker on a
/// [`Type::Fn`].
///
/// `Implicit` is the legacy state for fn-types that were constructed
/// before the borrow/own surface annotations existed. Semantically,
/// `Implicit ≡ Own` throughout the 18-series; the distinction exists
/// only so pre-18a 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, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ParamMode {
/// Pre-18a / unannotated. Treated as `Own` by the typechecker.
Implicit,
/// `(own T)` — caller transfers ownership; callee consumes.
Own,
/// `(borrow T)` — caller retains ownership; callee may not consume.
Borrow,
}
impl Default for ParamMode {
fn default() -> Self {
ParamMode::Implicit
}
}
impl ParamMode {
/// Used by the `skip_serializing_if` predicate on
/// [`Type::Fn::ret_mode`].
pub fn is_implicit(&self) -> bool {
matches!(self, ParamMode::Implicit)
}
}
/// Iter 18a: 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-18a
/// fixtures keep bit-identical hashes.
fn all_implicit(modes: &[ParamMode]) -> bool {
modes.iter().all(|m| m.is_implicit())
}
/// Iter 18a: equality of [`ParamMode`] for the purposes of `Type`
/// equality. `Implicit` and `Own` are treated as the same mode
/// throughout the 18-series; `Borrow` is distinct. This keeps
/// pre-18a fixtures (whose fn-types serialize `Implicit`) compatible
/// with newly-written 18a 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,
}
}
/// Iter 18a: 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 {
@@ -416,16 +523,32 @@ impl PartialEq for Type {
Type::Con { name: b, args: ba },
) => a == b && aa == ba,
(
Type::Fn { params: ap, ret: ar, effects: ae },
Type::Fn { params: bp, ret: br, effects: be },
Type::Fn {
params: ap,
param_modes: apm,
ret: ar,
ret_mode: arm,
effects: ae,
},
Type::Fn {
params: bp,
param_modes: bpm,
ret: br,
ret_mode: brm,
effects: be,
},
) => {
ap == bp && ar == br && {
let mut a = ae.clone();
let mut b = be.clone();
a.sort();
b.sort();
a == b
}
ap == bp
&& ar == br
&& mode_slices_eq(apm, bpm)
&& mode_eq(arm, brm)
&& {
let mut a = ae.clone();
let mut b = be.clone();
a.sort();
b.sort();
a == b
}
}
(Type::Var { name: a }, Type::Var { name: b }) => a == b,
(
+59 -1
View File
@@ -708,7 +708,7 @@ impl Desugarer {
// since LetRec itself doesn't quantify) is the inner
// type.
let inner_augmented_ty = match ty {
Type::Fn { params: ps, ret, effects } => {
Type::Fn { params: ps, ret, effects, .. } => {
let mut new_ps = ps.clone();
for (_, t) in &capture_types {
new_ps.push(t.clone());
@@ -717,6 +717,8 @@ impl Desugarer {
params: new_ps,
ret: ret.clone(),
effects: effects.clone(),
param_modes: vec![],
ret_mode: ParamMode::Implicit,
}
}
Type::Forall { .. } => panic!(
@@ -1519,6 +1521,8 @@ mod tests {
params: vec![Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec!["xs".into()],
body: body_match,
@@ -1572,6 +1576,8 @@ mod tests {
params: vec![Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec!["xs".into()],
body: original.clone(),
@@ -1601,6 +1607,8 @@ mod tests {
params: vec![Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec!["n".into()],
body: Box::new(Term::Var { name: "n".into() }),
@@ -1624,6 +1632,8 @@ mod tests {
params: vec![],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec![],
body: fact_letrec_term(),
@@ -1683,6 +1693,8 @@ mod tests {
params: vec![Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec!["n".into()],
// (let-rec helper (params x) (type Int -> Int)
@@ -1694,6 +1706,8 @@ mod tests {
params: vec![Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec!["x".into()],
body: Box::new(Term::App {
@@ -1727,6 +1741,8 @@ mod tests {
params: vec![Type::int(), Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
"lifted fn type should have capture appended; got {:?}",
lifted.ty
@@ -1795,6 +1811,8 @@ mod tests {
params: vec![Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec!["x".into()],
body: Box::new(Term::App {
@@ -1821,6 +1839,8 @@ mod tests {
params: vec![],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec![],
body: Term::Let {
@@ -1889,6 +1909,8 @@ mod tests {
params: vec![Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec!["z".into()],
body: Box::new(Term::App {
@@ -1933,6 +1955,8 @@ mod tests {
}],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec!["p".into()],
body: outer_body,
@@ -1979,6 +2003,8 @@ mod tests {
params: vec![Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec!["x".into()],
body: Box::new(Term::Let {
@@ -2006,6 +2032,8 @@ mod tests {
params: vec![],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec![],
body: letrec,
@@ -2035,6 +2063,8 @@ mod tests {
params: vec![Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec!["x".into()],
body: Box::new(Term::App {
@@ -2062,6 +2092,8 @@ mod tests {
params: vec![],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec![],
body: letrec,
@@ -2150,6 +2182,8 @@ mod tests {
params: vec![Type::int()],
ret: Box::new(Type::Var { name: "a".into() }),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec!["k".into()],
body: Box::new(Term::If {
@@ -2193,6 +2227,8 @@ mod tests {
params: vec![Type::Var { name: "a".into() }],
ret: Box::new(Type::Var { name: "a".into() }),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
}),
},
params: vec!["x".into()],
@@ -2265,6 +2301,8 @@ mod tests {
params: vec![Type::int()],
ret: Box::new(Type::Var { name: "a".into() }),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec!["k".into()],
body: Box::new(Term::Var { name: "x".into() }),
@@ -2290,6 +2328,8 @@ mod tests {
params: vec![Type::Var { name: "a".into() }],
ret: Box::new(Type::Var { name: "a".into() }),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
}),
},
params: vec!["x".into()],
@@ -2327,6 +2367,8 @@ mod tests {
params: vec![Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec!["j".into()],
body: Box::new(Term::App {
@@ -2346,6 +2388,8 @@ mod tests {
params: vec![Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec!["i".into()],
body: Box::new(inner),
@@ -2365,6 +2409,8 @@ mod tests {
params: vec![],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec![],
body: outer,
@@ -2397,6 +2443,8 @@ mod tests {
params: vec![Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec!["j".into()],
body: Box::new(Term::App {
@@ -2419,6 +2467,8 @@ mod tests {
params: vec![Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec!["i".into()],
body: Box::new(inner),
@@ -2438,6 +2488,8 @@ mod tests {
params: vec![],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec![],
body: outer,
@@ -2554,6 +2606,8 @@ mod tests {
params: vec![Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec!["n".into()],
body: body_match,
@@ -2624,6 +2678,8 @@ mod tests {
params: vec![Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec!["xs".into()],
body: body_match,
@@ -2702,6 +2758,8 @@ mod tests {
params: vec![Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec!["n".into()],
body: body_match,
+2
View File
@@ -59,6 +59,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 {
+3 -1
View File
@@ -136,7 +136,7 @@ pub fn type_to_string(t: &Type) -> String {
}
}
Type::Var { name } => name.clone(),
Type::Fn { params, ret, effects } => {
Type::Fn { params, ret, effects, .. } => {
let p = params
.iter()
.map(type_to_string)
@@ -171,6 +171,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 {
+122 -7
View File
@@ -27,8 +27,9 @@
//! type ::= type-var | type-con | fn-type | forall-type
//! type-var ::= ident
//! type-con ::= "(" "con" ident type* ")"
//! fn-type ::= "(" "fn-type" "(" "params" type* ")"
//! "(" "ret" type ")"
//! fn-type-param ::= type | "(" "borrow" type ")" | "(" "own" type ")"
//! fn-type ::= "(" "fn-type" "(" "params" fn-type-param* ")"
//! "(" "ret" fn-type-param ")"
//! effects-clause? ")"
//! forall-type ::= "(" "forall" "(" "vars" ident+ ")" type ")"
//! effects-clause::= "(" "effects" ident+ ")"
@@ -80,7 +81,8 @@
//! [`ailang_core::ast::Import::alias`].
use ailang_core::ast::{
Arm, ConstDef, Ctor, Def, FnDef, Import, Literal, Module, Pattern, Term, Type, TypeDef,
Arm, ConstDef, Ctor, Def, FnDef, Import, Literal, Module, ParamMode, Pattern, Term, Type,
TypeDef,
};
use ailang_core::SCHEMA;
use thiserror::Error;
@@ -559,6 +561,20 @@ impl<'a> Parser<'a> {
"con" => self.parse_type_con(),
"fn-type" => self.parse_fn_type(),
"forall" => self.parse_forall_type(),
// Iter 18a: `borrow` / `own` are valid only as
// wrappers around `fn-type` params or `ret`. At
// top-level type position they are a parse error
// with a clear message.
"borrow" | "own" => {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
Err(ParseError::Production {
production: "type",
message: format!(
"`{head}` may only appear inside fn-type params or ret"
),
pos,
})
}
other => {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
Err(ParseError::Production {
@@ -602,18 +618,21 @@ impl<'a> Parser<'a> {
fn parse_fn_type(&mut self) -> Result<Type, ParseError> {
self.expect_lparen("fn-type")?;
self.expect_keyword("fn-type")?;
// (params type*)
// (params fn-type-param*)
self.expect_lparen("fn-type params")?;
self.expect_keyword("params")?;
let mut params: Vec<Type> = Vec::new();
let mut param_modes: Vec<ParamMode> = Vec::new();
while !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
params.push(self.parse_type()?);
let (t, m) = self.parse_param_with_mode()?;
params.push(t);
param_modes.push(m);
}
self.expect_rparen("fn-type params")?;
// (ret type)
// (ret fn-type-param)
self.expect_lparen("fn-type ret")?;
self.expect_keyword("ret")?;
let ret = self.parse_type()?;
let (ret, ret_mode) = self.parse_param_with_mode()?;
self.expect_rparen("fn-type ret")?;
// optional (effects ident+)
let mut effects: Vec<String> = Vec::new();
@@ -621,13 +640,50 @@ impl<'a> Parser<'a> {
effects = self.parse_effects_clause()?;
}
self.expect_rparen("fn-type")?;
// If every entry is Implicit, store as `vec![]` so canonical
// JSON serialisation omits the field — preserves pre-18a
// hashes for any fixture that still uses bare types.
let stored_modes = if param_modes.iter().all(|m| matches!(m, ParamMode::Implicit)) {
Vec::new()
} else {
param_modes
};
Ok(Type::Fn {
params,
ret: Box::new(ret),
effects,
param_modes: stored_modes,
ret_mode,
})
}
/// Iter 18a: parse one fn-type slot — a type, optionally wrapped
/// in `(borrow T)` or `(own T)`. The mode is `Implicit` for a
/// bare type, `Borrow` for `(borrow T)`, `Own` for `(own T)`.
fn parse_param_with_mode(&mut self) -> Result<(Type, ParamMode), ParseError> {
if let Some(head) = self.peek_head_ident() {
match head {
"borrow" => {
self.expect_lparen("borrow")?;
self.expect_keyword("borrow")?;
let inner = self.parse_type()?;
self.expect_rparen("borrow")?;
return Ok((inner, ParamMode::Borrow));
}
"own" => {
self.expect_lparen("own")?;
self.expect_keyword("own")?;
let inner = self.parse_type()?;
self.expect_rparen("own")?;
return Ok((inner, ParamMode::Own));
}
_ => {}
}
}
let t = self.parse_type()?;
Ok((t, ParamMode::Implicit))
}
fn parse_effects_clause(&mut self) -> Result<Vec<String>, ParseError> {
self.expect_lparen("effects-clause")?;
self.expect_keyword("effects")?;
@@ -1116,6 +1172,65 @@ mod tests {
assert!(matches!(m.defs.len(), 1));
}
/// Iter 18a: `(borrow T)` and `(own T)` wrappers in fn-type
/// param/ret slots round-trip into [`ParamMode::Borrow`] /
/// [`ParamMode::Own`] on `Type::Fn`. A bare type stays
/// [`ParamMode::Implicit`] (and its mode is elided from the
/// canonical form).
#[test]
fn parses_borrow_and_own_modes_on_fn_type_slots() {
let m = parse(
r#"
(module m
(fn f
(type (fn-type
(params (borrow (con Int)) (con Bool))
(ret (own (con Int)))))
(params x y)
(body x)))
"#,
)
.unwrap();
let ty = match &m.defs[0] {
Def::Fn(fd) => &fd.ty,
_ => panic!("expected fn"),
};
match ty {
Type::Fn { param_modes, ret_mode, .. } => {
assert_eq!(
param_modes,
&vec![ParamMode::Borrow, ParamMode::Implicit],
"first param parsed as `(borrow ...)`, second as bare"
);
assert_eq!(*ret_mode, ParamMode::Own, "ret parsed as `(own ...)`");
}
other => panic!("expected Type::Fn, got {other:?}"),
}
}
/// Iter 18a: `(borrow T)` outside an `fn-type` param/ret slot is
/// a parse error. Specifically, a `const` whose declared type is
/// `(borrow ...)` must be rejected with a clear message — modes
/// are *not* a top-level type production.
#[test]
fn rejects_borrow_at_top_level_type_position() {
let err = parse(
r#"
(module m
(const c
(type (borrow (con Int)))
(body 0)))
"#,
)
.err()
.expect("parse should fail");
let msg = format!("{err}");
assert!(
msg.contains("borrow"),
"diagnostic should mention `borrow`, got: {msg}"
);
}
/// Iter 16b.1: minimal `(let-rec ...)` round-trips through the
/// parser into a `Term::LetRec` whose `name`, `params`, `body` and
/// `in_term` line up with the source.
+28 -4
View File
@@ -8,7 +8,8 @@
//! per level. Comments are NOT emitted.
use ailang_core::ast::{
Arm, ConstDef, Ctor, Def, FnDef, Import, Literal, Module, Pattern, Term, Type, TypeDef,
Arm, ConstDef, Ctor, Def, FnDef, Import, Literal, Module, ParamMode, Pattern, Term, Type,
TypeDef,
};
/// Print a module in form (A).
@@ -169,6 +170,26 @@ fn write_const_def(out: &mut String, cd: &ConstDef, level: usize) {
// ---- types ----------------------------------------------------------------
/// Iter 18a: print one fn-type param/ret slot, wrapping with
/// `(borrow ...)` or `(own ...)` when the slot has an explicit
/// mode. `Implicit` is printed bare so pre-18a fixtures round-trip
/// unchanged.
fn write_fn_type_slot(out: &mut String, t: &Type, mode: ParamMode) {
match mode {
ParamMode::Implicit => write_type(out, t),
ParamMode::Own => {
out.push_str("(own ");
write_type(out, t);
out.push(')');
}
ParamMode::Borrow => {
out.push_str("(borrow ");
write_type(out, t);
out.push(')');
}
}
}
fn write_type(out: &mut String, t: &Type) {
match t {
Type::Var { name } => out.push_str(name),
@@ -183,16 +204,19 @@ fn write_type(out: &mut String, t: &Type) {
}
Type::Fn {
params,
param_modes,
ret,
ret_mode,
effects,
} => {
out.push_str("(fn-type (params");
for p in params {
for (i, p) in params.iter().enumerate() {
out.push(' ');
write_type(out, p);
let mode = param_modes.get(i).copied().unwrap_or(ParamMode::Implicit);
write_fn_type_slot(out, p, mode);
}
out.push_str(") (ret ");
write_type(out, ret);
write_fn_type_slot(out, ret, *ret_mode);
out.push(')');
if !effects.is_empty() {
out.push_str(" (effects");
+4 -4
View File
@@ -972,9 +972,9 @@ Memory layout (Iter 18b):
types, the recursion is replaced by a worklist loop (Iter 18e).
Codegen for `Term::Ctor` / `Term::Lam` env / closure pair under
`--memory=rc` calls `ailang_rc_alloc(SIZE)`. Iter 18b stops there
`--alloc=rc` calls `ailang_rc_alloc(SIZE)`. Iter 18b stops there
— inc/dec instrumentation is added in Iter 18c, once the
inference is wired up. Until then, `--memory=rc` deliberately
inference is wired up. Until then, `--alloc=rc` deliberately
leaks like the pre-Boehm era; this is purely about plumbing.
### Migration plan
@@ -984,8 +984,8 @@ leaks like the pre-Boehm era; this is purely about plumbing.
codegen change. `(con T)` ≡ `(own T)`. Existing fixtures
unchanged.
2. **Iter 18b:** RC runtime. `runtime/rc.c` with header layout +
alloc/inc/dec. Codegen `--memory=rc` routes allocation through
`rc_alloc`; **no inc/dec yet**. `--memory=gc` remains default.
alloc/inc/dec. Codegen `--alloc=rc` routes allocation through
`rc_alloc`; **no inc/dec yet**. `--alloc=gc` remains default.
3. **Iter 18c:** uniqueness inference + codegen inc/dec
instrumentation. Combines 18a's annotations with intra-fn
dataflow. Linear-by-default enforcement turns on. `(clone X)`
+1
View File
@@ -0,0 +1 @@
{"defs":[{"ctors":[{"fields":[],"name":"Nil"},{"fields":[{"k":"con","name":"Int"},{"k":"con","name":"List"}],"name":"Cons"}],"doc":"Monomorphic singly-linked Int list — boxed, recursive.","kind":"type","name":"List"},{"body":{"arms":[{"body":{"lit":{"kind":"int","value":0},"t":"lit"},"pat":{"ctor":"Nil","fields":[],"p":"ctor"}},{"body":{"args":[{"lit":{"kind":"int","value":1},"t":"lit"},{"args":[{"name":"t","t":"var"}],"fn":{"name":"list_length","t":"var"},"t":"app"}],"fn":{"name":"+","t":"var"},"t":"app"},"pat":{"ctor":"Cons","fields":[{"name":"h","p":"var"},{"name":"t","p":"var"}],"p":"ctor"}}],"scrutinee":{"name":"xs","t":"var"},"t":"match"},"doc":"Borrow xs, count its elements. Iter 18a: `(borrow (con List))`.","kind":"fn","name":"list_length","params":["xs"],"type":{"effects":[],"k":"fn","param_modes":["borrow"],"params":[{"k":"con","name":"List"}],"ret":{"k":"con","name":"Int"}}},{"body":{"arms":[{"body":{"lit":{"kind":"int","value":0},"t":"lit"},"pat":{"ctor":"Nil","fields":[],"p":"ctor"}},{"body":{"args":[{"name":"h","t":"var"},{"args":[{"name":"t","t":"var"}],"fn":{"name":"sum_list","t":"var"},"t":"app"}],"fn":{"name":"+","t":"var"},"t":"app"},"pat":{"ctor":"Cons","fields":[{"name":"h","p":"var"},{"name":"t","p":"var"}],"p":"ctor"}}],"scrutinee":{"name":"xs","t":"var"},"t":"match"},"doc":"Consume xs, sum its elements. Iter 18a: `(own (con List))`.","kind":"fn","name":"sum_list","params":["xs"],"type":{"effects":[],"k":"fn","param_modes":["own"],"params":[{"k":"con","name":"List"}],"ret":{"k":"con","name":"Int"}}},{"body":{"body":{"lhs":{"args":[{"args":[{"name":"xs","t":"var"}],"fn":{"name":"list_length","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"args":[{"args":[{"name":"xs","t":"var"}],"fn":{"name":"sum_list","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"t":"seq"},"name":"xs","t":"let","value":{"args":[{"lit":{"kind":"int","value":1},"t":"lit"},{"args":[{"lit":{"kind":"int","value":2},"t":"lit"},{"args":[{"lit":{"kind":"int","value":3},"t":"lit"},{"args":[],"ctor":"Nil","t":"ctor","type":"List"}],"ctor":"Cons","t":"ctor","type":"List"}],"ctor":"Cons","t":"ctor","type":"List"}],"ctor":"Cons","t":"ctor","type":"List"}},"doc":"Build [1,2,3]; print list_length (3) then sum_list (6).","kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[],"name":"borrow_own_demo","schema":"ailang/v0"}
+64
View File
@@ -0,0 +1,64 @@
; Iter 18a — `(borrow T)` and `(own T)` mode annotations on
; fn-type params and ret slots.
;
; This fixture exercises the schema/parser/printer/typechecker path
; that Iter 18a adds. It does *not* exercise enforcement: under the
; semantics shipping with 18a, every mode is treated as
; `Implicit ≡ Own` and `(borrow ...)` / `(own ...)` are accepted but
; have no codegen consequence. Linearity / borrow checking comes in
; Iter 18c.
;
; What round-trips through the form-A printer:
; (borrow (con List)) — list_length's xs parameter
; (own (con List)) — sum_list's xs parameter
;
; Expected stdout (one int per line, via io/print_int + a newline):
; 3
; 6
(module borrow_own_demo
(data List
(doc "Monomorphic singly-linked Int list — boxed, recursive.")
(ctor Nil)
(ctor Cons (con Int) (con List)))
(fn list_length
(doc "Borrow xs, count its elements. Iter 18a: `(borrow (con List))`.")
(type
(fn-type
(params (borrow (con List)))
(ret (con Int))))
(params xs)
(body
(match xs
(case (pat-ctor Nil) 0)
(case (pat-ctor Cons h t)
(app + 1 (app list_length t))))))
(fn sum_list
(doc "Consume xs, sum its elements. Iter 18a: `(own (con List))`.")
(type
(fn-type
(params (own (con List)))
(ret (con Int))))
(params xs)
(body
(match xs
(case (pat-ctor Nil) 0)
(case (pat-ctor Cons h t)
(app + h (app sum_list t))))))
(fn main
(doc "Build [1,2,3]; print list_length (3) then sum_list (6).")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body
(let xs
(term-ctor List Cons 1
(term-ctor List Cons 2
(term-ctor List Cons 3
(term-ctor List Nil))))
(seq
(do io/print_int (app list_length xs))
(do io/print_int (app sum_list xs)))))))