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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user