Files
AILang/crates/ailang-surface/src/parse.rs
T

2060 lines
76 KiB
Rust

//! Recursive-descent parser for form (A).
//!
//! The grammar (EBNF) lives next to its implementation. Each production
//! is one Rust function in this file. No look-ahead beyond a single
//! token is required; the parser is line-by-line auditable.
//!
//! ```text
//! module ::= "(" "module" ident def* ")"
//! def ::= data-def | fn-def | const-def | import-clause
//! data-def ::= "(" "data" ident vars-clause? data-attr* ")"
//! vars-clause ::= "(" "vars" ident+ ")"
//! data-attr ::= doc-attr | ctor-decl
//! ctor-decl ::= "(" "ctor" ident type* ")"
//! doc-attr ::= "(" "doc" string ")"
//!
//! fn-def ::= "(" "fn" ident fn-attr* ")"
//! fn-attr ::= doc-attr | suppress-attr | type-attr | params-attr | body-attr
//! suppress-attr ::= "(" "suppress" "(" "code" string ")"
//! "(" "because" string ")" ")"
//! type-attr ::= "(" "type" type ")"
//! params-attr ::= "(" "params" ident* ")"
//! body-attr ::= "(" "body" term ")"
//!
//! const-def ::= "(" "const" ident const-attr+ ")"
//! const-attr ::= type-attr | body-attr | doc-attr
//!
//! import-clause ::= "(" "import" ident ("as" ident)? ")"
//!
//! type ::= type-var | type-con | fn-type | forall-type
//! type-var ::= ident
//! type-con ::= "(" "con" ident 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+ ")"
//!
//! term ::= var-ref | int-lit | str-lit | bool-lit | unit-lit
//! | app-term | tail-app-term | match-term | ctor-term
//! | do-term | tail-do-term | seq-term | lam-term | if-term
//! | let-term | let-rec-term | clone-term
//! var-ref ::= ident ; reserved: true/false → bool-lit
//! int-lit ::= integer ; numeric atom
//! str-lit ::= string ; string atom
//! bool-lit ::= "true" | "false"
//! unit-lit ::= "(" "lit-unit" ")"
//! app-term ::= "(" "app" term term+ ")"
//! tail-app-term ::= "(" "tail-app" term term+ ")" ; Iter 14e
//! ctor-term ::= "(" "term-ctor" ident ident term* ")"
//! match-term ::= "(" "match" term case-arm+ ")"
//! case-arm ::= "(" "case" pattern term ")"
//! do-term ::= "(" "do" ident term* ")"
//! tail-do-term ::= "(" "tail-do" ident term* ")" ; Iter 14e
//! seq-term ::= "(" "seq" term term ")"
//! lam-term ::= "(" "lam" "(" "params" typed-param* ")"
//! "(" "ret" type ")"
//! effects-clause? body-attr ")"
//! typed-param ::= "(" "typed" ident type ")"
//! if-term ::= "(" "if" term term term ")"
//! let-term ::= "(" "let" ident term term ")"
//! let-rec-term ::= "(" "let-rec" ident
//! "(" "params" ident* ")"
//! type-attr
//! body-attr
//! "(" "in" term ")" ")"
//! clone-term ::= "(" "clone" term ")" ; Iter 18c.1
//! reuse-as-term ::= "(" "reuse-as" term term ")" ; Iter 18d.1
//!
//! pattern ::= pat-var | pat-ctor | pat-lit | pat-wild
//! pat-var ::= ident
//! pat-ctor ::= "(" "pat-ctor" ident pattern* ")"
//! pat-lit ::= "(" "pat-lit" lit-form ")"
//! pat-wild ::= "_"
//! lit-form ::= integer | "true" | "false" | string
//! ```
//!
//! Notes on the form (deviations from the spec in DESIGN.md Decision 6):
//!
//! - The `lam` form carries `paramTypes`, a `ret` type, and an
//! optional `effects` clause. The original DESIGN.md sketch left
//! these out; the AST stores them per-lambda and so the form must
//! round-trip them.
//! - The `import` form admits an optional `as` alias to round-trip
//! [`ailang_core::ast::Import::alias`].
use ailang_core::ast::{
Arm, ClassDef, ClassMethod, ConstDef, Ctor, Def, FnDef, Import, Literal, Module,
ParamMode, Pattern, SuperclassRef, Suppress, Term,
Type, TypeDef,
};
use ailang_core::SCHEMA;
use thiserror::Error;
use crate::lex::{tokenize, LexError, Tok, Token};
/// Errors raised during parsing.
#[derive(Debug, Error)]
pub enum ParseError {
#[error("lex error: {0}")]
Lex(#[from] LexError),
#[error("parse error: expected {expected}, got {got} at byte {pos}")]
Unexpected {
expected: String,
got: String,
pos: usize,
},
#[error("parse error: unexpected end of input, expected {expected}")]
UnexpectedEof { expected: String },
#[error("parse error in {production}: {message} at byte {pos}")]
Production {
production: &'static str,
message: String,
pos: usize,
},
}
/// Parse a form-(A) source string into an [`ailang_core::ast::Module`].
///
/// The schema field is set to [`ailang_core::SCHEMA`]; the form does
/// not carry it explicitly because the form itself implies the
/// version.
pub fn parse(input: &str) -> Result<Module, ParseError> {
let toks = tokenize(input)?;
let mut p = Parser::new(&toks);
let m = p.parse_module()?;
if p.cur < p.toks.len() {
return Err(ParseError::Unexpected {
expected: "end of input".into(),
got: tok_label(&p.toks[p.cur].tok),
pos: p.toks[p.cur].span.start,
});
}
Ok(m)
}
/// Parse a single form-A term — the concrete-syntax counterpart of
/// [`Term`]. This is the dual of [`crate::print::term_to_form_a`] and is
/// used by callers that produce form-A snippets out-of-band, e.g. the
/// `suggested_rewrites` payload of `ail check --json` (Iter 18c.2). The
/// input must consume to EOF after the term — extra trailing tokens
/// produce [`ParseError::Unexpected`].
///
/// Round-trip: `parse_term(term_to_form_a(t)) ≡ t` for every term `t`
/// the surface can express.
pub fn parse_term(input: &str) -> Result<Term, ParseError> {
let toks = tokenize(input)?;
let mut p = Parser::new(&toks);
let t = p.parse_term()?;
if p.cur < p.toks.len() {
return Err(ParseError::Unexpected {
expected: "end of input".into(),
got: tok_label(&p.toks[p.cur].tok),
pos: p.toks[p.cur].span.start,
});
}
Ok(t)
}
fn tok_label(t: &Tok) -> String {
match t {
Tok::LParen => "`(`".into(),
Tok::RParen => "`)`".into(),
Tok::Int(v) => format!("integer `{v}`"),
Tok::Str(s) => format!("string {s:?}"),
Tok::Ident(s) => format!("ident `{s}`"),
}
}
struct Parser<'a> {
toks: &'a [Token],
cur: usize,
}
impl<'a> Parser<'a> {
fn new(toks: &'a [Token]) -> Self {
Self { toks, cur: 0 }
}
fn peek(&self) -> Option<&Token> {
self.toks.get(self.cur)
}
fn expect_lparen(&mut self, ctx: &'static str) -> Result<(), ParseError> {
match self.peek() {
Some(Token { tok: Tok::LParen, .. }) => {
self.cur += 1;
Ok(())
}
Some(t) => Err(ParseError::Unexpected {
expected: format!("`(` (start of {ctx})"),
got: tok_label(&t.tok),
pos: t.span.start,
}),
None => Err(ParseError::UnexpectedEof {
expected: format!("`(` (start of {ctx})"),
}),
}
}
fn expect_rparen(&mut self, ctx: &'static str) -> Result<(), ParseError> {
match self.peek() {
Some(Token { tok: Tok::RParen, .. }) => {
self.cur += 1;
Ok(())
}
Some(t) => Err(ParseError::Unexpected {
expected: format!("`)` (end of {ctx})"),
got: tok_label(&t.tok),
pos: t.span.start,
}),
None => Err(ParseError::UnexpectedEof {
expected: format!("`)` (end of {ctx})"),
}),
}
}
/// Consume an ident atom matching `expected`. Used for keyword tags
/// like `module`, `data`, `con`, etc.
fn expect_keyword(&mut self, kw: &'static str) -> Result<(), ParseError> {
match self.peek() {
Some(Token { tok: Tok::Ident(s), span }) if s == kw => {
let _ = span;
self.cur += 1;
Ok(())
}
Some(t) => Err(ParseError::Unexpected {
expected: format!("`{kw}`"),
got: tok_label(&t.tok),
pos: t.span.start,
}),
None => Err(ParseError::UnexpectedEof {
expected: format!("`{kw}`"),
}),
}
}
/// Consume any ident atom and return its text.
fn expect_ident(&mut self, ctx: &'static str) -> Result<String, ParseError> {
match self.peek().cloned() {
Some(Token { tok: Tok::Ident(s), .. }) => {
self.cur += 1;
Ok(s)
}
Some(t) => Err(ParseError::Unexpected {
expected: format!("ident ({ctx})"),
got: tok_label(&t.tok),
pos: t.span.start,
}),
None => Err(ParseError::UnexpectedEof {
expected: format!("ident ({ctx})"),
}),
}
}
/// Try to consume an ident matching `kw`. On success advance and
/// return true; otherwise leave position unchanged and return
/// false.
fn try_keyword(&mut self, kw: &str) -> bool {
match self.peek() {
Some(Token { tok: Tok::Ident(s), .. }) if s == kw => {
self.cur += 1;
true
}
_ => false,
}
}
/// Look at the head ident of a parenthesised form without
/// consuming. Used to dispatch on the head keyword.
fn peek_head_ident(&self) -> Option<&str> {
if let Some(Token { tok: Tok::LParen, .. }) = self.toks.get(self.cur) {
if let Some(Token { tok: Tok::Ident(s), .. }) = self.toks.get(self.cur + 1) {
return Some(s.as_str());
}
}
None
}
// ---- module ---------------------------------------------------------
fn parse_module(&mut self) -> Result<Module, ParseError> {
self.expect_lparen("module")?;
self.expect_keyword("module")?;
let name = self.expect_ident("module name")?;
let mut imports: Vec<Import> = Vec::new();
let mut defs: Vec<Def> = Vec::new();
loop {
match self.peek() {
Some(Token { tok: Tok::RParen, .. }) => break,
None => {
return Err(ParseError::UnexpectedEof {
expected: "`)` to end module or another def".into(),
});
}
_ => {}
}
let head = self.peek_head_ident().ok_or_else(|| {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
ParseError::Production {
production: "module",
message: "expected `(` followed by a def-head keyword (\
`data`, `fn`, `const`, `import`)"
.into(),
pos,
}
})?;
match head {
"import" => imports.push(self.parse_import()?),
"data" => defs.push(Def::Type(self.parse_data()?)),
"fn" => defs.push(Def::Fn(self.parse_fn()?)),
"const" => defs.push(Def::Const(self.parse_const()?)),
"class" => defs.push(Def::Class(self.parse_class()?)),
other => {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
return Err(ParseError::Production {
production: "module",
message: format!(
"unknown def head `{other}`; expected `data`, `fn`, `const`, `class`, or `import`"
),
pos,
});
}
}
}
self.expect_rparen("module")?;
Ok(Module {
schema: SCHEMA.to_string(),
name,
imports,
defs,
})
}
// ---- imports --------------------------------------------------------
fn parse_import(&mut self) -> Result<Import, ParseError> {
self.expect_lparen("import-clause")?;
self.expect_keyword("import")?;
let module = self.expect_ident("import module name")?;
let alias = if self.try_keyword("as") {
Some(self.expect_ident("import alias")?)
} else {
None
};
self.expect_rparen("import-clause")?;
Ok(Import { module, alias })
}
// ---- data def -------------------------------------------------------
fn parse_data(&mut self) -> Result<TypeDef, ParseError> {
self.expect_lparen("data-def")?;
self.expect_keyword("data")?;
let name = self.expect_ident("data name")?;
// Optional vars clause: `(vars a b ...)`
let mut vars: Vec<String> = Vec::new();
if let Some("vars") = self.peek_head_ident() {
self.expect_lparen("vars-clause")?;
self.expect_keyword("vars")?;
// vars+ : at least one
let first = self.expect_ident("type variable")?;
vars.push(first);
while !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
vars.push(self.expect_ident("type variable")?);
}
self.expect_rparen("vars-clause")?;
}
let mut doc: Option<String> = None;
let mut ctors: Vec<Ctor> = Vec::new();
let mut drop_iterative = false;
loop {
match self.peek_head_ident() {
Some("doc") => {
let s = self.parse_doc()?;
doc = Some(s);
}
Some("ctor") => {
ctors.push(self.parse_ctor()?);
}
Some("drop-iterative") => {
// Iter 18e: `(drop-iterative)` opt-in annotation.
// Takes no arguments — it is a flag. A second
// `(drop-iterative)` clause is a parse error
// (rejected here so that the JSON schema's
// `drop_iterative: bool` round-trips unambiguously).
self.expect_lparen("drop-iterative-attr")?;
self.expect_keyword("drop-iterative")?;
if !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
return Err(ParseError::Production {
production: "data-def",
message:
"drop-iterative takes no arguments; expected `)`"
.into(),
pos,
});
}
self.expect_rparen("drop-iterative-attr")?;
if drop_iterative {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
return Err(ParseError::Production {
production: "data-def",
message: "duplicate `drop-iterative` attribute"
.into(),
pos,
});
}
drop_iterative = true;
}
Some(other) => {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
return Err(ParseError::Production {
production: "data-def",
message: format!(
"unknown data attribute `{other}`; expected `doc`, `ctor`, or `drop-iterative`"
),
pos,
});
}
None => break,
}
}
self.expect_rparen("data-def")?;
Ok(TypeDef {
name,
vars,
ctors,
doc,
drop_iterative,
})
}
fn parse_doc(&mut self) -> Result<String, ParseError> {
self.expect_lparen("doc-attr")?;
self.expect_keyword("doc")?;
let s = match self.peek().cloned() {
Some(Token { tok: Tok::Str(s), .. }) => {
self.cur += 1;
s
}
Some(t) => {
return Err(ParseError::Unexpected {
expected: "string literal (doc body)".into(),
got: tok_label(&t.tok),
pos: t.span.start,
});
}
None => {
return Err(ParseError::UnexpectedEof {
expected: "string literal (doc body)".into(),
});
}
};
self.expect_rparen("doc-attr")?;
Ok(s)
}
fn parse_ctor(&mut self) -> Result<Ctor, ParseError> {
self.expect_lparen("ctor-decl")?;
self.expect_keyword("ctor")?;
let name = self.expect_ident("ctor name")?;
let mut fields: Vec<Type> = Vec::new();
while !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
fields.push(self.parse_type()?);
}
self.expect_rparen("ctor-decl")?;
Ok(Ctor { name, fields })
}
// ---- fn def ---------------------------------------------------------
fn parse_fn(&mut self) -> Result<FnDef, ParseError> {
self.expect_lparen("fn-def")?;
self.expect_keyword("fn")?;
let name = self.expect_ident("fn name")?;
let mut doc: Option<String> = None;
let mut ty: Option<Type> = None;
let mut params: Option<Vec<String>> = None;
let mut body: Option<Term> = None;
// Iter 19b: every `(suppress ...)` clause appends one entry. Order
// is preserved (matches the on-disk JSON-AST order).
let mut suppress: Vec<Suppress> = Vec::new();
loop {
match self.peek_head_ident() {
Some("doc") => doc = Some(self.parse_doc()?),
Some("suppress") => suppress.push(self.parse_suppress_attr()?),
Some("type") => {
let t = self.parse_type_attr()?;
ty = Some(t);
}
Some("params") => {
let p = self.parse_params_attr()?;
params = Some(p);
}
Some("body") => {
let b = self.parse_body_attr()?;
body = Some(b);
}
Some(other) => {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
return Err(ParseError::Production {
production: "fn-def",
message: format!(
"unknown fn attribute `{other}`; expected `doc`, `suppress`, `type`, `params`, or `body`"
),
pos,
});
}
None => break,
}
}
self.expect_rparen("fn-def")?;
let ty = ty.ok_or_else(|| ParseError::Production {
production: "fn-def",
message: format!("fn `{name}` is missing required `(type ...)` attribute"),
pos: 0,
})?;
let params = params.ok_or_else(|| ParseError::Production {
production: "fn-def",
message: format!("fn `{name}` is missing required `(params ...)` attribute"),
pos: 0,
})?;
let body = body.ok_or_else(|| ParseError::Production {
production: "fn-def",
message: format!("fn `{name}` is missing required `(body ...)` attribute"),
pos: 0,
})?;
Ok(FnDef {
name,
ty,
params,
body,
doc,
suppress,
})
}
/// Iter 19b: parse one `(suppress (code "<c>") (because "<r>"))`
/// clause, returning a [`Suppress`] entry. Unknown sub-keywords
/// inside the clause are rejected with [`ParseError::Production`].
/// The `because` text is allowed to be empty here (the typechecker
/// emits `empty-suppress-reason` instead of the parser, so the
/// invalid form round-trips through the surface for diagnostic
/// purposes).
fn parse_suppress_attr(&mut self) -> Result<Suppress, ParseError> {
self.expect_lparen("suppress-attr")?;
self.expect_keyword("suppress")?;
let mut code: Option<String> = None;
let mut because: Option<String> = None;
loop {
match self.peek_head_ident() {
Some("code") => {
self.expect_lparen("suppress.code")?;
self.expect_keyword("code")?;
code = Some(self.expect_string("code body")?);
self.expect_rparen("suppress.code")?;
}
Some("because") => {
self.expect_lparen("suppress.because")?;
self.expect_keyword("because")?;
because = Some(self.expect_string("because body")?);
self.expect_rparen("suppress.because")?;
}
Some(other) => {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
return Err(ParseError::Production {
production: "suppress-attr",
message: format!(
"unknown suppress sub-attribute `{other}`; expected `code` or `because`"
),
pos,
});
}
None => break,
}
}
self.expect_rparen("suppress-attr")?;
let code = code.ok_or_else(|| ParseError::Production {
production: "suppress-attr",
message: "suppress is missing required `(code ...)`".into(),
pos: 0,
})?;
let because = because.ok_or_else(|| ParseError::Production {
production: "suppress-attr",
message: "suppress is missing required `(because ...)`".into(),
pos: 0,
})?;
Ok(Suppress { code, because })
}
/// Iter 19b: helper — consume one string-literal token. Used by
/// [`Self::parse_suppress_attr`].
fn expect_string(&mut self, ctx: &'static str) -> Result<String, ParseError> {
match self.peek().cloned() {
Some(Token { tok: Tok::Str(s), .. }) => {
self.cur += 1;
Ok(s)
}
Some(t) => Err(ParseError::Unexpected {
expected: format!("string literal ({ctx})"),
got: tok_label(&t.tok),
pos: t.span.start,
}),
None => Err(ParseError::UnexpectedEof {
expected: format!("string literal ({ctx})"),
}),
}
}
fn parse_type_attr(&mut self) -> Result<Type, ParseError> {
self.expect_lparen("type-attr")?;
self.expect_keyword("type")?;
let t = self.parse_type()?;
self.expect_rparen("type-attr")?;
Ok(t)
}
fn parse_params_attr(&mut self) -> Result<Vec<String>, ParseError> {
self.expect_lparen("params-attr")?;
self.expect_keyword("params")?;
let mut out = Vec::new();
while !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
out.push(self.expect_ident("param name")?);
}
self.expect_rparen("params-attr")?;
Ok(out)
}
fn parse_body_attr(&mut self) -> Result<Term, ParseError> {
self.expect_lparen("body-attr")?;
self.expect_keyword("body")?;
let t = self.parse_term()?;
self.expect_rparen("body-attr")?;
Ok(t)
}
// ---- const def ------------------------------------------------------
fn parse_const(&mut self) -> Result<ConstDef, ParseError> {
self.expect_lparen("const-def")?;
self.expect_keyword("const")?;
let name = self.expect_ident("const name")?;
let mut doc: Option<String> = None;
let mut ty: Option<Type> = None;
let mut value: Option<Term> = None;
loop {
match self.peek_head_ident() {
Some("doc") => doc = Some(self.parse_doc()?),
Some("type") => ty = Some(self.parse_type_attr()?),
Some("body") => value = Some(self.parse_body_attr()?),
Some(other) => {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
return Err(ParseError::Production {
production: "const-def",
message: format!(
"unknown const attribute `{other}`; expected `doc`, `type`, or `body`"
),
pos,
});
}
None => break,
}
}
self.expect_rparen("const-def")?;
let ty = ty.ok_or_else(|| ParseError::Production {
production: "const-def",
message: format!("const `{name}` is missing required `(type ...)`"),
pos: 0,
})?;
let value = value.ok_or_else(|| ParseError::Production {
production: "const-def",
message: format!("const `{name}` is missing required `(body ...)`"),
pos: 0,
})?;
Ok(ConstDef {
name,
ty,
value,
doc,
})
}
// ---- class def -----------------------------------------------------
fn parse_class(&mut self) -> Result<ClassDef, ParseError> {
self.expect_lparen("class-def")?;
self.expect_keyword("class")?;
let name = self.expect_ident("class name")?;
let mut param: Option<String> = None;
let mut superclass: Option<SuperclassRef> = None;
let mut doc: Option<String> = None;
let mut methods: Vec<ClassMethod> = Vec::new();
loop {
match self.peek_head_ident() {
Some("param") => {
if param.is_some() {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
return Err(ParseError::Production {
production: "class-def",
message: format!(
"class `{name}` has duplicate `(param ...)` clause"
),
pos,
});
}
self.expect_lparen("class.param")?;
self.expect_keyword("param")?;
param = Some(self.expect_ident("class param ident")?);
self.expect_rparen("class.param")?;
}
Some("superclass") => {
if superclass.is_some() {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
return Err(ParseError::Production {
production: "class-def",
message: format!(
"class `{name}` has duplicate `(superclass ...)` clause"
),
pos,
});
}
superclass = Some(self.parse_superclass()?);
}
Some("doc") => {
if doc.is_some() {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
return Err(ParseError::Production {
production: "class-def",
message: format!(
"class `{name}` has duplicate `(doc ...)` clause"
),
pos,
});
}
doc = Some(self.parse_doc()?);
}
Some("method") => {
methods.push(self.parse_class_method()?);
}
Some(other) => {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
return Err(ParseError::Production {
production: "class-def",
message: format!(
"unknown class attribute `{other}`; expected `param`, `superclass`, `doc`, or `method`"
),
pos,
});
}
None => break,
}
}
self.expect_rparen("class-def")?;
let param = param.ok_or_else(|| ParseError::Production {
production: "class-def",
message: format!("class `{name}` is missing required `(param ...)` clause"),
pos: 0,
})?;
Ok(ClassDef {
name,
param,
superclass,
methods,
doc,
})
}
fn parse_superclass(&mut self) -> Result<SuperclassRef, ParseError> {
self.expect_lparen("superclass-clause")?;
self.expect_keyword("superclass")?;
// (class Name)
self.expect_lparen("superclass.class")?;
self.expect_keyword("class")?;
let class = self.expect_ident("superclass name")?;
self.expect_rparen("superclass.class")?;
// (type ident)
self.expect_lparen("superclass.type")?;
self.expect_keyword("type")?;
let type_ = self.expect_ident("superclass param ident")?;
self.expect_rparen("superclass.type")?;
self.expect_rparen("superclass-clause")?;
Ok(SuperclassRef { class, type_ })
}
fn parse_class_method(&mut self) -> Result<ClassMethod, ParseError> {
self.expect_lparen("class.method")?;
self.expect_keyword("method")?;
let name = self.expect_ident("class method name")?;
let mut ty: Option<Type> = None;
let mut default: Option<Term> = None;
loop {
match self.peek_head_ident() {
Some("type") => {
if ty.is_some() {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
return Err(ParseError::Production {
production: "class.method",
message: format!(
"method `{name}` has duplicate `(type ...)` clause"
),
pos,
});
}
ty = Some(self.parse_type_attr()?);
}
Some("default") => {
if default.is_some() {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
return Err(ParseError::Production {
production: "class.method",
message: format!(
"method `{name}` has duplicate `(default ...)` clause"
),
pos,
});
}
default = Some(self.parse_default_attr()?);
}
Some(other) => {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
return Err(ParseError::Production {
production: "class.method",
message: format!(
"unknown class.method attribute `{other}`; expected `type` or `default`"
),
pos,
});
}
None => break,
}
}
self.expect_rparen("class.method")?;
let ty = ty.ok_or_else(|| ParseError::Production {
production: "class.method",
message: format!("method `{name}` is missing required `(type ...)` clause"),
pos: 0,
})?;
Ok(ClassMethod { name, ty, default })
}
fn parse_default_attr(&mut self) -> Result<Term, ParseError> {
self.expect_lparen("default-attr")?;
self.expect_keyword("default")?;
let t = self.parse_term()?;
self.expect_rparen("default-attr")?;
Ok(t)
}
// ---- types ----------------------------------------------------------
fn parse_type(&mut self) -> Result<Type, ParseError> {
match self.peek() {
Some(Token { tok: Tok::LParen, .. }) => {
let head = self.peek_head_ident().ok_or_else(|| {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
ParseError::Production {
production: "type",
message: "expected `(con ...)`, `(fn-type ...)`, or `(forall ...)`"
.into(),
pos,
}
})?;
match head {
"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 {
production: "type",
message: format!(
"unknown type head `{other}`; expected `con`, `fn-type`, or `forall`"
),
pos,
})
}
}
}
Some(Token { tok: Tok::Ident(s), .. }) => {
let s = s.clone();
self.cur += 1;
Ok(Type::Var { name: s })
}
Some(t) => Err(ParseError::Unexpected {
expected: "type expression".into(),
got: tok_label(&t.tok),
pos: t.span.start,
}),
None => Err(ParseError::UnexpectedEof {
expected: "type expression".into(),
}),
}
}
fn parse_type_con(&mut self) -> Result<Type, ParseError> {
self.expect_lparen("type-con")?;
self.expect_keyword("con")?;
let name = self.expect_ident("type constructor name")?;
let mut args: Vec<Type> = Vec::new();
while !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
args.push(self.parse_type()?);
}
self.expect_rparen("type-con")?;
Ok(Type::Con { name, args })
}
fn parse_fn_type(&mut self) -> Result<Type, ParseError> {
self.expect_lparen("fn-type")?;
self.expect_keyword("fn-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, .. })) {
let (t, m) = self.parse_param_with_mode()?;
params.push(t);
param_modes.push(m);
}
self.expect_rparen("fn-type params")?;
// (ret fn-type-param)
self.expect_lparen("fn-type ret")?;
self.expect_keyword("ret")?;
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();
if let Some("effects") = self.peek_head_ident() {
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")?;
// 1+ idents per grammar; round-trip allows empty too (to support
// any future use), but the printer never emits an empty clause.
let mut out = Vec::new();
while !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
out.push(self.expect_ident("effect name")?);
}
if out.is_empty() {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
return Err(ParseError::Production {
production: "effects-clause",
message: "expected at least one effect name".into(),
pos,
});
}
self.expect_rparen("effects-clause")?;
Ok(out)
}
fn parse_forall_type(&mut self) -> Result<Type, ParseError> {
self.expect_lparen("forall-type")?;
self.expect_keyword("forall")?;
// (vars ident+)
self.expect_lparen("forall vars")?;
self.expect_keyword("vars")?;
let mut vars = Vec::new();
let first = self.expect_ident("type variable")?;
vars.push(first);
while !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
vars.push(self.expect_ident("type variable")?);
}
self.expect_rparen("forall vars")?;
let body = self.parse_type()?;
self.expect_rparen("forall-type")?;
Ok(Type::Forall {
vars,
constraints: vec![],
body: Box::new(body),
})
}
// ---- terms ----------------------------------------------------------
fn parse_term(&mut self) -> Result<Term, ParseError> {
match self.peek().cloned() {
Some(Token { tok: Tok::LParen, .. }) => {
let head = self.peek_head_ident().ok_or_else(|| {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
ParseError::Production {
production: "term",
message: "expected a term-head keyword after `(`".into(),
pos,
}
})?;
match head {
"lit-unit" => self.parse_lit_unit(),
"app" => self.parse_app(),
"tail-app" => self.parse_tail_app(),
"term-ctor" => self.parse_term_ctor(),
"match" => self.parse_match(),
"do" => self.parse_do(),
"tail-do" => self.parse_tail_do(),
"seq" => self.parse_seq(),
"lam" => self.parse_lam(),
"if" => self.parse_if(),
"let" => self.parse_let(),
"let-rec" => self.parse_let_rec(),
"clone" => self.parse_clone(),
"reuse-as" => self.parse_reuse_as(),
other => {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
Err(ParseError::Production {
production: "term",
message: format!(
"unknown term head `{other}`; expected one of \
`app`, `tail-app`, `lam`, `let`, `let-rec`, `if`, `match`, `do`, \
`tail-do`, `seq`, `term-ctor`, `clone`, `reuse-as`, `lit-unit`"
),
pos,
})
}
}
}
Some(Token { tok: Tok::Ident(s), .. }) => {
self.cur += 1;
if s == "true" {
Ok(Term::Lit { lit: Literal::Bool { value: true } })
} else if s == "false" {
Ok(Term::Lit { lit: Literal::Bool { value: false } })
} else {
Ok(Term::Var { name: s })
}
}
Some(Token { tok: Tok::Int(v), .. }) => {
self.cur += 1;
Ok(Term::Lit { lit: Literal::Int { value: v } })
}
Some(Token { tok: Tok::Str(s), .. }) => {
self.cur += 1;
Ok(Term::Lit { lit: Literal::Str { value: s } })
}
Some(t) => Err(ParseError::Unexpected {
expected: "term".into(),
got: tok_label(&t.tok),
pos: t.span.start,
}),
None => Err(ParseError::UnexpectedEof {
expected: "term".into(),
}),
}
}
fn parse_lit_unit(&mut self) -> Result<Term, ParseError> {
self.expect_lparen("unit-lit")?;
self.expect_keyword("lit-unit")?;
self.expect_rparen("unit-lit")?;
Ok(Term::Lit { lit: Literal::Unit })
}
fn parse_app(&mut self) -> Result<Term, ParseError> {
self.expect_lparen("app-term")?;
self.expect_keyword("app")?;
self.parse_app_body(false, "app-term")
}
/// Iter 14e: `(tail-app callee arg+)` — same shape as `app` but
/// constructs `Term::App { tail: true, .. }`. The typechecker's
/// tail-position pass verifies that the call really is in tail
/// position; an unmarked call in tail position is also legal.
fn parse_tail_app(&mut self) -> Result<Term, ParseError> {
self.expect_lparen("tail-app-term")?;
self.expect_keyword("tail-app")?;
self.parse_app_body(true, "tail-app-term")
}
/// Body shared by [`Self::parse_app`] and [`Self::parse_tail_app`]:
/// callee + 1+ args + closing `)`.
fn parse_app_body(
&mut self,
tail: bool,
production: &'static str,
) -> Result<Term, ParseError> {
let callee = self.parse_term()?;
// 1+ args
if matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
return Err(ParseError::Production {
production,
message: "expected at least one argument".into(),
pos,
});
}
let mut args = Vec::new();
while !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
args.push(self.parse_term()?);
}
self.expect_rparen(production)?;
Ok(Term::App {
callee: Box::new(callee),
args,
tail,
})
}
fn parse_term_ctor(&mut self) -> Result<Term, ParseError> {
self.expect_lparen("ctor-term")?;
self.expect_keyword("term-ctor")?;
let type_name = self.expect_ident("ADT type name")?;
let ctor = self.expect_ident("ctor name")?;
let mut args = Vec::new();
while !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
args.push(self.parse_term()?);
}
self.expect_rparen("ctor-term")?;
Ok(Term::Ctor {
type_name,
ctor,
args,
})
}
fn parse_match(&mut self) -> Result<Term, ParseError> {
self.expect_lparen("match-term")?;
self.expect_keyword("match")?;
let scrutinee = self.parse_term()?;
let mut arms = Vec::new();
while !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
arms.push(self.parse_case_arm()?);
}
if arms.is_empty() {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
return Err(ParseError::Production {
production: "match-term",
message: "expected at least one `(case ...)` arm".into(),
pos,
});
}
self.expect_rparen("match-term")?;
Ok(Term::Match {
scrutinee: Box::new(scrutinee),
arms,
})
}
fn parse_case_arm(&mut self) -> Result<Arm, ParseError> {
self.expect_lparen("case-arm")?;
self.expect_keyword("case")?;
let pat = self.parse_pattern()?;
let body = self.parse_term()?;
self.expect_rparen("case-arm")?;
Ok(Arm { pat, body })
}
fn parse_do(&mut self) -> Result<Term, ParseError> {
self.expect_lparen("do-term")?;
self.expect_keyword("do")?;
self.parse_do_body(false, "do-term")
}
/// Iter 14e: `(tail-do op arg*)` — same shape as `do` but
/// constructs `Term::Do { tail: true, .. }`.
fn parse_tail_do(&mut self) -> Result<Term, ParseError> {
self.expect_lparen("tail-do-term")?;
self.expect_keyword("tail-do")?;
self.parse_do_body(true, "tail-do-term")
}
/// Body shared by [`Self::parse_do`] and [`Self::parse_tail_do`].
fn parse_do_body(
&mut self,
tail: bool,
production: &'static str,
) -> Result<Term, ParseError> {
let op = self.expect_ident("effect op (e.g. `io/print_int`)")?;
let mut args = Vec::new();
while !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
args.push(self.parse_term()?);
}
self.expect_rparen(production)?;
Ok(Term::Do { op, args, tail })
}
fn parse_seq(&mut self) -> Result<Term, ParseError> {
self.expect_lparen("seq-term")?;
self.expect_keyword("seq")?;
let lhs = self.parse_term()?;
let rhs = self.parse_term()?;
self.expect_rparen("seq-term")?;
Ok(Term::Seq {
lhs: Box::new(lhs),
rhs: Box::new(rhs),
})
}
fn parse_lam(&mut self) -> Result<Term, ParseError> {
self.expect_lparen("lam-term")?;
self.expect_keyword("lam")?;
// (params (typed name type)*)
self.expect_lparen("lam params")?;
self.expect_keyword("params")?;
let mut params: Vec<String> = Vec::new();
let mut param_tys: Vec<Type> = Vec::new();
while !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
self.expect_lparen("typed-param")?;
self.expect_keyword("typed")?;
let pname = self.expect_ident("lambda param name")?;
let pty = self.parse_type()?;
self.expect_rparen("typed-param")?;
params.push(pname);
param_tys.push(pty);
}
self.expect_rparen("lam params")?;
// (ret type)
self.expect_lparen("lam ret")?;
self.expect_keyword("ret")?;
let ret_ty = self.parse_type()?;
self.expect_rparen("lam ret")?;
// optional effects clause
let mut effects: Vec<String> = Vec::new();
if let Some("effects") = self.peek_head_ident() {
effects = self.parse_effects_clause()?;
}
// (body term)
let body = self.parse_body_attr()?;
self.expect_rparen("lam-term")?;
Ok(Term::Lam {
params,
param_tys,
ret_ty: Box::new(ret_ty),
effects,
body: Box::new(body),
})
}
fn parse_if(&mut self) -> Result<Term, ParseError> {
self.expect_lparen("if-term")?;
self.expect_keyword("if")?;
let cond = self.parse_term()?;
let then = self.parse_term()?;
let else_ = self.parse_term()?;
self.expect_rparen("if-term")?;
Ok(Term::If {
cond: Box::new(cond),
then: Box::new(then),
else_: Box::new(else_),
})
}
fn parse_let(&mut self) -> Result<Term, ParseError> {
self.expect_lparen("let-term")?;
self.expect_keyword("let")?;
let name = self.expect_ident("let-bound name")?;
let value = self.parse_term()?;
let body = self.parse_term()?;
self.expect_rparen("let-term")?;
Ok(Term::Let {
name,
value: Box::new(value),
body: Box::new(body),
})
}
/// Iter 16b.1: `(let-rec NAME (params PARAM*) (type T) (body TERM)
/// (in TERM))` — local recursive fn-shaped binding. Eliminated by
/// the desugar pass (lifted to a synthetic top-level fn) before
/// typecheck. The body's recursive references to `NAME` are
/// rewritten to the lifted name; the `in`-clause becomes the term
/// that replaces the LetRec.
fn parse_let_rec(&mut self) -> Result<Term, ParseError> {
self.expect_lparen("let-rec-term")?;
self.expect_keyword("let-rec")?;
let name = self.expect_ident("let-rec name")?;
let params = self.parse_params_attr()?;
let ty = self.parse_type_attr()?;
let body = self.parse_body_attr()?;
// (in TERM)
self.expect_lparen("let-rec in-clause")?;
self.expect_keyword("in")?;
let in_term = self.parse_term()?;
self.expect_rparen("let-rec in-clause")?;
self.expect_rparen("let-rec-term")?;
Ok(Term::LetRec {
name,
ty,
params,
body: Box::new(body),
in_term: Box::new(in_term),
})
}
/// Iter 18c.1: `(clone TERM)` — explicit RC clone wrapper. In 18c.1
/// the wrapper is identity for typechecker and codegen; in 18c.3
/// the codegen will emit `call void @ailang_rc_inc` before yielding
/// the inner value's SSA reg under `--alloc=rc`.
fn parse_clone(&mut self) -> Result<Term, ParseError> {
self.expect_lparen("clone-term")?;
self.expect_keyword("clone")?;
// Exactly one inner term, then `)`.
if matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
return Err(ParseError::Production {
production: "clone-term",
message: "clone expects exactly one term argument".into(),
pos,
});
}
let inner = self.parse_term()?;
if !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
return Err(ParseError::Production {
production: "clone-term",
message: "clone expects exactly one term argument".into(),
pos,
});
}
self.expect_rparen("clone-term")?;
Ok(Term::Clone {
value: Box::new(inner),
})
}
/// Iter 18d.1: `(reuse-as SOURCE BODY)` — explicit reuse-as wrapper.
/// In 18d.1 the wrapper is identity for codegen (lowers `body` and
/// drops `source`); 18d.2 will lower this as in-place rewrite under
/// `--alloc=rc`. The parser accepts any term in either slot — the
/// "source must be a bare Var" rule and the "body must be allocating"
/// rule are enforced at typecheck/linearity time, not the parser.
fn parse_reuse_as(&mut self) -> Result<Term, ParseError> {
self.expect_lparen("reuse-as-term")?;
self.expect_keyword("reuse-as")?;
// Exactly two inner terms, then `)`.
if matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
return Err(ParseError::Production {
production: "reuse-as-term",
message: "reuse-as expects exactly two term arguments".into(),
pos,
});
}
let source = self.parse_term()?;
if matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
return Err(ParseError::Production {
production: "reuse-as-term",
message: "reuse-as expects exactly two term arguments".into(),
pos,
});
}
let body = self.parse_term()?;
if !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
return Err(ParseError::Production {
production: "reuse-as-term",
message: "reuse-as expects exactly two term arguments".into(),
pos,
});
}
self.expect_rparen("reuse-as-term")?;
Ok(Term::ReuseAs {
source: Box::new(source),
body: Box::new(body),
})
}
// ---- patterns -------------------------------------------------------
fn parse_pattern(&mut self) -> Result<Pattern, ParseError> {
match self.peek().cloned() {
Some(Token { tok: Tok::LParen, .. }) => {
let head = self.peek_head_ident().ok_or_else(|| {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
ParseError::Production {
production: "pattern",
message: "expected `pat-ctor` or `pat-lit` after `(`".into(),
pos,
}
})?;
match head {
"pat-ctor" => self.parse_pat_ctor(),
"pat-lit" => self.parse_pat_lit(),
other => {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
Err(ParseError::Production {
production: "pattern",
message: format!(
"unknown pattern head `{other}`; expected `pat-ctor` or `pat-lit`"
),
pos,
})
}
}
}
Some(Token { tok: Tok::Ident(s), .. }) => {
self.cur += 1;
if s == "_" {
Ok(Pattern::Wild)
} else {
Ok(Pattern::Var { name: s })
}
}
Some(t) => Err(ParseError::Unexpected {
expected: "pattern".into(),
got: tok_label(&t.tok),
pos: t.span.start,
}),
None => Err(ParseError::UnexpectedEof {
expected: "pattern".into(),
}),
}
}
fn parse_pat_ctor(&mut self) -> Result<Pattern, ParseError> {
self.expect_lparen("pat-ctor")?;
self.expect_keyword("pat-ctor")?;
let ctor = self.expect_ident("ctor name")?;
let mut fields = Vec::new();
while !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
fields.push(self.parse_pattern()?);
}
self.expect_rparen("pat-ctor")?;
Ok(Pattern::Ctor { ctor, fields })
}
fn parse_pat_lit(&mut self) -> Result<Pattern, ParseError> {
self.expect_lparen("pat-lit")?;
self.expect_keyword("pat-lit")?;
let lit = match self.peek().cloned() {
Some(Token { tok: Tok::Int(v), .. }) => {
self.cur += 1;
Literal::Int { value: v }
}
Some(Token { tok: Tok::Str(s), .. }) => {
self.cur += 1;
Literal::Str { value: s }
}
Some(Token { tok: Tok::Ident(s), span }) => {
if s == "true" {
self.cur += 1;
Literal::Bool { value: true }
} else if s == "false" {
self.cur += 1;
Literal::Bool { value: false }
} else {
return Err(ParseError::Unexpected {
expected: "literal form (integer, string, `true`, or `false`)".into(),
got: tok_label(&Tok::Ident(s)),
pos: span.start,
});
}
}
Some(t) => {
return Err(ParseError::Unexpected {
expected: "literal form (integer, string, `true`, or `false`)".into(),
got: tok_label(&t.tok),
pos: t.span.start,
});
}
None => {
return Err(ParseError::UnexpectedEof {
expected: "literal form".into(),
});
}
};
self.expect_rparen("pat-lit")?;
Ok(Pattern::Lit { lit })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_minimal_module() {
let m = parse(
r#"
(module hello
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (do io/print_str "Hello, AILang."))))
"#,
)
.unwrap();
assert_eq!(m.name, "hello");
assert_eq!(m.defs.len(), 1);
}
#[test]
fn parses_var_and_int() {
let m = parse(
r#"
(module m
(fn id
(type (fn-type (params (con Int)) (ret (con Int))))
(params x)
(body x)))
"#,
)
.unwrap();
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 18c.1: `(clone X)` parses to `Term::Clone { value: Var "x" }`.
#[test]
fn parses_clone_wraps_inner_term() {
let m = parse(
r#"
(module m
(fn id
(type (fn-type (params (con Int)) (ret (con Int))))
(params x)
(body (clone x))))
"#,
)
.unwrap();
let body = match &m.defs[0] {
Def::Fn(fd) => &fd.body,
_ => panic!("expected fn"),
};
match body {
Term::Clone { value } => match value.as_ref() {
Term::Var { name } => assert_eq!(name, "x"),
other => panic!("expected Var inside Clone, got {other:?}"),
},
other => panic!("expected Clone, got {other:?}"),
}
}
/// Iter 18c.1: `(clone)` with no inner term is rejected with a
/// clear message.
#[test]
fn rejects_clone_without_argument() {
let err = parse(
r#"
(module m
(fn id
(type (fn-type (params (con Int)) (ret (con Int))))
(params x)
(body (clone))))
"#,
)
.err()
.expect("parse should fail");
let msg = format!("{err}");
assert!(
msg.contains("clone expects exactly one term argument"),
"diagnostic should explain clone's arity, got: {msg}"
);
}
/// Iter 18d.1: `(reuse-as xs (term-ctor List Cons (...)))` parses to
/// `Term::ReuseAs { source: Var "xs", body: Ctor "Cons" ... }`.
#[test]
fn parses_reuse_as_wraps_source_and_body() {
let m = parse(
r#"
(module m
(data List (vars a)
(ctor Nil)
(ctor Cons a (con List a)))
(fn f
(type (fn-type (params (own (con List (con Int)))) (ret (own (con List (con Int))))))
(params xs)
(body (reuse-as xs (term-ctor List Cons 1 xs)))))
"#,
)
.unwrap();
let body = match &m.defs[1] {
Def::Fn(fd) => &fd.body,
_ => panic!("expected fn"),
};
match body {
Term::ReuseAs { source, body } => {
assert!(matches!(source.as_ref(), Term::Var { name } if name == "xs"));
match body.as_ref() {
Term::Ctor { type_name, ctor, args } => {
assert_eq!(type_name, "List");
assert_eq!(ctor, "Cons");
assert_eq!(args.len(), 2);
}
other => panic!("expected Ctor inside ReuseAs body, got {other:?}"),
}
}
other => panic!("expected ReuseAs, got {other:?}"),
}
}
/// Iter 18d.1: `(reuse-as)` with no args is rejected.
#[test]
fn rejects_reuse_as_with_no_arguments() {
let err = parse(
r#"
(module m
(fn f
(type (fn-type (params (con Int)) (ret (con Int))))
(params x)
(body (reuse-as))))
"#,
)
.err()
.expect("parse should fail");
let msg = format!("{err}");
assert!(
msg.contains("reuse-as expects exactly two term arguments"),
"diagnostic should explain reuse-as's arity, got: {msg}"
);
}
/// Iter 18d.1: `(reuse-as x)` with a single arg is rejected.
#[test]
fn rejects_reuse_as_with_one_argument() {
let err = parse(
r#"
(module m
(fn f
(type (fn-type (params (con Int)) (ret (con Int))))
(params x)
(body (reuse-as x))))
"#,
)
.err()
.expect("parse should fail");
let msg = format!("{err}");
assert!(
msg.contains("reuse-as expects exactly two term arguments"),
"diagnostic should explain reuse-as's arity, got: {msg}"
);
}
/// Iter 18d.1: round-trip via `parse_term` / `term_to_form_a` —
/// `(reuse-as xs (term-ctor List Cons 1 xs))` survives a print/parse
/// cycle as the same `Term::ReuseAs`.
#[test]
fn parse_term_round_trip_reuse_as() {
use crate::print::term_to_form_a;
let original = Term::ReuseAs {
source: Box::new(Term::Var { name: "xs".into() }),
body: Box::new(Term::Ctor {
type_name: "List".into(),
ctor: "Cons".into(),
args: vec![
Term::Lit { lit: Literal::Int { value: 1 } },
Term::Var { name: "xs".into() },
],
}),
};
let printed = term_to_form_a(&original);
let parsed = parse_term(&printed).expect("parse_term should succeed");
// Compare by canonical bytes (Term has no PartialEq) — easiest
// structural equality is via the printer.
assert_eq!(term_to_form_a(&parsed), printed);
}
/// Iter 18e: `(data T (drop-iterative))` parses with
/// `drop_iterative = true` AND round-trips through the printer
/// back to the same canonical surface form.
#[test]
fn parses_drop_iterative_annotation_on_data_decl() {
use crate::print::print;
let src = r#"
(module m
(data Tree
(vars a)
(ctor Leaf)
(ctor Node a (con Tree a) (con Tree a))
(drop-iterative)))
"#;
let m = parse(src).expect("parse should succeed");
match &m.defs[0] {
Def::Type(td) => {
assert_eq!(td.name, "Tree");
assert!(td.drop_iterative, "drop_iterative flag must be set");
}
_ => panic!("expected Def::Type"),
}
// Round-trip through the printer.
let printed = print(&m);
let m2 = parse(&printed).expect("re-parse should succeed");
match &m2.defs[0] {
Def::Type(td) => {
assert!(td.drop_iterative, "drop_iterative must survive print/parse");
}
_ => panic!("expected Def::Type"),
}
}
/// Iter 18e: `(data T)` with no `(drop-iterative)` clause parses
/// with `drop_iterative = false`. Default state must be the
/// pre-18e shape so legacy fixtures' canonical bytes are stable.
#[test]
fn parses_data_without_drop_iterative_defaults_to_false() {
let m = parse(
r#"
(module m
(data Tree
(vars a)
(ctor Leaf)
(ctor Node a (con Tree a) (con Tree a))))
"#,
)
.expect("parse should succeed");
match &m.defs[0] {
Def::Type(td) => {
assert!(!td.drop_iterative, "drop_iterative must default to false");
}
_ => panic!("expected Def::Type"),
}
}
/// Iter 18e: `(drop-iterative ...arg...)` is rejected — the
/// annotation is a flag with no payload.
#[test]
fn rejects_drop_iterative_with_arguments() {
let err = parse(
r#"
(module m
(data T
(ctor MkT)
(drop-iterative oops)))
"#,
)
.err()
.expect("parse should fail");
let msg = format!("{err}");
assert!(
msg.contains("drop-iterative takes no arguments"),
"diagnostic should explain drop-iterative's shape, 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.
#[test]
fn parses_minimal_let_rec() {
let m = parse(
r#"
(module m
(fn main
(type (fn-type (params) (ret (con Int))))
(params)
(body
(let-rec f
(params x)
(type (fn-type (params (con Int)) (ret (con Int))))
(body x)
(in (app f 1))))))
"#,
)
.unwrap();
let body = match &m.defs[0] {
Def::Fn(fd) => &fd.body,
_ => panic!("expected fn"),
};
match body {
Term::LetRec { name, params, body, in_term, .. } => {
assert_eq!(name, "f");
assert_eq!(params, &vec!["x".to_string()]);
assert!(matches!(body.as_ref(), Term::Var { name } if name == "x"));
match in_term.as_ref() {
Term::App { callee, args, .. } => {
assert!(matches!(callee.as_ref(), Term::Var { name } if name == "f"));
assert_eq!(args.len(), 1);
}
other => panic!("expected App in in-clause, got {other:?}"),
}
}
other => panic!("expected LetRec, got {other:?}"),
}
}
/// Iter 19b: a `(fn ...)` carrying a single
/// `(suppress (code "...") (because "..."))` clause parses into
/// [`FnDef::suppress`] with the corresponding [`Suppress`] entry.
#[test]
fn parses_single_suppress_clause_on_fn_def() {
let m = crate::parse::parse(
r#"
(module t
(fn f
(suppress (code "over-strict-mode") (because "test reason"))
(type (fn-type (params) (ret (con Int))))
(params)
(body 0)))
"#,
)
.unwrap();
match &m.defs[0] {
Def::Fn(fd) => {
assert_eq!(fd.suppress.len(), 1);
assert_eq!(fd.suppress[0].code, "over-strict-mode");
assert_eq!(fd.suppress[0].because, "test reason");
}
_ => panic!("expected fn"),
}
}
/// Iter 19b: multiple `(suppress ...)` clauses accumulate into
/// `FnDef::suppress` in declaration order. A second clause does
/// NOT overwrite the first.
#[test]
fn parses_multiple_suppress_clauses_in_order() {
let m = crate::parse::parse(
r#"
(module t
(fn f
(suppress (code "over-strict-mode") (because "first"))
(suppress (code "other-code") (because "second"))
(type (fn-type (params) (ret (con Int))))
(params)
(body 0)))
"#,
)
.unwrap();
match &m.defs[0] {
Def::Fn(fd) => {
assert_eq!(fd.suppress.len(), 2);
assert_eq!(fd.suppress[0].code, "over-strict-mode");
assert_eq!(fd.suppress[0].because, "first");
assert_eq!(fd.suppress[1].code, "other-code");
assert_eq!(fd.suppress[1].because, "second");
}
_ => panic!("expected fn"),
}
}
/// Iter 19b: a `(fn ...)` without a `(suppress ...)` clause has
/// `FnDef::suppress` empty (the default — round-trip identity
/// with pre-19b fixtures).
#[test]
fn parses_fn_def_without_suppress_has_empty_vec() {
let m = crate::parse::parse(
r#"
(module t
(fn f
(type (fn-type (params) (ret (con Int))))
(params)
(body 0)))
"#,
)
.unwrap();
match &m.defs[0] {
Def::Fn(fd) => {
assert!(fd.suppress.is_empty());
}
_ => panic!("expected fn"),
}
}
/// Iter 19b: a `(suppress ...)` with `(because "")` (empty
/// reason) still parses cleanly — the parser is intentionally
/// permissive; the typechecker is what emits
/// `empty-suppress-reason`. This keeps the surface symmetric
/// (a malformed input round-trips through the printer for
/// diagnostic display).
#[test]
fn parses_suppress_with_empty_because_string() {
let m = crate::parse::parse(
r#"
(module t
(fn f
(suppress (code "over-strict-mode") (because ""))
(type (fn-type (params) (ret (con Int))))
(params)
(body 0)))
"#,
)
.unwrap();
match &m.defs[0] {
Def::Fn(fd) => {
assert_eq!(fd.suppress.len(), 1);
assert_eq!(fd.suppress[0].because, "");
}
_ => panic!("expected fn"),
}
}
/// Iter 19b: an unknown sub-attribute inside `(suppress ...)`
/// produces a `ParseError::Production` naming the bad keyword and
/// listing the legal ones (`code` / `because`).
#[test]
fn rejects_suppress_with_unknown_subattribute() {
let err = crate::parse::parse(
r#"
(module t
(fn f
(suppress (code "over-strict-mode") (because "ok") (oops "x"))
(type (fn-type (params) (ret (con Int))))
(params)
(body 0)))
"#,
)
.unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains("oops") && msg.contains("code") && msg.contains("because"),
"error should name the bad keyword and the legal ones; got: {msg}"
);
}
#[test]
fn parses_minimal_class_def() {
let src = r#"(module M
(class Foo
(param a)
(method m
(type (fn-type (params (con a)) (ret (con Int)))))))"#;
let m = parse(src).expect("parse ok");
assert_eq!(m.defs.len(), 1, "one def");
match &m.defs[0] {
ailang_core::ast::Def::Class(c) => {
assert_eq!(c.name, "Foo");
assert_eq!(c.param, "a");
assert!(c.superclass.is_none(), "no superclass");
assert!(c.doc.is_none(), "no doc");
assert_eq!(c.methods.len(), 1, "one method");
assert_eq!(c.methods[0].name, "m");
assert!(c.methods[0].default.is_none(),
"no default");
}
other => panic!("expected Def::Class, got {other:?}"),
}
}
#[test]
fn parses_class_def_with_superclass_and_default() {
let src = r#"(module M
(class Bar
(param a)
(superclass (class Foo) (type a))
(doc "bar extends foo")
(method m
(type (fn-type (params (con a)) (ret (con Int))))
(default 0))))"#;
let m = parse(src).expect("parse ok");
let c = match &m.defs[0] {
ailang_core::ast::Def::Class(c) => c,
_ => panic!("expected Class"),
};
let sc = c.superclass.as_ref().expect("has superclass");
assert_eq!(sc.class, "Foo");
assert_eq!(sc.type_, "a");
assert_eq!(c.doc.as_deref(), Some("bar extends foo"));
assert!(c.methods[0].default.is_some(),
"method default present");
}
}