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