//! Recursive-descent parser: laid-out token stream → typed AST (src/ast.rs).
//!
//! Error recovery is per-declaration: an unparseable declaration becomes
//! `Decl::Unknown` plus a diagnostic, and parsing continues at the next
//! virtual semicolon. The parser never panics and never aborts the file.

use crate::ast::*;
use crate::layout::resolve_layout;
use crate::lexer::{lex, Pos, Tok, Token};

pub fn parse_module(source: &str) -> (Module, Vec<ParseDiagnostic>) {
    let (tokens, lex_errors) = lex(source);
    let tokens = resolve_layout(tokens);
    let mut p = Parser {
        toks: tokens,
        src_len: source.len(),
        i: 0,
        depth: 0,
        diags: lex_errors
            .into_iter()
            .map(|e| {
                let b = byte_of_pos(source, e.pos);
                ParseDiagnostic {
                    message: e.message,
                    pos: e.pos,
                    span: crate::ast::Span::new(b, b),
                    category: DiagnosticCategory::Lex,
                }
            })
            .collect(),
    };
    let mut module = p.module();
    module.span = crate::ast::Span::new(0, source.len());
    (module, p.diags)
}

/// Byte offset of a 1-based (line, column) position, for mapping a lexer error
/// (which carries only line/column) to a byte span. Replays the lexer's own
/// column accounting, including tab stops, so the byte is exact even on lines
/// with leading tabs.
fn byte_of_pos(source: &str, pos: Pos) -> usize {
    let mut line = 1usize;
    let mut col = 1usize;
    for (idx, ch) in source.char_indices() {
        if line == pos.line && col == pos.column {
            return idx;
        }
        match ch {
            '\n' => {
                line += 1;
                col = 1;
            }
            '\t' => col = ((col - 1) / crate::lexer::TAB_STOP + 1) * crate::lexer::TAB_STOP + 1,
            _ => col += 1,
        }
    }
    source.len()
}

struct Parser {
    toks: Vec<Token>,
    /// Source byte length — span fallback when a node consumes no real token.
    src_len: usize,
    i: usize,
    diags: Vec<ParseDiagnostic>,
    /// Expression/pattern recursion depth; bounded so hostile inputs
    /// (thousands of nested parens) cannot overflow the stack.
    depth: u32,
}

const MAX_DEPTH: u32 = 128;

impl Parser {
    /// Byte span of every non-virtual token consumed since token index `from`
    /// (a function's entry cursor). This is the node's full extent: first real
    /// token's `start` to last real token's `end`. Virtual layout tokens carry
    /// no bytes and are skipped, so spans tile the source and never include the
    /// trailing whitespace a `VRBrace`/`VSemi` sits on.
    fn node_span(&self, from: usize) -> crate::ast::Span {
        let mut a = from;
        while a < self.i && self.toks[a].is_virtual() {
            a += 1;
        }
        let mut b = self.i;
        while b > a && self.toks[b - 1].is_virtual() {
            b -= 1;
        }
        if a >= b {
            // No real token consumed (e.g. an empty error node): zero-width
            // span at the next real byte position so it still nests inside its
            // parent. Use `a` (past any leading virtual tokens) — `from` itself
            // may be a virtual token whose byte offset is a meaningless 0.
            let p = self.byte_at(a);
            return crate::ast::Span::new(p, p);
        }
        crate::ast::Span::new(self.toks[a].start, self.toks[b - 1].end)
    }

    /// Byte offset where token `i` begins, or the source end past the last one.
    fn byte_at(&self, i: usize) -> usize {
        self.toks.get(i).map(|t| t.start).unwrap_or(self.src_len)
    }

    /// End byte of the last non-virtual token consumed so far — for nodes
    /// whose start comes from an already-parsed child rather than the entry
    /// cursor (e.g. `record with { .. }` built around its base expression).
    fn end_byte(&self) -> usize {
        let mut b = self.i;
        while b > 0 && self.toks[b - 1].is_virtual() {
            b -= 1;
        }
        if b == 0 {
            0
        } else {
            self.toks[b - 1].end
        }
    }
}

impl Parser {
    // ----- cursor primitives -------------------------------------------

    fn peek(&self) -> Option<&Tok> {
        self.toks.get(self.i).map(|t| &t.tok)
    }

    fn peek_at(&self, n: usize) -> Option<&Tok> {
        self.toks.get(self.i + n).map(|t| &t.tok)
    }

    fn pos(&self) -> Pos {
        self.toks
            .get(self.i)
            .or_else(|| self.toks.last())
            .map_or(Pos { line: 1, column: 1 }, |t| t.pos)
    }

    fn bump(&mut self) -> Option<Token> {
        let t = self.toks.get(self.i).cloned();
        if t.is_some() {
            self.i += 1;
        }
        t
    }

    fn at_keyword(&self, kw: &str) -> bool {
        self.peek().is_some_and(|t| t.is_keyword(kw))
    }

    fn eat_keyword(&mut self, kw: &str) -> bool {
        if self.at_keyword(kw) {
            self.i += 1;
            true
        } else {
            false
        }
    }

    fn at_op(&self, op: &str) -> bool {
        self.peek().is_some_and(|t| t.is_op(op))
    }

    fn eat_op(&mut self, op: &str) -> bool {
        if self.at_op(op) {
            self.i += 1;
            true
        } else {
            false
        }
    }

    fn at(&self, tok: &Tok) -> bool {
        self.peek() == Some(tok)
    }

    fn eat(&mut self, tok: &Tok) -> bool {
        if self.at(tok) {
            self.i += 1;
            true
        } else {
            false
        }
    }

    /// Emit a `Malformed` diagnostic at the current token (the common case).
    fn diag(&mut self, message: impl Into<String>) {
        self.diag_cat(DiagnosticCategory::Malformed, message);
    }

    /// Emit a diagnostic with an explicit recovery category. The span is the
    /// current token's byte extent (the offending token), so consumers get an
    /// end position, not just a start.
    fn diag_cat(&mut self, category: DiagnosticCategory, message: impl Into<String>) {
        let pos = self.pos();
        let span = self.cur_span();
        self.diags.push(ParseDiagnostic {
            message: message.into(),
            pos,
            span,
            category,
        });
    }

    /// Byte span of the next real (non-virtual) token, or a zero-width span at
    /// end-of-input. Used to anchor a diagnostic to the offending token.
    fn cur_span(&self) -> crate::ast::Span {
        let mut j = self.i;
        while self.toks.get(j).is_some_and(|t| t.is_virtual()) {
            j += 1;
        }
        self.toks.get(j).map_or_else(
            || crate::ast::Span::new(self.src_len, self.src_len),
            |t| crate::ast::Span::new(t.start, t.end),
        )
    }

    /// Skip tokens until the end of the current block item: a VSemi or
    /// VRBrace at nesting depth zero (relative to here). Consumes neither.
    fn skip_to_item_end(&mut self) {
        let mut depth = 0usize;
        let mut brackets = 0usize;
        while let Some(t) = self.peek() {
            match t {
                Tok::VLBrace => depth += 1,
                Tok::VRBrace => {
                    if depth == 0 {
                        return;
                    }
                    depth -= 1;
                }
                Tok::VSemi if depth == 0 && brackets == 0 => return,
                Tok::LParen | Tok::LBracket | Tok::LBrace => brackets += 1,
                Tok::RParen | Tok::RBracket | Tok::RBrace => {
                    if brackets == 0 {
                        // Closing bracket of an enclosing construct: stop
                        // before it so the caller can match it.
                        return;
                    }
                    brackets -= 1;
                }
                _ => {}
            }
            self.i += 1;
        }
    }

    /// Raw text of tokens from `start` to the current position.
    fn slice_text(&self, start: usize) -> String {
        render_token_slice(&self.toks[start..self.i])
    }

    // ----- module ------------------------------------------------------

    fn module(&mut self) -> Module {
        let pos = self.pos();
        let header_start = self.i;
        let mut header = crate::ast::Span::new(0, 0);
        let mut name = "Unknown".to_string();

        if self.eat_keyword("module") {
            if let Some(Tok::UpperId { qualifier, name: n }) = self.peek().cloned() {
                self.bump();
                name = match qualifier {
                    Some(q) => format!("{}.{}", q, n),
                    None => n,
                };
            }
            // Optional export list.
            if self.at(&Tok::LParen) {
                self.skip_balanced_parens();
            }
            if !self.eat_keyword("where") {
                self.diag("expected 'where' after module header");
            }
            header = self.node_span(header_start);
        }

        let mut imports = Vec::new();
        let mut decls: Vec<Decl> = Vec::new();

        let in_block = self.eat(&Tok::VLBrace) || self.eat(&Tok::LBrace);
        loop {
            while self.eat(&Tok::VSemi) || self.eat(&Tok::Semi) {}
            match self.peek() {
                None => break,
                Some(Tok::VRBrace) | Some(Tok::RBrace) => {
                    self.bump();
                    break;
                }
                // A stray closing bracket inside a block is garbage from a
                // failed item parse — record it as an Unknown declaration so
                // its bytes stay covered, then continue (skip_to_item_end
                // deliberately stops before unmatched closers).
                Some(Tok::RParen) | Some(Tok::RBracket) => {
                    let cpos = self.pos();
                    let cstart = self.i;
                    self.bump();
                    decls.push(Decl::Unknown {
                        raw: self.slice_text(cstart),
                        pos: cpos,
                        span: self.node_span(cstart),
                    });
                    continue;
                }
                _ => {}
            }
            let before = self.i;
            self.declaration(&mut imports, &mut decls);
            if self.i == before {
                // Defensive: guarantee progress even on a parser bug.
                self.bump();
            }
        }
        let _ = in_block;

        merge_functions(&mut decls);

        Module {
            name,
            pos,
            header,
            imports,
            decls,
            span: crate::ast::Span::new(0, self.src_len),
        }
    }

    fn skip_balanced_parens(&mut self) {
        let mut depth = 0usize;
        while let Some(t) = self.peek() {
            match t {
                Tok::LParen => depth += 1,
                Tok::RParen => {
                    depth -= 1;
                    if depth == 0 {
                        self.i += 1;
                        return;
                    }
                }
                _ => {}
            }
            self.i += 1;
        }
    }

    /// If the cursor sits on an infix operator equation with a pattern
    /// left operand (`[] !! _ = ...`, `None <?> s = ...`), skip it and
    /// return true. Operators have no IR surface.
    fn try_infix_operator_decl(&mut self) -> bool {
        let snap = self.i;
        let saved_diags = self.diags.len();
        if self.pattern().is_some() && matches!(self.peek(), Some(Tok::Op(o)) if !is_reserved_op(o))
        {
            self.skip_to_item_end();
            return true;
        }
        self.i = snap;
        self.diags.truncate(saved_diags);
        false
    }

    fn declaration(&mut self, imports: &mut Vec<ImportDecl>, decls: &mut Vec<Decl>) {
        let pos = self.pos();
        let start = self.i;
        if matches!(
            self.peek(),
            Some(Tok::UpperId { .. } | Tok::LBracket | Tok::LParen)
        ) && self.try_infix_operator_decl()
        {
            decls.push(Decl::Unknown {
                raw: self.slice_text(start),
                pos,
                span: self.node_span(start),
            });
            return;
        }
        match self.peek() {
            Some(t) if t.is_keyword("import") => {
                let imp = self.import_decl();
                // The `(...)` import list / `hiding (...)` clause is consumed
                // here, after the decl is built; fold it into the span so the
                // import covers its whole source extent.
                self.skip_to_item_end();
                if let Some(mut imp) = imp {
                    imp.span = self.node_span(start);
                    imports.push(imp);
                }
            }
            Some(t) if t.is_keyword("template") => {
                // `template T = ...` (template-let synonym) is exotic; only
                // `template Name with/where` is a template declaration.
                match self.template_decl() {
                    Some(t) => decls.push(Decl::Template(t)),
                    None => {
                        self.skip_to_item_end();
                        decls.push(Decl::Unknown {
                            raw: self.slice_text(start),
                            span: self.node_span(start),
                            pos,
                        });
                    }
                }
            }
            Some(t) if t.is_keyword("interface") => match self.interface_decl() {
                Some(i) => decls.push(Decl::Interface(i)),
                None => {
                    self.skip_to_item_end();
                    decls.push(Decl::Unknown {
                        raw: self.slice_text(start),
                        span: self.node_span(start),
                        pos,
                    });
                }
            },
            Some(t)
                if matches!(
                    t.keyword(),
                    // Fixity declarations, class-default declarations, and
                    // pattern synonyms have no IR surface.
                    Some("infix" | "infixl" | "infixr" | "default" | "pattern")
                ) =>
            {
                self.skip_to_item_end();
                decls.push(Decl::Unknown {
                    raw: self.slice_text(start),
                    pos,
                    span: self.node_span(start),
                });
            }
            Some(t)
                if matches!(
                    t.keyword(),
                    Some(
                        "data"
                            | "type"
                            | "newtype"
                            | "class"
                            | "instance"
                            | "exception"
                            | "deriving"
                    )
                ) =>
            {
                let keyword = t.keyword().unwrap().to_string();
                self.bump();
                let name = match self.peek() {
                    Some(Tok::UpperId { qualifier, name }) => {
                        let n = qualifier
                            .as_ref()
                            .map_or_else(|| name.clone(), |q| format!("{}.{}", q, name));
                        self.bump();
                        n
                    }
                    _ => String::new(),
                };
                self.skip_to_item_end();
                decls.push(Decl::TypeDef {
                    keyword,
                    name,
                    pos,
                    span: self.node_span(start),
                });
            }
            Some(Tok::LowerId { .. }) => match self.function_item() {
                Some(d) => decls.push(d),
                None => {
                    self.skip_to_item_end();
                    decls.push(Decl::Unknown {
                        raw: self.slice_text(start),
                        span: self.node_span(start),
                        pos,
                    });
                }
            },
            // Operator definition or signature: `(<=) = curry Lte`.
            Some(Tok::LParen)
                if matches!(self.peek_at(1), Some(Tok::Op(_)))
                    && self.peek_at(2) == Some(&Tok::RParen) =>
            {
                self.skip_to_item_end();
                decls.push(Decl::Unknown {
                    raw: self.slice_text(start),
                    span: self.node_span(start),
                    pos,
                });
            }
            // Top-level pattern binding: `[a, b, c] = ...`, `(x, y) = ...`.
            Some(Tok::LParen) | Some(Tok::LBracket) => {
                if self.binding().is_none() {
                    self.diag_cat(
                        DiagnosticCategory::SkippedDecl,
                        "unparseable top-level pattern binding",
                    );
                }
                self.skip_to_item_end();
                decls.push(Decl::Unknown {
                    raw: self.slice_text(start),
                    span: self.node_span(start),
                    pos,
                });
            }
            _ => {
                self.diag_cat(
                    DiagnosticCategory::SkippedDecl,
                    format!("unrecognized declaration: {:?}", self.peek()),
                );
                self.skip_to_item_end();
                decls.push(Decl::Unknown {
                    raw: self.slice_text(start),
                    span: self.node_span(start),
                    pos,
                });
            }
        }
    }

    // ----- imports -----------------------------------------------------

    fn import_decl(&mut self) -> Option<ImportDecl> {
        let pos = self.pos();
        let start_i = self.i;
        self.bump(); // import
        let mut qualified = self.eat_keyword("qualified");
        // Package-qualified import: `import qualified "pkg-name" Main as V1`.
        if matches!(self.peek(), Some(Tok::StringLit(_))) {
            self.bump();
        }
        let module_name = match self.peek().cloned() {
            Some(Tok::UpperId { qualifier, name }) => {
                self.bump();
                match qualifier {
                    Some(q) => format!("{}.{}", q, name),
                    None => name,
                }
            }
            _ => {
                self.diag("expected module name after 'import'");
                return None;
            }
        };
        // ImportQualifiedPost style: `import DA.Map qualified as Map`.
        if self.eat_keyword("qualified") {
            qualified = true;
        }
        let mut alias = None;
        if self.eat_keyword("as") {
            if let Some(Tok::UpperId { qualifier, name }) = self.peek().cloned() {
                self.bump();
                alias = Some(match qualifier {
                    Some(q) => format!("{}.{}", q, name),
                    None => name,
                });
            }
        }
        // `hiding (...)` / import list — consumed by skip_to_item_end.
        Some(ImportDecl {
            module_name,
            qualified,
            alias,
            pos,
            span: self.node_span(start_i),
        })
    }

    // ----- templates ---------------------------------------------------

    fn upper_name(&mut self) -> Option<String> {
        match self.peek().cloned() {
            Some(Tok::UpperId { qualifier, name }) => {
                self.bump();
                Some(match qualifier {
                    Some(q) => format!("{}.{}", q, name),
                    None => name,
                })
            }
            _ => None,
        }
    }

    fn template_decl(&mut self) -> Option<TemplateDecl> {
        let pos = self.pos();
        let start_i = self.i;
        self.bump(); // template
        if self.at_keyword("instance") {
            return None; // legacy `template instance` — not a template
        }
        let name = self.upper_name()?;

        let mut fields = Vec::new();
        if self.eat_keyword("with") {
            (fields, _) = self.field_block();
        }
        let body = if self.eat_keyword("where") {
            self.template_body()
        } else {
            Vec::new()
        };
        Some(TemplateDecl {
            name,
            fields,
            body,
            pos,
            span: self.node_span(start_i),
        })
    }

    /// `{ name : Type ; name2, name3 : Type ; ... }` (virtual or explicit).
    /// Returns the fields plus a "dangling" flag: true when the block was
    /// entered but abandoned early because its first item is not a field
    /// (an empty `with` whose layout block swallowed the next clause) —
    /// the caller must discard the block's eventual closing VRBrace.
    fn field_block(&mut self) -> (Vec<FieldDecl>, bool) {
        let mut fields = Vec::new();
        if !(self.eat(&Tok::VLBrace) || self.eat(&Tok::LBrace)) {
            return (fields, false);
        }
        loop {
            while self.eat(&Tok::VSemi) || self.eat(&Tok::Semi) {}
            match self.peek() {
                None => break,
                Some(Tok::VRBrace) | Some(Tok::RBrace) => {
                    self.bump();
                    break;
                }
                // A stray closing bracket inside a block is garbage from a
                // failed item parse — discard it or the loop cannot make
                // progress (skip_to_item_end deliberately stops before
                // unmatched closers).
                Some(Tok::RParen) | Some(Tok::RBracket) => {
                    self.bump();
                    continue;
                }
                _ => {}
            }
            // A field item must look like `name [, name] : Type`. If the
            // next item doesn't (an empty `with` swallowed the following
            // clause into its layout block — `with` + comment + `controller`),
            // stop without consuming so the caller can parse the clause.
            {
                let mut j = self.i;
                while let Some(Tok::LowerId {
                    qualifier: None, ..
                }) = self.toks.get(j).map(|t| &t.tok)
                {
                    j += 1;
                    match self.toks.get(j).map(|t| &t.tok) {
                        Some(Tok::Comma) => j += 1,
                        _ => break,
                    }
                }
                let is_field = j > self.i
                    && self
                        .toks
                        .get(j)
                        .map(|t| &t.tok)
                        .is_some_and(|t| t.is_op(":"));
                if !is_field {
                    return (fields, true);
                }
            }
            let before = self.i;
            // One or more comma-separated names, then `:`, then the type.
            let mut names: Vec<(String, Pos, Span)> = Vec::new();
            while let Some(Tok::LowerId {
                qualifier: None,
                name,
            }) = self.peek().cloned()
            {
                let p = self.pos();
                let nspan = Span::new(self.toks[self.i].start, self.toks[self.i].end);
                self.bump();
                names.push((name, p, nspan));
                if !self.eat(&Tok::Comma) {
                    break;
                }
            }
            if names.is_empty() || !self.eat_op(":") {
                self.diag("expected 'name : Type' field");
                self.skip_to_item_end();
                let _ = before;
                continue;
            }
            let ty_start = self.i;
            self.skip_to_item_end();
            let ty = parse_type_from_tokens(&self.toks[ty_start..self.i]);
            // The type is shared by all names but sits after the last one, so
            // only the last field can span `name : Type` without overlapping a
            // sibling; earlier names of `x, y : T` stay name-only. daml-fmt
            // reads the type extent off the last field of a comma group.
            let type_end = self.end_byte();
            let last = names.len() - 1;
            for (idx, (name, p, nspan)) in names.into_iter().enumerate() {
                let span = if idx == last {
                    Span::new(nspan.start, type_end.max(nspan.end))
                } else {
                    nspan
                };
                fields.push(FieldDecl {
                    name,
                    ty: ty.clone(),
                    pos: p,
                    span,
                });
            }
        }
        (fields, false)
    }

    // ----- template body ------------------------------------------------

    fn template_body(&mut self) -> Vec<TemplateBodyDecl> {
        let mut body = Vec::new();
        if !(self.eat(&Tok::VLBrace) || self.eat(&Tok::LBrace)) {
            return body;
        }
        loop {
            while self.eat(&Tok::VSemi) || self.eat(&Tok::Semi) {}
            match self.peek() {
                None => break,
                Some(Tok::VRBrace) | Some(Tok::RBrace) => {
                    self.bump();
                    break;
                }
                // A stray closing bracket inside a block is garbage from a
                // failed item parse — discard it or the loop cannot make
                // progress (skip_to_item_end deliberately stops before
                // unmatched closers).
                Some(Tok::RParen) | Some(Tok::RBracket) => {
                    self.bump();
                    continue;
                }
                _ => {}
            }
            let pos = self.pos();
            let start = self.i;
            let decl = self.template_body_item(pos, start);
            body.push(decl);
        }
        body
    }

    fn template_body_item(&mut self, pos: Pos, start: usize) -> TemplateBodyDecl {
        match self.peek().and_then(|t| t.keyword()) {
            Some("signatory") => {
                self.bump();
                let parties = self.expr_comma_list();
                self.skip_to_item_end();
                TemplateBodyDecl::Signatory {
                    parties,
                    pos,
                    span: self.node_span(start),
                }
            }
            Some("observer") => {
                self.bump();
                let parties = self.expr_comma_list();
                self.skip_to_item_end();
                TemplateBodyDecl::Observer {
                    parties,
                    pos,
                    span: self.node_span(start),
                }
            }
            Some("ensure") => {
                self.bump();
                let expr = self.expr();
                self.skip_to_item_end();
                TemplateBodyDecl::Ensure {
                    expr,
                    pos,
                    span: self.node_span(start),
                }
            }
            Some("key") => {
                self.bump();
                let expr_start = self.i;
                let expr = self.expr();
                let ty = if self.eat_op(":") {
                    let ty_start = self.i;
                    self.skip_to_item_end();
                    parse_type_from_tokens(&self.toks[ty_start..self.i])
                } else {
                    // The expression parser consumes `: Type` annotations;
                    // recover the key type from the last top-level colon.
                    let mut depth = 0i32;
                    let mut colon = None;
                    for j in expr_start..self.i {
                        match &self.toks[j].tok {
                            Tok::LParen | Tok::LBracket => depth += 1,
                            Tok::RParen | Tok::RBracket => depth -= 1,
                            Tok::Op(o) if o == ":" && depth == 0 => colon = Some(j),
                            _ => {}
                        }
                    }
                    let ty = colon.and_then(|j| parse_type_from_tokens(&self.toks[j + 1..self.i]));
                    self.skip_to_item_end();
                    ty
                };
                TemplateBodyDecl::Key {
                    expr,
                    ty,
                    pos,
                    span: self.node_span(start),
                }
            }
            Some("maintainer") => {
                self.bump();
                let expr = self.expr();
                self.skip_to_item_end();
                TemplateBodyDecl::Maintainer {
                    expr,
                    pos,
                    span: self.node_span(start),
                }
            }
            Some("choice")
            | Some("nonconsuming")
            | Some("preconsuming")
            | Some("postconsuming") => self.choice_decl().map_or_else(
                || {
                    self.skip_to_item_end();
                    TemplateBodyDecl::Other {
                        raw: self.slice_text(start),
                        span: self.node_span(start),
                        pos,
                    }
                },
                TemplateBodyDecl::Choice,
            ),
            Some("interface") => self.interface_instance_decl().map_or_else(
                || {
                    self.skip_to_item_end();
                    TemplateBodyDecl::Other {
                        raw: self.slice_text(start),
                        span: self.node_span(start),
                        pos,
                    }
                },
                TemplateBodyDecl::InterfaceInstance,
            ),
            Some("controller") => {
                // Legacy Daml 1.x `controller <party> can` choice blocks are
                // not analyzed — fail loud instead of silently dropping the
                // choices inside.
                self.diag_cat(
                    DiagnosticCategory::UnsupportedSyntax,
                    "legacy 'controller ... can' syntax is not supported; \
                     choices inside this block are not analyzed",
                );
                self.skip_to_item_end();
                TemplateBodyDecl::Other {
                    raw: self.slice_text(start),
                    span: self.node_span(start),
                    pos,
                }
            }
            _ => {
                self.skip_to_item_end();
                TemplateBodyDecl::Other {
                    raw: self.slice_text(start),
                    span: self.node_span(start),
                    pos,
                }
            }
        }
    }

    fn choice_decl(&mut self) -> Option<ChoiceDecl> {
        let pos = self.pos();
        let start_i = self.i;
        let consuming = match self.peek().and_then(|t| t.keyword()) {
            Some("nonconsuming") => {
                self.bump();
                Consuming::NonConsuming
            }
            Some("preconsuming") => {
                self.bump();
                Consuming::PreConsuming
            }
            Some("postconsuming") => {
                self.bump();
                Consuming::PostConsuming
            }
            _ => Consuming::Consuming,
        };
        if !self.eat_keyword("choice") {
            return None;
        }
        let name = self.upper_name()?;
        let return_ty = if self.eat_op(":") {
            let ty_start = self.i;
            self.skip_type_tokens();
            parse_type_from_tokens(&self.toks[ty_start..self.i])
        } else {
            None
        };
        let mut params = Vec::new();
        let mut dangling = false;
        if self.eat_keyword("with") {
            (params, dangling) = self.field_block();
        }
        let mut observers = Vec::new();
        let mut controllers = Vec::new();
        loop {
            // Inside a dangling (empty) with-block the controller/observer/
            // do clauses sit at the block's column, so layout separates
            // them with virtual semicolons — consume those.
            if dangling {
                while self.eat(&Tok::VSemi) {}
            }
            if self.eat_keyword("observer") {
                observers = self.expr_comma_list_no_do();
            } else if self.eat_keyword("controller") {
                controllers = self.expr_comma_list_no_do();
            } else {
                break;
            }
        }
        if dangling {
            while self.eat(&Tok::VSemi) {}
        }
        let body = if self
            .peek()
            .is_some_and(|t| !matches!(t, Tok::VSemi | Tok::VRBrace | Tok::Semi | Tok::RBrace))
        {
            Some(self.expr())
        } else {
            None
        };
        self.skip_to_item_end();
        if dangling {
            // Discard the abandoned with-block's closing brace so it does
            // not terminate the enclosing template/interface body.
            self.eat(&Tok::VRBrace);
            self.skip_to_item_end();
        }
        Some(ChoiceDecl {
            name,
            consuming,
            return_ty,
            params,
            controllers,
            observers,
            body,
            pos,
            span: self.node_span(start_i),
        })
    }

    /// Consume type tokens up to (not including) a layout boundary or a
    /// `with`/`controller`/`observer`/`do`/`where` keyword at bracket depth 0.
    fn skip_type_tokens(&mut self) {
        let mut brackets = 0usize;
        while let Some(t) = self.peek() {
            match t {
                Tok::VSemi | Tok::VRBrace | Tok::VLBrace | Tok::Semi => return,
                Tok::LParen | Tok::LBracket => brackets += 1,
                Tok::RParen | Tok::RBracket => {
                    if brackets == 0 {
                        return;
                    }
                    brackets -= 1;
                }
                _ if brackets == 0
                    && matches!(
                        t.keyword(),
                        Some("with" | "controller" | "observer" | "do" | "where")
                    ) =>
                {
                    return
                }
                _ => {}
            }
            self.i += 1;
        }
    }

    // ----- interfaces ----------------------------------------------------

    fn interface_decl(&mut self) -> Option<InterfaceDecl> {
        let pos = self.pos();
        let start_i = self.i;
        self.bump(); // interface
        if self.at_keyword("instance") {
            // Top-level retroactive interface instance: skip gracefully.
            return None;
        }
        let name = self.upper_name()?;
        let mut requires = Vec::new();
        if self.eat_keyword("requires") {
            while let Some(r) = self.upper_name() {
                requires.push(r);
                if !self.eat(&Tok::Comma) {
                    break;
                }
            }
        }
        if !self.eat_keyword("where") {
            return None;
        }
        let mut viewtype = None;
        let mut methods = Vec::new();
        let mut choices = Vec::new();
        if !(self.eat(&Tok::VLBrace) || self.eat(&Tok::LBrace)) {
            return Some(InterfaceDecl {
                name,
                requires,
                viewtype,
                methods,
                choices,
                pos,
                span: self.node_span(start_i),
            });
        }
        loop {
            while self.eat(&Tok::VSemi) || self.eat(&Tok::Semi) {}
            match self.peek() {
                None => break,
                Some(Tok::VRBrace) | Some(Tok::RBrace) => {
                    self.bump();
                    break;
                }
                // A stray closing bracket inside a block is garbage from a
                // failed item parse — discard it or the loop cannot make
                // progress (skip_to_item_end deliberately stops before
                // unmatched closers).
                Some(Tok::RParen) | Some(Tok::RBracket) => {
                    self.bump();
                    continue;
                }
                _ => {}
            }
            match self.peek().and_then(|t| t.keyword()) {
                Some("viewtype") => {
                    self.bump();
                    viewtype = self.upper_name();
                    self.skip_to_item_end();
                }
                Some("choice")
                | Some("nonconsuming")
                | Some("preconsuming")
                | Some("postconsuming") => {
                    if let Some(c) = self.choice_decl() {
                        choices.push(c);
                    } else {
                        self.skip_to_item_end();
                    }
                }
                _ => {
                    // Method signature `name : Type`; anything else (default
                    // implementations, ensure, ...) is skipped.
                    let mpos = self.pos();
                    if let Some(Tok::LowerId {
                        qualifier: None,
                        name: mname,
                    }) = self.peek().cloned()
                    {
                        if self.peek_at(1).is_some_and(|t| t.is_op(":")) {
                            let mstart = self.toks[self.i].start;
                            self.bump();
                            self.bump();
                            let ty_start = self.i;
                            self.skip_to_item_end();
                            // Single name: span the whole `name : Type`.
                            methods.push(FieldDecl {
                                name: mname,
                                ty: parse_type_from_tokens(&self.toks[ty_start..self.i]),
                                pos: mpos,
                                span: Span::new(mstart, self.end_byte().max(mstart)),
                            });
                            continue;
                        }
                    }
                    self.skip_to_item_end();
                }
            }
        }
        Some(InterfaceDecl {
            name,
            requires,
            viewtype,
            methods,
            choices,
            pos,
            span: self.node_span(start_i),
        })
    }

    /// `interface instance I for T where { method-bindings }`
    fn interface_instance_decl(&mut self) -> Option<InterfaceInstanceDecl> {
        let pos = self.pos();
        let start_i = self.i;
        self.bump(); // interface
        if !self.eat_keyword("instance") {
            return None;
        }
        let interface_name = self.upper_name()?;
        let for_template = if self.eat_keyword("for") {
            self.upper_name().unwrap_or_default()
        } else {
            String::new()
        };
        let mut methods = Vec::new();
        if self.eat_keyword("where") && (self.eat(&Tok::VLBrace) || self.eat(&Tok::LBrace)) {
            loop {
                while self.eat(&Tok::VSemi) || self.eat(&Tok::Semi) {}
                match self.peek() {
                    None => break,
                    Some(Tok::VRBrace) | Some(Tok::RBrace) => {
                        self.bump();
                        break;
                    }
                    // Stray closer: discard so the loop always progresses.
                    Some(Tok::RParen) | Some(Tok::RBracket) => {
                        self.bump();
                        continue;
                    }
                    _ => {}
                }
                if let Some(b) = self.binding() {
                    methods.push(b);
                } else {
                    self.skip_to_item_end();
                }
            }
        }
        Some(InterfaceInstanceDecl {
            interface_name,
            for_template,
            methods,
            pos,
            span: self.node_span(start_i),
        })
    }

    // ----- functions -----------------------------------------------------

    /// A top-level item starting with a lowercase identifier: type
    /// signature or function equation. Operator definitions and other
    /// exotica return None.
    fn function_item(&mut self) -> Option<Decl> {
        let pos = self.pos();
        let start_i = self.i;
        let name = match self.peek().cloned() {
            Some(Tok::LowerId {
                qualifier: None,
                name,
            }) => name,
            _ => return None,
        };

        // Type signature: `name [, name2] : Type`
        let mut j = self.i + 1;
        let mut is_sig = false;
        loop {
            match self.toks.get(j).map(|t| &t.tok) {
                Some(Tok::Comma) => {
                    j += 1;
                    if matches!(
                        self.toks.get(j).map(|t| &t.tok),
                        Some(Tok::LowerId {
                            qualifier: None,
                            ..
                        })
                    ) {
                        j += 1;
                        continue;
                    }
                    break;
                }
                Some(Tok::Op(o)) if o == ":" => {
                    is_sig = true;
                    break;
                }
                _ => break,
            }
        }
        if is_sig {
            self.bump(); // name
            while self.eat(&Tok::Comma) {
                self.bump(); // more names
            }
            self.eat_op(":");
            let ty_start = self.i;
            self.skip_to_item_end();
            let ty = parse_type_from_tokens(&self.toks[ty_start..self.i]);
            return Some(Decl::Function(FunctionDecl {
                name,
                ty,
                equations: Vec::new(),
                pos,
                sig_span: Some(self.node_span(start_i)),
                span: self.node_span(start_i),
            }));
        }

        // Function equation: name pats (= expr | guards), optional where.
        self.bump(); // name
        let mut params = Vec::new();
        while !self.at_op("=") && !self.at_op("|") {
            // Combined signature + body: `name (x : a) : RetType = expr` —
            // consume the return-type annotation up to the `=`.
            if self.at_op(":") {
                self.bump();
                let mut brackets = 0usize;
                while let Some(t) = self.peek() {
                    match t {
                        Tok::Op(o) if o == "=" && brackets == 0 => break,
                        Tok::VSemi | Tok::VRBrace | Tok::Semi | Tok::RBrace => break,
                        Tok::LParen | Tok::LBracket => brackets += 1,
                        Tok::RParen | Tok::RBracket => brackets = brackets.saturating_sub(1),
                        _ => {}
                    }
                    self.i += 1;
                }
                continue;
            }
            // Infix operator definition: `f $ x = f x`, `as <&> f = ...` —
            // operators have no IR surface; skip the item silently.
            if matches!(self.peek(), Some(Tok::Op(o)) if !is_reserved_op(o)) {
                self.skip_to_item_end();
                return None;
            }
            match self.peek() {
                None | Some(Tok::VSemi) | Some(Tok::VRBrace) | Some(Tok::Semi)
                | Some(Tok::RBrace) => {
                    self.diag(format!("could not parse equation for '{}'", name));
                    return None;
                }
                _ => {}
            }
            match self.pattern_atom() {
                Some(p) => params.push(p),
                None => {
                    self.diag(format!("bad parameter pattern in '{}'", name));
                    return None;
                }
            }
        }
        let (body, guards) = self.equation_rhs()?;
        let where_bindings = if self.eat_keyword("where") {
            self.binding_block()
        } else {
            Vec::new()
        };
        self.skip_to_item_end();
        Some(Decl::Function(FunctionDecl {
            name,
            ty: None,
            equations: vec![Equation {
                params,
                body,
                guards,
                where_bindings,
                pos,
                span: self.node_span(start_i),
            }],
            pos,
            sig_span: None,
            span: self.node_span(start_i),
        }))
    }

    /// `= expr` or `| guard = expr | guard = expr ...`
    fn equation_rhs(&mut self) -> Option<(Expr, Vec<(Expr, Expr)>)> {
        if self.eat_op("=") {
            return Some((self.expr(), Vec::new()));
        }
        let mut guards = Vec::new();
        while self.eat_op("|") {
            // Comma-separated guard qualifiers, each a boolean expression
            // or a pattern guard `pat <- expr`.
            let g = loop {
                let g = self.expr();
                if self.eat_op("<-") {
                    let _ = self.expr(); // pattern guard: keep the pattern side
                }
                if !self.eat(&Tok::Comma) {
                    break g;
                }
            };
            if !self.eat_op("=") {
                self.diag("expected '=' after guard");
                return None;
            }
            let e = self.expr();
            guards.push((g, e));
        }
        let first = guards.first()?.1.clone();
        Some((first, guards))
    }

    /// `{ binding ; binding ; ... }` for let/where blocks.
    fn binding_block(&mut self) -> Vec<Binding> {
        let mut bindings = Vec::new();
        if !(self.eat(&Tok::VLBrace) || self.eat(&Tok::LBrace)) {
            return bindings;
        }
        loop {
            while self.eat(&Tok::VSemi) || self.eat(&Tok::Semi) {}
            match self.peek() {
                None => break,
                Some(Tok::VRBrace) | Some(Tok::RBrace) => {
                    self.bump();
                    break;
                }
                // A stray closing bracket inside a block is garbage from a
                // failed item parse — discard it or the loop cannot make
                // progress (skip_to_item_end deliberately stops before
                // unmatched closers).
                Some(Tok::RParen) | Some(Tok::RBracket) => {
                    self.bump();
                    continue;
                }
                _ => {}
            }
            match self.binding() {
                Some(b) => bindings.push(b),
                None => self.skip_to_item_end(),
            }
        }
        bindings
    }

    /// One binding: `pat = expr`, `f x y = expr`, guarded variants, or a
    /// type signature (skipped, returns None).
    fn binding(&mut self) -> Option<Binding> {
        let pos = self.pos();
        let start_i = self.i;
        // Operator binding or signature: `(==) : Text -> Bool = ...` —
        // skip the whole item; operators aren't surfaced in the IR.
        if self.at(&Tok::LParen)
            && matches!(self.peek_at(1), Some(Tok::Op(_)))
            && self.peek_at(2) == Some(&Tok::RParen)
        {
            self.skip_to_item_end();
            return None;
        }
        let pat = self.pattern_atom()?;
        let mut params = Vec::new();
        loop {
            if self.at_op("=") {
                self.bump();
                let expr = self.expr();
                // Bindings can carry their own where blocks.
                if self.eat_keyword("where") {
                    let _ = self.binding_block();
                }
                return Some(Binding {
                    pat,
                    params,
                    expr,
                    pos,
                    span: self.node_span(start_i),
                });
            }
            if self.at_op("|") {
                let (body, _) = self.equation_rhs()?;
                if self.eat_keyword("where") {
                    let _ = self.binding_block();
                }
                return Some(Binding {
                    pat,
                    params,
                    expr: body,
                    pos,
                    span: self.node_span(start_i),
                });
            }
            if self.at_op(":") {
                if params.is_empty() {
                    // Type signature inside a let/where block — skip it.
                    self.skip_to_item_end();
                    return None;
                }
                // Combined signature + body: `f (x : a) : Ret = expr` —
                // consume the return-type annotation up to the `=`.
                self.bump();
                let mut brackets = 0usize;
                while let Some(t) = self.peek() {
                    match t {
                        Tok::Op(o) if o == "=" && brackets == 0 => break,
                        Tok::VSemi | Tok::VRBrace | Tok::Semi | Tok::RBrace => break,
                        Tok::LParen | Tok::LBracket => brackets += 1,
                        Tok::RParen | Tok::RBracket => brackets = brackets.saturating_sub(1),
                        _ => {}
                    }
                    self.i += 1;
                }
                continue;
            }
            // Infix operator binding with a pattern operand:
            // `None <?> s = ...` in a where/let block.
            if matches!(self.peek(), Some(Tok::Op(o)) if !is_reserved_op(o)) {
                self.skip_to_item_end();
                return None;
            }
            match self.peek() {
                None | Some(Tok::VSemi) | Some(Tok::VRBrace) | Some(Tok::Semi)
                | Some(Tok::RBrace) => return None,
                _ => {}
            }
            params.push(self.pattern_atom()?);
        }
    }

    // ----- patterns ------------------------------------------------------

    fn pattern_atom(&mut self) -> Option<Pat> {
        if self.depth >= MAX_DEPTH {
            return None;
        }
        self.depth += 1;
        let result = self.pattern_atom_inner();
        self.depth -= 1;
        result
    }

    fn pattern_atom_inner(&mut self) -> Option<Pat> {
        let pos = self.pos();
        let start_i = self.i;
        // Lazy / strict pattern markers: `~(as, bs)`, `!x`.
        if self.at_op("~") || self.at_op("!") {
            self.bump();
            return self.pattern_atom();
        }
        match self.peek().cloned() {
            Some(Tok::LowerId {
                qualifier: None,
                name,
            }) => {
                self.bump();
                if name == "_" {
                    return Some(Pat::Wild {
                        pos,
                        span: self.node_span(start_i),
                    });
                }
                if self.at_op("@") {
                    self.bump();
                    let inner = self.pattern_atom()?;
                    return Some(Pat::As {
                        name,
                        pat: Box::new(inner),
                        pos,
                        span: self.node_span(start_i),
                    });
                }
                Some(Pat::Var {
                    name,
                    pos,
                    span: self.node_span(start_i),
                })
            }
            Some(Tok::Op(o)) if o == "_" => {
                self.bump();
                Some(Pat::Wild {
                    pos,
                    span: self.node_span(start_i),
                })
            }
            Some(Tok::UpperId { qualifier, name }) => {
                self.bump();
                // Record pattern `Foo {..}` / `Foo {x = y}` /
                // `Foo with claim; tag`.
                if self.at(&Tok::LBrace) {
                    self.skip_balanced_braces();
                } else if self.eat_keyword("with") {
                    let _ = self.record_fields();
                }
                Some(Pat::Con {
                    qualifier,
                    name,
                    args: Vec::new(),
                    pos,
                    span: self.node_span(start_i),
                })
            }
            Some(Tok::IntLit(text)) => {
                self.bump();
                Some(Pat::Lit {
                    kind: LitKind::Int,
                    text,
                    pos,
                    span: self.node_span(start_i),
                })
            }
            Some(Tok::DecimalLit(text)) => {
                self.bump();
                Some(Pat::Lit {
                    kind: LitKind::Decimal,
                    text,
                    pos,
                    span: self.node_span(start_i),
                })
            }
            Some(Tok::StringLit(text)) => {
                self.bump();
                Some(Pat::Lit {
                    kind: LitKind::Text,
                    text,
                    pos,
                    span: self.node_span(start_i),
                })
            }
            Some(Tok::CharLit(text)) => {
                self.bump();
                Some(Pat::Lit {
                    kind: LitKind::Char,
                    text,
                    pos,
                    span: self.node_span(start_i),
                })
            }
            Some(Tok::LParen) => {
                self.bump();
                if self.eat(&Tok::RParen) {
                    return Some(Pat::Con {
                        qualifier: None,
                        name: "()".to_string(),
                        args: Vec::new(),
                        pos,
                        span: self.node_span(start_i),
                    });
                }
                // View pattern `(expr -> pat)`: scan for a top-level `->`
                // inside these parens; the expression side is discarded and
                // the pattern after the arrow is the binding. A top-level
                // `:` before the arrow means the arrow belongs to a type
                // annotation (`(f : Int -> Bool)`), not a view pattern.
                {
                    let mut depth = 0usize;
                    let mut j = self.i;
                    let mut arrow = None;
                    while let Some(t) = self.toks.get(j).map(|t| &t.tok) {
                        match t {
                            Tok::LParen | Tok::LBracket => depth += 1,
                            Tok::RParen | Tok::RBracket => {
                                if depth == 0 {
                                    break;
                                }
                                depth -= 1;
                            }
                            Tok::Op(o) if o == ":" && depth == 0 => break,
                            Tok::Op(o) if o == "->" && depth == 0 => {
                                arrow = Some(j);
                                break;
                            }
                            Tok::VSemi | Tok::VRBrace => break,
                            // A lambda's arrow belongs to the lambda.
                            Tok::Op(o) if o == "\\" => break,
                            _ => {}
                        }
                        j += 1;
                    }
                    if let Some(j) = arrow {
                        self.i = j + 1; // skip the view expression and `->`
                        let inner = self.pattern()?;
                        self.eat(&Tok::RParen);
                        return Some(inner);
                    }
                }
                let first = self.pattern()?;
                // Type-annotated pattern `(e : AnyException)`: skip the type.
                if self.at_op(":") {
                    let mut depth = 0usize;
                    while let Some(t) = self.peek() {
                        match t {
                            Tok::LParen | Tok::LBracket => depth += 1,
                            Tok::RParen if depth == 0 => break,
                            Tok::RParen | Tok::RBracket => depth -= 1,
                            Tok::VSemi | Tok::VRBrace => break,
                            _ => {}
                        }
                        self.i += 1;
                    }
                }
                if self.at(&Tok::Comma) {
                    let mut items = vec![first];
                    while self.eat(&Tok::Comma) {
                        items.push(self.pattern()?);
                    }
                    self.eat(&Tok::RParen);
                    return Some(Pat::Tuple {
                        items,
                        pos,
                        span: self.node_span(start_i),
                    });
                }
                self.eat(&Tok::RParen);
                Some(first)
            }
            Some(Tok::LBracket) => {
                self.bump();
                let mut items = Vec::new();
                if !self.eat(&Tok::RBracket) {
                    loop {
                        items.push(self.pattern()?);
                        if !self.eat(&Tok::Comma) {
                            break;
                        }
                    }
                    self.eat(&Tok::RBracket);
                }
                Some(Pat::List {
                    items,
                    pos,
                    span: self.node_span(start_i),
                })
            }
            _ => None,
        }
    }

    /// Full pattern: constructor applications and infix cons `x :: xs`.
    fn pattern(&mut self) -> Option<Pat> {
        if self.depth >= MAX_DEPTH {
            return None;
        }
        self.depth += 1;
        let result = self.pattern_inner();
        self.depth -= 1;
        result
    }

    fn pattern_inner(&mut self) -> Option<Pat> {
        let pos = self.pos();
        let start_i = self.i;
        let first = match self.peek().cloned() {
            Some(Tok::UpperId { qualifier, name }) => {
                self.bump();
                if self.at(&Tok::LBrace) || self.at_keyword("with") {
                    if self.eat_keyword("with") {
                        let _ = self.record_fields();
                    } else {
                        self.skip_balanced_braces();
                    }
                    Pat::Con {
                        qualifier,
                        name,
                        args: Vec::new(),
                        pos,
                        span: self.node_span(start_i),
                    }
                } else {
                    let mut args = Vec::new();
                    while let Some(a) = self.try_pattern_atom() {
                        args.push(a);
                    }
                    Pat::Con {
                        qualifier,
                        name,
                        args,
                        pos,
                        span: self.node_span(start_i),
                    }
                }
            }
            _ => self.pattern_atom()?,
        };
        if self.at_op("::") {
            self.bump();
            let rest = self.pattern()?;
            return Some(Pat::Con {
                qualifier: None,
                name: "::".to_string(),
                args: vec![first, rest],
                pos,
                span: self.node_span(start_i),
            });
        }
        Some(first)
    }

    fn try_pattern_atom(&mut self) -> Option<Pat> {
        match self.peek() {
            Some(Tok::LowerId {
                qualifier: None, ..
            })
            | Some(Tok::UpperId { .. })
            | Some(Tok::IntLit(_))
            | Some(Tok::DecimalLit(_))
            | Some(Tok::StringLit(_))
            | Some(Tok::CharLit(_))
            | Some(Tok::LParen)
            | Some(Tok::LBracket) => self.pattern_atom(),
            _ => None,
        }
    }

    fn skip_balanced_braces(&mut self) {
        let mut depth = 0usize;
        while let Some(t) = self.peek() {
            match t {
                Tok::LBrace => depth += 1,
                Tok::RBrace => {
                    depth -= 1;
                    if depth == 0 {
                        self.i += 1;
                        return;
                    }
                }
                _ => {}
            }
            self.i += 1;
        }
    }

    // ----- expressions ---------------------------------------------------

    fn expr(&mut self) -> Expr {
        self.expr_prec(0, true)
    }

    fn expr_no_do(&mut self) -> Expr {
        self.expr_prec(0, false)
    }

    /// Comma-separated expressions (signatory/observer/controller lists).
    fn expr_comma_list(&mut self) -> Vec<Expr> {
        let mut out = vec![self.expr()];
        while self.eat(&Tok::Comma) {
            out.push(self.expr());
        }
        out
    }

    fn expr_comma_list_no_do(&mut self) -> Vec<Expr> {
        let mut out = vec![self.expr_no_do()];
        while self.eat(&Tok::Comma) {
            out.push(self.expr_no_do());
        }
        out
    }

    fn expr_prec(&mut self, min_prec: u8, allow_do: bool) -> Expr {
        let pos = self.pos();
        let start_i = self.i;
        if self.depth >= MAX_DEPTH {
            // Hostile nesting: degrade to raw text instead of recursing, and
            // report it so the degraded region is not silently mistaken for
            // unsupported syntax. `skip_to_item_end` below consumes the rest of
            // the item, so this trips about once per affected declaration.
            self.diag_cat(
                DiagnosticCategory::RecursionLimit,
                "expression nesting too deep; truncated to raw text",
            );
            let start = self.i;
            self.skip_to_item_end();
            if self.i == start {
                self.bump();
            }
            return Expr::Error {
                raw: self.slice_text(start),
                span: self.node_span(start),
                pos,
            };
        }
        self.depth += 1;
        let result = self.expr_prec_inner(min_prec, allow_do, pos, start_i);
        self.depth -= 1;
        result
    }

    fn expr_prec_inner(&mut self, min_prec: u8, allow_do: bool, pos: Pos, start_i: usize) -> Expr {
        let mut lhs = match self.unary(allow_do) {
            Some(e) => e,
            None => {
                // Unparseable here: degrade to raw text up to the item end.
                let start = self.i;
                self.skip_to_item_end();
                if self.i == start {
                    self.bump();
                }
                return Expr::Error {
                    raw: self.slice_text(start),
                    span: self.node_span(start),
                    pos,
                };
            }
        };
        loop {
            let (op, prec, right_assoc) = match self.peek() {
                Some(Tok::Op(o)) => {
                    let o = o.clone();
                    if is_reserved_op(&o) {
                        // `e : Type` annotation: consume the type, keep e.
                        if o == ":" {
                            self.bump();
                            self.skip_type_tokens();
                            continue;
                        }
                        break;
                    }
                    let (p, r) = fixity(&o);
                    (o, p, r)
                }
                Some(Tok::Backtick) => {
                    // `e `div` e` — infix function application.
                    let name = match self.peek_at(1) {
                        Some(Tok::LowerId { qualifier, name })
                        | Some(Tok::UpperId { qualifier, name }) => qualifier
                            .as_ref()
                            .map_or_else(|| name.clone(), |q| format!("{}.{}", q, name)),
                        _ => break,
                    };
                    if self.peek_at(2) != Some(&Tok::Backtick) {
                        break;
                    }
                    (format!("`{}`", name), 9, false)
                }
                _ => break,
            };
            if prec < min_prec {
                break;
            }
            self.bump();
            if op.starts_with('`') {
                self.bump();
                self.bump();
            }
            let next_min = if right_assoc { prec } else { prec + 1 };
            let rhs = self.expr_prec(next_min, allow_do);
            lhs = Expr::BinOp {
                op,
                lhs: Box::new(lhs),
                rhs: Box::new(rhs),
                pos,
                span: self.node_span(start_i),
            };
        }
        lhs
    }

    fn unary(&mut self, allow_do: bool) -> Option<Expr> {
        let pos = self.pos();
        let start_i = self.i;
        if self.at_op("-") {
            self.bump();
            let e = self.unary(allow_do)?;
            return Some(Expr::Neg {
                expr: Box::new(e),
                pos,
                span: self.node_span(start_i),
            });
        }
        self.application(allow_do)
    }

    fn application(&mut self, allow_do: bool) -> Option<Expr> {
        let pos = self.pos();
        let start_i = self.i;
        let head0 = self.atom(allow_do)?;
        let mut head = self.projection_tail(head0);
        let mut args = Vec::new();
        loop {
            // Record syntax binds tighter than application:
            // `create Foo with x = 1` applies create to (Foo with {x = 1}).
            if self.at_keyword("with") {
                let target = args.pop().unwrap_or_else(|| {
                    std::mem::replace(
                        &mut head,
                        Expr::Error {
                            raw: String::new(),
                            pos,
                            span: Span::default(),
                        },
                    )
                });
                self.bump(); // with
                let fields = self.record_fields();
                let tpos = target.pos();
                let sp = Span::new(target.span().start, self.end_byte());
                let rec = Expr::Record {
                    base: Box::new(target),
                    fields,
                    pos: tpos,
                    span: sp,
                };
                if matches!(head, Expr::Error { ref raw, .. } if raw.is_empty()) {
                    head = rec;
                } else {
                    args.push(rec);
                }
                continue;
            }
            if !allow_do && self.at_keyword("do") {
                break;
            }
            // Type application `f @Type x` — consume and drop the type atom.
            if self.at_op("@") {
                self.bump();
                match self.peek() {
                    Some(Tok::UpperId { .. }) | Some(Tok::LowerId { .. }) => {
                        self.bump();
                    }
                    Some(Tok::LParen) => self.skip_balanced_parens(),
                    Some(Tok::LBracket) => {
                        let mut depth = 0usize;
                        while let Some(t) = self.peek() {
                            match t {
                                Tok::LBracket => depth += 1,
                                Tok::RBracket => {
                                    depth -= 1;
                                    if depth == 0 {
                                        self.i += 1;
                                        break;
                                    }
                                }
                                _ => {}
                            }
                            self.i += 1;
                        }
                    }
                    _ => {}
                }
                continue;
            }
            match self.try_atom(allow_do) {
                Some(a) => args.push(self.projection_tail(a)),
                None => break,
            }
        }
        if args.is_empty() {
            Some(head)
        } else {
            Some(Expr::App {
                func: Box::new(head),
                args,
                pos,
                span: self.node_span(start_i),
            })
        }
    }

    /// `{ f = e ; g ; .. }` after `with` (virtual block) or explicit braces.
    fn record_fields(&mut self) -> Vec<FieldAssign> {
        let mut fields = Vec::new();
        let explicit = self.at(&Tok::LBrace);
        if !(self.eat(&Tok::VLBrace) || self.eat(&Tok::LBrace)) {
            return fields;
        }
        loop {
            while self.eat(&Tok::VSemi) || self.eat(&Tok::Semi) || self.eat(&Tok::Comma) {}
            match self.peek() {
                None => break,
                Some(Tok::VRBrace) if !explicit => {
                    self.bump();
                    break;
                }
                Some(Tok::RBrace) => {
                    self.bump();
                    break;
                }
                // Stray closer: discard so the loop always progresses.
                Some(Tok::RParen) | Some(Tok::RBracket) => {
                    self.bump();
                    continue;
                }
                _ => {}
            }
            let pos = self.pos();
            let start_i = self.i;
            if self.at_op("..") {
                self.bump();
                fields.push(FieldAssign {
                    name: "..".to_string(),
                    value: None,
                    pos,
                    span: self.node_span(start_i),
                });
                continue;
            }
            let name = match self.peek().cloned() {
                Some(Tok::LowerId {
                    qualifier: None,
                    name,
                }) => {
                    self.bump();
                    name
                }
                _ => {
                    self.skip_to_item_end();
                    continue;
                }
            };
            if self.eat_op("=") {
                let value = self.expr_prec(1, true);
                fields.push(FieldAssign {
                    name,
                    value: Some(value),
                    pos,
                    span: self.node_span(start_i),
                });
            } else {
                // Pun: `Foo with owner`.
                fields.push(FieldAssign {
                    name,
                    value: None,
                    pos,
                    span: self.node_span(start_i),
                });
            }
        }
        fields
    }

    fn try_atom(&mut self, allow_do: bool) -> Option<Expr> {
        match self.peek() {
            Some(Tok::LowerId { .. }) => {
                let kw = self.peek().and_then(|t| t.keyword());
                match kw {
                    // Block argument: `script do ...`, `submit p do ...`.
                    Some("do") if allow_do => self.atom(allow_do),
                    // Keywords that begin expressions are fine as atoms in
                    // head position but must not be slurped as arguments.
                    Some(
                        "if" | "case" | "do" | "let" | "try" | "where" | "then" | "else" | "of"
                        | "in" | "controller" | "with" | "catch",
                    ) => None,
                    _ => self.atom(allow_do),
                }
            }
            Some(Tok::UpperId { .. })
            | Some(Tok::IntLit(_))
            | Some(Tok::DecimalLit(_))
            | Some(Tok::StringLit(_))
            | Some(Tok::CharLit(_))
            | Some(Tok::LParen)
            | Some(Tok::LBracket) => self.atom(allow_do),
            // Bare trailing lambda argument: `forA xs \x -> ...`.
            Some(Tok::Op(o)) if o == "\\" => self.atom(allow_do),
            _ => None,
        }
    }

    /// Fold tight (whitespace-free) `.field` record projections onto `base`.
    /// Projection binds tighter than application, so `length this.note` is
    /// `length (this.note)` not `(length this).note`, and a chain `a.b.c`
    /// left-nests. A *spaced* dot (`f . g`, composition) is not tight and is
    /// left to the binary-operator layer untouched. Qualified names
    /// (`Map.lookup`) are already a single token and never reach here.
    fn projection_tail(&mut self, mut base: Expr) -> Expr {
        while self.at_tight_projection() {
            let start = base.span().start;
            let pos = base.pos();
            self.bump(); // '.'
            let field_tok = self.bump().expect("tight projection guarantees a field");
            let (qualifier, name) = match field_tok.tok {
                Tok::LowerId { qualifier, name } => (qualifier, name),
                other => unreachable!("at_tight_projection guarantees LowerId, got {other:?}"),
            };
            let field = Expr::Var {
                qualifier,
                name,
                pos: field_tok.pos,
                span: Span::new(field_tok.start, field_tok.end),
            };
            base = Expr::BinOp {
                op: ".".to_string(),
                lhs: Box::new(base),
                rhs: Box::new(field),
                pos,
                span: Span::new(start, self.end_byte()),
            };
        }
        base
    }

    /// True when the cursor sits on a `.` that abuts a real token on its left
    /// and an unqualified lowercase field on its right with no whitespace on
    /// either side — i.e. a record projection, not function composition.
    fn at_tight_projection(&self) -> bool {
        if self.i == 0 {
            return false;
        }
        let dot = match self.toks.get(self.i) {
            Some(t) => t,
            None => return false,
        };
        if !matches!(&dot.tok, Tok::Op(o) if o == ".") {
            return false;
        }
        // Tight on the left: the dot abuts the base's last byte. The previous
        // token must be real — a virtual layout token here means a newline or
        // dedent sat between base and dot, which can never be a tight dot.
        let prev = &self.toks[self.i - 1];
        if prev.is_virtual() || prev.end != dot.start {
            return false;
        }
        // Tight on the right: an unqualified lowercase field abuts the dot.
        self.toks.get(self.i + 1).is_some_and(|t| {
            matches!(
                &t.tok,
                Tok::LowerId {
                    qualifier: None,
                    ..
                }
            ) && t.start == dot.end
        })
    }

    fn atom(&mut self, allow_do: bool) -> Option<Expr> {
        let pos = self.pos();
        let start_i = self.i;
        match self.peek().cloned() {
            Some(Tok::LowerId { qualifier, name }) => {
                match name.as_str() {
                    "if" if qualifier.is_none() => return self.if_expr(),
                    "case" if qualifier.is_none() => return self.case_expr(),
                    "do" if qualifier.is_none() => {
                        if !allow_do {
                            return None;
                        }
                        return self.do_expr();
                    }
                    "let" if qualifier.is_none() => return self.let_expr(),
                    "try" if qualifier.is_none() => return self.try_expr(),
                    _ => {}
                }
                self.bump();
                Some(Expr::Var {
                    qualifier,
                    name,
                    pos,
                    span: self.node_span(start_i),
                })
            }
            Some(Tok::UpperId { qualifier, name }) => {
                self.bump();
                let base = Expr::Con {
                    qualifier,
                    name,
                    pos,
                    span: self.node_span(start_i),
                };
                // Explicit-brace record syntax: `Foo {x = 1}`.
                if self.at(&Tok::LBrace) {
                    let fields = self.record_fields();
                    return Some(Expr::Record {
                        base: Box::new(base),
                        fields,
                        pos,
                        span: self.node_span(start_i),
                    });
                }
                Some(base)
            }
            Some(Tok::IntLit(text)) => {
                self.bump();
                Some(Expr::Lit {
                    kind: LitKind::Int,
                    text,
                    pos,
                    span: self.node_span(start_i),
                })
            }
            Some(Tok::DecimalLit(text)) => {
                self.bump();
                Some(Expr::Lit {
                    kind: LitKind::Decimal,
                    text,
                    pos,
                    span: self.node_span(start_i),
                })
            }
            Some(Tok::StringLit(text)) => {
                self.bump();
                Some(Expr::Lit {
                    kind: LitKind::Text,
                    text,
                    pos,
                    span: self.node_span(start_i),
                })
            }
            Some(Tok::CharLit(text)) => {
                self.bump();
                Some(Expr::Lit {
                    kind: LitKind::Char,
                    text,
                    pos,
                    span: self.node_span(start_i),
                })
            }
            Some(Tok::Op(o)) if o == "\\" => self.lambda_expr(),
            Some(Tok::LParen) => self.paren_expr(),
            Some(Tok::LBracket) => self.list_expr(),
            _ => None,
        }
    }

    fn if_expr(&mut self) -> Option<Expr> {
        let pos = self.pos();
        let start_i = self.i;
        self.bump(); // if
        let cond = self.expr();
        self.eat(&Tok::VSemi); // DoAndIfThenElse style
        if !self.eat_keyword("then") {
            self.diag("expected 'then'");
            return Some(Expr::Error {
                raw: format!("if {}", cond.render()),
                pos,
                span: self.node_span(start_i),
            });
        }
        let then_branch = self.expr();
        self.eat(&Tok::VSemi);
        if !self.eat_keyword("else") {
            self.diag("expected 'else'");
            return Some(Expr::Error {
                raw: format!("if {} then {}", cond.render(), then_branch.render()),
                pos,
                span: self.node_span(start_i),
            });
        }
        let else_branch = self.expr();
        Some(Expr::If {
            cond: Box::new(cond),
            then_branch: Box::new(then_branch),
            else_branch: Box::new(else_branch),
            pos,
            span: self.node_span(start_i),
        })
    }

    fn case_expr(&mut self) -> Option<Expr> {
        let pos = self.pos();
        let start_i = self.i;
        self.bump(); // case
        let scrutinee = self.expr_no_do();
        if !self.eat_keyword("of") {
            self.diag("expected 'of' in case expression");
            return Some(Expr::Error {
                raw: format!("case {}", scrutinee.render()),
                pos,
                span: self.node_span(start_i),
            });
        }
        let mut alts = Vec::new();
        if self.eat(&Tok::VLBrace) || self.eat(&Tok::LBrace) {
            loop {
                while self.eat(&Tok::VSemi) || self.eat(&Tok::Semi) {}
                match self.peek() {
                    None => break,
                    Some(Tok::VRBrace) | Some(Tok::RBrace) => {
                        self.bump();
                        break;
                    }
                    // Stray closer: discard so the loop always progresses.
                    Some(Tok::RParen) | Some(Tok::RBracket) => {
                        self.bump();
                        continue;
                    }
                    _ => {}
                }
                // An alternative can carry a `where` block for its body.
                if self.eat_keyword("where") {
                    let _ = self.binding_block();
                    continue;
                }
                match self.case_alt() {
                    Some(a) => alts.push(a),
                    None => self.skip_to_item_end(),
                }
            }
        }
        Some(Expr::Case {
            scrutinee: Box::new(scrutinee),
            alts,
            pos,
            span: self.node_span(start_i),
        })
    }

    fn case_alt(&mut self) -> Option<Alt> {
        let pos = self.pos();
        let start_i = self.i;
        let pat = self.pattern()?;
        if self.at_op("|") {
            // Guarded alternative(s): take the first body, consume all.
            // Each guard is comma-separated qualifiers, each a boolean
            // expression or a pattern guard `pat <- expr`.
            let mut first: Option<Expr> = None;
            while self.eat_op("|") {
                loop {
                    let _guard = self.expr();
                    if self.eat_op("<-") {
                        let _ = self.expr();
                    }
                    if !self.eat(&Tok::Comma) {
                        break;
                    }
                }
                if !self.eat_op("->") {
                    self.diag("expected '->' in guarded case alternative");
                    return None;
                }
                let body = self.expr();
                if first.is_none() {
                    first = Some(body);
                }
            }
            return Some(Alt {
                pat,
                body: first?,
                pos,
                span: self.node_span(start_i),
            });
        }
        if !self.eat_op("->") {
            self.diag("expected '->' in case alternative");
            return None;
        }
        let body = self.expr();
        Some(Alt {
            pat,
            body,
            pos,
            span: self.node_span(start_i),
        })
    }

    fn do_expr(&mut self) -> Option<Expr> {
        let pos = self.pos();
        let start_i = self.i;
        self.bump(); // do
        let mut stmts = Vec::new();
        if self.eat(&Tok::VLBrace) || self.eat(&Tok::LBrace) {
            loop {
                while self.eat(&Tok::VSemi) || self.eat(&Tok::Semi) {}
                match self.peek() {
                    None => break,
                    Some(Tok::VRBrace) | Some(Tok::RBrace) => {
                        self.bump();
                        break;
                    }
                    // Stray closer: discard so the loop always progresses.
                    Some(Tok::RParen) | Some(Tok::RBracket) => {
                        self.bump();
                        continue;
                    }
                    _ => {}
                }
                stmts.push(self.do_stmt());
            }
        }
        Some(Expr::Do {
            stmts,
            pos,
            span: self.node_span(start_i),
        })
    }

    fn do_stmt(&mut self) -> DoStmt {
        let pos = self.pos();
        let start_i = self.i;
        if self.at_keyword("let") {
            self.bump();
            let bindings = self.binding_block();
            // `let ... in body` as a statement is an expression.
            if self.eat_keyword("in") {
                let body = self.expr();
                return DoStmt::Expr {
                    expr: Expr::LetIn {
                        bindings,
                        body: Box::new(body),
                        pos,
                        span: self.node_span(start_i),
                    },
                    pos,
                    span: self.node_span(start_i),
                };
            }
            return DoStmt::Let {
                bindings,
                pos,
                span: self.node_span(start_i),
            };
        }
        // Try `pat <- expr` with rollback.
        let snapshot = self.i;
        if let Some(pat) = self.try_bind_pattern() {
            if self.at_op("<-") {
                self.bump();
                let expr = self.expr();
                return DoStmt::Bind {
                    pat,
                    expr,
                    pos,
                    span: self.node_span(start_i),
                };
            }
        }
        self.i = snapshot;
        let expr = self.expr();
        DoStmt::Expr {
            expr,
            pos,
            span: self.node_span(start_i),
        }
    }

    /// Pattern attempt for `pat <- ...`; restores nothing itself (caller
    /// rolls back on failure).
    fn try_bind_pattern(&mut self) -> Option<Pat> {
        self.pattern()
    }

    fn let_expr(&mut self) -> Option<Expr> {
        let pos = self.pos();
        let start_i = self.i;
        self.bump(); // let
        let bindings = self.binding_block();
        if self.eat_keyword("in") {
            let body = self.expr();
            return Some(Expr::LetIn {
                bindings,
                body: Box::new(body),
                pos,
                span: self.node_span(start_i),
            });
        }
        // `let` without `in` outside a do block — degrade gracefully.
        Some(Expr::LetIn {
            bindings,
            body: Box::new(Expr::Error {
                raw: String::new(),
                pos,
                span: self.node_span(start_i),
            }),
            pos,
            span: self.node_span(start_i),
        })
    }

    fn try_expr(&mut self) -> Option<Expr> {
        let pos = self.pos();
        let start_i = self.i;
        self.bump(); // try
        let body = self.expr();
        let mut handlers = Vec::new();
        self.eat(&Tok::VSemi);
        if self.eat_keyword("catch") {
            if self.eat(&Tok::VLBrace) || self.eat(&Tok::LBrace) {
                loop {
                    while self.eat(&Tok::VSemi) || self.eat(&Tok::Semi) {}
                    match self.peek() {
                        None => break,
                        Some(Tok::VRBrace) | Some(Tok::RBrace) => {
                            self.bump();
                            break;
                        }
                        // Stray closer: discard so the loop always progresses.
                        Some(Tok::RParen) | Some(Tok::RBracket) => {
                            self.bump();
                            continue;
                        }
                        _ => {}
                    }
                    match self.case_alt() {
                        Some(a) => handlers.push(a),
                        None => self.skip_to_item_end(),
                    }
                }
            } else if let Some(a) = self.case_alt() {
                // Single-alternative catch on the same line.
                handlers.push(a);
            }
        }
        Some(Expr::Try {
            body: Box::new(body),
            handlers,
            pos,
            span: self.node_span(start_i),
        })
    }

    fn lambda_expr(&mut self) -> Option<Expr> {
        let pos = self.pos();
        let start_i = self.i;
        self.bump(); // backslash
                     // `\case` — lambda-case: one implicit argument matched by the alts.
        if self.eat_keyword("case") {
            let mut alts = Vec::new();
            if self.eat(&Tok::VLBrace) || self.eat(&Tok::LBrace) {
                loop {
                    while self.eat(&Tok::VSemi) || self.eat(&Tok::Semi) {}
                    match self.peek() {
                        None => break,
                        Some(Tok::VRBrace) | Some(Tok::RBrace) => {
                            self.bump();
                            break;
                        }
                        Some(Tok::RParen) | Some(Tok::RBracket) => {
                            self.bump();
                            continue;
                        }
                        _ => {}
                    }
                    match self.case_alt() {
                        Some(a) => alts.push(a),
                        None => self.skip_to_item_end(),
                    }
                }
            }
            return Some(Expr::Lambda {
                params: vec![Pat::Var {
                    name: "_".to_string(),
                    pos,
                    span: Span::new(self.byte_at(start_i), self.byte_at(start_i)),
                }],
                body: Box::new(Expr::Case {
                    scrutinee: Box::new(Expr::Var {
                        qualifier: None,
                        name: "_".to_string(),
                        pos,
                        span: Span::new(self.byte_at(start_i), self.byte_at(start_i)),
                    }),
                    alts,
                    pos,
                    span: self.node_span(start_i),
                }),
                pos,
                span: self.node_span(start_i),
            });
        }
        let mut params = Vec::new();
        while !self.at_op("->") {
            match self.pattern_atom() {
                Some(p) => params.push(p),
                None => {
                    self.diag("bad lambda parameter");
                    let start = self.i;
                    self.skip_to_item_end();
                    return Some(Expr::Error {
                        raw: format!("\\{}", self.slice_text(start)),
                        pos,
                        span: self.node_span(start_i),
                    });
                }
            }
        }
        self.bump(); // ->
        let body = self.expr();
        Some(Expr::Lambda {
            params,
            body: Box::new(body),
            pos,
            span: self.node_span(start_i),
        })
    }

    fn paren_expr(&mut self) -> Option<Expr> {
        let pos = self.pos();
        let start_i = self.i;
        self.bump(); // (
        if self.eat(&Tok::RParen) {
            return Some(Expr::Con {
                qualifier: None,
                name: "()".to_string(),
                pos,
                span: self.node_span(start_i),
            });
        }
        // Operator section / operator reference: `(+)`, `(+ 1)`.
        if let Some(Tok::Op(o)) = self.peek().cloned() {
            if !is_reserved_op(&o) && o != "\\" && o != "-" {
                self.bump();
                if self.eat(&Tok::RParen) {
                    return Some(Expr::Section {
                        op: o,
                        operand: None,
                        left: false,
                        pos,
                        span: self.node_span(start_i),
                    });
                }
                let operand = self.expr();
                self.eat(&Tok::RParen);
                return Some(Expr::Section {
                    op: o,
                    operand: Some(Box::new(operand)),
                    left: false,
                    pos,
                    span: self.node_span(start_i),
                });
            }
        }
        let first = self.expr();
        if self.at(&Tok::Comma) {
            let mut items = vec![first];
            while self.eat(&Tok::Comma) {
                items.push(self.expr());
            }
            self.eat(&Tok::RParen);
            return Some(Expr::Tuple {
                items,
                pos,
                span: self.node_span(start_i),
            });
        }
        // Left section: `(x +)`.
        if let Some(Tok::Op(o)) = self.peek().cloned() {
            if !is_reserved_op(&o) && self.peek_at(1) == Some(&Tok::RParen) {
                self.bump();
                self.bump();
                return Some(Expr::Section {
                    op: o,
                    operand: Some(Box::new(first)),
                    left: true,
                    pos,
                    span: self.node_span(start_i),
                });
            }
        }
        self.eat(&Tok::RParen);
        Some(first)
    }

    fn list_expr(&mut self) -> Option<Expr> {
        let pos = self.pos();
        let start_i = self.i;
        self.bump(); // [
        let mut items = Vec::new();
        if self.eat(&Tok::RBracket) {
            return Some(Expr::List {
                items,
                pos,
                span: self.node_span(start_i),
            });
        }
        loop {
            let e = self.expr();
            // Range: `[a .. b]` / `[a ..]`.
            if self.at_op("..") {
                self.bump();
                let hi = if self.at(&Tok::RBracket) {
                    Expr::Error {
                        raw: String::new(),
                        pos,
                        span: self.node_span(start_i),
                    }
                } else {
                    self.expr()
                };
                self.eat(&Tok::RBracket);
                return Some(Expr::BinOp {
                    op: "..".to_string(),
                    lhs: Box::new(e),
                    rhs: Box::new(hi),
                    pos,
                    span: self.node_span(start_i),
                });
            }
            // List comprehension: degrade the qualifier part to raw text.
            if self.at_op("|") {
                let start = self.i;
                let mut brackets = 1usize;
                while let Some(t) = self.peek() {
                    match t {
                        Tok::LBracket => brackets += 1,
                        Tok::RBracket => {
                            brackets -= 1;
                            if brackets == 0 {
                                break;
                            }
                        }
                        Tok::VSemi | Tok::VRBrace => break,
                        _ => {}
                    }
                    self.i += 1;
                }
                let raw = self.slice_text(start);
                self.eat(&Tok::RBracket);
                return Some(Expr::App {
                    func: Box::new(e),
                    args: vec![Expr::Error {
                        raw,
                        pos,
                        span: self.node_span(start_i),
                    }],
                    pos,
                    span: self.node_span(start_i),
                });
            }
            items.push(e);
            if !self.eat(&Tok::Comma) {
                break;
            }
        }
        self.eat(&Tok::RBracket);
        Some(Expr::List {
            items,
            pos,
            span: self.node_span(start_i),
        })
    }
}

/// Operators that structure declarations and can never be expression infix
/// operators.
fn is_reserved_op(op: &str) -> bool {
    matches!(op, "=" | "<-" | "->" | "|" | ":" | "=>" | "@" | "\\" | "..")
}

/// (precedence, right-assoc) — Haskell defaults; unknown operators get
/// infixl 9.
fn fixity(op: &str) -> (u8, bool) {
    match op {
        "$" | "$!" => (1, true),
        ">>=" | ">>" | "=<<" | "<&>" => (2, false),
        "||" => (3, true),
        "&&" => (4, true),
        "==" | "/=" | "<" | "<=" | ">" | ">=" => (5, false),
        "::" | "++" | "<>" => (6, true),
        "+" | "-" => (7, false),
        "*" | "/" => (8, false),
        "^" | "**" => (9, true),
        "." | "!!" => (10, true),
        _ => (9, false),
    }
}

/// Merge type signatures and successive equations of the same function into
/// one `Decl::Function`, preserving first-seen order.
/// Bounding span of a function's equations (their first start to last end).
/// `None` for a signature-only function (no equations yet).
fn equations_extent(eqs: &[Equation]) -> Option<Span> {
    let mut it = eqs.iter();
    let first = it.next()?;
    let mut s = first.span;
    for e in it {
        s.start = s.start.min(e.span.start);
        s.end = s.end.max(e.span.end);
    }
    Some(s)
}

fn merge_functions(decls: &mut Vec<Decl>) {
    let mut out: Vec<Decl> = Vec::with_capacity(decls.len());
    for decl in decls.drain(..) {
        match decl {
            Decl::Function(f) => {
                let existing = out.iter_mut().find_map(|d| match d {
                    Decl::Function(g) if g.name == f.name => Some(g),
                    _ => None,
                });
                match existing {
                    Some(g) => {
                        if g.ty.is_none() {
                            g.ty = f.ty.clone();
                        }
                        if g.sig_span.is_none() {
                            g.sig_span = f.sig_span;
                        }
                        // The function's reported position is its first
                        // equation, not its type signature.
                        if g.equations.is_empty() && !f.equations.is_empty() {
                            g.pos = f.pos;
                        }
                        g.equations.extend(f.equations);
                        // A function's span is the extent of its equations,
                        // which are contiguous in well-formed source. A type
                        // signature can sit apart (with unrelated decls in
                        // between), so it is tracked in `sig_span` and never
                        // folded into `span` — doing so could straddle a
                        // sibling decl and break the nesting invariant.
                        g.span = equations_extent(&g.equations)
                            .or(g.sig_span)
                            .unwrap_or(g.span);
                    }
                    None => out.push(Decl::Function(f)),
                }
            }
            other => out.push(other),
        }
    }
    *decls = out;
}

/// Parse a type from a slice of the type's tokens (e.g. the tokens between a
/// field's `:` and the item end). PURE: it never touches the main parser cursor
/// and never affects any span, so it is invisible to daml-fmt. Returns `None`
/// when the whole slice does not parse cleanly as a type.
///
/// Grammar (precedence low → high): constraint `C => T`, function `a -> b`
/// (right-assoc), application `head arg...` (left-assoc), then atoms (`Con`,
/// `Var`, `[T]`, `()`, `(T)`, `(a, b)`).
pub(crate) fn parse_type_from_tokens(tokens: &[Token]) -> Option<Type> {
    // Virtual layout tokens carry no type meaning; drop them so a stray VSemi
    // in the slice can't sink an otherwise-clean parse.
    let real_tokens: Vec<&Token> = tokens.iter().filter(|t| !t.is_virtual()).collect();
    if real_tokens.is_empty() {
        return None;
    }
    let mut parser = TypeTokenParser {
        tokens: &real_tokens,
        cursor: 0,
    };
    let ty = parser.parse_type()?;
    // Require the whole slice to be consumed: a partial parse means the type
    // had a shape we don't model, so report unknown rather than a half-truth.
    if parser.cursor == real_tokens.len() {
        Some(ty)
    } else {
        None
    }
}

struct TypeTokenParser<'a> {
    tokens: &'a [&'a Token],
    cursor: usize,
}

/// Result of parsing one atom: a real type, or a dropped type-level nat literal
/// (`Numeric 10` — not a type, so it never enters the App arg list).
enum TypeAtom {
    ParsedType(Type),
    DroppedLiteral(Span),
}

impl<'a> TypeTokenParser<'a> {
    fn peek(&self) -> Option<&'a Token> {
        self.tokens.get(self.cursor).copied()
    }

    fn eat_op(&mut self, op: &str) -> bool {
        if self.peek().is_some_and(|t| t.tok.is_op(op)) {
            self.cursor += 1;
            true
        } else {
            false
        }
    }

    /// Full type: a constraint context `=> body`, or a function `a -> b`, or a
    /// bare application.
    fn parse_type(&mut self) -> Option<Type> {
        let lhs = self.parse_application_type()?;
        if self.eat_op("=>") {
            // `lhs` was the constraint context; drop it, keep the body.
            let body = self.parse_type()?;
            let span = Span::new(lhs.span().start, body.span().end);
            return Some(Type::Constrained(Box::new(body), span));
        }
        if self.eat_op("->") {
            let rhs = self.parse_type()?;
            let span = Span::new(lhs.span().start, rhs.span().end);
            return Some(Type::Fun(Box::new(lhs), Box::new(rhs), span));
        }
        Some(lhs)
    }

    /// Application spine: one head atom applied to zero or more argument atoms.
    fn parse_application_type(&mut self) -> Option<Type> {
        let head = match self.parse_atom()? {
            TypeAtom::ParsedType(t) => t,
            // A bare nat literal is not a type.
            TypeAtom::DroppedLiteral(_) => return None,
        };
        let mut args = Vec::new();
        let start = head.span().start;
        let mut end = head.span().end;
        loop {
            // Only continue the spine if the next token can START an atom; an
            // operator (`->`, `=>`) or closer ends it.
            if !self.is_at_type_atom_start() {
                break;
            }
            match self.parse_atom()? {
                TypeAtom::ParsedType(t) => {
                    end = t.span().end;
                    args.push(t);
                }
                TypeAtom::DroppedLiteral(span) => {
                    // `Numeric 10` — drop the `10` as structure but keep it in
                    // the enclosing type span.
                    end = span.end;
                }
            }
        }
        let span = Span::new(start, end);
        if args.is_empty() {
            Some(head.with_span(span))
        } else {
            Some(Type::App(Box::new(head), args, span))
        }
    }

    /// True if the current token can begin an atom (so the application spine
    /// should keep going).
    fn is_at_type_atom_start(&self) -> bool {
        matches!(
            self.peek().map(|t| &t.tok),
            Some(Tok::UpperId { .. })
                | Some(Tok::LowerId { .. })
                | Some(Tok::IntLit(_))
                | Some(Tok::DecimalLit(_))
                | Some(Tok::LBracket)
                | Some(Tok::LParen)
        )
    }

    fn parse_atom(&mut self) -> Option<TypeAtom> {
        let tok = self.peek()?;
        match &tok.tok {
            Tok::UpperId { qualifier, name } => {
                let con = Type::Con {
                    qualifier: qualifier.clone(),
                    name: name.clone(),
                    span: Span::new(tok.start, tok.end),
                };
                self.cursor += 1;
                Some(TypeAtom::ParsedType(con))
            }
            Tok::LowerId { name, .. } => {
                // Type variable (`a`, `n`). Qualified lowercase never appears in
                // a real type position; treat the name as the variable.
                let var = Type::Var(name.clone(), Span::new(tok.start, tok.end));
                self.cursor += 1;
                Some(TypeAtom::ParsedType(var))
            }
            Tok::IntLit(_) | Tok::DecimalLit(_) => {
                // Type-level nat literal (`Numeric 10`): consumed, but dropped.
                self.cursor += 1;
                Some(TypeAtom::DroppedLiteral(Span::new(tok.start, tok.end)))
            }
            Tok::LBracket => {
                let start = tok.start;
                self.cursor += 1;
                let inner = self.parse_type()?;
                self.eat_token(Tok::RBracket).map(|end| {
                    TypeAtom::ParsedType(Type::List(Box::new(inner), Span::new(start, end.end)))
                })
            }
            Tok::LParen => {
                let start = tok.start;
                self.cursor += 1;
                if let Some(end) = self.eat_token(Tok::RParen) {
                    // ()
                    return Some(TypeAtom::ParsedType(Type::Unit(Span::new(start, end.end))));
                }
                let first = self.parse_type()?;
                if self.peek().map(|t| &t.tok) == Some(&Tok::Comma) {
                    let mut items = vec![first];
                    while self.eat_token(Tok::Comma).is_some() {
                        items.push(self.parse_type()?);
                    }
                    self.eat_token(Tok::RParen).map(|end| {
                        TypeAtom::ParsedType(Type::Tuple(items, Span::new(start, end.end)))
                    })
                } else {
                    self.eat_token(Tok::RParen).map(|end| {
                        // Grouping parens.
                        TypeAtom::ParsedType(first.with_span(Span::new(start, end.end)))
                    })
                }
            }
            _ => None,
        }
    }

    fn eat_token(&mut self, tok: Tok) -> Option<&'a Token> {
        if self.peek().is_some_and(|t| t.tok == tok) {
            let t = self.peek();
            self.cursor += 1;
            t
        } else {
            None
        }
    }
}

fn render_token_slice(tokens: &[Token]) -> String {
    let mut s = String::new();
    let mut prev_no_space_after = true;
    for t in tokens {
        let (text, no_space_before, no_space_after): (String, bool, bool) = match &t.tok {
            Tok::LowerId { qualifier, name } | Tok::UpperId { qualifier, name } => (
                qualifier
                    .as_ref()
                    .map_or_else(|| name.clone(), |q| format!("{}.{}", q, name)),
                false,
                false,
            ),
            Tok::Op(o) => (o.clone(), false, false),
            Tok::IntLit(n) | Tok::DecimalLit(n) => (n.clone(), false, false),
            Tok::StringLit(v) => (format!("{:?}", v), false, false),
            Tok::CharLit(v) => (format!("'{}'", v), false, false),
            Tok::LParen => ("(".to_string(), false, true),
            Tok::RParen => (")".to_string(), true, false),
            Tok::LBracket => ("[".to_string(), false, true),
            Tok::RBracket => ("]".to_string(), true, false),
            Tok::LBrace => ("{".to_string(), false, true),
            Tok::RBrace => ("}".to_string(), true, false),
            Tok::Comma => (",".to_string(), true, false),
            Tok::Semi | Tok::VSemi => (";".to_string(), true, false),
            Tok::Backtick => ("`".to_string(), false, false),
            Tok::VLBrace | Tok::VRBrace => continue,
        };
        if !s.is_empty() && !no_space_before && !prev_no_space_after {
            s.push(' ');
        }
        s.push_str(&text);
        prev_no_space_after = no_space_after;
    }
    s
}

#[cfg(test)]
mod type_tests {
    use super::*;
    use crate::lexer::lex;

    /// Parse a bare type string straight through the lexer. A single-line type
    /// has no layout-significant newlines, so no virtual tokens appear — this
    /// exercises the type grammar in isolation.
    fn ty(s: &str) -> Option<Type> {
        let (toks, errs) = lex(s);
        assert!(errs.is_empty(), "lex errors for {s:?}: {errs:?}");
        parse_type_from_tokens(&toks)
    }

    fn con(name: &str) -> Type {
        Type::Con {
            qualifier: None,
            name: name.to_string(),
            span: Span::default(),
        }
    }

    fn qualified_con(qualifier: &str, name: &str) -> Type {
        Type::Con {
            qualifier: Some(qualifier.to_string()),
            name: name.to_string(),
            span: Span::default(),
        }
    }

    fn app(head: Type, args: Vec<Type>) -> Type {
        Type::App(Box::new(head), args, Span::default())
    }

    fn list(inner: Type) -> Type {
        Type::List(Box::new(inner), Span::default())
    }

    fn tuple(items: Vec<Type>) -> Type {
        Type::Tuple(items, Span::default())
    }

    fn fun(param: Type, result: Type) -> Type {
        Type::Fun(Box::new(param), Box::new(result), Span::default())
    }

    fn var(name: &str) -> Type {
        Type::Var(name.to_string(), Span::default())
    }

    fn unit() -> Type {
        Type::Unit(Span::default())
    }

    fn constrained(body: Type) -> Type {
        Type::Constrained(Box::new(body), Span::default())
    }

    #[test]
    fn atoms() {
        assert_eq!(ty("Party"), Some(con("Party")));
        assert_eq!(ty("Decimal"), Some(con("Decimal")));
        assert_eq!(ty("a"), Some(var("a")));
        assert_eq!(ty("()"), Some(unit()));
    }

    #[test]
    fn application_vs_constructor() {
        // The whole point of the new model: `ContractId Foo` is an APPLICATION,
        // not one opaque name.
        assert_eq!(
            ty("ContractId Foo"),
            Some(app(con("ContractId"), vec![con("Foo")]))
        );
        assert_eq!(
            ty("Optional (ContractId Foo)"),
            Some(app(
                con("Optional"),
                vec![app(con("ContractId"), vec![con("Foo")])]
            ))
        );
        assert_eq!(
            ty("Map Text Int"),
            Some(app(con("Map"), vec![con("Text"), con("Int")]))
        );
    }

    #[test]
    fn qualified_constructor_keeps_qualifier() {
        assert_eq!(
            ty("DA.Map.Map Text Int"),
            Some(app(
                qualified_con("DA.Map", "Map"),
                vec![con("Text"), con("Int")]
            ))
        );
    }

    #[test]
    fn list_and_tuple() {
        assert_eq!(ty("[Text]"), Some(list(con("Text"))));
        assert_eq!(
            ty("(Int, Text)"),
            Some(tuple(vec![con("Int"), con("Text")]))
        );
        // A tuple is NOT a grouping paren — must stay a Tuple, never collapse.
        assert_eq!(
            ty("(a, b, c)"),
            Some(tuple(vec![var("a"), var("b"), var("c")]))
        );
        // Single grouping paren unwraps.
        assert_eq!(ty("(Text)"), Some(con("Text")));
    }

    #[test]
    fn function_types_are_arrows_not_names() {
        // These are exactly the corpus strings the old matcher swallowed into
        // one opaque `Named`.
        assert_eq!(ty("Int -> Int"), Some(fun(con("Int"), con("Int"))));
        // Right associativity: `a -> b -> c` == `a -> (b -> c)`.
        assert_eq!(
            ty("Int -> Text -> Bool"),
            Some(fun(con("Int"), fun(con("Text"), con("Bool"))))
        );
        assert_eq!(
            ty("Party -> Script ()"),
            Some(fun(con("Party"), app(con("Script"), vec![unit()])))
        );
    }

    #[test]
    fn script_application() {
        // `Script ()` ×147 in the corpus — an application flattened to `Named`
        // before. Now a real App.
        assert_eq!(ty("Script ()"), Some(app(con("Script"), vec![unit()])));
    }

    #[test]
    fn numeric_nat_literal_is_dropped() {
        // `Numeric 10`: the `10` is a type-level nat, not a type, so it drops
        // and the head Con stands alone. `Numeric n` keeps the type variable.
        assert_eq!(ty("Numeric 10"), Some(con("Numeric")));
        assert_eq!(ty("Numeric n"), Some(app(con("Numeric"), vec![var("n")])));
    }

    #[test]
    fn constraint_context_is_dropped_body_kept() {
        // `NumericScale n => Numeric 37 -> Numeric n` — a constrained function.
        // Context dropped; body (the arrow) kept.
        assert_eq!(
            ty("NumericScale n => Numeric 37 -> Numeric n"),
            Some(constrained(fun(
                con("Numeric"),
                app(con("Numeric"), vec![var("n")])
            )))
        );
        // Tuple context `(Eq a, Show a) => a` also drops cleanly.
        assert_eq!(ty("(Eq a, Show a) => a"), Some(constrained(var("a"))));
    }

    #[test]
    fn unparseable_is_none() {
        // A trailing arrow with no body is not a clean type → unknown (None),
        // never a half-parse.
        assert_eq!(ty("Int ->"), None);
        assert_eq!(ty("-> Int"), None);
    }

    #[test]
    fn ty_is_populated_through_real_parse() {
        // End-to-end: the wiring actually fills `ty` on template fields and the
        // choice return type, from the real token stream.
        let src = r#"module M where
template T
  with
    owner : Party
    held : ContractId Asset
  where
    signatory owner
    choice Go : Optional (ContractId Asset)
      controller owner
      do
        pure None
"#;
        let (m, _) = parse_module(src);
        let t = match &m.decls[0] {
            Decl::Template(t) => t,
            other => panic!("expected template, got {other:?}"),
        };
        assert_eq!(t.fields[0].ty, Some(con("Party")));
        assert_eq!(
            t.fields[1].ty,
            Some(app(con("ContractId"), vec![con("Asset")]))
        );
        let choice = match &t
            .body
            .iter()
            .find(|d| matches!(d, TemplateBodyDecl::Choice(_)))
        {
            Some(TemplateBodyDecl::Choice(c)) => (*c).clone(),
            _ => panic!("expected choice"),
        };
        assert_eq!(
            choice.return_ty,
            Some(app(
                con("Optional"),
                vec![app(con("ContractId"), vec![con("Asset")])]
            ))
        );
    }

    #[test]
    fn ty_is_populated_on_key_and_interface_method() {
        // The other two type-bearing nodes: a template `key ... : T` and an
        // interface method signature both fill `ty` from the token stream.
        let src = r#"module M where
template T
  with
    owner : Party
  where
    signatory owner
    key owner : Party
    maintainer owner

interface I where
  getAmount : Numeric 10
"#;
        let (m, _) = parse_module(src);
        let t = match &m.decls[0] {
            Decl::Template(t) => t,
            other => panic!("expected template, got {other:?}"),
        };
        let key_ty = t.body.iter().find_map(|d| match d {
            TemplateBodyDecl::Key { ty, .. } => Some(ty.clone()),
            _ => None,
        });
        assert_eq!(key_ty, Some(Some(con("Party"))));

        let iface = match &m.decls[1] {
            Decl::Interface(i) => i,
            other => panic!("expected interface, got {other:?}"),
        };
        // `Numeric 10` — the nat literal is dropped, leaving the bare head Con.
        assert_eq!(iface.methods[0].ty, Some(con("Numeric")));
    }
}
