2060 lines
76 KiB
Rust
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");
|
|
}
|
|
}
|