8860600e37
Add `(let-rec NAME (params ...) (type ...) (body ...) (in ...))` as a form-A surface and a `Term::LetRec` AST variant. The 16a desugar pass lifts each LetRec whose body has no captures from the enclosing scope to a synthetic top-level fn `<hint>$lr_N` and substitutes the original name; typecheck and codegen never see LetRec. Capture detection panics at desugar time, queued for 16b.2. Tests: 95 → 99 (+1 e2e local_rec_factorial_demo, +2 desugar unit, +1 parse unit). The new fixture `examples/local_rec_demo` runs `fact` at n=1, 3, 5 → prints 1, 6, 120. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1160 lines
41 KiB
Rust
1160 lines
41 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 | type-attr | params-attr | body-attr
|
|
//! 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 ::= "(" "fn-type" "(" "params" type* ")"
|
|
//! "(" "ret" type ")"
|
|
//! 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
|
|
//! 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 ")" ")"
|
|
//!
|
|
//! 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, ConstDef, Ctor, Def, FnDef, Import, Literal, Module, Pattern, 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)
|
|
}
|
|
|
|
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()?)),
|
|
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`, 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();
|
|
loop {
|
|
match self.peek_head_ident() {
|
|
Some("doc") => {
|
|
let s = self.parse_doc()?;
|
|
doc = Some(s);
|
|
}
|
|
Some("ctor") => {
|
|
ctors.push(self.parse_ctor()?);
|
|
}
|
|
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` or `ctor`"
|
|
),
|
|
pos,
|
|
});
|
|
}
|
|
None => break,
|
|
}
|
|
}
|
|
self.expect_rparen("data-def")?;
|
|
Ok(TypeDef {
|
|
name,
|
|
vars,
|
|
ctors,
|
|
doc,
|
|
})
|
|
}
|
|
|
|
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;
|
|
loop {
|
|
match self.peek_head_ident() {
|
|
Some("doc") => doc = Some(self.parse_doc()?),
|
|
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`, `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,
|
|
})
|
|
}
|
|
|
|
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,
|
|
})
|
|
}
|
|
|
|
// ---- 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(),
|
|
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 type*)
|
|
self.expect_lparen("fn-type params")?;
|
|
self.expect_keyword("params")?;
|
|
let mut params: Vec<Type> = Vec::new();
|
|
while !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
|
|
params.push(self.parse_type()?);
|
|
}
|
|
self.expect_rparen("fn-type params")?;
|
|
// (ret type)
|
|
self.expect_lparen("fn-type ret")?;
|
|
self.expect_keyword("ret")?;
|
|
let ret = self.parse_type()?;
|
|
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")?;
|
|
Ok(Type::Fn {
|
|
params,
|
|
ret: Box::new(ret),
|
|
effects,
|
|
})
|
|
}
|
|
|
|
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,
|
|
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(),
|
|
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`, `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),
|
|
})
|
|
}
|
|
|
|
// ---- 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 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:?}"),
|
|
}
|
|
}
|
|
}
|