//! 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, Constraint, ConstDef, Ctor, Def, FnDef, Import, InstanceDef,
    InstanceMethod, 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::Float(bits) => format!("float `0x{bits:016x}`"),
        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()?)),
                "instance" => defs.push(Def::Instance(self.parse_instance()?)),
                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`, `instance`, 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") => {
                    if doc.is_some() {
                        return Err(self.duplicate_clause_err("data-def", &format!("data `{name}`"), "doc"));
                    }
                    doc = Some(self.parse_doc()?);
                }
                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") => {
                    if doc.is_some() {
                        return Err(self.duplicate_clause_err("fn-def", &format!("fn `{name}`"), "doc"));
                    }
                    doc = Some(self.parse_doc()?);
                }
                Some("suppress") => suppress.push(self.parse_suppress_attr()?),
                Some("type") => {
                    if ty.is_some() {
                        return Err(self.duplicate_clause_err("fn-def", &format!("fn `{name}`"), "type"));
                    }
                    ty = Some(self.parse_type_attr()?);
                }
                Some("params") => {
                    if params.is_some() {
                        return Err(self.duplicate_clause_err("fn-def", &format!("fn `{name}`"), "params"));
                    }
                    params = Some(self.parse_params_attr()?);
                }
                Some("body") => {
                    if body.is_some() {
                        return Err(self.duplicate_clause_err("fn-def", &format!("fn `{name}`"), "body"));
                    }
                    body = Some(self.parse_body_attr()?);
                }
                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 duplicate_clause_err(
        &self,
        production: &'static str,
        subject: &str,
        clause: &'static str,
    ) -> ParseError {
        let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
        ParseError::Production {
            production,
            message: format!("{subject} has duplicate `({clause} ...)` clause"),
            pos,
        }
    }

    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") => {
                    if doc.is_some() {
                        return Err(self.duplicate_clause_err("const-def", &format!("const `{name}`"), "doc"));
                    }
                    doc = Some(self.parse_doc()?);
                }
                Some("type") => {
                    if ty.is_some() {
                        return Err(self.duplicate_clause_err("const-def", &format!("const `{name}`"), "type"));
                    }
                    ty = Some(self.parse_type_attr()?);
                }
                Some("body") => {
                    if value.is_some() {
                        return Err(self.duplicate_clause_err("const-def", &format!("const `{name}`"), "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() {
                        return Err(self.duplicate_clause_err("class-def", &format!("class `{name}`"), "param"));
                    }
                    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() {
                        return Err(self.duplicate_clause_err("class-def", &format!("class `{name}`"), "superclass"));
                    }
                    superclass = Some(self.parse_superclass()?);
                }
                Some("doc") => {
                    if doc.is_some() {
                        return Err(self.duplicate_clause_err("class-def", &format!("class `{name}`"), "doc"));
                    }
                    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() {
                        return Err(self.duplicate_clause_err("class.method", &format!("method `{name}`"), "type"));
                    }
                    ty = Some(self.parse_type_attr()?);
                }
                Some("default") => {
                    if default.is_some() {
                        return Err(self.duplicate_clause_err("class.method", &format!("method `{name}`"), "default"));
                    }
                    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)
    }

    // ---- instance def --------------------------------------------------

    fn parse_instance(&mut self) -> Result<InstanceDef, ParseError> {
        self.expect_lparen("instance-def")?;
        self.expect_keyword("instance")?;
        let mut class: Option<String> = None;
        let mut type_: Option<Type> = None;
        let mut doc: Option<String> = None;
        let mut methods: Vec<InstanceMethod> = Vec::new();
        loop {
            match self.peek_head_ident() {
                Some("class") => {
                    if class.is_some() {
                        return Err(self.duplicate_clause_err("instance-def", "instance", "class"));
                    }
                    self.expect_lparen("instance.class")?;
                    self.expect_keyword("class")?;
                    class = Some(self.expect_ident("instance class name")?);
                    self.expect_rparen("instance.class")?;
                }
                Some("type") => {
                    if type_.is_some() {
                        return Err(self.duplicate_clause_err("instance-def", "instance", "type"));
                    }
                    type_ = Some(self.parse_type_attr()?);
                }
                Some("doc") => {
                    if doc.is_some() {
                        return Err(self.duplicate_clause_err("instance-def", "instance", "doc"));
                    }
                    doc = Some(self.parse_doc()?);
                }
                Some("method") => {
                    methods.push(self.parse_instance_method()?);
                }
                Some(other) => {
                    let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
                    return Err(ParseError::Production {
                        production: "instance-def",
                        message: format!(
                            "unknown instance attribute `{other}`; expected `class`, `type`, `doc`, or `method`"
                        ),
                        pos,
                    });
                }
                None => break,
            }
        }
        self.expect_rparen("instance-def")?;
        let class = class.ok_or_else(|| ParseError::Production {
            production: "instance-def",
            message: "instance is missing required `(class ...)` clause".into(),
            pos: 0,
        })?;
        let type_ = type_.ok_or_else(|| ParseError::Production {
            production: "instance-def",
            message: "instance is missing required `(type ...)` clause".into(),
            pos: 0,
        })?;
        Ok(InstanceDef {
            class,
            type_,
            methods,
            doc,
        })
    }

    fn parse_instance_method(&mut self) -> Result<InstanceMethod, ParseError> {
        self.expect_lparen("instance.method")?;
        self.expect_keyword("method")?;
        let name = self.expect_ident("instance method name")?;
        let mut body: Option<Term> = None;
        loop {
            match self.peek_head_ident() {
                Some("body") => {
                    if body.is_some() {
                        return Err(self.duplicate_clause_err("instance.method", &format!("instance method `{name}`"), "body"));
                    }
                    body = Some(self.parse_body_attr()?);
                }
                Some(other) => {
                    let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
                    return Err(ParseError::Production {
                        production: "instance.method",
                        message: format!(
                            "unknown instance.method attribute `{other}`; expected `body`"
                        ),
                        pos,
                    });
                }
                None => break,
            }
        }
        self.expect_rparen("instance.method")?;
        let body = body.ok_or_else(|| ParseError::Production {
            production: "instance.method",
            message: format!(
                "instance method `{name}` is missing required `(body ...)` clause"
            ),
            pos: 0,
        })?;
        Ok(InstanceMethod { name, body })
    }

    // ---- 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")?;
        // optional (constraints (constraint <Class> <type>)+ )
        let constraints = if let Some("constraints") = self.peek_head_ident() {
            self.parse_forall_constraints_clause()?
        } else {
            Vec::new()
        };
        let body = self.parse_type()?;
        self.expect_rparen("forall-type")?;
        Ok(Type::Forall {
            vars,
            constraints,
            body: Box::new(body),
        })
    }

    fn parse_forall_constraints_clause(&mut self) -> Result<Vec<Constraint>, ParseError> {
        self.expect_lparen("forall.constraints")?;
        self.expect_keyword("constraints")?;
        let mut out = Vec::new();
        while !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
            out.push(self.parse_constraint()?);
        }
        if out.is_empty() {
            let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
            return Err(ParseError::Production {
                production: "forall.constraints",
                message: "(constraints …) clause must contain at least one (constraint …)".into(),
                pos,
            });
        }
        self.expect_rparen("forall.constraints")?;
        Ok(out)
    }

    fn parse_constraint(&mut self) -> Result<Constraint, ParseError> {
        self.expect_lparen("constraint")?;
        self.expect_keyword("constraint")?;
        let class = self.expect_ident("constraint class name")?;
        let type_ = self.parse_type()?;
        self.expect_rparen("constraint")?;
        Ok(Constraint { class, type_ })
    }

    // ---- 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::Float(bits), .. }) => {
                self.cur += 1;
                Ok(Term::Lit { lit: Literal::Float { bits } })
            }
            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::Float(bits), .. }) => {
                self.cur += 1;
                Literal::Float { bits }
            }
            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, float, string, `true`, or `false`)".into(),
                        got: tok_label(&Tok::Ident(s)),
                        pos: span.start,
                    });
                }
            }
            Some(t) => {
                return Err(ParseError::Unexpected {
                    expected: "literal form (integer, float, 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");
    }

    #[test]
    fn parses_minimal_instance_def() {
        let src = r#"(module M
  (instance
    (class Foo)
    (type (con Int))
    (method m
      (body 5))))"#;
        let m = parse(src).expect("parse ok");
        match &m.defs[0] {
            ailang_core::ast::Def::Instance(i) => {
                assert_eq!(i.class, "Foo");
                match &i.type_ {
                    ailang_core::ast::Type::Con { name, args } => {
                        assert_eq!(name, "Int");
                        assert!(args.is_empty(), "Int has no args");
                    }
                    other => panic!("expected Type::Con, got {other:?}"),
                }
                assert!(i.doc.is_none());
                assert_eq!(i.methods.len(), 1);
                assert_eq!(i.methods[0].name, "m");
            }
            other => panic!("expected Def::Instance, got {other:?}"),
        }
    }

    #[test]
    fn parses_instance_def_with_doc() {
        let src = r#"(module M
  (instance
    (class Foo)
    (type (con Int))
    (doc "instance for ints")
    (method m (body 7))))"#;
        let m = parse(src).expect("parse ok");
        let i = match &m.defs[0] {
            ailang_core::ast::Def::Instance(i) => i,
            _ => panic!("expected Instance"),
        };
        assert_eq!(i.doc.as_deref(), Some("instance for ints"));
    }

    #[test]
    fn parse_fn_rejects_duplicate_doc_clause() {
        let err = parse(
            r#"
            (module m
              (fn f
                (doc "first")
                (doc "second")
                (type (fn-type (params) (ret (con Unit))))
                (params)
                (body (do io/print_str "x"))))
            "#,
        )
        .unwrap_err();
        let msg = format!("{err:?}");
        assert!(
            msg.contains("fn `f` has duplicate `(doc ...)` clause"),
            "expected duplicate-doc diagnostic, got: {msg}"
        );
    }

    #[test]
    fn parse_fn_rejects_duplicate_type_clause() {
        let err = parse(
            r#"
            (module m
              (fn f
                (type (fn-type (params) (ret (con Unit))))
                (type (fn-type (params) (ret (con Unit))))
                (params)
                (body (do io/print_str "x"))))
            "#,
        )
        .unwrap_err();
        let msg = format!("{err:?}");
        assert!(
            msg.contains("fn `f` has duplicate `(type ...)` clause"),
            "expected duplicate-type diagnostic, got: {msg}"
        );
    }

    #[test]
    fn parse_fn_rejects_duplicate_params_clause() {
        let err = parse(
            r#"
            (module m
              (fn f
                (type (fn-type (params) (ret (con Unit))))
                (params)
                (params)
                (body (do io/print_str "x"))))
            "#,
        )
        .unwrap_err();
        let msg = format!("{err:?}");
        assert!(
            msg.contains("fn `f` has duplicate `(params ...)` clause"),
            "expected duplicate-params diagnostic, got: {msg}"
        );
    }

    #[test]
    fn parse_fn_rejects_duplicate_body_clause() {
        let err = parse(
            r#"
            (module m
              (fn f
                (type (fn-type (params) (ret (con Unit))))
                (params)
                (body (do io/print_str "x"))
                (body (do io/print_str "y"))))
            "#,
        )
        .unwrap_err();
        let msg = format!("{err:?}");
        assert!(
            msg.contains("fn `f` has duplicate `(body ...)` clause"),
            "expected duplicate-body diagnostic, got: {msg}"
        );
    }

    #[test]
    fn parse_const_rejects_duplicate_doc_clause() {
        let err = parse(
            r#"
            (module m
              (const c
                (doc "first")
                (doc "second")
                (type (con Int))
                (body 42)))
            "#,
        )
        .unwrap_err();
        let msg = format!("{err:?}");
        assert!(
            msg.contains("const `c` has duplicate `(doc ...)` clause"),
            "expected duplicate-doc diagnostic, got: {msg}"
        );
    }

    #[test]
    fn parse_const_rejects_duplicate_type_clause() {
        let err = parse(
            r#"
            (module m
              (const c
                (type (con Int))
                (type (con Int))
                (body 42)))
            "#,
        )
        .unwrap_err();
        let msg = format!("{err:?}");
        assert!(
            msg.contains("const `c` has duplicate `(type ...)` clause"),
            "expected duplicate-type diagnostic, got: {msg}"
        );
    }

    #[test]
    fn parse_const_rejects_duplicate_body_clause() {
        let err = parse(
            r#"
            (module m
              (const c
                (type (con Int))
                (body 1)
                (body 2)))
            "#,
        )
        .unwrap_err();
        let msg = format!("{err:?}");
        assert!(
            msg.contains("const `c` has duplicate `(body ...)` clause"),
            "expected duplicate-body diagnostic, got: {msg}"
        );
    }

    #[test]
    fn parse_data_rejects_duplicate_doc_clause() {
        let err = parse(
            r#"
            (module m
              (data D
                (doc "first")
                (doc "second")
                (ctor C)))
            "#,
        )
        .unwrap_err();
        let msg = format!("{err:?}");
        assert!(
            msg.contains("data `D` has duplicate `(doc ...)` clause"),
            "expected duplicate-doc diagnostic, got: {msg}"
        );
    }

    /// Floats milestone iter 2.2 RED: `1.5` parses as
    /// `Term::Lit { lit: Literal::Float { bits: 0x3ff8_0000_0000_0000 } }`.
    /// Property protected: a `Tok::Float` produced by the lexer flows through
    /// `parse_term` into a `Literal::Float` AST node carrying the same bit
    /// pattern, so the surface→AST translation is bit-exact.
    #[test]
    fn parses_float_atom_term() {
        let t = parse_term("1.5").expect("parse term");
        match t {
            Term::Lit { lit: Literal::Float { bits } } => {
                assert_eq!(bits, 0x3ff8_0000_0000_0000u64);
            }
            other => panic!("expected Term::Lit::Float, got {other:?}"),
        }
    }
}
