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
+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");