Skip to main content

daml_parser/
parse.rs

1//! Recursive-descent parser: laid-out token stream → typed AST (src/ast.rs).
2//!
3//! Error recovery is per-declaration: an unparseable declaration becomes
4//! `Decl::Unknown` plus a diagnostic, and parsing continues at the next
5//! virtual semicolon. The parser never panics and never aborts the file.
6
7use crate::ast::{
8    Alt, Binding, ChoiceDecl, Consuming, Decl, DoStmt, Equation, ExpectedToken, Expr, FieldAssign,
9    FieldDecl, FunctionDecl, Identifier, ImportDecl, ImportStyle, InterfaceDecl,
10    InterfaceInstanceDecl, LitKind, MalformedSyntaxKind, Module, ModuleName, Operator,
11    ParseDiagnostic, ParseDiagnosticKind, Pat, SkippedDeclarationReason, Span, TemplateBodyDecl,
12    TemplateDecl, Type, TypeAnnotation, TypeAnnotationContext, UnsupportedSyntaxKind,
13};
14use crate::layout::resolve_layout;
15use crate::lexer::{lex, Pos, Token, TokenKind};
16use std::collections::HashMap;
17
18pub const MAX_RECURSION_DEPTH: u32 = 128;
19
20/// Result of tolerant module parsing.
21///
22/// The parser always returns a [`Module`], even when it had to recover from
23/// lexical errors, malformed syntax, unsupported syntax, or skipped
24/// declarations. Inspect [`Self::diagnostics`] (or use [`Self::has_errors`]) to
25/// decide whether the partial tree is acceptable. Use [`Self::into_result`] for
26/// callers that require a diagnostic-free parse.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct ParseModuleResult {
29    /// Parsed module tree. This is always present and may be partial when
30    /// diagnostics were recorded.
31    pub module: Module,
32    /// Recoverable parse and lex issues in source order.
33    pub diagnostics: Vec<ParseDiagnostic>,
34}
35
36/// Recoverable parse failure for strict callers.
37///
38/// Produced when [`parse_module_strict`] or [`ParseModuleResult::into_result`]
39/// reject a tolerant [`parse_module`] result because diagnostics were recorded.
40/// The partial module tree is retained for inspection but is not considered a
41/// successful parse.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct ParseModuleError {
44    diagnostics: Vec<ParseDiagnostic>,
45    module: Box<Module>,
46}
47
48impl ParseModuleError {
49    #[must_use]
50    pub fn diagnostics(&self) -> &[ParseDiagnostic] {
51        &self.diagnostics
52    }
53
54    #[must_use]
55    pub fn module(&self) -> &Module {
56        &self.module
57    }
58
59    #[must_use]
60    pub fn into_parts(self) -> (Vec<ParseDiagnostic>, Module) {
61        (self.diagnostics, *self.module)
62    }
63}
64
65impl std::fmt::Display for ParseModuleError {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        write!(
68            f,
69            "parse failed with {} diagnostic(s)",
70            self.diagnostics.len()
71        )?;
72        if let Some(first) = self.diagnostics.first() {
73            write!(f, ": {}", first.message)?;
74        }
75        Ok(())
76    }
77}
78
79impl std::error::Error for ParseModuleError {}
80
81#[derive(Clone, Copy, Debug)]
82enum DoExpressionMode {
83    Allow,
84    Disallow,
85}
86
87impl DoExpressionMode {
88    const fn allows_do(&self) -> bool {
89        matches!(self, Self::Allow)
90    }
91}
92
93impl ParseModuleResult {
94    /// True when tolerant parsing recorded any recoverable diagnostic.
95    #[must_use]
96    pub const fn has_errors(&self) -> bool {
97        !self.diagnostics.is_empty()
98    }
99
100    /// Split the partial module tree from its source-ordered diagnostics.
101    #[must_use]
102    pub fn into_parts(self) -> (Module, Vec<ParseDiagnostic>) {
103        (self.module, self.diagnostics)
104    }
105
106    /// Convert a tolerant parse result into a strict [`Result`].
107    ///
108    /// Returns [`Ok`] only when [`Self::diagnostics`] is empty. Any diagnostic —
109    /// lexical, malformed, skipped declaration, unsupported syntax, or
110    /// recursion-limit degradation — becomes [`Err`].
111    ///
112    /// # Errors
113    ///
114    /// Returns [`ParseModuleError`] when [`Self::diagnostics`] is non-empty.
115    pub fn into_result(self) -> Result<Module, ParseModuleError> {
116        if self.diagnostics.is_empty() {
117            Ok(self.module)
118        } else {
119            Err(ParseModuleError {
120                diagnostics: self.diagnostics,
121                module: Box::new(self.module),
122            })
123        }
124    }
125}
126
127/// Parse Daml `source` into a [`Module`] plus any [`ParseDiagnostic`]s, in
128/// source order.
129///
130/// This is the crate's entry point. It **never panics and never aborts the
131/// file**: recovery is per-declaration, so an unparseable declaration becomes a
132/// [`Decl::Unknown`] (with a diagnostic) and parsing continues at the next
133/// declaration. A `Module` is therefore always returned, even for badly broken
134/// input — a non-empty diagnostics list signals problems, not a missing tree.
135///
136/// ```
137/// let result = daml_parser::parse::parse_module("module M where\n");
138/// assert_eq!(result.module.name, "M");
139/// assert!(result.diagnostics.is_empty());
140/// ```
141#[must_use]
142pub fn parse_module(source: &str) -> ParseModuleResult {
143    let lexed = lex(source);
144    let tokens = lexed.tokens;
145    let lex_errors = lexed.errors;
146    let tokens = resolve_layout(tokens);
147    let mut p = Parser {
148        toks: tokens,
149        src_len: source.len(),
150        i: 0,
151        depth: 0,
152        diags: lex_errors
153            .into_iter()
154            .map(|e| {
155                let range = e.byte_range_in(source);
156                ParseDiagnostic::new(
157                    ParseDiagnosticKind::Lex(e.kind.clone()),
158                    e.to_string(),
159                    e.pos,
160                    crate::ast::Span::from_usize(range.start, range.end),
161                )
162            })
163            .collect(),
164    };
165    let mut module = p.module();
166    module.span = crate::ast::Span::from_usize(0, source.len());
167    ParseModuleResult {
168        module,
169        diagnostics: p.diags,
170    }
171}
172
173/// Parse Daml `source` into a [`Module`], treating any diagnostic as failure.
174///
175/// This is a thin wrapper over [`parse_module`] followed by
176/// [`ParseModuleResult::into_result`]. Use it for build/CI paths that must not
177/// proceed on recoverable parse problems. For editors, formatters, and other
178/// tools that need partial structure plus diagnostics, keep using
179/// [`parse_module`].
180///
181/// ```
182/// use daml_parser::parse::parse_module_strict;
183///
184/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
185/// let module = parse_module_strict("module M where\nfoo : Int\nfoo = 1\n")?;
186/// assert_eq!(module.name, "M");
187///
188/// let strict = parse_module_strict("module M where\n%%% junk\n");
189/// assert!(matches!(
190///     strict.as_ref(),
191///     Err(err) if !err.diagnostics().is_empty()
192/// ));
193/// # Ok(())
194/// # }
195/// ```
196///
197/// # Errors
198///
199/// Returns [`ParseModuleError`] when [`parse_module`] records any diagnostic.
200pub fn parse_module_strict(source: &str) -> Result<Module, ParseModuleError> {
201    parse_module(source).into_result()
202}
203
204struct Parser {
205    toks: Vec<Token>,
206    /// Source byte length — span fallback when a node consumes no real token.
207    src_len: usize,
208    i: usize,
209    diags: Vec<ParseDiagnostic>,
210    /// Expression/pattern recursion depth; bounded so hostile inputs
211    /// (thousands of nested parens) cannot overflow the stack.
212    depth: u32,
213}
214
215impl Parser {
216    /// Byte span of every non-virtual token consumed since token index `from`
217    /// (a function's entry cursor). This is the node's full extent: first real
218    /// token's `start` to last real token's `end`. Virtual layout tokens carry
219    /// no bytes and are skipped, so spans tile the source and never include the
220    /// trailing whitespace a `VRBrace`/`VSemi` sits on.
221    fn node_span(&self, from: usize) -> crate::ast::Span {
222        let mut a = from;
223        while a < self.i && self.toks[a].is_virtual() {
224            a += 1;
225        }
226        let mut b = self.i;
227        while b > a && self.toks[b - 1].is_virtual() {
228            b -= 1;
229        }
230        if a >= b {
231            // No real token consumed (e.g. an empty error node): zero-width
232            // span at the next real byte position so it still nests inside its
233            // parent. Use `a` (past any leading virtual tokens) — `from` itself
234            // may be a virtual token whose byte offset is a meaningless 0.
235            let p = self.byte_at(a);
236            return crate::ast::Span::from_usize(p, p);
237        }
238        crate::ast::Span::from_usize(self.toks[a].start, self.toks[b - 1].end)
239    }
240
241    /// Byte offset where token `i` begins, or the source end past the last one.
242    fn byte_at(&self, i: usize) -> usize {
243        self.toks.get(i).map(|t| t.start).unwrap_or(self.src_len)
244    }
245
246    /// End byte of the last non-virtual token consumed so far — for nodes
247    /// whose start comes from an already-parsed child rather than the entry
248    /// cursor (e.g. `record with { .. }` built around its base expression).
249    fn end_byte(&self) -> usize {
250        let mut b = self.i;
251        while b > 0 && self.toks[b - 1].is_virtual() {
252            b -= 1;
253        }
254        if b == 0 {
255            0
256        } else {
257            self.toks[b - 1].end
258        }
259    }
260}
261
262impl Parser {
263    // ----- cursor primitives -------------------------------------------
264
265    fn peek(&self) -> Option<&TokenKind> {
266        self.toks.get(self.i).map(|t| &t.kind)
267    }
268
269    fn peek_at(&self, n: usize) -> Option<&TokenKind> {
270        self.toks.get(self.i + n).map(|t| &t.kind)
271    }
272
273    fn pos(&self) -> Pos {
274        self.toks
275            .get(self.i)
276            .or_else(|| self.toks.last())
277            .map_or(Pos { line: 1, column: 1 }, |t| t.pos)
278    }
279
280    fn bump(&mut self) -> Option<Token> {
281        let t = self.toks.get(self.i).cloned();
282        if t.is_some() {
283            self.i += 1;
284        }
285        t
286    }
287
288    fn at_keyword(&self, kw: &str) -> bool {
289        self.peek().is_some_and(|t| t.is_keyword(kw))
290    }
291
292    fn eat_keyword(&mut self, kw: &str) -> bool {
293        if self.at_keyword(kw) {
294            self.i += 1;
295            true
296        } else {
297            false
298        }
299    }
300
301    fn at_op(&self, op: &str) -> bool {
302        self.peek().is_some_and(|t| t.is_op(op))
303    }
304
305    fn eat_op(&mut self, op: &str) -> bool {
306        if self.at_op(op) {
307            self.i += 1;
308            true
309        } else {
310            false
311        }
312    }
313
314    fn at(&self, tok: &TokenKind) -> bool {
315        self.peek() == Some(tok)
316    }
317
318    fn eat(&mut self, tok: &TokenKind) -> bool {
319        if self.at(tok) {
320            self.i += 1;
321            true
322        } else {
323            false
324        }
325    }
326
327    /// Emit an expected-token diagnostic at the current token.
328    fn diag_expected(&mut self, expected: ExpectedToken, message: impl Into<String>) {
329        self.diag_kind(ParseDiagnosticKind::ExpectedToken(expected), message);
330    }
331
332    /// Emit a malformed-syntax diagnostic at the current token.
333    fn diag_malformed(&mut self, kind: MalformedSyntaxKind, message: impl Into<String>) {
334        self.diag_kind(ParseDiagnosticKind::MalformedSyntax(kind), message);
335    }
336
337    /// Emit a diagnostic with an explicit typed recovery reason. The span is
338    /// the current token's byte extent (the offending token), so consumers get
339    /// an end position, not just a start.
340    fn diag_kind(&mut self, kind: ParseDiagnosticKind, message: impl Into<String>) {
341        let pos = self.pos();
342        let span = self.cur_span();
343        self.diags
344            .push(ParseDiagnostic::new(kind, message, pos, span));
345    }
346
347    fn parse_type_annotation(
348        &mut self,
349        type_start: usize,
350        type_end: usize,
351        context: TypeAnnotationContext,
352    ) -> TypeAnnotation {
353        let type_start = type_start.min(self.toks.len());
354        let type_end = type_end.min(self.toks.len());
355        let tokens = &self.toks[type_start..type_end];
356        let ty = parse_type_from_tokens(tokens).or_else(|| {
357            let trimmed = Self::trim_type_tokens_for_parse(tokens);
358            if trimmed < tokens.len() {
359                parse_type_from_tokens(&tokens[..trimmed])
360            } else {
361                None
362            }
363        });
364        match ty {
365            Some(ty) => TypeAnnotation::Present(ty),
366            None => {
367                let span = self.span_of_token_range(type_start, type_end);
368                self.diags.push(ParseDiagnostic::new(
369                    ParseDiagnosticKind::MalformedTypeAnnotation(context),
370                    format!("malformed {} type annotation", context.as_str()),
371                    self.pos_of_token(type_start),
372                    span,
373                ));
374                TypeAnnotation::Malformed { span }
375            }
376        }
377    }
378
379    /// Trim trailing tokens that belong to declaration tails (for example `= expr`
380    /// in typed function signatures, explicit semicolons, or the next field
381    /// declaration) so we can parse valid corpus forms that our tiny type
382    /// parser doesn't model directly.
383    fn trim_type_tokens_for_parse(tokens: &[Token]) -> usize {
384        let mut depth = 0usize;
385        let mut bracket_depth = 0usize;
386        let mut i = 0usize;
387        while i < tokens.len() {
388            match &tokens[i].kind {
389                TokenKind::LParen | TokenKind::LBracket => {
390                    depth += 1;
391                    i += 1;
392                }
393                TokenKind::RParen | TokenKind::RBracket => {
394                    depth = depth.saturating_sub(1);
395                    i += 1;
396                }
397                TokenKind::LBrace => {
398                    bracket_depth += 1;
399                    i += 1;
400                }
401                TokenKind::RBrace => {
402                    bracket_depth = bracket_depth.saturating_sub(1);
403                    i += 1;
404                }
405                TokenKind::Op(o) if o.as_str() == "=" && depth == 0 && bracket_depth == 0 => {
406                    return i;
407                }
408                TokenKind::Semi | TokenKind::VSemi if depth == 0 && bracket_depth == 0 => {
409                    return i;
410                }
411                TokenKind::Comma
412                    if depth == 0
413                        && bracket_depth == 0
414                        && matches!(
415                            tokens.get(i + 1),
416                            Some(Token {
417                                kind: TokenKind::LowerId {
418                                    qualifier: None,
419                                    ..
420                                },
421                                ..
422                            })
423                        )
424                        && matches!(
425                            tokens.get(i + 2),
426                            Some(Token { kind: TokenKind::Op(o), .. }) if o.as_str() == ":"
427                        ) =>
428                {
429                    return i;
430                }
431                _ => {
432                    i += 1;
433                }
434            }
435        }
436        tokens.len()
437    }
438
439    fn pos_of_token(&self, idx: usize) -> Pos {
440        self.toks.get(idx).map_or_else(|| self.pos(), |tok| tok.pos)
441    }
442
443    fn span_of_token_range(&self, start: usize, end: usize) -> Span {
444        let start = start.min(self.toks.len());
445        let end = end.min(self.toks.len());
446        let span_start = self.byte_at(start);
447        if end <= start {
448            return Span::from_usize(span_start, span_start);
449        }
450
451        let mut cursor = end;
452        while cursor > start {
453            cursor -= 1;
454            let token = &self.toks[cursor];
455            if !token.is_virtual() {
456                return Span::from_usize(span_start, token.end);
457            }
458        }
459
460        Span::from_usize(span_start, span_start)
461    }
462
463    /// Byte span of the next real (non-virtual) token, or a zero-width span at
464    /// end-of-input. Used to anchor a diagnostic to the offending token.
465    fn cur_span(&self) -> crate::ast::Span {
466        let mut j = self.i;
467        while self.toks.get(j).is_some_and(|t| t.is_virtual()) {
468            j += 1;
469        }
470        self.toks.get(j).map_or_else(
471            || crate::ast::Span::from_usize(self.src_len, self.src_len),
472            |t| crate::ast::Span::from_usize(t.start, t.end),
473        )
474    }
475
476    /// Skip tokens until the end of the current block item: a `VSemi` or
477    /// `VRBrace` at nesting depth zero (relative to here). Consumes neither.
478    fn skip_to_item_end(&mut self) {
479        let mut depth = 0usize;
480        let mut brackets = 0usize;
481        while let Some(t) = self.peek() {
482            match t {
483                TokenKind::VLBrace => depth += 1,
484                TokenKind::VRBrace => {
485                    if depth == 0 {
486                        return;
487                    }
488                    depth -= 1;
489                }
490                TokenKind::VSemi if depth == 0 && brackets == 0 => return,
491                TokenKind::LParen | TokenKind::LBracket | TokenKind::LBrace => brackets += 1,
492                TokenKind::RParen | TokenKind::RBracket | TokenKind::RBrace => {
493                    if brackets == 0 {
494                        // Closing bracket of an enclosing construct: stop
495                        // before it so the caller can match it.
496                        return;
497                    }
498                    brackets -= 1;
499                }
500                _ => {}
501            }
502            self.i += 1;
503        }
504    }
505
506    /// Raw text of tokens from `start` to the current position.
507    fn slice_text(&self, start: usize) -> String {
508        render_token_slice(&self.toks[start..self.i])
509    }
510
511    // ----- module ------------------------------------------------------
512
513    fn module(&mut self) -> Module {
514        let pos = self.pos();
515        let header_start = self.i;
516        let mut header = crate::ast::Span::from_usize(0, 0);
517        let mut name = ModuleName::from("Unknown");
518
519        if self.eat_keyword("module") {
520            if let Some(TokenKind::UpperId { qualifier, name: n }) = self.peek().cloned() {
521                self.bump();
522                name = match qualifier {
523                    Some(q) => format!("{q}.{n}").into(),
524                    None => n.into(),
525                };
526            }
527            // Optional export list.
528            if self.at(&TokenKind::LParen) {
529                self.skip_balanced_parens();
530            }
531            if !self.eat_keyword("where") {
532                self.diag_expected(
533                    ExpectedToken::WhereAfterModuleHeader,
534                    "expected 'where' after module header",
535                );
536            }
537            header = self.node_span(header_start);
538        }
539
540        let mut imports = Vec::new();
541        let mut decls: Vec<Decl> = Vec::new();
542
543        // Consume the opening brace of the module body if present. The result
544        // is unused: the loop below terminates on the matching close brace or
545        // end-of-input regardless of whether the block was braced.
546        let _ = self.eat(&TokenKind::VLBrace) || self.eat(&TokenKind::LBrace);
547        loop {
548            while self.eat(&TokenKind::VSemi) || self.eat(&TokenKind::Semi) {}
549            match self.peek() {
550                None => break,
551                Some(TokenKind::VRBrace | TokenKind::RBrace) => {
552                    self.bump();
553                    break;
554                }
555                // A stray closing bracket inside a block is garbage from a
556                // failed item parse — record it as an Unknown declaration so
557                // its bytes stay covered, then continue (skip_to_item_end
558                // deliberately stops before unmatched closers).
559                Some(TokenKind::RParen | TokenKind::RBracket) => {
560                    let cpos = self.pos();
561                    let cstart = self.i;
562                    self.bump();
563                    decls.push(Decl::Unknown {
564                        raw: self.slice_text(cstart),
565                        pos: cpos,
566                        span: self.node_span(cstart),
567                    });
568                    continue;
569                }
570                _ => {}
571            }
572            let before = self.i;
573            self.declaration(&mut imports, &mut decls);
574            if self.i == before {
575                // Defensive: guarantee progress even on a parser bug.
576                self.bump();
577            }
578        }
579
580        merge_functions(&mut decls);
581
582        Module {
583            name,
584            pos,
585            header,
586            imports,
587            decls,
588            span: crate::ast::Span::from_usize(0, self.src_len),
589        }
590    }
591
592    fn skip_balanced_parens(&mut self) {
593        let mut depth = 0usize;
594        while let Some(t) = self.peek() {
595            match t {
596                TokenKind::LParen => depth += 1,
597                TokenKind::RParen => {
598                    if depth == 0 {
599                        return;
600                    }
601                    depth -= 1;
602                    if depth == 0 {
603                        self.i += 1;
604                        return;
605                    }
606                }
607                _ => {}
608            }
609            self.i += 1;
610        }
611    }
612
613    /// If the cursor sits on an infix operator equation with a pattern
614    /// left operand (`[] !! _ = ...`, `None <?> s = ...`), skip it and
615    /// return true. Operators have no IR surface.
616    fn try_infix_operator_decl(&mut self) -> bool {
617        let snap = self.i;
618        let saved_diags = self.diags.len();
619        if self.pattern().is_some()
620            && matches!(self.peek(), Some(TokenKind::Op(o)) if !is_reserved_op(o))
621        {
622            self.skip_to_item_end();
623            return true;
624        }
625        self.i = snap;
626        self.diags.truncate(saved_diags);
627        false
628    }
629
630    fn declaration(&mut self, imports: &mut Vec<ImportDecl>, decls: &mut Vec<Decl>) {
631        let pos = self.pos();
632        let start = self.i;
633        if matches!(
634            self.peek(),
635            Some(TokenKind::UpperId { .. } | TokenKind::LBracket | TokenKind::LParen)
636        ) && self.try_infix_operator_decl()
637        {
638            decls.push(Decl::Unknown {
639                raw: self.slice_text(start),
640                pos,
641                span: self.node_span(start),
642            });
643            return;
644        }
645        match self.peek() {
646            Some(t) if t.is_keyword("import") => {
647                let imp = self.import_decl();
648                // The `(...)` import list / `hiding (...)` clause is consumed
649                // here, after the decl is built; fold it into the span so the
650                // import covers its whole source extent.
651                self.skip_to_item_end();
652                if let Some(mut imp) = imp {
653                    imp.span = self.node_span(start);
654                    imports.push(imp);
655                }
656            }
657            Some(t) if t.is_keyword("template") => {
658                // `template T = ...` (template-let synonym) is exotic; only
659                // `template Name with/where` is a template declaration.
660                match self.template_decl() {
661                    Some(t) => decls.push(Decl::Template(t)),
662                    None => {
663                        self.skip_to_item_end();
664                        decls.push(Decl::Unknown {
665                            raw: self.slice_text(start),
666                            span: self.node_span(start),
667                            pos,
668                        });
669                    }
670                }
671            }
672            Some(t) if t.is_keyword("interface") => match self.interface_decl() {
673                Some(i) => decls.push(Decl::Interface(i)),
674                None => {
675                    self.skip_to_item_end();
676                    decls.push(Decl::Unknown {
677                        raw: self.slice_text(start),
678                        span: self.node_span(start),
679                        pos,
680                    });
681                }
682            },
683            Some(t)
684                if matches!(
685                    t.keyword(),
686                    // Fixity declarations, class-default declarations, and
687                    // pattern synonyms have no IR surface.
688                    Some("infix" | "infixl" | "infixr" | "default" | "pattern")
689                ) =>
690            {
691                self.skip_to_item_end();
692                decls.push(Decl::Unknown {
693                    raw: self.slice_text(start),
694                    pos,
695                    span: self.node_span(start),
696                });
697            }
698            Some(t)
699                if matches!(
700                    t.keyword(),
701                    Some(
702                        "data"
703                            | "type"
704                            | "newtype"
705                            | "class"
706                            | "instance"
707                            | "exception"
708                            | "deriving"
709                    )
710                ) =>
711            {
712                let keyword = t
713                    .keyword()
714                    .expect("declaration-head keyword token")
715                    .to_string();
716                self.bump();
717                let name = match self.peek() {
718                    Some(TokenKind::UpperId { qualifier, name }) => {
719                        let n = qualifier
720                            .as_ref()
721                            .map_or_else(|| name.to_string(), |q| format!("{q}.{name}"));
722                        self.bump();
723                        n
724                    }
725                    _ => String::new(),
726                };
727                self.skip_to_item_end();
728                decls.push(Decl::TypeDef {
729                    keyword,
730                    name: name.into(),
731                    pos,
732                    span: self.node_span(start),
733                });
734            }
735            Some(TokenKind::LowerId { .. }) => match self.function_item() {
736                Some(d) => decls.push(d),
737                None => {
738                    self.skip_to_item_end();
739                    decls.push(Decl::Unknown {
740                        raw: self.slice_text(start),
741                        span: self.node_span(start),
742                        pos,
743                    });
744                }
745            },
746            // Operator definition or signature: `(<=) = curry Lte`.
747            Some(TokenKind::LParen)
748                if matches!(self.peek_at(1), Some(TokenKind::Op(_)))
749                    && self.peek_at(2) == Some(&TokenKind::RParen) =>
750            {
751                self.skip_to_item_end();
752                decls.push(Decl::Unknown {
753                    raw: self.slice_text(start),
754                    span: self.node_span(start),
755                    pos,
756                });
757            }
758            // Top-level pattern binding: `[a, b, c] = ...`, `(x, y) = ...`.
759            Some(TokenKind::LParen | TokenKind::LBracket) => {
760                if self.binding().is_none() {
761                    self.diag_kind(
762                        ParseDiagnosticKind::SkippedDeclaration(
763                            SkippedDeclarationReason::TopLevelPatternBinding,
764                        ),
765                        "unparseable top-level pattern binding",
766                    );
767                }
768                self.skip_to_item_end();
769                decls.push(Decl::Unknown {
770                    raw: self.slice_text(start),
771                    span: self.node_span(start),
772                    pos,
773                });
774            }
775            _ => {
776                self.diag_kind(
777                    ParseDiagnosticKind::SkippedDeclaration(
778                        SkippedDeclarationReason::UnrecognizedDeclaration,
779                    ),
780                    format!("unrecognized declaration: {:?}", self.peek()),
781                );
782                self.skip_to_item_end();
783                decls.push(Decl::Unknown {
784                    raw: self.slice_text(start),
785                    span: self.node_span(start),
786                    pos,
787                });
788            }
789        }
790    }
791
792    // ----- imports -----------------------------------------------------
793
794    fn import_decl(&mut self) -> Option<ImportDecl> {
795        let pos = self.pos();
796        let start_i = self.i;
797        self.bump(); // import
798        let mut style = if self.eat_keyword("qualified") {
799            ImportStyle::Qualified
800        } else {
801            ImportStyle::Unqualified
802        };
803        // Package-qualified import: `import qualified "pkg-name" Main as V1`.
804        if matches!(self.peek(), Some(TokenKind::StringLit(_))) {
805            self.bump();
806        }
807        let module_name = match self.peek().cloned() {
808            Some(TokenKind::UpperId { qualifier, name }) => {
809                self.bump();
810                match qualifier {
811                    Some(q) => format!("{q}.{name}").into(),
812                    None => name.into(),
813                }
814            }
815            _ => {
816                self.diag_expected(
817                    ExpectedToken::ModuleNameAfterImport,
818                    "expected module name after 'import'",
819                );
820                return None;
821            }
822        };
823        // ImportQualifiedPost style: `import DA.Map qualified as Map`.
824        if self.eat_keyword("qualified") {
825            style = ImportStyle::Qualified;
826        }
827        let mut alias = None;
828        if self.eat_keyword("as") {
829            if let Some(TokenKind::UpperId { qualifier, name }) = self.peek().cloned() {
830                self.bump();
831                alias = Some(match qualifier {
832                    Some(q) => format!("{q}.{name}").into(),
833                    None => name.into(),
834                });
835            }
836        }
837        // `hiding (...)` / import list — consumed by skip_to_item_end.
838        Some(ImportDecl {
839            module_name,
840            style,
841            alias,
842            pos,
843            span: self.node_span(start_i),
844        })
845    }
846
847    // ----- templates ---------------------------------------------------
848
849    fn upper_name(&mut self) -> Option<ModuleName> {
850        match self.peek().cloned() {
851            Some(TokenKind::UpperId { qualifier, name }) => {
852                self.bump();
853                Some(match qualifier {
854                    Some(q) => format!("{q}.{name}").into(),
855                    None => name.into(),
856                })
857            }
858            _ => None,
859        }
860    }
861
862    fn template_decl(&mut self) -> Option<TemplateDecl> {
863        let pos = self.pos();
864        let start_i = self.i;
865        self.bump(); // template
866        if self.at_keyword("instance") {
867            return None; // legacy `template instance` — not a template
868        }
869        let name = self.upper_name()?.to_string().into();
870
871        let fields = self
872            .eat_keyword("with")
873            .then(|| self.field_block())
874            .map(|parsed| parsed.fields)
875            .unwrap_or_default();
876        let body = if self.eat_keyword("where") {
877            self.template_body()
878        } else {
879            Vec::new()
880        };
881        Some(TemplateDecl {
882            name,
883            fields,
884            body,
885            pos,
886            span: self.node_span(start_i),
887        })
888    }
889
890    /// `{ name : Type ; name2, name3 : Type ; ... }` (virtual or explicit).
891    /// Returns the parsed fields and a "dangling" marker when the block was
892    /// entered but abandoned early because its first item is not a field
893    /// (an empty `with` whose layout block swallowed the next clause). The
894    /// caller should discard the block's eventual closing `VRBrace` when this
895    /// happens.
896    fn field_block(&mut self) -> FieldBlock {
897        let mut fields = Vec::new();
898        if !(self.eat(&TokenKind::VLBrace) || self.eat(&TokenKind::LBrace)) {
899            return FieldBlock {
900                fields,
901                dangling: false,
902            };
903        }
904        loop {
905            while self.eat(&TokenKind::VSemi) || self.eat(&TokenKind::Semi) {}
906            match self.peek() {
907                None => break,
908                Some(TokenKind::VRBrace | TokenKind::RBrace) => {
909                    self.bump();
910                    break;
911                }
912                // A stray closing bracket inside a block is garbage from a
913                // failed item parse — discard it or the loop cannot make
914                // progress (skip_to_item_end deliberately stops before
915                // unmatched closers).
916                Some(TokenKind::RParen | TokenKind::RBracket) => {
917                    self.bump();
918                    continue;
919                }
920                _ => {}
921            }
922            // A field item must look like `name [, name] : Type`. If the
923            // next item doesn't (an empty `with` swallowed the following
924            // clause into its layout block — `with` + comment + `controller`),
925            // stop without consuming so the caller can parse the clause.
926            {
927                let mut j = self.i;
928                while let Some(TokenKind::LowerId {
929                    qualifier: None, ..
930                }) = self.toks.get(j).map(|t| &t.kind)
931                {
932                    j += 1;
933                    match self.toks.get(j).map(|t| &t.kind) {
934                        Some(TokenKind::Comma) => j += 1,
935                        _ => break,
936                    }
937                }
938                let is_field = j > self.i
939                    && self
940                        .toks
941                        .get(j)
942                        .map(|t| &t.kind)
943                        .is_some_and(|t| t.is_op(":"));
944                if !is_field {
945                    return FieldBlock {
946                        fields,
947                        dangling: true,
948                    };
949                }
950            }
951            // One or more comma-separated names, then `:`, then the type.
952            let mut names: Vec<(Identifier, Pos, Span)> = Vec::new();
953            while let Some(TokenKind::LowerId {
954                qualifier: None,
955                name,
956            }) = self.peek().cloned()
957            {
958                let p = self.pos();
959                let nspan = Span::from_usize(self.toks[self.i].start, self.toks[self.i].end);
960                self.bump();
961                names.push((name, p, nspan));
962                if !self.eat(&TokenKind::Comma) {
963                    break;
964                }
965            }
966            if names.is_empty() || !self.eat_op(":") {
967                self.diag_expected(
968                    ExpectedToken::FieldNameTypePair,
969                    "expected 'name : Type' field",
970                );
971                self.skip_to_item_end();
972                continue;
973            }
974            let ty_start = self.i;
975            self.skip_to_item_end();
976            let ty = self.parse_type_annotation(ty_start, self.i, TypeAnnotationContext::Field);
977            // The type is shared by all names but sits after the last one, so
978            // only the last field can span `name : Type` without overlapping a
979            // sibling; earlier names of `x, y : T` stay name-only. daml-fmt
980            // reads the type extent off the last field of a comma group.
981            let type_end = self.end_byte();
982            let last = names.len() - 1;
983            for (idx, (name, p, nspan)) in names.into_iter().enumerate() {
984                let span = if idx == last {
985                    Span::from_usize(nspan.start_usize(), type_end.max(nspan.end_usize()))
986                } else {
987                    nspan
988                };
989                fields.push(FieldDecl {
990                    name: name.to_string().into(),
991                    ty: ty.clone(),
992                    pos: p,
993                    span,
994                });
995            }
996        }
997        FieldBlock {
998            fields,
999            dangling: false,
1000        }
1001    }
1002
1003    // ----- template body ------------------------------------------------
1004
1005    fn template_body(&mut self) -> Vec<TemplateBodyDecl> {
1006        let mut body = Vec::new();
1007        if !(self.eat(&TokenKind::VLBrace) || self.eat(&TokenKind::LBrace)) {
1008            return body;
1009        }
1010        loop {
1011            while self.eat(&TokenKind::VSemi) || self.eat(&TokenKind::Semi) {}
1012            match self.peek() {
1013                None => break,
1014                Some(TokenKind::VRBrace | TokenKind::RBrace) => {
1015                    self.bump();
1016                    break;
1017                }
1018                // A stray closing bracket inside a block is garbage from a
1019                // failed item parse — discard it or the loop cannot make
1020                // progress (skip_to_item_end deliberately stops before
1021                // unmatched closers).
1022                Some(TokenKind::RParen | TokenKind::RBracket) => {
1023                    self.bump();
1024                    continue;
1025                }
1026                _ => {}
1027            }
1028            let pos = self.pos();
1029            let start = self.i;
1030            let decl = self.template_body_item(pos, start);
1031            body.push(decl);
1032        }
1033        body
1034    }
1035
1036    fn template_body_item(&mut self, pos: Pos, start: usize) -> TemplateBodyDecl {
1037        match self.peek().and_then(|t| t.keyword()) {
1038            Some("signatory") => {
1039                self.bump();
1040                let parties = self.expr_comma_list();
1041                self.skip_to_item_end();
1042                TemplateBodyDecl::Signatory {
1043                    parties,
1044                    pos,
1045                    span: self.node_span(start),
1046                }
1047            }
1048            Some("observer") => {
1049                self.bump();
1050                let parties = self.expr_comma_list();
1051                self.skip_to_item_end();
1052                TemplateBodyDecl::Observer {
1053                    parties,
1054                    pos,
1055                    span: self.node_span(start),
1056                }
1057            }
1058            Some("ensure") => {
1059                self.bump();
1060                let expr = self.expr();
1061                self.skip_to_item_end();
1062                TemplateBodyDecl::Ensure {
1063                    expr,
1064                    pos,
1065                    span: self.node_span(start),
1066                }
1067            }
1068            Some("key") => {
1069                self.bump();
1070                let expr_start = self.i;
1071                let expr = self.expr();
1072                let ty = if self.eat_op(":") {
1073                    let ty_start = self.i;
1074                    self.skip_to_item_end();
1075                    self.parse_type_annotation(ty_start, self.i, TypeAnnotationContext::Key)
1076                } else {
1077                    // The expression parser consumes `: Type` annotations;
1078                    // recover the key type from the last top-level colon.
1079                    let mut depth = 0i32;
1080                    let mut colon = None;
1081                    for j in expr_start..self.i {
1082                        match &self.toks[j].kind {
1083                            TokenKind::LParen | TokenKind::LBracket => depth += 1,
1084                            TokenKind::RParen | TokenKind::RBracket if depth > 0 => depth -= 1,
1085                            TokenKind::Op(o) if o.as_str() == ":" && depth == 0 => colon = Some(j),
1086                            _ => {}
1087                        }
1088                    }
1089                    let ty = colon.map_or(TypeAnnotation::Absent, |j| {
1090                        self.parse_type_annotation(j + 1, self.i, TypeAnnotationContext::Key)
1091                    });
1092                    self.skip_to_item_end();
1093                    ty
1094                };
1095                TemplateBodyDecl::Key {
1096                    expr,
1097                    ty,
1098                    pos,
1099                    span: self.node_span(start),
1100                }
1101            }
1102            Some("maintainer") => {
1103                self.bump();
1104                let expr = self.expr();
1105                self.skip_to_item_end();
1106                TemplateBodyDecl::Maintainer {
1107                    expr,
1108                    pos,
1109                    span: self.node_span(start),
1110                }
1111            }
1112            Some("choice" | "nonconsuming" | "preconsuming" | "postconsuming") => {
1113                self.choice_decl().map_or_else(
1114                    || {
1115                        self.skip_to_item_end();
1116                        TemplateBodyDecl::Other {
1117                            raw: self.slice_text(start),
1118                            span: self.node_span(start),
1119                            pos,
1120                        }
1121                    },
1122                    TemplateBodyDecl::Choice,
1123                )
1124            }
1125            Some("interface") => self.interface_instance_decl().map_or_else(
1126                || {
1127                    self.skip_to_item_end();
1128                    TemplateBodyDecl::Other {
1129                        raw: self.slice_text(start),
1130                        span: self.node_span(start),
1131                        pos,
1132                    }
1133                },
1134                TemplateBodyDecl::InterfaceInstance,
1135            ),
1136            Some("controller") => {
1137                // Legacy Daml 1.x `controller <party> can` choice blocks are
1138                // not analyzed — fail loud instead of silently dropping the
1139                // choices inside.
1140                self.diag_kind(
1141                    ParseDiagnosticKind::UnsupportedSyntax(
1142                        UnsupportedSyntaxKind::LegacyControllerCan,
1143                    ),
1144                    "legacy 'controller ... can' syntax is not supported; \
1145                     choices inside this block are not analyzed",
1146                );
1147                self.skip_to_item_end();
1148                TemplateBodyDecl::Other {
1149                    raw: self.slice_text(start),
1150                    span: self.node_span(start),
1151                    pos,
1152                }
1153            }
1154            _ => {
1155                self.skip_to_item_end();
1156                TemplateBodyDecl::Other {
1157                    raw: self.slice_text(start),
1158                    span: self.node_span(start),
1159                    pos,
1160                }
1161            }
1162        }
1163    }
1164
1165    fn choice_decl(&mut self) -> Option<ChoiceDecl> {
1166        let pos = self.pos();
1167        let start_i = self.i;
1168        let consuming = match self.peek().and_then(|t| t.keyword()) {
1169            Some("nonconsuming") => {
1170                self.bump();
1171                Consuming::NonConsuming
1172            }
1173            Some("preconsuming") => {
1174                self.bump();
1175                Consuming::PreConsuming
1176            }
1177            Some("postconsuming") => {
1178                self.bump();
1179                Consuming::PostConsuming
1180            }
1181            _ => Consuming::Consuming,
1182        };
1183        if !self.eat_keyword("choice") {
1184            return None;
1185        }
1186        let name = self.upper_name()?.to_string().into();
1187        let return_ty = if self.eat_op(":") {
1188            let ty_start = self.i;
1189            self.skip_type_tokens();
1190            self.parse_type_annotation(ty_start, self.i, TypeAnnotationContext::Choice)
1191        } else {
1192            TypeAnnotation::Absent
1193        };
1194        let (params, dangling) = if self.eat_keyword("with") {
1195            let parsed = self.field_block();
1196            (parsed.fields, parsed.dangling)
1197        } else {
1198            (Vec::new(), false)
1199        };
1200        let mut observers = Vec::new();
1201        let mut controllers = Vec::new();
1202        loop {
1203            // Inside a dangling (empty) with-block the controller/observer/
1204            // do clauses sit at the block's column, so layout separates
1205            // them with virtual semicolons — consume those.
1206            if dangling {
1207                while self.eat(&TokenKind::VSemi) {}
1208            }
1209            if self.eat_keyword("observer") {
1210                observers = self.expr_comma_list_no_do();
1211            } else if self.eat_keyword("controller") {
1212                controllers = self.expr_comma_list_no_do();
1213            } else {
1214                break;
1215            }
1216        }
1217        if dangling {
1218            while self.eat(&TokenKind::VSemi) {}
1219        }
1220        let body = if self.peek().is_some_and(|t| {
1221            !matches!(
1222                t,
1223                TokenKind::VSemi | TokenKind::VRBrace | TokenKind::Semi | TokenKind::RBrace
1224            )
1225        }) {
1226            Some(self.expr())
1227        } else {
1228            None
1229        };
1230        self.skip_to_item_end();
1231        if dangling {
1232            // Discard the abandoned with-block's closing brace so it does
1233            // not terminate the enclosing template/interface body.
1234            self.eat(&TokenKind::VRBrace);
1235            self.skip_to_item_end();
1236        }
1237        Some(ChoiceDecl {
1238            name,
1239            consuming,
1240            return_ty,
1241            params,
1242            controllers,
1243            observers,
1244            body,
1245            pos,
1246            span: self.node_span(start_i),
1247        })
1248    }
1249
1250    /// Consume type tokens up to (not including) a layout boundary or a
1251    /// `with`/`controller`/`observer`/`do`/`where` keyword at bracket depth 0.
1252    fn skip_type_tokens(&mut self) {
1253        let mut brackets = 0usize;
1254        while let Some(t) = self.peek() {
1255            match t {
1256                TokenKind::VSemi | TokenKind::VRBrace | TokenKind::VLBrace | TokenKind::Semi => {
1257                    return
1258                }
1259                TokenKind::LParen | TokenKind::LBracket => brackets += 1,
1260                TokenKind::RParen | TokenKind::RBracket => {
1261                    if brackets == 0 {
1262                        return;
1263                    }
1264                    brackets -= 1;
1265                }
1266                _ if brackets == 0
1267                    && matches!(
1268                        t.keyword(),
1269                        Some("with" | "controller" | "observer" | "do" | "where")
1270                    ) =>
1271                {
1272                    return
1273                }
1274                _ => {}
1275            }
1276            self.i += 1;
1277        }
1278    }
1279
1280    // ----- interfaces ----------------------------------------------------
1281
1282    fn interface_decl(&mut self) -> Option<InterfaceDecl> {
1283        let pos = self.pos();
1284        let start_i = self.i;
1285        self.bump(); // interface
1286        if self.at_keyword("instance") {
1287            // Top-level retroactive interface instance: skip gracefully.
1288            return None;
1289        }
1290        let name = self.upper_name()?.to_string().into();
1291        let mut requires = Vec::new();
1292        if self.eat_keyword("requires") {
1293            while let Some(r) = self.upper_name() {
1294                requires.push(r);
1295                if !self.eat(&TokenKind::Comma) {
1296                    break;
1297                }
1298            }
1299        }
1300        if !self.eat_keyword("where") {
1301            return None;
1302        }
1303        let mut viewtype = None;
1304        let mut methods = Vec::new();
1305        let mut choices = Vec::new();
1306        if !(self.eat(&TokenKind::VLBrace) || self.eat(&TokenKind::LBrace)) {
1307            return Some(InterfaceDecl {
1308                name,
1309                requires,
1310                viewtype,
1311                methods,
1312                choices,
1313                pos,
1314                span: self.node_span(start_i),
1315            });
1316        }
1317        loop {
1318            while self.eat(&TokenKind::VSemi) || self.eat(&TokenKind::Semi) {}
1319            match self.peek() {
1320                None => break,
1321                Some(TokenKind::VRBrace | TokenKind::RBrace) => {
1322                    self.bump();
1323                    break;
1324                }
1325                // A stray closing bracket inside a block is garbage from a
1326                // failed item parse — discard it or the loop cannot make
1327                // progress (skip_to_item_end deliberately stops before
1328                // unmatched closers).
1329                Some(TokenKind::RParen | TokenKind::RBracket) => {
1330                    self.bump();
1331                    continue;
1332                }
1333                _ => {}
1334            }
1335            match self.peek().and_then(|t| t.keyword()) {
1336                Some("viewtype") => {
1337                    self.bump();
1338                    viewtype = self.upper_name();
1339                    self.skip_to_item_end();
1340                }
1341                Some("choice" | "nonconsuming" | "preconsuming" | "postconsuming") => {
1342                    if let Some(c) = self.choice_decl() {
1343                        choices.push(c);
1344                    } else {
1345                        self.skip_to_item_end();
1346                    }
1347                }
1348                _ => {
1349                    // Method signature `name : Type`; anything else (default
1350                    // implementations, ensure, ...) is skipped.
1351                    let mpos = self.pos();
1352                    if let Some(TokenKind::LowerId {
1353                        qualifier: None,
1354                        name: mname,
1355                    }) = self.peek().cloned()
1356                    {
1357                        if self.peek_at(1).is_some_and(|t| t.is_op(":")) {
1358                            let mstart = self.toks[self.i].start;
1359                            self.bump();
1360                            self.bump();
1361                            let ty_start = self.i;
1362                            self.skip_to_item_end();
1363                            // Single name: span the whole `name : Type`.
1364                            methods.push(FieldDecl {
1365                                name: mname,
1366                                ty: self.parse_type_annotation(
1367                                    ty_start,
1368                                    self.i,
1369                                    TypeAnnotationContext::InterfaceMethod,
1370                                ),
1371                                pos: mpos,
1372                                span: Span::from_usize(mstart, self.end_byte().max(mstart)),
1373                            });
1374                            continue;
1375                        }
1376                    }
1377                    self.skip_to_item_end();
1378                }
1379            }
1380        }
1381        Some(InterfaceDecl {
1382            name,
1383            requires,
1384            viewtype,
1385            methods,
1386            choices,
1387            pos,
1388            span: self.node_span(start_i),
1389        })
1390    }
1391
1392    /// `interface instance I for T where { method-bindings }`
1393    fn interface_instance_decl(&mut self) -> Option<InterfaceInstanceDecl> {
1394        let pos = self.pos();
1395        let start_i = self.i;
1396        self.bump(); // interface
1397        if !self.eat_keyword("instance") {
1398            return None;
1399        }
1400        let interface_name = self.upper_name()?;
1401        let for_template = if self.eat_keyword("for") {
1402            self.upper_name().map_or_else(
1403                || {
1404                    self.diag_expected(
1405                        ExpectedToken::TemplateNameAfterInterfaceInstanceFor,
1406                        "interface instance missing template name after 'for'",
1407                    );
1408                    None
1409                },
1410                Some,
1411            )
1412        } else {
1413            None
1414        };
1415        let mut methods = Vec::new();
1416        if self.eat_keyword("where")
1417            && (self.eat(&TokenKind::VLBrace) || self.eat(&TokenKind::LBrace))
1418        {
1419            loop {
1420                while self.eat(&TokenKind::VSemi) || self.eat(&TokenKind::Semi) {}
1421                match self.peek() {
1422                    None => break,
1423                    Some(TokenKind::VRBrace | TokenKind::RBrace) => {
1424                        self.bump();
1425                        break;
1426                    }
1427                    // Stray closer: discard so the loop always progresses.
1428                    Some(TokenKind::RParen | TokenKind::RBracket) => {
1429                        self.bump();
1430                        continue;
1431                    }
1432                    _ => {}
1433                }
1434                if let Some(b) = self.binding() {
1435                    methods.push(b);
1436                } else {
1437                    self.skip_to_item_end();
1438                }
1439            }
1440        }
1441        Some(InterfaceInstanceDecl {
1442            interface_name,
1443            for_template,
1444            methods,
1445            pos,
1446            span: self.node_span(start_i),
1447        })
1448    }
1449
1450    // ----- functions -----------------------------------------------------
1451
1452    /// A top-level item starting with a lowercase identifier: type
1453    /// signature or function equation. Operator definitions and other
1454    /// exotica return None.
1455    fn function_item(&mut self) -> Option<Decl> {
1456        let pos = self.pos();
1457        let start_i = self.i;
1458        let Some(TokenKind::LowerId {
1459            qualifier: None,
1460            name,
1461        }) = self.peek().cloned()
1462        else {
1463            return None;
1464        };
1465
1466        // Type signature: `name [, name2] : Type`
1467        let mut j = self.i + 1;
1468        let mut is_sig = false;
1469        loop {
1470            match self.toks.get(j).map(|t| &t.kind) {
1471                Some(TokenKind::Comma) => {
1472                    j += 1;
1473                    if matches!(
1474                        self.toks.get(j).map(|t| &t.kind),
1475                        Some(TokenKind::LowerId {
1476                            qualifier: None,
1477                            ..
1478                        })
1479                    ) {
1480                        j += 1;
1481                        continue;
1482                    }
1483                    break;
1484                }
1485                Some(TokenKind::Op(o)) if o.as_str() == ":" => {
1486                    is_sig = true;
1487                    break;
1488                }
1489                _ => break,
1490            }
1491        }
1492        if is_sig {
1493            self.bump(); // name
1494            while self.eat(&TokenKind::Comma) {
1495                self.bump(); // more names
1496            }
1497            self.eat_op(":");
1498            let ty_start = self.i;
1499            self.skip_to_item_end();
1500            let ty = self.parse_type_annotation(ty_start, self.i, TypeAnnotationContext::Function);
1501            return Some(Decl::Function(FunctionDecl {
1502                name,
1503                ty,
1504                equations: Vec::new(),
1505                pos,
1506                sig_span: Some(self.node_span(start_i)),
1507                span: self.node_span(start_i),
1508            }));
1509        }
1510
1511        // Function equation: name pats (= expr | guards), optional where.
1512        self.bump(); // name
1513        let mut params = Vec::new();
1514        while !self.at_op("=") && !self.at_op("|") {
1515            // Combined signature + body: `name (x : a) : RetType = expr` —
1516            // consume the return-type annotation up to the `=`.
1517            if self.at_op(":") {
1518                self.bump();
1519                let mut brackets = 0usize;
1520                while let Some(t) = self.peek() {
1521                    match t {
1522                        TokenKind::Op(o) if o.as_str() == "=" && brackets == 0 => break,
1523                        TokenKind::VSemi
1524                        | TokenKind::VRBrace
1525                        | TokenKind::Semi
1526                        | TokenKind::RBrace => break,
1527                        TokenKind::LParen | TokenKind::LBracket => brackets += 1,
1528                        TokenKind::RParen | TokenKind::RBracket => {
1529                            brackets = brackets.saturating_sub(1)
1530                        }
1531                        _ => {}
1532                    }
1533                    self.i += 1;
1534                }
1535                continue;
1536            }
1537            // Infix operator definition: `f $ x = f x`, `as <&> f = ...` —
1538            // operators have no IR surface; skip the item silently.
1539            if matches!(self.peek(), Some(TokenKind::Op(o)) if !is_reserved_op(o)) {
1540                self.skip_to_item_end();
1541                return None;
1542            }
1543            match self.peek() {
1544                None
1545                | Some(
1546                    TokenKind::VSemi | TokenKind::VRBrace | TokenKind::Semi | TokenKind::RBrace,
1547                ) => {
1548                    self.diag_malformed(
1549                        MalformedSyntaxKind::FunctionEquation,
1550                        format!("could not parse equation for '{name}'"),
1551                    );
1552                    return None;
1553                }
1554                _ => {}
1555            }
1556            match self.pattern_atom() {
1557                Some(p) => params.push(p),
1558                None => {
1559                    self.diag_malformed(
1560                        MalformedSyntaxKind::FunctionParameterPattern,
1561                        format!("bad parameter pattern in '{name}'"),
1562                    );
1563                    return None;
1564                }
1565            }
1566        }
1567        let (body, guards) = self.equation_rhs()?;
1568        let where_bindings = if self.eat_keyword("where") {
1569            self.binding_block()
1570        } else {
1571            Vec::new()
1572        };
1573        self.skip_to_item_end();
1574        Some(Decl::Function(FunctionDecl {
1575            name,
1576            ty: TypeAnnotation::Absent,
1577            equations: vec![Equation {
1578                params,
1579                body,
1580                guards,
1581                where_bindings,
1582                pos,
1583                span: self.node_span(start_i),
1584            }],
1585            pos,
1586            sig_span: None,
1587            span: self.node_span(start_i),
1588        }))
1589    }
1590
1591    /// `= expr` or `| guard = expr | guard = expr ...`
1592    fn equation_rhs(&mut self) -> Option<(Expr, Vec<(Expr, Expr)>)> {
1593        if self.eat_op("=") {
1594            return Some((self.expr(), Vec::new()));
1595        }
1596        let mut guards = Vec::new();
1597        while self.eat_op("|") {
1598            // Comma-separated guard qualifiers, each a boolean expression
1599            // or a pattern guard `pat <- expr`.
1600            let g = loop {
1601                let g = self.expr();
1602                if self.eat_op("<-") {
1603                    let _ = self.expr(); // pattern guard: keep the pattern side
1604                }
1605                if !self.eat(&TokenKind::Comma) {
1606                    break g;
1607                }
1608            };
1609            if !self.eat_op("=") {
1610                self.diag_expected(ExpectedToken::EqualsAfterGuard, "expected '=' after guard");
1611                return None;
1612            }
1613            let e = self.expr();
1614            guards.push((g, e));
1615        }
1616        if guards.is_empty() {
1617            self.diag_expected(
1618                ExpectedToken::EqualsOrGuardedRightHandSide,
1619                "expected '=' or guarded right-hand side in equation",
1620            );
1621            None
1622        } else {
1623            let first = guards[0].1.clone();
1624            Some((first, guards))
1625        }
1626    }
1627
1628    /// `{ binding ; binding ; ... }` for let/where blocks.
1629    fn binding_block(&mut self) -> Vec<Binding> {
1630        let mut bindings = Vec::new();
1631        if !(self.eat(&TokenKind::VLBrace) || self.eat(&TokenKind::LBrace)) {
1632            return bindings;
1633        }
1634        loop {
1635            while self.eat(&TokenKind::VSemi) || self.eat(&TokenKind::Semi) {}
1636            match self.peek() {
1637                None => break,
1638                Some(TokenKind::VRBrace | TokenKind::RBrace) => {
1639                    self.bump();
1640                    break;
1641                }
1642                // A stray closing bracket inside a block is garbage from a
1643                // failed item parse — discard it or the loop cannot make
1644                // progress (skip_to_item_end deliberately stops before
1645                // unmatched closers).
1646                Some(TokenKind::RParen | TokenKind::RBracket) => {
1647                    self.bump();
1648                    continue;
1649                }
1650                _ => {}
1651            }
1652            match self.binding() {
1653                Some(b) => bindings.push(b),
1654                None => self.skip_to_item_end(),
1655            }
1656        }
1657        bindings
1658    }
1659
1660    /// One binding: `pat = expr`, `f x y = expr`, guarded variants, or a
1661    /// type signature (skipped, returns None).
1662    fn binding(&mut self) -> Option<Binding> {
1663        let pos = self.pos();
1664        let start_i = self.i;
1665        // Operator binding or signature: `(==) : Text -> Bool = ...` —
1666        // skip the whole item; operators aren't surfaced in the IR.
1667        if self.at(&TokenKind::LParen)
1668            && matches!(self.peek_at(1), Some(TokenKind::Op(_)))
1669            && self.peek_at(2) == Some(&TokenKind::RParen)
1670        {
1671            self.skip_to_item_end();
1672            return None;
1673        }
1674        let pat = self.pattern_atom()?;
1675        let mut params = Vec::new();
1676        loop {
1677            if self.at_op("=") {
1678                self.bump();
1679                let expr = self.expr();
1680                // Bindings can carry their own where blocks.
1681                if self.eat_keyword("where") {
1682                    let _ = self.binding_block();
1683                }
1684                return Some(Binding {
1685                    pat,
1686                    params,
1687                    expr,
1688                    pos,
1689                    span: self.node_span(start_i),
1690                });
1691            }
1692            if self.at_op("|") {
1693                let (body, _) = self.equation_rhs()?;
1694                if self.eat_keyword("where") {
1695                    let _ = self.binding_block();
1696                }
1697                return Some(Binding {
1698                    pat,
1699                    params,
1700                    expr: body,
1701                    pos,
1702                    span: self.node_span(start_i),
1703                });
1704            }
1705            if self.at_op(":") {
1706                if params.is_empty() {
1707                    // Type signature inside a let/where block — skip it.
1708                    self.skip_to_item_end();
1709                    return None;
1710                }
1711                // Combined signature + body: `f (x : a) : Ret = expr` —
1712                // consume the return-type annotation up to the `=`.
1713                self.bump();
1714                let mut brackets = 0usize;
1715                while let Some(t) = self.peek() {
1716                    match t {
1717                        TokenKind::Op(o) if o.as_str() == "=" && brackets == 0 => break,
1718                        TokenKind::VSemi
1719                        | TokenKind::VRBrace
1720                        | TokenKind::Semi
1721                        | TokenKind::RBrace => break,
1722                        TokenKind::LParen | TokenKind::LBracket => brackets += 1,
1723                        TokenKind::RParen | TokenKind::RBracket => {
1724                            brackets = brackets.saturating_sub(1)
1725                        }
1726                        _ => {}
1727                    }
1728                    self.i += 1;
1729                }
1730                continue;
1731            }
1732            // Infix operator binding with a pattern operand:
1733            // `None <?> s = ...` in a where/let block.
1734            if matches!(self.peek(), Some(TokenKind::Op(o)) if !is_reserved_op(o)) {
1735                self.skip_to_item_end();
1736                return None;
1737            }
1738            match self.peek() {
1739                None
1740                | Some(
1741                    TokenKind::VSemi | TokenKind::VRBrace | TokenKind::Semi | TokenKind::RBrace,
1742                ) => return None,
1743                _ => {}
1744            }
1745            params.push(self.pattern_atom()?);
1746        }
1747    }
1748
1749    // ----- patterns ------------------------------------------------------
1750
1751    fn pattern_atom(&mut self) -> Option<Pat> {
1752        if self.depth >= MAX_RECURSION_DEPTH {
1753            return None;
1754        }
1755        self.depth += 1;
1756        let result = self.pattern_atom_inner();
1757        self.depth -= 1;
1758        result
1759    }
1760
1761    fn pattern_atom_inner(&mut self) -> Option<Pat> {
1762        let pos = self.pos();
1763        let start_i = self.i;
1764        // Lazy / strict pattern markers: `~(as, bs)`, `!x`.
1765        if self.at_op("~") || self.at_op("!") {
1766            self.bump();
1767            return self.pattern_atom();
1768        }
1769        match self.peek().cloned() {
1770            Some(TokenKind::LowerId {
1771                qualifier: None,
1772                name,
1773            }) => {
1774                self.bump();
1775                if name == "_" {
1776                    return Some(Pat::Wild {
1777                        pos,
1778                        span: self.node_span(start_i),
1779                    });
1780                }
1781                if self.at_op("@") {
1782                    self.bump();
1783                    let inner = self.pattern_atom()?;
1784                    return Some(Pat::As {
1785                        name,
1786                        pat: Box::new(inner),
1787                        pos,
1788                        span: self.node_span(start_i),
1789                    });
1790                }
1791                Some(Pat::Var {
1792                    name,
1793                    pos,
1794                    span: self.node_span(start_i),
1795                })
1796            }
1797            Some(TokenKind::Op(o)) if o.as_str() == "_" => {
1798                self.bump();
1799                Some(Pat::Wild {
1800                    pos,
1801                    span: self.node_span(start_i),
1802                })
1803            }
1804            Some(TokenKind::UpperId { qualifier, name }) => {
1805                self.bump();
1806                // Record pattern `Foo {..}` / `Foo {x = y}` /
1807                // `Foo with claim; tag`.
1808                if self.at(&TokenKind::LBrace) {
1809                    self.skip_balanced_braces();
1810                } else if self.eat_keyword("with") {
1811                    let _ = self.record_fields();
1812                }
1813                Some(Pat::Con {
1814                    qualifier,
1815                    name,
1816                    args: Vec::new(),
1817                    pos,
1818                    span: self.node_span(start_i),
1819                })
1820            }
1821            Some(TokenKind::IntLit(text)) => {
1822                self.bump();
1823                Some(Pat::Lit {
1824                    kind: LitKind::Int,
1825                    text,
1826                    pos,
1827                    span: self.node_span(start_i),
1828                })
1829            }
1830            Some(TokenKind::DecimalLit(text)) => {
1831                self.bump();
1832                Some(Pat::Lit {
1833                    kind: LitKind::Decimal,
1834                    text,
1835                    pos,
1836                    span: self.node_span(start_i),
1837                })
1838            }
1839            Some(TokenKind::StringLit(text)) => {
1840                self.bump();
1841                Some(Pat::Lit {
1842                    kind: LitKind::Text,
1843                    text,
1844                    pos,
1845                    span: self.node_span(start_i),
1846                })
1847            }
1848            Some(TokenKind::CharLit(text)) => {
1849                self.bump();
1850                Some(Pat::Lit {
1851                    kind: LitKind::Char,
1852                    text,
1853                    pos,
1854                    span: self.node_span(start_i),
1855                })
1856            }
1857            Some(TokenKind::LParen) => {
1858                self.bump();
1859                if self.eat(&TokenKind::RParen) {
1860                    return Some(Pat::Con {
1861                        qualifier: None,
1862                        name: "()".into(),
1863                        args: Vec::new(),
1864                        pos,
1865                        span: self.node_span(start_i),
1866                    });
1867                }
1868                // View pattern `(expr -> pat)`: scan for a top-level `->`
1869                // inside these parens; the expression side is discarded and
1870                // the pattern after the arrow is the binding. A top-level
1871                // `:` before the arrow means the arrow belongs to a type
1872                // annotation (`(f : Int -> Bool)`), not a view pattern.
1873                {
1874                    let mut depth = 0usize;
1875                    let mut j = self.i;
1876                    let mut arrow = None;
1877                    while let Some(t) = self.toks.get(j).map(|t| &t.kind) {
1878                        match t {
1879                            TokenKind::LParen | TokenKind::LBracket => depth += 1,
1880                            TokenKind::RParen | TokenKind::RBracket => {
1881                                if depth == 0 {
1882                                    break;
1883                                }
1884                                depth -= 1;
1885                            }
1886                            TokenKind::Op(o) if o.as_str() == ":" && depth == 0 => break,
1887                            TokenKind::Op(o) if o.as_str() == "->" && depth == 0 => {
1888                                arrow = Some(j);
1889                                break;
1890                            }
1891                            TokenKind::VSemi | TokenKind::VRBrace => break,
1892                            // A lambda's arrow belongs to the lambda.
1893                            TokenKind::Op(o) if o.as_str() == "\\" => break,
1894                            _ => {}
1895                        }
1896                        j += 1;
1897                    }
1898                    if let Some(j) = arrow {
1899                        self.i = j + 1; // skip the view expression and `->`
1900                        let inner = self.pattern()?;
1901                        self.eat(&TokenKind::RParen);
1902                        return Some(inner);
1903                    }
1904                }
1905                let first = self.pattern()?;
1906                // Type-annotated pattern `(e : AnyException)`: skip the type.
1907                if self.at_op(":") {
1908                    let mut depth = 0usize;
1909                    while let Some(t) = self.peek() {
1910                        match t {
1911                            TokenKind::LParen | TokenKind::LBracket => depth += 1,
1912                            TokenKind::RParen if depth == 0 => break,
1913                            TokenKind::RParen | TokenKind::RBracket => {
1914                                depth = depth.saturating_sub(1)
1915                            }
1916                            TokenKind::VSemi | TokenKind::VRBrace => break,
1917                            _ => {}
1918                        }
1919                        self.i += 1;
1920                    }
1921                }
1922                if self.at(&TokenKind::Comma) {
1923                    let mut items = vec![first];
1924                    while self.eat(&TokenKind::Comma) {
1925                        items.push(self.pattern()?);
1926                    }
1927                    self.eat(&TokenKind::RParen);
1928                    return Some(Pat::Tuple {
1929                        items,
1930                        pos,
1931                        span: self.node_span(start_i),
1932                    });
1933                }
1934                self.eat(&TokenKind::RParen);
1935                Some(first)
1936            }
1937            Some(TokenKind::LBracket) => {
1938                self.bump();
1939                let mut items = Vec::new();
1940                if !self.eat(&TokenKind::RBracket) {
1941                    loop {
1942                        items.push(self.pattern()?);
1943                        if !self.eat(&TokenKind::Comma) {
1944                            break;
1945                        }
1946                    }
1947                    self.eat(&TokenKind::RBracket);
1948                }
1949                Some(Pat::List {
1950                    items,
1951                    pos,
1952                    span: self.node_span(start_i),
1953                })
1954            }
1955            _ => None,
1956        }
1957    }
1958
1959    /// Full pattern: constructor applications and infix cons `x :: xs`.
1960    fn pattern(&mut self) -> Option<Pat> {
1961        if self.depth >= MAX_RECURSION_DEPTH {
1962            return None;
1963        }
1964        self.depth += 1;
1965        let result = self.pattern_inner();
1966        self.depth -= 1;
1967        result
1968    }
1969
1970    fn pattern_inner(&mut self) -> Option<Pat> {
1971        let pos = self.pos();
1972        let start_i = self.i;
1973        let first = match self.peek().cloned() {
1974            Some(TokenKind::UpperId { qualifier, name }) => {
1975                self.bump();
1976                if self.at(&TokenKind::LBrace) || self.at_keyword("with") {
1977                    if self.eat_keyword("with") {
1978                        let _ = self.record_fields();
1979                    } else {
1980                        self.skip_balanced_braces();
1981                    }
1982                    Pat::Con {
1983                        qualifier,
1984                        name,
1985                        args: Vec::new(),
1986                        pos,
1987                        span: self.node_span(start_i),
1988                    }
1989                } else {
1990                    let mut args = Vec::new();
1991                    while let Some(a) = self.try_pattern_atom() {
1992                        args.push(a);
1993                    }
1994                    Pat::Con {
1995                        qualifier,
1996                        name,
1997                        args,
1998                        pos,
1999                        span: self.node_span(start_i),
2000                    }
2001                }
2002            }
2003            _ => self.pattern_atom()?,
2004        };
2005        if self.at_op("::") {
2006            self.bump();
2007            let rest = self.pattern()?;
2008            return Some(Pat::Con {
2009                qualifier: None,
2010                name: "::".into(),
2011                args: vec![first, rest],
2012                pos,
2013                span: self.node_span(start_i),
2014            });
2015        }
2016        Some(first)
2017    }
2018
2019    fn try_pattern_atom(&mut self) -> Option<Pat> {
2020        match self.peek() {
2021            Some(
2022                TokenKind::LowerId {
2023                    qualifier: None, ..
2024                }
2025                | TokenKind::UpperId { .. }
2026                | TokenKind::IntLit(_)
2027                | TokenKind::DecimalLit(_)
2028                | TokenKind::StringLit(_)
2029                | TokenKind::CharLit(_)
2030                | TokenKind::LParen
2031                | TokenKind::LBracket,
2032            ) => self.pattern_atom(),
2033            _ => None,
2034        }
2035    }
2036
2037    fn skip_balanced_braces(&mut self) {
2038        let mut depth = 0usize;
2039        while let Some(t) = self.peek() {
2040            match t {
2041                TokenKind::LBrace => depth += 1,
2042                TokenKind::RBrace => {
2043                    if depth == 0 {
2044                        return;
2045                    }
2046                    depth -= 1;
2047                    if depth == 0 {
2048                        self.i += 1;
2049                        return;
2050                    }
2051                }
2052                _ => {}
2053            }
2054            self.i += 1;
2055        }
2056    }
2057
2058    // ----- expressions ---------------------------------------------------
2059
2060    fn expr(&mut self) -> Expr {
2061        self.expr_prec(0, DoExpressionMode::Allow)
2062    }
2063
2064    fn expr_no_do(&mut self) -> Expr {
2065        self.expr_prec(0, DoExpressionMode::Disallow)
2066    }
2067
2068    /// Comma-separated expressions (signatory/observer/controller lists).
2069    fn expr_comma_list(&mut self) -> Vec<Expr> {
2070        let mut out = vec![self.expr()];
2071        while self.eat(&TokenKind::Comma) {
2072            out.push(self.expr());
2073        }
2074        out
2075    }
2076
2077    fn expr_comma_list_no_do(&mut self) -> Vec<Expr> {
2078        let mut out = vec![self.expr_no_do()];
2079        while self.eat(&TokenKind::Comma) {
2080            out.push(self.expr_no_do());
2081        }
2082        out
2083    }
2084
2085    fn expr_prec(&mut self, min_prec: u8, do_mode: DoExpressionMode) -> Expr {
2086        let pos = self.pos();
2087        let start_i = self.i;
2088        if self.depth >= MAX_RECURSION_DEPTH {
2089            // Hostile nesting: degrade to raw text instead of recursing, and
2090            // report it so the degraded region is not silently mistaken for
2091            // unsupported syntax. `skip_to_item_end` below consumes the rest of
2092            // the item, so this trips about once per affected declaration.
2093            self.diag_kind(
2094                ParseDiagnosticKind::RecursionLimit {
2095                    limit: MAX_RECURSION_DEPTH,
2096                },
2097                "expression nesting too deep; truncated to raw text",
2098            );
2099            let start = self.i;
2100            self.skip_to_item_end();
2101            if self.i == start {
2102                self.bump();
2103            }
2104            return Expr::Error {
2105                raw: self.slice_text(start),
2106                span: self.node_span(start),
2107                pos,
2108            };
2109        }
2110        self.depth += 1;
2111        let result = self.expr_prec_inner(min_prec, do_mode, pos, start_i);
2112        self.depth -= 1;
2113        result
2114    }
2115
2116    fn expr_prec_inner(
2117        &mut self,
2118        min_prec: u8,
2119        do_mode: DoExpressionMode,
2120        pos: Pos,
2121        start_i: usize,
2122    ) -> Expr {
2123        let Some(mut lhs) = self.unary(do_mode) else {
2124            // Unparseable here: degrade to raw text up to the item end.
2125            let start = self.i;
2126            self.skip_to_item_end();
2127            if self.i == start {
2128                self.bump();
2129            }
2130            return Expr::Error {
2131                raw: self.slice_text(start),
2132                span: self.node_span(start),
2133                pos,
2134            };
2135        };
2136        loop {
2137            let (op, prec, right_assoc) = match self.peek() {
2138                Some(TokenKind::Op(o)) => {
2139                    let o = o.clone();
2140                    if is_reserved_op(&o) {
2141                        // `e : Type` annotation: consume the type, keep e.
2142                        if o == ":" {
2143                            self.bump();
2144                            self.skip_type_tokens();
2145                            continue;
2146                        }
2147                        break;
2148                    }
2149                    let (p, r) = fixity(&o);
2150                    (o, p, r)
2151                }
2152                Some(TokenKind::Backtick) => {
2153                    // `e `div` e` — infix function application.
2154                    let name = match self.peek_at(1) {
2155                        Some(
2156                            TokenKind::LowerId { qualifier, name }
2157                            | TokenKind::UpperId { qualifier, name },
2158                        ) => qualifier
2159                            .as_ref()
2160                            .map_or_else(|| name.to_string(), |q| format!("{q}.{name}")),
2161                        _ => break,
2162                    };
2163                    if self.peek_at(2) != Some(&TokenKind::Backtick) {
2164                        break;
2165                    }
2166                    (format!("`{name}`").into(), 9, false)
2167                }
2168                _ => break,
2169            };
2170            if prec < min_prec {
2171                break;
2172            }
2173            self.bump();
2174            if op.starts_with('`') {
2175                self.bump();
2176                self.bump();
2177            }
2178            let next_min = if right_assoc { prec } else { prec + 1 };
2179            let rhs = self.expr_prec(next_min, do_mode);
2180            lhs = Expr::BinOp {
2181                op,
2182                lhs: Box::new(lhs),
2183                rhs: Box::new(rhs),
2184                pos,
2185                span: self.node_span(start_i),
2186            };
2187        }
2188        lhs
2189    }
2190
2191    fn unary(&mut self, do_mode: DoExpressionMode) -> Option<Expr> {
2192        let pos = self.pos();
2193        let start_i = self.i;
2194        if self.at_op("-") {
2195            self.bump();
2196            let e = self.unary(do_mode)?;
2197            return Some(Expr::Neg {
2198                expr: Box::new(e),
2199                pos,
2200                span: self.node_span(start_i),
2201            });
2202        }
2203        self.application(do_mode)
2204    }
2205
2206    fn application(&mut self, do_mode: DoExpressionMode) -> Option<Expr> {
2207        let pos = self.pos();
2208        let start_i = self.i;
2209        let head0 = self.atom(do_mode)?;
2210        let mut head = self.projection_tail(head0);
2211        let mut args = Vec::new();
2212        loop {
2213            // Record syntax binds tighter than application:
2214            // `create Foo with x = 1` applies create to (Foo with {x = 1}).
2215            if self.at_keyword("with") {
2216                let target = args.pop().unwrap_or_else(|| {
2217                    std::mem::replace(
2218                        &mut head,
2219                        Expr::Error {
2220                            raw: String::new(),
2221                            pos,
2222                            span: Span::default(),
2223                        },
2224                    )
2225                });
2226                self.bump(); // with
2227                let fields = self.record_fields();
2228                let tpos = target.pos();
2229                let sp = Span::from_usize(target.span().start_usize(), self.end_byte());
2230                let rec = Expr::Record {
2231                    base: Box::new(target),
2232                    fields,
2233                    pos: tpos,
2234                    span: sp,
2235                };
2236                if matches!(head, Expr::Error { ref raw, .. } if raw.is_empty()) {
2237                    head = rec;
2238                } else {
2239                    args.push(rec);
2240                }
2241                continue;
2242            }
2243            if !do_mode.allows_do() && self.at_keyword("do") {
2244                break;
2245            }
2246            // Type application `f @Type x` — consume and drop the type atom.
2247            if self.at_op("@") {
2248                self.bump();
2249                match self.peek() {
2250                    Some(TokenKind::UpperId { .. } | TokenKind::LowerId { .. }) => {
2251                        self.bump();
2252                    }
2253                    Some(TokenKind::LParen) => self.skip_balanced_parens(),
2254                    Some(TokenKind::LBracket) => {
2255                        let mut depth = 0usize;
2256                        while let Some(t) = self.peek() {
2257                            match t {
2258                                TokenKind::LBracket => depth += 1,
2259                                TokenKind::RBracket => {
2260                                    if depth == 0 {
2261                                        break;
2262                                    }
2263                                    depth -= 1;
2264                                    if depth == 0 {
2265                                        self.i += 1;
2266                                        break;
2267                                    }
2268                                }
2269                                _ => {}
2270                            }
2271                            self.i += 1;
2272                        }
2273                    }
2274                    _ => {}
2275                }
2276                continue;
2277            }
2278            match self.try_atom(do_mode) {
2279                Some(a) => args.push(self.projection_tail(a)),
2280                None => break,
2281            }
2282        }
2283        if args.is_empty() {
2284            Some(head)
2285        } else {
2286            Some(Expr::App {
2287                func: Box::new(head),
2288                args,
2289                pos,
2290                span: self.node_span(start_i),
2291            })
2292        }
2293    }
2294
2295    /// `{ f = e ; g ; .. }` after `with` (virtual block) or explicit braces.
2296    fn record_fields(&mut self) -> Vec<FieldAssign> {
2297        let mut fields = Vec::new();
2298        let explicit = self.at(&TokenKind::LBrace);
2299        if !(self.eat(&TokenKind::VLBrace) || self.eat(&TokenKind::LBrace)) {
2300            return fields;
2301        }
2302        loop {
2303            while self.eat(&TokenKind::VSemi)
2304                || self.eat(&TokenKind::Semi)
2305                || self.eat(&TokenKind::Comma)
2306            {}
2307            match self.peek() {
2308                None => break,
2309                Some(TokenKind::VRBrace) if !explicit => {
2310                    self.bump();
2311                    break;
2312                }
2313                Some(TokenKind::RBrace) => {
2314                    self.bump();
2315                    break;
2316                }
2317                // Stray closer: discard so the loop always progresses.
2318                Some(TokenKind::RParen | TokenKind::RBracket) => {
2319                    self.bump();
2320                    continue;
2321                }
2322                _ => {}
2323            }
2324            let pos = self.pos();
2325            let start_i = self.i;
2326            if self.at_op("..") {
2327                self.bump();
2328                fields.push(FieldAssign::Wildcard {
2329                    pos,
2330                    span: self.node_span(start_i),
2331                });
2332                continue;
2333            }
2334            let name = match self.peek().cloned() {
2335                Some(TokenKind::LowerId {
2336                    qualifier: None,
2337                    name,
2338                }) => {
2339                    self.bump();
2340                    name
2341                }
2342                _ => {
2343                    self.skip_to_item_end();
2344                    continue;
2345                }
2346            };
2347            if self.eat_op("=") {
2348                let value = self.expr_prec(1, DoExpressionMode::Allow);
2349                fields.push(FieldAssign::Assign {
2350                    name,
2351                    value,
2352                    pos,
2353                    span: self.node_span(start_i),
2354                });
2355            } else {
2356                // Pun: `Foo with owner`.
2357                fields.push(FieldAssign::Pun {
2358                    name,
2359                    pos,
2360                    span: self.node_span(start_i),
2361                });
2362            }
2363        }
2364        fields
2365    }
2366
2367    fn try_atom(&mut self, do_mode: DoExpressionMode) -> Option<Expr> {
2368        match self.peek() {
2369            Some(TokenKind::LowerId { .. }) => {
2370                let kw = self.peek().and_then(|t| t.keyword());
2371                match kw {
2372                    // Block argument: `script do ...`, `submit p do ...`.
2373                    Some("do") if do_mode.allows_do() => self.atom(do_mode),
2374                    // Keywords that begin expressions are fine as atoms in
2375                    // head position but must not be slurped as arguments.
2376                    Some(
2377                        "if" | "case" | "do" | "let" | "try" | "where" | "then" | "else" | "of"
2378                        | "in" | "controller" | "with" | "catch",
2379                    ) => None,
2380                    _ => self.atom(do_mode),
2381                }
2382            }
2383            Some(
2384                TokenKind::UpperId { .. }
2385                | TokenKind::IntLit(_)
2386                | TokenKind::DecimalLit(_)
2387                | TokenKind::StringLit(_)
2388                | TokenKind::CharLit(_)
2389                | TokenKind::LParen
2390                | TokenKind::LBracket,
2391            ) => self.atom(do_mode),
2392            // Bare trailing lambda argument: `forA xs \x -> ...`.
2393            Some(TokenKind::Op(o)) if o.as_str() == "\\" => self.atom(do_mode),
2394            _ => None,
2395        }
2396    }
2397
2398    /// Fold tight (whitespace-free) `.field` record projections onto `base`.
2399    /// Projection binds tighter than application, so `length this.note` is
2400    /// `length (this.note)` not `(length this).note`, and a chain `a.b.c`
2401    /// left-nests. A *spaced* dot (`f . g`, composition) is not tight and is
2402    /// left to the binary-operator layer untouched. Qualified names
2403    /// (`Map.lookup`) are already a single token and never reach here.
2404    fn projection_tail(&mut self, mut base: Expr) -> Expr {
2405        while self.at_tight_projection() {
2406            let start = base.span().start_usize();
2407            let pos = base.pos();
2408            self.bump(); // '.'
2409            let Some(field_tok) = self.bump() else {
2410                self.diag_expected(
2411                    ExpectedToken::ProjectionFieldAfterDot,
2412                    "expected projection field after '.'",
2413                );
2414                return base;
2415            };
2416            let TokenKind::LowerId { qualifier, name } = field_tok.kind else {
2417                self.diag_expected(
2418                    ExpectedToken::ProjectionFieldAfterDot,
2419                    "expected projection field after '.'",
2420                );
2421                return base;
2422            };
2423            let field = Expr::Var {
2424                qualifier,
2425                name,
2426                pos: field_tok.pos,
2427                span: Span::from_usize(field_tok.start, field_tok.end),
2428            };
2429            base = Expr::BinOp {
2430                op: ".".into(),
2431                lhs: Box::new(base),
2432                rhs: Box::new(field),
2433                pos,
2434                span: Span::from_usize(start, self.end_byte()),
2435            };
2436        }
2437        base
2438    }
2439
2440    /// True when the cursor sits on a `.` that abuts a real token on its left
2441    /// and an unqualified lowercase field on its right with no whitespace on
2442    /// either side — i.e. a record projection, not function composition.
2443    fn at_tight_projection(&self) -> bool {
2444        if self.i == 0 {
2445            return false;
2446        }
2447        let Some(dot) = self.toks.get(self.i) else {
2448            return false;
2449        };
2450        if !matches!(&dot.kind, TokenKind::Op(o) if o.as_str() == ".") {
2451            return false;
2452        }
2453        // Tight on the left: the dot abuts the base's last byte. The previous
2454        // token must be real — a virtual layout token here means a newline or
2455        // dedent sat between base and dot, which can never be a tight dot.
2456        let prev = &self.toks[self.i - 1];
2457        if prev.is_virtual() || prev.end != dot.start {
2458            return false;
2459        }
2460        // Tight on the right: an unqualified lowercase field abuts the dot.
2461        self.toks.get(self.i + 1).is_some_and(|t| {
2462            matches!(
2463                &t.kind,
2464                TokenKind::LowerId {
2465                    qualifier: None,
2466                    ..
2467                }
2468            ) && t.start == dot.end
2469        })
2470    }
2471
2472    fn atom(&mut self, do_mode: DoExpressionMode) -> Option<Expr> {
2473        let pos = self.pos();
2474        let start_i = self.i;
2475        match self.peek().cloned() {
2476            Some(TokenKind::LowerId { qualifier, name }) => {
2477                match name.as_str() {
2478                    "if" if qualifier.is_none() => return self.if_expr(),
2479                    "case" if qualifier.is_none() => return self.case_expr(),
2480                    "do" if qualifier.is_none() => {
2481                        if !do_mode.allows_do() {
2482                            return None;
2483                        }
2484                        return self.do_expr();
2485                    }
2486                    "let" if qualifier.is_none() => return self.let_expr(),
2487                    "try" if qualifier.is_none() => return self.try_expr(),
2488                    _ => {}
2489                }
2490                self.bump();
2491                Some(Expr::Var {
2492                    qualifier,
2493                    name,
2494                    pos,
2495                    span: self.node_span(start_i),
2496                })
2497            }
2498            Some(TokenKind::UpperId { qualifier, name }) => {
2499                self.bump();
2500                let base = Expr::Con {
2501                    qualifier,
2502                    name,
2503                    pos,
2504                    span: self.node_span(start_i),
2505                };
2506                // Explicit-brace record syntax: `Foo {x = 1}`.
2507                if self.at(&TokenKind::LBrace) {
2508                    let fields = self.record_fields();
2509                    return Some(Expr::Record {
2510                        base: Box::new(base),
2511                        fields,
2512                        pos,
2513                        span: self.node_span(start_i),
2514                    });
2515                }
2516                Some(base)
2517            }
2518            Some(TokenKind::IntLit(text)) => {
2519                self.bump();
2520                Some(Expr::Lit {
2521                    kind: LitKind::Int,
2522                    text,
2523                    pos,
2524                    span: self.node_span(start_i),
2525                })
2526            }
2527            Some(TokenKind::DecimalLit(text)) => {
2528                self.bump();
2529                Some(Expr::Lit {
2530                    kind: LitKind::Decimal,
2531                    text,
2532                    pos,
2533                    span: self.node_span(start_i),
2534                })
2535            }
2536            Some(TokenKind::StringLit(text)) => {
2537                self.bump();
2538                Some(Expr::Lit {
2539                    kind: LitKind::Text,
2540                    text,
2541                    pos,
2542                    span: self.node_span(start_i),
2543                })
2544            }
2545            Some(TokenKind::CharLit(text)) => {
2546                self.bump();
2547                Some(Expr::Lit {
2548                    kind: LitKind::Char,
2549                    text,
2550                    pos,
2551                    span: self.node_span(start_i),
2552                })
2553            }
2554            Some(TokenKind::Op(o)) if o.as_str() == "\\" => self.lambda_expr(),
2555            Some(TokenKind::LParen) => self.paren_expr(),
2556            Some(TokenKind::LBracket) => self.list_expr(),
2557            _ => None,
2558        }
2559    }
2560
2561    fn if_expr(&mut self) -> Option<Expr> {
2562        let pos = self.pos();
2563        let start_i = self.i;
2564        self.bump(); // if
2565        let cond = self.expr();
2566        self.eat(&TokenKind::VSemi); // DoAndIfThenElse style
2567        if !self.eat_keyword("then") {
2568            self.diag_expected(ExpectedToken::ThenKeyword, "expected 'then'");
2569            return Some(Expr::Error {
2570                raw: format!("if {}", cond.render()),
2571                pos,
2572                span: self.node_span(start_i),
2573            });
2574        }
2575        let then_branch = self.expr();
2576        self.eat(&TokenKind::VSemi);
2577        if !self.eat_keyword("else") {
2578            self.diag_expected(ExpectedToken::ElseKeyword, "expected 'else'");
2579            return Some(Expr::Error {
2580                raw: format!("if {} then {}", cond.render(), then_branch.render()),
2581                pos,
2582                span: self.node_span(start_i),
2583            });
2584        }
2585        let else_branch = self.expr();
2586        Some(Expr::If {
2587            cond: Box::new(cond),
2588            then_branch: Box::new(then_branch),
2589            else_branch: Box::new(else_branch),
2590            pos,
2591            span: self.node_span(start_i),
2592        })
2593    }
2594
2595    fn case_expr(&mut self) -> Option<Expr> {
2596        let pos = self.pos();
2597        let start_i = self.i;
2598        self.bump(); // case
2599        let scrutinee = self.expr_no_do();
2600        if !self.eat_keyword("of") {
2601            self.diag_expected(
2602                ExpectedToken::OfKeywordInCaseExpression,
2603                "expected 'of' in case expression",
2604            );
2605            return Some(Expr::Error {
2606                raw: format!("case {}", scrutinee.render()),
2607                pos,
2608                span: self.node_span(start_i),
2609            });
2610        }
2611        let mut alts = Vec::new();
2612        if self.eat(&TokenKind::VLBrace) || self.eat(&TokenKind::LBrace) {
2613            loop {
2614                while self.eat(&TokenKind::VSemi) || self.eat(&TokenKind::Semi) {}
2615                match self.peek() {
2616                    None => break,
2617                    Some(TokenKind::VRBrace | TokenKind::RBrace) => {
2618                        self.bump();
2619                        break;
2620                    }
2621                    // Stray closer: discard so the loop always progresses.
2622                    Some(TokenKind::RParen | TokenKind::RBracket) => {
2623                        self.bump();
2624                        continue;
2625                    }
2626                    _ => {}
2627                }
2628                // An alternative can carry a `where` block for its body.
2629                if self.eat_keyword("where") {
2630                    let _ = self.binding_block();
2631                    continue;
2632                }
2633                match self.case_alt() {
2634                    Some(a) => alts.push(a),
2635                    None => self.skip_to_item_end(),
2636                }
2637            }
2638        }
2639        Some(Expr::Case {
2640            scrutinee: Box::new(scrutinee),
2641            alts,
2642            pos,
2643            span: self.node_span(start_i),
2644        })
2645    }
2646
2647    fn case_alt(&mut self) -> Option<Alt> {
2648        let pos = self.pos();
2649        let start_i = self.i;
2650        let pat = self.pattern()?;
2651        if self.at_op("|") {
2652            // Guarded alternative(s): take the first body, consume all.
2653            // Each guard is comma-separated qualifiers, each a boolean
2654            // expression or a pattern guard `pat <- expr`.
2655            let mut first: Option<Expr> = None;
2656            while self.eat_op("|") {
2657                loop {
2658                    let _guard = self.expr();
2659                    if self.eat_op("<-") {
2660                        let _ = self.expr();
2661                    }
2662                    if !self.eat(&TokenKind::Comma) {
2663                        break;
2664                    }
2665                }
2666                if !self.eat_op("->") {
2667                    self.diag_expected(
2668                        ExpectedToken::ArrowInGuardedCaseAlternative,
2669                        "expected '->' in guarded case alternative",
2670                    );
2671                    return None;
2672                }
2673                let body = self.expr();
2674                if first.is_none() {
2675                    first = Some(body);
2676                }
2677            }
2678            return Some(Alt {
2679                pat,
2680                body: first?,
2681                pos,
2682                span: self.node_span(start_i),
2683            });
2684        }
2685        if !self.eat_op("->") {
2686            self.diag_expected(
2687                ExpectedToken::ArrowInCaseAlternative,
2688                "expected '->' in case alternative",
2689            );
2690            return None;
2691        }
2692        let body = self.expr();
2693        Some(Alt {
2694            pat,
2695            body,
2696            pos,
2697            span: self.node_span(start_i),
2698        })
2699    }
2700
2701    fn do_expr(&mut self) -> Option<Expr> {
2702        let pos = self.pos();
2703        let start_i = self.i;
2704        self.bump(); // do
2705        let mut stmts = Vec::new();
2706        if self.eat(&TokenKind::VLBrace) || self.eat(&TokenKind::LBrace) {
2707            loop {
2708                while self.eat(&TokenKind::VSemi) || self.eat(&TokenKind::Semi) {}
2709                match self.peek() {
2710                    None => break,
2711                    Some(TokenKind::VRBrace | TokenKind::RBrace) => {
2712                        self.bump();
2713                        break;
2714                    }
2715                    // Stray closer: discard so the loop always progresses.
2716                    Some(TokenKind::RParen | TokenKind::RBracket) => {
2717                        self.bump();
2718                        continue;
2719                    }
2720                    _ => {}
2721                }
2722                stmts.push(self.do_stmt());
2723            }
2724        }
2725        Some(Expr::Do {
2726            stmts,
2727            pos,
2728            span: self.node_span(start_i),
2729        })
2730    }
2731
2732    fn do_stmt(&mut self) -> DoStmt {
2733        let pos = self.pos();
2734        let start_i = self.i;
2735        if self.at_keyword("let") {
2736            self.bump();
2737            let bindings = self.binding_block();
2738            // `let ... in body` as a statement is an expression.
2739            if self.eat_keyword("in") {
2740                let body = self.expr();
2741                return DoStmt::Expr {
2742                    expr: Expr::LetIn {
2743                        bindings,
2744                        body: Box::new(body),
2745                        pos,
2746                        span: self.node_span(start_i),
2747                    },
2748                    pos,
2749                    span: self.node_span(start_i),
2750                };
2751            }
2752            return DoStmt::Let {
2753                bindings,
2754                pos,
2755                span: self.node_span(start_i),
2756            };
2757        }
2758        // Try `pat <- expr` with rollback.
2759        let snapshot = self.i;
2760        if let Some(pat) = self.try_bind_pattern() {
2761            if self.at_op("<-") {
2762                self.bump();
2763                let expr = self.expr();
2764                return DoStmt::Bind {
2765                    pat,
2766                    expr,
2767                    pos,
2768                    span: self.node_span(start_i),
2769                };
2770            }
2771        }
2772        self.i = snapshot;
2773        let expr = self.expr();
2774        DoStmt::Expr {
2775            expr,
2776            pos,
2777            span: self.node_span(start_i),
2778        }
2779    }
2780
2781    /// Pattern attempt for `pat <- ...`; restores nothing itself (caller
2782    /// rolls back on failure).
2783    fn try_bind_pattern(&mut self) -> Option<Pat> {
2784        self.pattern()
2785    }
2786
2787    fn let_expr(&mut self) -> Option<Expr> {
2788        let pos = self.pos();
2789        let start_i = self.i;
2790        self.bump(); // let
2791        let bindings = self.binding_block();
2792        if self.eat_keyword("in") {
2793            let body = self.expr();
2794            return Some(Expr::LetIn {
2795                bindings,
2796                body: Box::new(body),
2797                pos,
2798                span: self.node_span(start_i),
2799            });
2800        }
2801        // `let` without `in` outside a do block — degrade gracefully.
2802        Some(Expr::LetIn {
2803            bindings,
2804            body: Box::new(Expr::Error {
2805                raw: String::new(),
2806                pos,
2807                span: self.node_span(start_i),
2808            }),
2809            pos,
2810            span: self.node_span(start_i),
2811        })
2812    }
2813
2814    fn try_expr(&mut self) -> Option<Expr> {
2815        let pos = self.pos();
2816        let start_i = self.i;
2817        self.bump(); // try
2818        let body = self.expr();
2819        let mut handlers = Vec::new();
2820        self.eat(&TokenKind::VSemi);
2821        if self.eat_keyword("catch") {
2822            if self.eat(&TokenKind::VLBrace) || self.eat(&TokenKind::LBrace) {
2823                loop {
2824                    while self.eat(&TokenKind::VSemi) || self.eat(&TokenKind::Semi) {}
2825                    match self.peek() {
2826                        None => break,
2827                        Some(TokenKind::VRBrace | TokenKind::RBrace) => {
2828                            self.bump();
2829                            break;
2830                        }
2831                        // Stray closer: discard so the loop always progresses.
2832                        Some(TokenKind::RParen | TokenKind::RBracket) => {
2833                            self.bump();
2834                            continue;
2835                        }
2836                        _ => {}
2837                    }
2838                    match self.case_alt() {
2839                        Some(a) => handlers.push(a),
2840                        None => self.skip_to_item_end(),
2841                    }
2842                }
2843            } else if let Some(a) = self.case_alt() {
2844                // Single-alternative catch on the same line.
2845                handlers.push(a);
2846            }
2847        }
2848        Some(Expr::Try {
2849            body: Box::new(body),
2850            handlers,
2851            pos,
2852            span: self.node_span(start_i),
2853        })
2854    }
2855
2856    fn lambda_expr(&mut self) -> Option<Expr> {
2857        let pos = self.pos();
2858        let start_i = self.i;
2859        self.bump(); // backslash
2860                     // `\case` — lambda-case: one implicit argument matched by the alts.
2861        if self.eat_keyword("case") {
2862            let mut alts = Vec::new();
2863            if self.eat(&TokenKind::VLBrace) || self.eat(&TokenKind::LBrace) {
2864                loop {
2865                    while self.eat(&TokenKind::VSemi) || self.eat(&TokenKind::Semi) {}
2866                    match self.peek() {
2867                        None => break,
2868                        Some(TokenKind::VRBrace | TokenKind::RBrace) => {
2869                            self.bump();
2870                            break;
2871                        }
2872                        Some(TokenKind::RParen | TokenKind::RBracket) => {
2873                            self.bump();
2874                            continue;
2875                        }
2876                        _ => {}
2877                    }
2878                    match self.case_alt() {
2879                        Some(a) => alts.push(a),
2880                        None => self.skip_to_item_end(),
2881                    }
2882                }
2883            }
2884            return Some(Expr::Lambda {
2885                params: vec![Pat::Var {
2886                    name: "_".into(),
2887                    pos,
2888                    span: Span::from_usize(self.byte_at(start_i), self.byte_at(start_i)),
2889                }],
2890                body: Box::new(Expr::Case {
2891                    scrutinee: Box::new(Expr::Var {
2892                        qualifier: None,
2893                        name: "_".into(),
2894                        pos,
2895                        span: Span::from_usize(self.byte_at(start_i), self.byte_at(start_i)),
2896                    }),
2897                    alts,
2898                    pos,
2899                    span: self.node_span(start_i),
2900                }),
2901                pos,
2902                span: self.node_span(start_i),
2903            });
2904        }
2905        let mut params = Vec::new();
2906        while !self.at_op("->") {
2907            match self.pattern_atom() {
2908                Some(p) => params.push(p),
2909                None => {
2910                    self.diag_malformed(
2911                        MalformedSyntaxKind::LambdaParameter,
2912                        "bad lambda parameter",
2913                    );
2914                    let start = self.i;
2915                    self.skip_to_item_end();
2916                    return Some(Expr::Error {
2917                        raw: format!("\\{}", self.slice_text(start)),
2918                        pos,
2919                        span: self.node_span(start_i),
2920                    });
2921                }
2922            }
2923        }
2924        self.bump(); // ->
2925        let body = self.expr();
2926        Some(Expr::Lambda {
2927            params,
2928            body: Box::new(body),
2929            pos,
2930            span: self.node_span(start_i),
2931        })
2932    }
2933
2934    fn paren_expr(&mut self) -> Option<Expr> {
2935        let pos = self.pos();
2936        let start_i = self.i;
2937        self.bump(); // (
2938        if self.eat(&TokenKind::RParen) {
2939            return Some(Expr::Con {
2940                qualifier: None,
2941                name: "()".into(),
2942                pos,
2943                span: self.node_span(start_i),
2944            });
2945        }
2946        // Operator section / operator reference: `(+)`, `(+ 1)`.
2947        if let Some(TokenKind::Op(o)) = self.peek().cloned() {
2948            if !is_reserved_op(&o) && o != "\\" && o != "-" {
2949                self.bump();
2950                if self.eat(&TokenKind::RParen) {
2951                    return Some(Expr::OperatorRef {
2952                        op: o,
2953                        pos,
2954                        span: self.node_span(start_i),
2955                    });
2956                }
2957                let operand = self.expr();
2958                self.eat(&TokenKind::RParen);
2959                return Some(Expr::RightSection {
2960                    op: o,
2961                    operand: Box::new(operand),
2962                    pos,
2963                    span: self.node_span(start_i),
2964                });
2965            }
2966        }
2967        let first = self.expr();
2968        if self.at(&TokenKind::Comma) {
2969            let mut items = vec![first];
2970            while self.eat(&TokenKind::Comma) {
2971                items.push(self.expr());
2972            }
2973            self.eat(&TokenKind::RParen);
2974            return Some(Expr::Tuple {
2975                items,
2976                pos,
2977                span: self.node_span(start_i),
2978            });
2979        }
2980        // Left section: `(x +)`.
2981        if let Some(TokenKind::Op(o)) = self.peek().cloned() {
2982            if !is_reserved_op(&o) && self.peek_at(1) == Some(&TokenKind::RParen) {
2983                self.bump();
2984                self.bump();
2985                return Some(Expr::LeftSection {
2986                    op: o,
2987                    operand: Box::new(first),
2988                    pos,
2989                    span: self.node_span(start_i),
2990                });
2991            }
2992        }
2993        self.eat(&TokenKind::RParen);
2994        Some(first)
2995    }
2996
2997    fn list_expr(&mut self) -> Option<Expr> {
2998        let pos = self.pos();
2999        let start_i = self.i;
3000        self.bump(); // [
3001        let mut items = Vec::new();
3002        if self.eat(&TokenKind::RBracket) {
3003            return Some(Expr::List {
3004                items,
3005                pos,
3006                span: self.node_span(start_i),
3007            });
3008        }
3009        loop {
3010            let e = self.expr();
3011            // Range: `[a .. b]` / `[a ..]`.
3012            if self.at_op("..") {
3013                self.bump();
3014                let hi = if self.at(&TokenKind::RBracket) {
3015                    Expr::Error {
3016                        raw: String::new(),
3017                        pos,
3018                        span: self.node_span(start_i),
3019                    }
3020                } else {
3021                    self.expr()
3022                };
3023                self.eat(&TokenKind::RBracket);
3024                return Some(Expr::BinOp {
3025                    op: "..".into(),
3026                    lhs: Box::new(e),
3027                    rhs: Box::new(hi),
3028                    pos,
3029                    span: self.node_span(start_i),
3030                });
3031            }
3032            // List comprehension: degrade the qualifier part to raw text.
3033            if self.at_op("|") {
3034                let start = self.i;
3035                let mut brackets = 1usize;
3036                while let Some(t) = self.peek() {
3037                    match t {
3038                        TokenKind::LBracket => brackets += 1,
3039                        TokenKind::RBracket => {
3040                            brackets -= 1;
3041                            if brackets == 0 {
3042                                break;
3043                            }
3044                        }
3045                        TokenKind::VSemi | TokenKind::VRBrace => break,
3046                        _ => {}
3047                    }
3048                    self.i += 1;
3049                }
3050                let raw = self.slice_text(start);
3051                self.eat(&TokenKind::RBracket);
3052                return Some(Expr::App {
3053                    func: Box::new(e),
3054                    args: vec![Expr::Error {
3055                        raw,
3056                        pos,
3057                        span: self.node_span(start_i),
3058                    }],
3059                    pos,
3060                    span: self.node_span(start_i),
3061                });
3062            }
3063            items.push(e);
3064            if !self.eat(&TokenKind::Comma) {
3065                break;
3066            }
3067        }
3068        self.eat(&TokenKind::RBracket);
3069        Some(Expr::List {
3070            items,
3071            pos,
3072            span: self.node_span(start_i),
3073        })
3074    }
3075}
3076
3077/// Operators that structure declarations and can never be expression infix
3078/// operators.
3079fn is_reserved_op(op: &str) -> bool {
3080    matches!(op, "=" | "<-" | "->" | "|" | ":" | "=>" | "@" | "\\" | "..")
3081}
3082
3083/// (precedence, right-assoc) — Haskell defaults; unknown operators get
3084/// infixl 9.
3085fn fixity(op: &str) -> (u8, bool) {
3086    match op {
3087        "$" | "$!" => (1, true),
3088        ">>=" | ">>" | "=<<" | "<&>" => (2, false),
3089        "||" => (3, true),
3090        "&&" => (4, true),
3091        "==" | "/=" | "<" | "<=" | ">" | ">=" => (5, false),
3092        "::" | "++" | "<>" => (6, true),
3093        "+" | "-" => (7, false),
3094        "*" | "/" => (8, false),
3095        "^" | "**" => (9, true),
3096        "." | "!!" => (10, true),
3097        _ => (9, false),
3098    }
3099}
3100
3101/// Merge type signatures and successive equations of the same function into
3102/// one `Decl::Function`, preserving first-seen order.
3103/// Bounding span of a function's equations (their first start to last end).
3104/// `None` for a signature-only function (no equations yet).
3105fn equations_extent(eqs: &[Equation]) -> Option<Span> {
3106    let mut it = eqs.iter();
3107    let first = it.next()?;
3108    let mut s = first.span;
3109    for e in it {
3110        s.start = s.start.min(e.span.start);
3111        s.end = s.end.max(e.span.end);
3112    }
3113    Some(s)
3114}
3115
3116fn merge_functions(decls: &mut Vec<Decl>) {
3117    let mut out: Vec<Decl> = Vec::with_capacity(decls.len());
3118    let mut function_index_by_name: HashMap<Identifier, usize> = HashMap::new();
3119    for decl in decls.drain(..) {
3120        match decl {
3121            Decl::Function(f) => {
3122                if let Some(existing_index) = function_index_by_name.get(&f.name).copied() {
3123                    let Decl::Function(g) = &mut out[existing_index] else {
3124                        out.push(Decl::Function(f));
3125                        continue;
3126                    };
3127                    if g.ty.is_absent() {
3128                        g.ty = f.ty.clone();
3129                    }
3130                    if g.sig_span.is_none() {
3131                        g.sig_span = f.sig_span;
3132                    }
3133                    // The function's reported position is its first equation,
3134                    // not its type signature.
3135                    if g.equations.is_empty() && !f.equations.is_empty() {
3136                        g.pos = f.pos;
3137                    }
3138                    g.equations.extend(f.equations);
3139                    // A function's span is the extent of its equations, which
3140                    // are contiguous in well-formed source. A type signature
3141                    // can sit apart (with unrelated decls in between), so it is
3142                    // tracked in `sig_span` and never folded into `span` —
3143                    // doing so could straddle a sibling decl and break the
3144                    // nesting invariant.
3145                    g.span = equations_extent(&g.equations)
3146                        .or(g.sig_span)
3147                        .unwrap_or(g.span);
3148                } else {
3149                    function_index_by_name.insert(f.name.clone(), out.len());
3150                    out.push(Decl::Function(f));
3151                }
3152            }
3153            other => out.push(other),
3154        }
3155    }
3156    *decls = out;
3157}
3158
3159/// Parse a type from a slice of the type's tokens (e.g. the tokens between a
3160/// field's `:` and the item end). PURE: it never touches the main parser cursor
3161/// and never affects any span, so it is invisible to daml-fmt. Returns `None`
3162/// when the whole slice does not parse cleanly as a type.
3163///
3164/// Grammar (precedence low → high): constraint `C => T`, function `a -> b`
3165/// (right-assoc), application `head arg...` (left-assoc), then atoms (`Con`,
3166/// `Var`, `[T]`, `()`, `(T)`, `(a, b)`).
3167pub(crate) fn parse_type_from_tokens(tokens: &[Token]) -> Option<Type> {
3168    // Virtual layout tokens carry no type meaning; drop them so a stray VSemi
3169    // in the slice can't sink an otherwise-clean parse.
3170    let real_tokens: Vec<&Token> = tokens.iter().filter(|t| !t.is_virtual()).collect();
3171    if real_tokens.is_empty() {
3172        return None;
3173    }
3174    let mut parser = TypeTokenParser {
3175        tokens: &real_tokens,
3176        cursor: 0,
3177    };
3178    let ty = parser.parse_type()?;
3179    // Require the whole slice to be consumed: a partial parse means the type
3180    // had a shape we don't model, so report unknown rather than a half-truth.
3181    if parser.cursor == real_tokens.len() {
3182        Some(ty)
3183    } else {
3184        None
3185    }
3186}
3187
3188struct TypeTokenParser<'a> {
3189    tokens: &'a [&'a Token],
3190    cursor: usize,
3191}
3192
3193#[derive(Debug)]
3194struct FieldBlock {
3195    fields: Vec<FieldDecl>,
3196    dangling: bool,
3197}
3198
3199/// Result of parsing one atom: a real type, or a dropped type-level nat literal
3200/// (`Numeric 10` — not a type, so it never enters the App arg list).
3201enum TypeAtom {
3202    ParsedType(Type),
3203    DroppedLiteral(Span),
3204}
3205
3206impl<'a> TypeTokenParser<'a> {
3207    fn peek(&self) -> Option<&'a Token> {
3208        self.tokens.get(self.cursor).copied()
3209    }
3210
3211    fn eat_op(&mut self, op: &str) -> bool {
3212        if self.peek().is_some_and(|t| t.kind.is_op(op)) {
3213            self.cursor += 1;
3214            true
3215        } else {
3216            false
3217        }
3218    }
3219
3220    /// Full type: a constraint context `=> body`, or a function `a -> b`, or a
3221    /// bare application.
3222    fn parse_type(&mut self) -> Option<Type> {
3223        let lhs = if self.eat_keyword("forall") {
3224            self.parse_forall_type()?
3225        } else {
3226            self.parse_application_type()?
3227        };
3228        if self.eat_op("=>") {
3229            // `lhs` was the constraint context; drop it, keep the body.
3230            let body = self.parse_type()?;
3231            let span = Span::from_usize(lhs.span().start_usize(), body.span().end_usize());
3232            return Some(Type::Constrained(Box::new(body), span));
3233        }
3234        if self.eat_op("->") {
3235            let rhs = self.parse_type()?;
3236            let span = Span::from_usize(lhs.span().start_usize(), rhs.span().end_usize());
3237            return Some(Type::Fun(Box::new(lhs), Box::new(rhs), span));
3238        }
3239        Some(lhs)
3240    }
3241
3242    /// Application spine: one head atom applied to zero or more argument atoms.
3243    fn parse_application_type(&mut self) -> Option<Type> {
3244        let head = match self.parse_atom()? {
3245            TypeAtom::ParsedType(t) => t,
3246            // A bare nat literal is not a type.
3247            TypeAtom::DroppedLiteral(_) => return None,
3248        };
3249        let mut args = Vec::new();
3250        let start = head.span().start_usize();
3251        let mut end = head.span().end_usize();
3252        loop {
3253            // Only continue the spine if the next token can START an atom; an
3254            // operator (`->`, `=>`) or closer ends it.
3255            if !self.is_at_type_atom_start() {
3256                break;
3257            }
3258            match self.parse_atom()? {
3259                TypeAtom::ParsedType(t) => {
3260                    end = t.span().end_usize();
3261                    args.push(t);
3262                }
3263                TypeAtom::DroppedLiteral(span) => {
3264                    // `Numeric 10` — drop the `10` as structure but keep it in
3265                    // the enclosing type span.
3266                    end = span.end_usize();
3267                }
3268            }
3269        }
3270        let span = Span::from_usize(start, end);
3271        if args.is_empty() {
3272            Some(head.with_span(span))
3273        } else {
3274            Some(Type::App(Box::new(head), args, span))
3275        }
3276    }
3277
3278    /// True if the current token can begin an atom (so the application spine
3279    /// should keep going).
3280    fn is_at_type_atom_start(&self) -> bool {
3281        matches!(
3282            self.peek().map(|t| &t.kind),
3283            Some(
3284                TokenKind::UpperId { .. }
3285                    | TokenKind::LowerId { .. }
3286                    | TokenKind::IntLit(_)
3287                    | TokenKind::DecimalLit(_)
3288                    | TokenKind::StringLit(_)
3289                    | TokenKind::CharLit(_)
3290                    | TokenKind::LBracket
3291                    | TokenKind::LParen
3292            )
3293        )
3294    }
3295
3296    fn parse_atom(&mut self) -> Option<TypeAtom> {
3297        let tok = self.peek()?;
3298        match &tok.kind {
3299            TokenKind::UpperId { qualifier, name } => {
3300                let con = Type::Con {
3301                    qualifier: qualifier.clone(),
3302                    name: name.clone(),
3303                    span: Span::from_usize(tok.start, tok.end),
3304                };
3305                self.cursor += 1;
3306                Some(TypeAtom::ParsedType(con))
3307            }
3308            TokenKind::LowerId { name, .. } => {
3309                // Type variable (`a`, `n`). Qualified lowercase never appears in
3310                // a real type position; treat the name as the variable.
3311                let var = Type::Var(name.clone(), Span::from_usize(tok.start, tok.end));
3312                self.cursor += 1;
3313                Some(TypeAtom::ParsedType(var))
3314            }
3315            TokenKind::IntLit(_) | TokenKind::DecimalLit(_) => {
3316                // Type-level nat literal (`Numeric 10`): consumed, but dropped.
3317                self.cursor += 1;
3318                Some(TypeAtom::DroppedLiteral(Span::from_usize(
3319                    tok.start, tok.end,
3320                )))
3321            }
3322            TokenKind::StringLit(text) => {
3323                self.cursor += 1;
3324                Some(TypeAtom::ParsedType(Type::Lit {
3325                    kind: LitKind::Text,
3326                    text: text.clone(),
3327                    span: Span::from_usize(tok.start, tok.end),
3328                }))
3329            }
3330            TokenKind::CharLit(text) => {
3331                self.cursor += 1;
3332                Some(TypeAtom::ParsedType(Type::Lit {
3333                    kind: LitKind::Char,
3334                    text: text.clone(),
3335                    span: Span::from_usize(tok.start, tok.end),
3336                }))
3337            }
3338            TokenKind::LBracket => {
3339                let start = tok.start;
3340                self.cursor += 1;
3341                let inner = self.parse_type()?;
3342                self.eat_token(&TokenKind::RBracket).map(|end| {
3343                    TypeAtom::ParsedType(Type::List(
3344                        Box::new(inner),
3345                        Span::from_usize(start, end.end),
3346                    ))
3347                })
3348            }
3349            TokenKind::LParen => {
3350                let start = tok.start;
3351                self.cursor += 1;
3352                if let Some(op) = self.eat_token_if_operator() {
3353                    let mut name = op.as_str().to_string();
3354                    while matches!(
3355                        self.tokens.get(self.cursor).map(|t| &t.kind),
3356                        Some(TokenKind::Op(_))
3357                    ) {
3358                        self.cursor += 1;
3359                        if let TokenKind::Op(o) = &self.tokens[self.cursor - 1].kind {
3360                            name.push_str(o.as_str());
3361                        }
3362                    }
3363                    if self.eat_token(&TokenKind::RParen).is_some()
3364                        && self
3365                            .tokens
3366                            .get(self.cursor)
3367                            .is_some_and(|t| Self::is_type_atom_start(&t.kind))
3368                    {
3369                        let end = self.tokens[self.cursor - 1];
3370                        return Some(TypeAtom::ParsedType(Type::Con {
3371                            qualifier: None,
3372                            name: name.into(),
3373                            span: Span::from_usize(start, end.end),
3374                        }));
3375                    }
3376                    return None;
3377                }
3378                if matches!(
3379                    self.tokens.get(self.cursor).map(|t| &t.kind),
3380                    Some(TokenKind::Comma)
3381                ) {
3382                    let mut name = String::from(",");
3383                    self.cursor += 1;
3384                    while matches!(
3385                        self.tokens.get(self.cursor).map(|t| &t.kind),
3386                        Some(TokenKind::Comma)
3387                    ) {
3388                        self.cursor += 1;
3389                        name.push(',');
3390                    }
3391                    if self.eat_token(&TokenKind::RParen).is_some()
3392                        && self
3393                            .tokens
3394                            .get(self.cursor)
3395                            .is_some_and(|t| Self::is_type_atom_start(&t.kind))
3396                    {
3397                        let end = self.tokens[self.cursor - 1];
3398                        return Some(TypeAtom::ParsedType(Type::Con {
3399                            qualifier: None,
3400                            name: name.into(),
3401                            span: Span::from_usize(start, end.end),
3402                        }));
3403                    }
3404                    return None;
3405                }
3406                if let Some(end) = self.eat_token(&TokenKind::RParen) {
3407                    // ()
3408                    return Some(TypeAtom::ParsedType(Type::Unit(Span::from_usize(
3409                        start, end.end,
3410                    ))));
3411                }
3412                let first = self.parse_type()?;
3413                if self.peek().map(|t| &t.kind) == Some(&TokenKind::Comma) {
3414                    let mut items = vec![first];
3415                    while self.eat_token(&TokenKind::Comma).is_some() {
3416                        items.push(self.parse_type()?);
3417                    }
3418                    self.eat_token(&TokenKind::RParen).map(|end| {
3419                        TypeAtom::ParsedType(Type::Tuple(items, Span::from_usize(start, end.end)))
3420                    })
3421                } else {
3422                    self.eat_token(&TokenKind::RParen).map(|end| {
3423                        // Grouping parens.
3424                        TypeAtom::ParsedType(first.with_span(Span::from_usize(start, end.end)))
3425                    })
3426                }
3427            }
3428            _ => None,
3429        }
3430    }
3431
3432    fn eat_keyword(&mut self, kw: &str) -> bool {
3433        if self.peek().is_some_and(|t| t.kind.is_keyword(kw)) {
3434            self.cursor += 1;
3435            true
3436        } else {
3437            false
3438        }
3439    }
3440
3441    fn parse_forall_type(&mut self) -> Option<Type> {
3442        let start = self
3443            .tokens
3444            .get(self.cursor.wrapping_sub(1))
3445            .map(|t| t.start)
3446            .unwrap_or_default();
3447        while self.cursor < self.tokens.len() {
3448            if self.peek().is_some_and(|t| t.kind.is_op(".")) {
3449                self.cursor += 1;
3450                let body = self.parse_type()?;
3451                let body_span = body.span();
3452                return Some(body.with_span(Span::from_usize(start, body_span.end_usize())));
3453            }
3454            self.cursor += 1;
3455        }
3456        None
3457    }
3458
3459    fn eat_token(&mut self, tok: &TokenKind) -> Option<&'a Token> {
3460        if self.peek().is_some_and(|t| t.kind == *tok) {
3461            let t = self.peek();
3462            self.cursor += 1;
3463            t
3464        } else {
3465            None
3466        }
3467    }
3468
3469    fn eat_token_if_operator(&mut self) -> Option<&'a Operator> {
3470        match self.peek() {
3471            Some(Token {
3472                kind: TokenKind::Op(op),
3473                ..
3474            }) => {
3475                self.cursor += 1;
3476                Some(op)
3477            }
3478            _ => None,
3479        }
3480    }
3481
3482    const fn is_type_atom_start(kind: &TokenKind) -> bool {
3483        matches!(
3484            kind,
3485            TokenKind::UpperId { .. }
3486                | TokenKind::LowerId { .. }
3487                | TokenKind::IntLit(_)
3488                | TokenKind::DecimalLit(_)
3489                | TokenKind::StringLit(_)
3490                | TokenKind::CharLit(_)
3491                | TokenKind::LParen
3492                | TokenKind::LBracket
3493        )
3494    }
3495}
3496
3497fn render_token_slice(tokens: &[Token]) -> String {
3498    let mut s = String::new();
3499    let mut prev_no_space_after = true;
3500    for t in tokens {
3501        let (text, no_space_before, no_space_after): (String, bool, bool) = match &t.kind {
3502            TokenKind::LowerId { qualifier, name } | TokenKind::UpperId { qualifier, name } => (
3503                qualifier
3504                    .as_ref()
3505                    .map_or_else(|| name.to_string(), |q| format!("{q}.{name}")),
3506                false,
3507                false,
3508            ),
3509            TokenKind::Op(o) => (o.to_string(), false, false),
3510            TokenKind::IntLit(n) | TokenKind::DecimalLit(n) => (n.clone(), false, false),
3511            TokenKind::StringLit(v) => (format!("{v:?}"), false, false),
3512            TokenKind::CharLit(v) => (format!("'{v}'"), false, false),
3513            TokenKind::LParen => ("(".to_string(), false, true),
3514            TokenKind::RParen => (")".to_string(), true, false),
3515            TokenKind::LBracket => ("[".to_string(), false, true),
3516            TokenKind::RBracket => ("]".to_string(), true, false),
3517            TokenKind::LBrace => ("{".to_string(), false, true),
3518            TokenKind::RBrace => ("}".to_string(), true, false),
3519            TokenKind::Comma => (",".to_string(), true, false),
3520            TokenKind::Semi | TokenKind::VSemi => (";".to_string(), true, false),
3521            TokenKind::Backtick => ("`".to_string(), false, false),
3522            TokenKind::VLBrace | TokenKind::VRBrace => continue,
3523        };
3524        if !s.is_empty() && !no_space_before && !prev_no_space_after {
3525            s.push(' ');
3526        }
3527        s.push_str(&text);
3528        prev_no_space_after = no_space_after;
3529    }
3530    s
3531}
3532
3533// Unit tests for `parse_type_from_tokens` (private type-grammar phase) stay here;
3534// full-module type wiring and other observable parse behavior live in integration tests.
3535#[cfg(test)]
3536mod type_tests {
3537    use super::*;
3538    use crate::lexer::lex;
3539
3540    /// Parse a bare type string straight through the lexer. A single-line type
3541    /// has no layout-significant newlines, so no virtual tokens appear — this
3542    /// exercises the private type-grammar phase in isolation.
3543    fn ty(s: &str) -> Option<Type> {
3544        let (toks, errs) = lex(s).into_parts();
3545        assert!(errs.is_empty(), "lex errors for {s:?}: {errs:?}");
3546        parse_type_from_tokens(&toks)
3547    }
3548
3549    fn con(name: &str) -> Type {
3550        Type::Con {
3551            qualifier: None,
3552            name: name.into(),
3553            span: Span::default(),
3554        }
3555    }
3556
3557    fn qualified_con(qualifier: &str, name: &str) -> Type {
3558        Type::Con {
3559            qualifier: Some(qualifier.into()),
3560            name: name.into(),
3561            span: Span::default(),
3562        }
3563    }
3564
3565    fn app(head: Type, args: Vec<Type>) -> Type {
3566        Type::App(Box::new(head), args, Span::default())
3567    }
3568
3569    fn list(inner: Type) -> Type {
3570        Type::List(Box::new(inner), Span::default())
3571    }
3572
3573    fn tuple(items: Vec<Type>) -> Type {
3574        Type::Tuple(items, Span::default())
3575    }
3576
3577    fn fun(param: Type, result: Type) -> Type {
3578        Type::Fun(Box::new(param), Box::new(result), Span::default())
3579    }
3580
3581    fn var(name: &str) -> Type {
3582        Type::Var(name.into(), Span::default())
3583    }
3584
3585    fn unit() -> Type {
3586        Type::Unit(Span::default())
3587    }
3588
3589    fn constrained(body: Type) -> Type {
3590        Type::Constrained(Box::new(body), Span::default())
3591    }
3592
3593    fn text_lit(value: &str) -> Type {
3594        Type::Lit {
3595            kind: LitKind::Text,
3596            text: value.to_string(),
3597            span: Span::default(),
3598        }
3599    }
3600
3601    fn char_lit(value: &str) -> Type {
3602        Type::Lit {
3603            kind: LitKind::Char,
3604            text: value.to_string(),
3605            span: Span::default(),
3606        }
3607    }
3608
3609    #[test]
3610    fn atoms() {
3611        assert_eq!(ty("Party"), Some(con("Party")));
3612        assert_eq!(ty("Decimal"), Some(con("Decimal")));
3613        assert_eq!(ty("a"), Some(var("a")));
3614        assert_eq!(ty("()"), Some(unit()));
3615    }
3616
3617    #[test]
3618    fn application_vs_constructor() {
3619        // The whole point of the new model: `ContractId Foo` is an APPLICATION,
3620        // not one opaque name.
3621        assert_eq!(
3622            ty("ContractId Foo"),
3623            Some(app(con("ContractId"), vec![con("Foo")]))
3624        );
3625        assert_eq!(
3626            ty("Optional (ContractId Foo)"),
3627            Some(app(
3628                con("Optional"),
3629                vec![app(con("ContractId"), vec![con("Foo")])]
3630            ))
3631        );
3632        assert_eq!(
3633            ty("Map Text Int"),
3634            Some(app(con("Map"), vec![con("Text"), con("Int")]))
3635        );
3636    }
3637
3638    #[test]
3639    fn qualified_constructor_keeps_qualifier() {
3640        assert_eq!(
3641            ty("DA.Map.Map Text Int"),
3642            Some(app(
3643                qualified_con("DA.Map", "Map"),
3644                vec![con("Text"), con("Int")]
3645            ))
3646        );
3647    }
3648
3649    #[test]
3650    fn list_and_tuple() {
3651        assert_eq!(ty("[Text]"), Some(list(con("Text"))));
3652        assert_eq!(
3653            ty("(Int, Text)"),
3654            Some(tuple(vec![con("Int"), con("Text")]))
3655        );
3656        // A tuple is NOT a grouping paren — must stay a Tuple, never collapse.
3657        assert_eq!(
3658            ty("(a, b, c)"),
3659            Some(tuple(vec![var("a"), var("b"), var("c")]))
3660        );
3661        // Single grouping paren unwraps.
3662        assert_eq!(ty("(Text)"), Some(con("Text")));
3663    }
3664
3665    #[test]
3666    fn function_types_are_arrows_not_names() {
3667        // These are exactly the corpus strings the old matcher swallowed into
3668        // one opaque `Named`.
3669        assert_eq!(ty("Int -> Int"), Some(fun(con("Int"), con("Int"))));
3670        // Right associativity: `a -> b -> c` == `a -> (b -> c)`.
3671        assert_eq!(
3672            ty("Int -> Text -> Bool"),
3673            Some(fun(con("Int"), fun(con("Text"), con("Bool"))))
3674        );
3675        assert_eq!(
3676            ty("Party -> Script ()"),
3677            Some(fun(con("Party"), app(con("Script"), vec![unit()])))
3678        );
3679    }
3680
3681    #[test]
3682    fn script_application() {
3683        // `Script ()` ×147 in the corpus — an application flattened to `Named`
3684        // before. Now a real App.
3685        assert_eq!(ty("Script ()"), Some(app(con("Script"), vec![unit()])));
3686    }
3687
3688    #[test]
3689    fn numeric_nat_literal_is_dropped() {
3690        // `Numeric 10`: the `10` is a type-level nat, not a type, so it drops
3691        // and the head Con stands alone. `Numeric n` keeps the type variable.
3692        assert_eq!(ty("Numeric 10"), Some(con("Numeric")));
3693        assert_eq!(ty("Numeric n"), Some(app(con("Numeric"), vec![var("n")])));
3694    }
3695
3696    #[test]
3697    fn string_and_char_type_literals_are_structured() {
3698        // `HasField "observers"` — the field name is a type-level string
3699        // literal, not opaque syntax that collapses to malformed diagnostics.
3700        assert_eq!(
3701            ty(r#"HasField "observers""#),
3702            Some(app(con("HasField"), vec![text_lit("observers")]))
3703        );
3704        assert_eq!(
3705            ty(r#"HasField "observers" t PartiesMap"#),
3706            Some(app(
3707                con("HasField"),
3708                vec![text_lit("observers"), var("t"), con("PartiesMap")]
3709            ))
3710        );
3711        assert_eq!(
3712            ty(r"HasField 'x'"),
3713            Some(app(con("HasField"), vec![char_lit("x")]))
3714        );
3715    }
3716
3717    #[test]
3718    fn constraint_context_is_dropped_body_kept() {
3719        // `NumericScale n => Numeric 37 -> Numeric n` — a constrained function.
3720        // Context dropped; body (the arrow) kept.
3721        assert_eq!(
3722            ty("NumericScale n => Numeric 37 -> Numeric n"),
3723            Some(constrained(fun(
3724                con("Numeric"),
3725                app(con("Numeric"), vec![var("n")])
3726            )))
3727        );
3728        // Tuple context `(Eq a, Show a) => a` also drops cleanly.
3729        assert_eq!(ty("(Eq a, Show a) => a"), Some(constrained(var("a"))));
3730    }
3731
3732    #[test]
3733    fn unparseable_is_none() {
3734        // A trailing arrow with no body is not a clean type → unknown (None),
3735        // never a half-parse.
3736        assert_eq!(ty("Int ->"), None);
3737        assert_eq!(ty("-> Int"), None);
3738    }
3739}