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) | Some(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) | Some(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) | Some(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) | Some(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) | Some(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) | Some(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) | Some(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")
817            | Some("nonconsuming")
818            | Some("preconsuming")
819            | Some("postconsuming") => self.choice_decl().map_or_else(
820                || {
821                    self.skip_to_item_end();
822                    TemplateBodyDecl::Other {
823                        raw: self.slice_text(start),
824                        span: self.node_span(start),
825                        pos,
826                    }
827                },
828                TemplateBodyDecl::Choice,
829            ),
830            Some("interface") => self.interface_instance_decl().map_or_else(
831                || {
832                    self.skip_to_item_end();
833                    TemplateBodyDecl::Other {
834                        raw: self.slice_text(start),
835                        span: self.node_span(start),
836                        pos,
837                    }
838                },
839                TemplateBodyDecl::InterfaceInstance,
840            ),
841            Some("controller") => {
842                // Legacy Daml 1.x `controller <party> can` choice blocks are
843                // not analyzed — fail loud instead of silently dropping the
844                // choices inside.
845                self.diag_cat(
846                    DiagnosticCategory::UnsupportedSyntax,
847                    "legacy 'controller ... can' syntax is not supported; \
848                     choices inside this block are not analyzed",
849                );
850                self.skip_to_item_end();
851                TemplateBodyDecl::Other {
852                    raw: self.slice_text(start),
853                    span: self.node_span(start),
854                    pos,
855                }
856            }
857            _ => {
858                self.skip_to_item_end();
859                TemplateBodyDecl::Other {
860                    raw: self.slice_text(start),
861                    span: self.node_span(start),
862                    pos,
863                }
864            }
865        }
866    }
867
868    fn choice_decl(&mut self) -> Option<ChoiceDecl> {
869        let pos = self.pos();
870        let start_i = self.i;
871        let consuming = match self.peek().and_then(|t| t.keyword()) {
872            Some("nonconsuming") => {
873                self.bump();
874                Consuming::NonConsuming
875            }
876            Some("preconsuming") => {
877                self.bump();
878                Consuming::PreConsuming
879            }
880            Some("postconsuming") => {
881                self.bump();
882                Consuming::PostConsuming
883            }
884            _ => Consuming::Consuming,
885        };
886        if !self.eat_keyword("choice") {
887            return None;
888        }
889        let name = self.upper_name()?;
890        let return_ty = if self.eat_op(":") {
891            let ty_start = self.i;
892            self.skip_type_tokens();
893            parse_type_from_tokens(&self.toks[ty_start..self.i])
894        } else {
895            None
896        };
897        let mut params = Vec::new();
898        let mut dangling = false;
899        if self.eat_keyword("with") {
900            (params, dangling) = self.field_block();
901        }
902        let mut observers = Vec::new();
903        let mut controllers = Vec::new();
904        loop {
905            // Inside a dangling (empty) with-block the controller/observer/
906            // do clauses sit at the block's column, so layout separates
907            // them with virtual semicolons — consume those.
908            if dangling {
909                while self.eat(&Tok::VSemi) {}
910            }
911            if self.eat_keyword("observer") {
912                observers = self.expr_comma_list_no_do();
913            } else if self.eat_keyword("controller") {
914                controllers = self.expr_comma_list_no_do();
915            } else {
916                break;
917            }
918        }
919        if dangling {
920            while self.eat(&Tok::VSemi) {}
921        }
922        let body = if self
923            .peek()
924            .is_some_and(|t| !matches!(t, Tok::VSemi | Tok::VRBrace | Tok::Semi | Tok::RBrace))
925        {
926            Some(self.expr())
927        } else {
928            None
929        };
930        self.skip_to_item_end();
931        if dangling {
932            // Discard the abandoned with-block's closing brace so it does
933            // not terminate the enclosing template/interface body.
934            self.eat(&Tok::VRBrace);
935            self.skip_to_item_end();
936        }
937        Some(ChoiceDecl {
938            name,
939            consuming,
940            return_ty,
941            params,
942            controllers,
943            observers,
944            body,
945            pos,
946            span: self.node_span(start_i),
947        })
948    }
949
950    /// Consume type tokens up to (not including) a layout boundary or a
951    /// `with`/`controller`/`observer`/`do`/`where` keyword at bracket depth 0.
952    fn skip_type_tokens(&mut self) {
953        let mut brackets = 0usize;
954        while let Some(t) = self.peek() {
955            match t {
956                Tok::VSemi | Tok::VRBrace | Tok::VLBrace | Tok::Semi => return,
957                Tok::LParen | Tok::LBracket => brackets += 1,
958                Tok::RParen | Tok::RBracket => {
959                    if brackets == 0 {
960                        return;
961                    }
962                    brackets -= 1;
963                }
964                _ if brackets == 0
965                    && matches!(
966                        t.keyword(),
967                        Some("with" | "controller" | "observer" | "do" | "where")
968                    ) =>
969                {
970                    return
971                }
972                _ => {}
973            }
974            self.i += 1;
975        }
976    }
977
978    // ----- interfaces ----------------------------------------------------
979
980    fn interface_decl(&mut self) -> Option<InterfaceDecl> {
981        let pos = self.pos();
982        let start_i = self.i;
983        self.bump(); // interface
984        if self.at_keyword("instance") {
985            // Top-level retroactive interface instance: skip gracefully.
986            return None;
987        }
988        let name = self.upper_name()?;
989        let mut requires = Vec::new();
990        if self.eat_keyword("requires") {
991            while let Some(r) = self.upper_name() {
992                requires.push(r);
993                if !self.eat(&Tok::Comma) {
994                    break;
995                }
996            }
997        }
998        if !self.eat_keyword("where") {
999            return None;
1000        }
1001        let mut viewtype = None;
1002        let mut methods = Vec::new();
1003        let mut choices = Vec::new();
1004        if !(self.eat(&Tok::VLBrace) || self.eat(&Tok::LBrace)) {
1005            return Some(InterfaceDecl {
1006                name,
1007                requires,
1008                viewtype,
1009                methods,
1010                choices,
1011                pos,
1012                span: self.node_span(start_i),
1013            });
1014        }
1015        loop {
1016            while self.eat(&Tok::VSemi) || self.eat(&Tok::Semi) {}
1017            match self.peek() {
1018                None => break,
1019                Some(Tok::VRBrace) | Some(Tok::RBrace) => {
1020                    self.bump();
1021                    break;
1022                }
1023                // A stray closing bracket inside a block is garbage from a
1024                // failed item parse — discard it or the loop cannot make
1025                // progress (skip_to_item_end deliberately stops before
1026                // unmatched closers).
1027                Some(Tok::RParen) | Some(Tok::RBracket) => {
1028                    self.bump();
1029                    continue;
1030                }
1031                _ => {}
1032            }
1033            match self.peek().and_then(|t| t.keyword()) {
1034                Some("viewtype") => {
1035                    self.bump();
1036                    viewtype = self.upper_name();
1037                    self.skip_to_item_end();
1038                }
1039                Some("choice")
1040                | Some("nonconsuming")
1041                | Some("preconsuming")
1042                | Some("postconsuming") => {
1043                    if let Some(c) = self.choice_decl() {
1044                        choices.push(c);
1045                    } else {
1046                        self.skip_to_item_end();
1047                    }
1048                }
1049                _ => {
1050                    // Method signature `name : Type`; anything else (default
1051                    // implementations, ensure, ...) is skipped.
1052                    let mpos = self.pos();
1053                    if let Some(Tok::LowerId {
1054                        qualifier: None,
1055                        name: mname,
1056                    }) = self.peek().cloned()
1057                    {
1058                        if self.peek_at(1).is_some_and(|t| t.is_op(":")) {
1059                            let mstart = self.toks[self.i].start;
1060                            self.bump();
1061                            self.bump();
1062                            let ty_start = self.i;
1063                            self.skip_to_item_end();
1064                            // Single name: span the whole `name : Type`.
1065                            methods.push(FieldDecl {
1066                                name: mname,
1067                                ty: parse_type_from_tokens(&self.toks[ty_start..self.i]),
1068                                pos: mpos,
1069                                span: Span::new(mstart, self.end_byte().max(mstart)),
1070                            });
1071                            continue;
1072                        }
1073                    }
1074                    self.skip_to_item_end();
1075                }
1076            }
1077        }
1078        Some(InterfaceDecl {
1079            name,
1080            requires,
1081            viewtype,
1082            methods,
1083            choices,
1084            pos,
1085            span: self.node_span(start_i),
1086        })
1087    }
1088
1089    /// `interface instance I for T where { method-bindings }`
1090    fn interface_instance_decl(&mut self) -> Option<InterfaceInstanceDecl> {
1091        let pos = self.pos();
1092        let start_i = self.i;
1093        self.bump(); // interface
1094        if !self.eat_keyword("instance") {
1095            return None;
1096        }
1097        let interface_name = self.upper_name()?;
1098        let for_template = if self.eat_keyword("for") {
1099            self.upper_name().unwrap_or_default()
1100        } else {
1101            String::new()
1102        };
1103        let mut methods = Vec::new();
1104        if self.eat_keyword("where") && (self.eat(&Tok::VLBrace) || self.eat(&Tok::LBrace)) {
1105            loop {
1106                while self.eat(&Tok::VSemi) || self.eat(&Tok::Semi) {}
1107                match self.peek() {
1108                    None => break,
1109                    Some(Tok::VRBrace) | Some(Tok::RBrace) => {
1110                        self.bump();
1111                        break;
1112                    }
1113                    // Stray closer: discard so the loop always progresses.
1114                    Some(Tok::RParen) | Some(Tok::RBracket) => {
1115                        self.bump();
1116                        continue;
1117                    }
1118                    _ => {}
1119                }
1120                if let Some(b) = self.binding() {
1121                    methods.push(b);
1122                } else {
1123                    self.skip_to_item_end();
1124                }
1125            }
1126        }
1127        Some(InterfaceInstanceDecl {
1128            interface_name,
1129            for_template,
1130            methods,
1131            pos,
1132            span: self.node_span(start_i),
1133        })
1134    }
1135
1136    // ----- functions -----------------------------------------------------
1137
1138    /// A top-level item starting with a lowercase identifier: type
1139    /// signature or function equation. Operator definitions and other
1140    /// exotica return None.
1141    fn function_item(&mut self) -> Option<Decl> {
1142        let pos = self.pos();
1143        let start_i = self.i;
1144        let name = match self.peek().cloned() {
1145            Some(Tok::LowerId {
1146                qualifier: None,
1147                name,
1148            }) => name,
1149            _ => return None,
1150        };
1151
1152        // Type signature: `name [, name2] : Type`
1153        let mut j = self.i + 1;
1154        let mut is_sig = false;
1155        loop {
1156            match self.toks.get(j).map(|t| &t.tok) {
1157                Some(Tok::Comma) => {
1158                    j += 1;
1159                    if matches!(
1160                        self.toks.get(j).map(|t| &t.tok),
1161                        Some(Tok::LowerId {
1162                            qualifier: None,
1163                            ..
1164                        })
1165                    ) {
1166                        j += 1;
1167                        continue;
1168                    }
1169                    break;
1170                }
1171                Some(Tok::Op(o)) if o == ":" => {
1172                    is_sig = true;
1173                    break;
1174                }
1175                _ => break,
1176            }
1177        }
1178        if is_sig {
1179            self.bump(); // name
1180            while self.eat(&Tok::Comma) {
1181                self.bump(); // more names
1182            }
1183            self.eat_op(":");
1184            let ty_start = self.i;
1185            self.skip_to_item_end();
1186            let ty = parse_type_from_tokens(&self.toks[ty_start..self.i]);
1187            return Some(Decl::Function(FunctionDecl {
1188                name,
1189                ty,
1190                equations: Vec::new(),
1191                pos,
1192                sig_span: Some(self.node_span(start_i)),
1193                span: self.node_span(start_i),
1194            }));
1195        }
1196
1197        // Function equation: name pats (= expr | guards), optional where.
1198        self.bump(); // name
1199        let mut params = Vec::new();
1200        while !self.at_op("=") && !self.at_op("|") {
1201            // Combined signature + body: `name (x : a) : RetType = expr` —
1202            // consume the return-type annotation up to the `=`.
1203            if self.at_op(":") {
1204                self.bump();
1205                let mut brackets = 0usize;
1206                while let Some(t) = self.peek() {
1207                    match t {
1208                        Tok::Op(o) if o == "=" && brackets == 0 => break,
1209                        Tok::VSemi | Tok::VRBrace | Tok::Semi | Tok::RBrace => break,
1210                        Tok::LParen | Tok::LBracket => brackets += 1,
1211                        Tok::RParen | Tok::RBracket => brackets = brackets.saturating_sub(1),
1212                        _ => {}
1213                    }
1214                    self.i += 1;
1215                }
1216                continue;
1217            }
1218            // Infix operator definition: `f $ x = f x`, `as <&> f = ...` —
1219            // operators have no IR surface; skip the item silently.
1220            if matches!(self.peek(), Some(Tok::Op(o)) if !is_reserved_op(o)) {
1221                self.skip_to_item_end();
1222                return None;
1223            }
1224            match self.peek() {
1225                None | Some(Tok::VSemi) | Some(Tok::VRBrace) | Some(Tok::Semi)
1226                | Some(Tok::RBrace) => {
1227                    self.diag(format!("could not parse equation for '{}'", name));
1228                    return None;
1229                }
1230                _ => {}
1231            }
1232            match self.pattern_atom() {
1233                Some(p) => params.push(p),
1234                None => {
1235                    self.diag(format!("bad parameter pattern in '{}'", name));
1236                    return None;
1237                }
1238            }
1239        }
1240        let (body, guards) = self.equation_rhs()?;
1241        let where_bindings = if self.eat_keyword("where") {
1242            self.binding_block()
1243        } else {
1244            Vec::new()
1245        };
1246        self.skip_to_item_end();
1247        Some(Decl::Function(FunctionDecl {
1248            name,
1249            ty: None,
1250            equations: vec![Equation {
1251                params,
1252                body,
1253                guards,
1254                where_bindings,
1255                pos,
1256                span: self.node_span(start_i),
1257            }],
1258            pos,
1259            sig_span: None,
1260            span: self.node_span(start_i),
1261        }))
1262    }
1263
1264    /// `= expr` or `| guard = expr | guard = expr ...`
1265    fn equation_rhs(&mut self) -> Option<(Expr, Vec<(Expr, Expr)>)> {
1266        if self.eat_op("=") {
1267            return Some((self.expr(), Vec::new()));
1268        }
1269        let mut guards = Vec::new();
1270        while self.eat_op("|") {
1271            // Comma-separated guard qualifiers, each a boolean expression
1272            // or a pattern guard `pat <- expr`.
1273            let g = loop {
1274                let g = self.expr();
1275                if self.eat_op("<-") {
1276                    let _ = self.expr(); // pattern guard: keep the pattern side
1277                }
1278                if !self.eat(&Tok::Comma) {
1279                    break g;
1280                }
1281            };
1282            if !self.eat_op("=") {
1283                self.diag("expected '=' after guard");
1284                return None;
1285            }
1286            let e = self.expr();
1287            guards.push((g, e));
1288        }
1289        let first = guards.first()?.1.clone();
1290        Some((first, guards))
1291    }
1292
1293    /// `{ binding ; binding ; ... }` for let/where blocks.
1294    fn binding_block(&mut self) -> Vec<Binding> {
1295        let mut bindings = Vec::new();
1296        if !(self.eat(&Tok::VLBrace) || self.eat(&Tok::LBrace)) {
1297            return bindings;
1298        }
1299        loop {
1300            while self.eat(&Tok::VSemi) || self.eat(&Tok::Semi) {}
1301            match self.peek() {
1302                None => break,
1303                Some(Tok::VRBrace) | Some(Tok::RBrace) => {
1304                    self.bump();
1305                    break;
1306                }
1307                // A stray closing bracket inside a block is garbage from a
1308                // failed item parse — discard it or the loop cannot make
1309                // progress (skip_to_item_end deliberately stops before
1310                // unmatched closers).
1311                Some(Tok::RParen) | Some(Tok::RBracket) => {
1312                    self.bump();
1313                    continue;
1314                }
1315                _ => {}
1316            }
1317            match self.binding() {
1318                Some(b) => bindings.push(b),
1319                None => self.skip_to_item_end(),
1320            }
1321        }
1322        bindings
1323    }
1324
1325    /// One binding: `pat = expr`, `f x y = expr`, guarded variants, or a
1326    /// type signature (skipped, returns None).
1327    fn binding(&mut self) -> Option<Binding> {
1328        let pos = self.pos();
1329        let start_i = self.i;
1330        // Operator binding or signature: `(==) : Text -> Bool = ...` —
1331        // skip the whole item; operators aren't surfaced in the IR.
1332        if self.at(&Tok::LParen)
1333            && matches!(self.peek_at(1), Some(Tok::Op(_)))
1334            && self.peek_at(2) == Some(&Tok::RParen)
1335        {
1336            self.skip_to_item_end();
1337            return None;
1338        }
1339        let pat = self.pattern_atom()?;
1340        let mut params = Vec::new();
1341        loop {
1342            if self.at_op("=") {
1343                self.bump();
1344                let expr = self.expr();
1345                // Bindings can carry their own where blocks.
1346                if self.eat_keyword("where") {
1347                    let _ = self.binding_block();
1348                }
1349                return Some(Binding {
1350                    pat,
1351                    params,
1352                    expr,
1353                    pos,
1354                    span: self.node_span(start_i),
1355                });
1356            }
1357            if self.at_op("|") {
1358                let (body, _) = self.equation_rhs()?;
1359                if self.eat_keyword("where") {
1360                    let _ = self.binding_block();
1361                }
1362                return Some(Binding {
1363                    pat,
1364                    params,
1365                    expr: body,
1366                    pos,
1367                    span: self.node_span(start_i),
1368                });
1369            }
1370            if self.at_op(":") {
1371                if params.is_empty() {
1372                    // Type signature inside a let/where block — skip it.
1373                    self.skip_to_item_end();
1374                    return None;
1375                }
1376                // Combined signature + body: `f (x : a) : Ret = expr` —
1377                // consume the return-type annotation up to the `=`.
1378                self.bump();
1379                let mut brackets = 0usize;
1380                while let Some(t) = self.peek() {
1381                    match t {
1382                        Tok::Op(o) if o == "=" && brackets == 0 => break,
1383                        Tok::VSemi | Tok::VRBrace | Tok::Semi | Tok::RBrace => break,
1384                        Tok::LParen | Tok::LBracket => brackets += 1,
1385                        Tok::RParen | Tok::RBracket => brackets = brackets.saturating_sub(1),
1386                        _ => {}
1387                    }
1388                    self.i += 1;
1389                }
1390                continue;
1391            }
1392            // Infix operator binding with a pattern operand:
1393            // `None <?> s = ...` in a where/let block.
1394            if matches!(self.peek(), Some(Tok::Op(o)) if !is_reserved_op(o)) {
1395                self.skip_to_item_end();
1396                return None;
1397            }
1398            match self.peek() {
1399                None | Some(Tok::VSemi) | Some(Tok::VRBrace) | Some(Tok::Semi)
1400                | Some(Tok::RBrace) => return None,
1401                _ => {}
1402            }
1403            params.push(self.pattern_atom()?);
1404        }
1405    }
1406
1407    // ----- patterns ------------------------------------------------------
1408
1409    fn pattern_atom(&mut self) -> Option<Pat> {
1410        if self.depth >= MAX_DEPTH {
1411            return None;
1412        }
1413        self.depth += 1;
1414        let result = self.pattern_atom_inner();
1415        self.depth -= 1;
1416        result
1417    }
1418
1419    fn pattern_atom_inner(&mut self) -> Option<Pat> {
1420        let pos = self.pos();
1421        let start_i = self.i;
1422        // Lazy / strict pattern markers: `~(as, bs)`, `!x`.
1423        if self.at_op("~") || self.at_op("!") {
1424            self.bump();
1425            return self.pattern_atom();
1426        }
1427        match self.peek().cloned() {
1428            Some(Tok::LowerId {
1429                qualifier: None,
1430                name,
1431            }) => {
1432                self.bump();
1433                if name == "_" {
1434                    return Some(Pat::Wild {
1435                        pos,
1436                        span: self.node_span(start_i),
1437                    });
1438                }
1439                if self.at_op("@") {
1440                    self.bump();
1441                    let inner = self.pattern_atom()?;
1442                    return Some(Pat::As {
1443                        name,
1444                        pat: Box::new(inner),
1445                        pos,
1446                        span: self.node_span(start_i),
1447                    });
1448                }
1449                Some(Pat::Var {
1450                    name,
1451                    pos,
1452                    span: self.node_span(start_i),
1453                })
1454            }
1455            Some(Tok::Op(o)) if o == "_" => {
1456                self.bump();
1457                Some(Pat::Wild {
1458                    pos,
1459                    span: self.node_span(start_i),
1460                })
1461            }
1462            Some(Tok::UpperId { qualifier, name }) => {
1463                self.bump();
1464                // Record pattern `Foo {..}` / `Foo {x = y}` /
1465                // `Foo with claim; tag`.
1466                if self.at(&Tok::LBrace) {
1467                    self.skip_balanced_braces();
1468                } else if self.eat_keyword("with") {
1469                    let _ = self.record_fields();
1470                }
1471                Some(Pat::Con {
1472                    qualifier,
1473                    name,
1474                    args: Vec::new(),
1475                    pos,
1476                    span: self.node_span(start_i),
1477                })
1478            }
1479            Some(Tok::IntLit(text)) => {
1480                self.bump();
1481                Some(Pat::Lit {
1482                    kind: LitKind::Int,
1483                    text,
1484                    pos,
1485                    span: self.node_span(start_i),
1486                })
1487            }
1488            Some(Tok::DecimalLit(text)) => {
1489                self.bump();
1490                Some(Pat::Lit {
1491                    kind: LitKind::Decimal,
1492                    text,
1493                    pos,
1494                    span: self.node_span(start_i),
1495                })
1496            }
1497            Some(Tok::StringLit(text)) => {
1498                self.bump();
1499                Some(Pat::Lit {
1500                    kind: LitKind::Text,
1501                    text,
1502                    pos,
1503                    span: self.node_span(start_i),
1504                })
1505            }
1506            Some(Tok::CharLit(text)) => {
1507                self.bump();
1508                Some(Pat::Lit {
1509                    kind: LitKind::Char,
1510                    text,
1511                    pos,
1512                    span: self.node_span(start_i),
1513                })
1514            }
1515            Some(Tok::LParen) => {
1516                self.bump();
1517                if self.eat(&Tok::RParen) {
1518                    return Some(Pat::Con {
1519                        qualifier: None,
1520                        name: "()".to_string(),
1521                        args: Vec::new(),
1522                        pos,
1523                        span: self.node_span(start_i),
1524                    });
1525                }
1526                // View pattern `(expr -> pat)`: scan for a top-level `->`
1527                // inside these parens; the expression side is discarded and
1528                // the pattern after the arrow is the binding. A top-level
1529                // `:` before the arrow means the arrow belongs to a type
1530                // annotation (`(f : Int -> Bool)`), not a view pattern.
1531                {
1532                    let mut depth = 0usize;
1533                    let mut j = self.i;
1534                    let mut arrow = None;
1535                    while let Some(t) = self.toks.get(j).map(|t| &t.tok) {
1536                        match t {
1537                            Tok::LParen | Tok::LBracket => depth += 1,
1538                            Tok::RParen | Tok::RBracket => {
1539                                if depth == 0 {
1540                                    break;
1541                                }
1542                                depth -= 1;
1543                            }
1544                            Tok::Op(o) if o == ":" && depth == 0 => break,
1545                            Tok::Op(o) if o == "->" && depth == 0 => {
1546                                arrow = Some(j);
1547                                break;
1548                            }
1549                            Tok::VSemi | Tok::VRBrace => break,
1550                            // A lambda's arrow belongs to the lambda.
1551                            Tok::Op(o) if o == "\\" => break,
1552                            _ => {}
1553                        }
1554                        j += 1;
1555                    }
1556                    if let Some(j) = arrow {
1557                        self.i = j + 1; // skip the view expression and `->`
1558                        let inner = self.pattern()?;
1559                        self.eat(&Tok::RParen);
1560                        return Some(inner);
1561                    }
1562                }
1563                let first = self.pattern()?;
1564                // Type-annotated pattern `(e : AnyException)`: skip the type.
1565                if self.at_op(":") {
1566                    let mut depth = 0usize;
1567                    while let Some(t) = self.peek() {
1568                        match t {
1569                            Tok::LParen | Tok::LBracket => depth += 1,
1570                            Tok::RParen if depth == 0 => break,
1571                            Tok::RParen | Tok::RBracket => depth -= 1,
1572                            Tok::VSemi | Tok::VRBrace => break,
1573                            _ => {}
1574                        }
1575                        self.i += 1;
1576                    }
1577                }
1578                if self.at(&Tok::Comma) {
1579                    let mut items = vec![first];
1580                    while self.eat(&Tok::Comma) {
1581                        items.push(self.pattern()?);
1582                    }
1583                    self.eat(&Tok::RParen);
1584                    return Some(Pat::Tuple {
1585                        items,
1586                        pos,
1587                        span: self.node_span(start_i),
1588                    });
1589                }
1590                self.eat(&Tok::RParen);
1591                Some(first)
1592            }
1593            Some(Tok::LBracket) => {
1594                self.bump();
1595                let mut items = Vec::new();
1596                if !self.eat(&Tok::RBracket) {
1597                    loop {
1598                        items.push(self.pattern()?);
1599                        if !self.eat(&Tok::Comma) {
1600                            break;
1601                        }
1602                    }
1603                    self.eat(&Tok::RBracket);
1604                }
1605                Some(Pat::List {
1606                    items,
1607                    pos,
1608                    span: self.node_span(start_i),
1609                })
1610            }
1611            _ => None,
1612        }
1613    }
1614
1615    /// Full pattern: constructor applications and infix cons `x :: xs`.
1616    fn pattern(&mut self) -> Option<Pat> {
1617        if self.depth >= MAX_DEPTH {
1618            return None;
1619        }
1620        self.depth += 1;
1621        let result = self.pattern_inner();
1622        self.depth -= 1;
1623        result
1624    }
1625
1626    fn pattern_inner(&mut self) -> Option<Pat> {
1627        let pos = self.pos();
1628        let start_i = self.i;
1629        let first = match self.peek().cloned() {
1630            Some(Tok::UpperId { qualifier, name }) => {
1631                self.bump();
1632                if self.at(&Tok::LBrace) || self.at_keyword("with") {
1633                    if self.eat_keyword("with") {
1634                        let _ = self.record_fields();
1635                    } else {
1636                        self.skip_balanced_braces();
1637                    }
1638                    Pat::Con {
1639                        qualifier,
1640                        name,
1641                        args: Vec::new(),
1642                        pos,
1643                        span: self.node_span(start_i),
1644                    }
1645                } else {
1646                    let mut args = Vec::new();
1647                    while let Some(a) = self.try_pattern_atom() {
1648                        args.push(a);
1649                    }
1650                    Pat::Con {
1651                        qualifier,
1652                        name,
1653                        args,
1654                        pos,
1655                        span: self.node_span(start_i),
1656                    }
1657                }
1658            }
1659            _ => self.pattern_atom()?,
1660        };
1661        if self.at_op("::") {
1662            self.bump();
1663            let rest = self.pattern()?;
1664            return Some(Pat::Con {
1665                qualifier: None,
1666                name: "::".to_string(),
1667                args: vec![first, rest],
1668                pos,
1669                span: self.node_span(start_i),
1670            });
1671        }
1672        Some(first)
1673    }
1674
1675    fn try_pattern_atom(&mut self) -> Option<Pat> {
1676        match self.peek() {
1677            Some(Tok::LowerId {
1678                qualifier: None, ..
1679            })
1680            | Some(Tok::UpperId { .. })
1681            | Some(Tok::IntLit(_))
1682            | Some(Tok::DecimalLit(_))
1683            | Some(Tok::StringLit(_))
1684            | Some(Tok::CharLit(_))
1685            | Some(Tok::LParen)
1686            | Some(Tok::LBracket) => self.pattern_atom(),
1687            _ => None,
1688        }
1689    }
1690
1691    fn skip_balanced_braces(&mut self) {
1692        let mut depth = 0usize;
1693        while let Some(t) = self.peek() {
1694            match t {
1695                Tok::LBrace => depth += 1,
1696                Tok::RBrace => {
1697                    depth -= 1;
1698                    if depth == 0 {
1699                        self.i += 1;
1700                        return;
1701                    }
1702                }
1703                _ => {}
1704            }
1705            self.i += 1;
1706        }
1707    }
1708
1709    // ----- expressions ---------------------------------------------------
1710
1711    fn expr(&mut self) -> Expr {
1712        self.expr_prec(0, true)
1713    }
1714
1715    fn expr_no_do(&mut self) -> Expr {
1716        self.expr_prec(0, false)
1717    }
1718
1719    /// Comma-separated expressions (signatory/observer/controller lists).
1720    fn expr_comma_list(&mut self) -> Vec<Expr> {
1721        let mut out = vec![self.expr()];
1722        while self.eat(&Tok::Comma) {
1723            out.push(self.expr());
1724        }
1725        out
1726    }
1727
1728    fn expr_comma_list_no_do(&mut self) -> Vec<Expr> {
1729        let mut out = vec![self.expr_no_do()];
1730        while self.eat(&Tok::Comma) {
1731            out.push(self.expr_no_do());
1732        }
1733        out
1734    }
1735
1736    fn expr_prec(&mut self, min_prec: u8, allow_do: bool) -> Expr {
1737        let pos = self.pos();
1738        let start_i = self.i;
1739        if self.depth >= MAX_DEPTH {
1740            // Hostile nesting: degrade to raw text instead of recursing, and
1741            // report it so the degraded region is not silently mistaken for
1742            // unsupported syntax. `skip_to_item_end` below consumes the rest of
1743            // the item, so this trips about once per affected declaration.
1744            self.diag_cat(
1745                DiagnosticCategory::RecursionLimit,
1746                "expression nesting too deep; truncated to raw text",
1747            );
1748            let start = self.i;
1749            self.skip_to_item_end();
1750            if self.i == start {
1751                self.bump();
1752            }
1753            return Expr::Error {
1754                raw: self.slice_text(start),
1755                span: self.node_span(start),
1756                pos,
1757            };
1758        }
1759        self.depth += 1;
1760        let result = self.expr_prec_inner(min_prec, allow_do, pos, start_i);
1761        self.depth -= 1;
1762        result
1763    }
1764
1765    fn expr_prec_inner(&mut self, min_prec: u8, allow_do: bool, pos: Pos, start_i: usize) -> Expr {
1766        let mut lhs = match self.unary(allow_do) {
1767            Some(e) => e,
1768            None => {
1769                // Unparseable here: degrade to raw text up to the item end.
1770                let start = self.i;
1771                self.skip_to_item_end();
1772                if self.i == start {
1773                    self.bump();
1774                }
1775                return Expr::Error {
1776                    raw: self.slice_text(start),
1777                    span: self.node_span(start),
1778                    pos,
1779                };
1780            }
1781        };
1782        loop {
1783            let (op, prec, right_assoc) = match self.peek() {
1784                Some(Tok::Op(o)) => {
1785                    let o = o.clone();
1786                    if is_reserved_op(&o) {
1787                        // `e : Type` annotation: consume the type, keep e.
1788                        if o == ":" {
1789                            self.bump();
1790                            self.skip_type_tokens();
1791                            continue;
1792                        }
1793                        break;
1794                    }
1795                    let (p, r) = fixity(&o);
1796                    (o, p, r)
1797                }
1798                Some(Tok::Backtick) => {
1799                    // `e `div` e` — infix function application.
1800                    let name = match self.peek_at(1) {
1801                        Some(Tok::LowerId { qualifier, name })
1802                        | Some(Tok::UpperId { qualifier, name }) => qualifier
1803                            .as_ref()
1804                            .map_or_else(|| name.clone(), |q| format!("{}.{}", q, name)),
1805                        _ => break,
1806                    };
1807                    if self.peek_at(2) != Some(&Tok::Backtick) {
1808                        break;
1809                    }
1810                    (format!("`{}`", name), 9, false)
1811                }
1812                _ => break,
1813            };
1814            if prec < min_prec {
1815                break;
1816            }
1817            self.bump();
1818            if op.starts_with('`') {
1819                self.bump();
1820                self.bump();
1821            }
1822            let next_min = if right_assoc { prec } else { prec + 1 };
1823            let rhs = self.expr_prec(next_min, allow_do);
1824            lhs = Expr::BinOp {
1825                op,
1826                lhs: Box::new(lhs),
1827                rhs: Box::new(rhs),
1828                pos,
1829                span: self.node_span(start_i),
1830            };
1831        }
1832        lhs
1833    }
1834
1835    fn unary(&mut self, allow_do: bool) -> Option<Expr> {
1836        let pos = self.pos();
1837        let start_i = self.i;
1838        if self.at_op("-") {
1839            self.bump();
1840            let e = self.unary(allow_do)?;
1841            return Some(Expr::Neg {
1842                expr: Box::new(e),
1843                pos,
1844                span: self.node_span(start_i),
1845            });
1846        }
1847        self.application(allow_do)
1848    }
1849
1850    fn application(&mut self, allow_do: bool) -> Option<Expr> {
1851        let pos = self.pos();
1852        let start_i = self.i;
1853        let head0 = self.atom(allow_do)?;
1854        let mut head = self.projection_tail(head0);
1855        let mut args = Vec::new();
1856        loop {
1857            // Record syntax binds tighter than application:
1858            // `create Foo with x = 1` applies create to (Foo with {x = 1}).
1859            if self.at_keyword("with") {
1860                let target = args.pop().unwrap_or_else(|| {
1861                    std::mem::replace(
1862                        &mut head,
1863                        Expr::Error {
1864                            raw: String::new(),
1865                            pos,
1866                            span: Span::default(),
1867                        },
1868                    )
1869                });
1870                self.bump(); // with
1871                let fields = self.record_fields();
1872                let tpos = target.pos();
1873                let sp = Span::new(target.span().start, self.end_byte());
1874                let rec = Expr::Record {
1875                    base: Box::new(target),
1876                    fields,
1877                    pos: tpos,
1878                    span: sp,
1879                };
1880                if matches!(head, Expr::Error { ref raw, .. } if raw.is_empty()) {
1881                    head = rec;
1882                } else {
1883                    args.push(rec);
1884                }
1885                continue;
1886            }
1887            if !allow_do && self.at_keyword("do") {
1888                break;
1889            }
1890            // Type application `f @Type x` — consume and drop the type atom.
1891            if self.at_op("@") {
1892                self.bump();
1893                match self.peek() {
1894                    Some(Tok::UpperId { .. }) | Some(Tok::LowerId { .. }) => {
1895                        self.bump();
1896                    }
1897                    Some(Tok::LParen) => self.skip_balanced_parens(),
1898                    Some(Tok::LBracket) => {
1899                        let mut depth = 0usize;
1900                        while let Some(t) = self.peek() {
1901                            match t {
1902                                Tok::LBracket => depth += 1,
1903                                Tok::RBracket => {
1904                                    depth -= 1;
1905                                    if depth == 0 {
1906                                        self.i += 1;
1907                                        break;
1908                                    }
1909                                }
1910                                _ => {}
1911                            }
1912                            self.i += 1;
1913                        }
1914                    }
1915                    _ => {}
1916                }
1917                continue;
1918            }
1919            match self.try_atom(allow_do) {
1920                Some(a) => args.push(self.projection_tail(a)),
1921                None => break,
1922            }
1923        }
1924        if args.is_empty() {
1925            Some(head)
1926        } else {
1927            Some(Expr::App {
1928                func: Box::new(head),
1929                args,
1930                pos,
1931                span: self.node_span(start_i),
1932            })
1933        }
1934    }
1935
1936    /// `{ f = e ; g ; .. }` after `with` (virtual block) or explicit braces.
1937    fn record_fields(&mut self) -> Vec<FieldAssign> {
1938        let mut fields = Vec::new();
1939        let explicit = self.at(&Tok::LBrace);
1940        if !(self.eat(&Tok::VLBrace) || self.eat(&Tok::LBrace)) {
1941            return fields;
1942        }
1943        loop {
1944            while self.eat(&Tok::VSemi) || self.eat(&Tok::Semi) || self.eat(&Tok::Comma) {}
1945            match self.peek() {
1946                None => break,
1947                Some(Tok::VRBrace) if !explicit => {
1948                    self.bump();
1949                    break;
1950                }
1951                Some(Tok::RBrace) => {
1952                    self.bump();
1953                    break;
1954                }
1955                // Stray closer: discard so the loop always progresses.
1956                Some(Tok::RParen) | Some(Tok::RBracket) => {
1957                    self.bump();
1958                    continue;
1959                }
1960                _ => {}
1961            }
1962            let pos = self.pos();
1963            let start_i = self.i;
1964            if self.at_op("..") {
1965                self.bump();
1966                fields.push(FieldAssign {
1967                    name: "..".to_string(),
1968                    value: None,
1969                    pos,
1970                    span: self.node_span(start_i),
1971                });
1972                continue;
1973            }
1974            let name = match self.peek().cloned() {
1975                Some(Tok::LowerId {
1976                    qualifier: None,
1977                    name,
1978                }) => {
1979                    self.bump();
1980                    name
1981                }
1982                _ => {
1983                    self.skip_to_item_end();
1984                    continue;
1985                }
1986            };
1987            if self.eat_op("=") {
1988                let value = self.expr_prec(1, true);
1989                fields.push(FieldAssign {
1990                    name,
1991                    value: Some(value),
1992                    pos,
1993                    span: self.node_span(start_i),
1994                });
1995            } else {
1996                // Pun: `Foo with owner`.
1997                fields.push(FieldAssign {
1998                    name,
1999                    value: None,
2000                    pos,
2001                    span: self.node_span(start_i),
2002                });
2003            }
2004        }
2005        fields
2006    }
2007
2008    fn try_atom(&mut self, allow_do: bool) -> Option<Expr> {
2009        match self.peek() {
2010            Some(Tok::LowerId { .. }) => {
2011                let kw = self.peek().and_then(|t| t.keyword());
2012                match kw {
2013                    // Block argument: `script do ...`, `submit p do ...`.
2014                    Some("do") if allow_do => self.atom(allow_do),
2015                    // Keywords that begin expressions are fine as atoms in
2016                    // head position but must not be slurped as arguments.
2017                    Some(
2018                        "if" | "case" | "do" | "let" | "try" | "where" | "then" | "else" | "of"
2019                        | "in" | "controller" | "with" | "catch",
2020                    ) => None,
2021                    _ => self.atom(allow_do),
2022                }
2023            }
2024            Some(Tok::UpperId { .. })
2025            | Some(Tok::IntLit(_))
2026            | Some(Tok::DecimalLit(_))
2027            | Some(Tok::StringLit(_))
2028            | Some(Tok::CharLit(_))
2029            | Some(Tok::LParen)
2030            | Some(Tok::LBracket) => self.atom(allow_do),
2031            // Bare trailing lambda argument: `forA xs \x -> ...`.
2032            Some(Tok::Op(o)) if o == "\\" => self.atom(allow_do),
2033            _ => None,
2034        }
2035    }
2036
2037    /// Fold tight (whitespace-free) `.field` record projections onto `base`.
2038    /// Projection binds tighter than application, so `length this.note` is
2039    /// `length (this.note)` not `(length this).note`, and a chain `a.b.c`
2040    /// left-nests. A *spaced* dot (`f . g`, composition) is not tight and is
2041    /// left to the binary-operator layer untouched. Qualified names
2042    /// (`Map.lookup`) are already a single token and never reach here.
2043    fn projection_tail(&mut self, mut base: Expr) -> Expr {
2044        while self.at_tight_projection() {
2045            let start = base.span().start;
2046            let pos = base.pos();
2047            self.bump(); // '.'
2048            let field_tok = self.bump().expect("tight projection guarantees a field");
2049            let (qualifier, name) = match field_tok.tok {
2050                Tok::LowerId { qualifier, name } => (qualifier, name),
2051                other => unreachable!("at_tight_projection guarantees LowerId, got {other:?}"),
2052            };
2053            let field = Expr::Var {
2054                qualifier,
2055                name,
2056                pos: field_tok.pos,
2057                span: Span::new(field_tok.start, field_tok.end),
2058            };
2059            base = Expr::BinOp {
2060                op: ".".to_string(),
2061                lhs: Box::new(base),
2062                rhs: Box::new(field),
2063                pos,
2064                span: Span::new(start, self.end_byte()),
2065            };
2066        }
2067        base
2068    }
2069
2070    /// True when the cursor sits on a `.` that abuts a real token on its left
2071    /// and an unqualified lowercase field on its right with no whitespace on
2072    /// either side — i.e. a record projection, not function composition.
2073    fn at_tight_projection(&self) -> bool {
2074        if self.i == 0 {
2075            return false;
2076        }
2077        let dot = match self.toks.get(self.i) {
2078            Some(t) => t,
2079            None => return false,
2080        };
2081        if !matches!(&dot.tok, Tok::Op(o) if o == ".") {
2082            return false;
2083        }
2084        // Tight on the left: the dot abuts the base's last byte. The previous
2085        // token must be real — a virtual layout token here means a newline or
2086        // dedent sat between base and dot, which can never be a tight dot.
2087        let prev = &self.toks[self.i - 1];
2088        if prev.is_virtual() || prev.end != dot.start {
2089            return false;
2090        }
2091        // Tight on the right: an unqualified lowercase field abuts the dot.
2092        self.toks.get(self.i + 1).is_some_and(|t| {
2093            matches!(
2094                &t.tok,
2095                Tok::LowerId {
2096                    qualifier: None,
2097                    ..
2098                }
2099            ) && t.start == dot.end
2100        })
2101    }
2102
2103    fn atom(&mut self, allow_do: bool) -> Option<Expr> {
2104        let pos = self.pos();
2105        let start_i = self.i;
2106        match self.peek().cloned() {
2107            Some(Tok::LowerId { qualifier, name }) => {
2108                match name.as_str() {
2109                    "if" if qualifier.is_none() => return self.if_expr(),
2110                    "case" if qualifier.is_none() => return self.case_expr(),
2111                    "do" if qualifier.is_none() => {
2112                        if !allow_do {
2113                            return None;
2114                        }
2115                        return self.do_expr();
2116                    }
2117                    "let" if qualifier.is_none() => return self.let_expr(),
2118                    "try" if qualifier.is_none() => return self.try_expr(),
2119                    _ => {}
2120                }
2121                self.bump();
2122                Some(Expr::Var {
2123                    qualifier,
2124                    name,
2125                    pos,
2126                    span: self.node_span(start_i),
2127                })
2128            }
2129            Some(Tok::UpperId { qualifier, name }) => {
2130                self.bump();
2131                let base = Expr::Con {
2132                    qualifier,
2133                    name,
2134                    pos,
2135                    span: self.node_span(start_i),
2136                };
2137                // Explicit-brace record syntax: `Foo {x = 1}`.
2138                if self.at(&Tok::LBrace) {
2139                    let fields = self.record_fields();
2140                    return Some(Expr::Record {
2141                        base: Box::new(base),
2142                        fields,
2143                        pos,
2144                        span: self.node_span(start_i),
2145                    });
2146                }
2147                Some(base)
2148            }
2149            Some(Tok::IntLit(text)) => {
2150                self.bump();
2151                Some(Expr::Lit {
2152                    kind: LitKind::Int,
2153                    text,
2154                    pos,
2155                    span: self.node_span(start_i),
2156                })
2157            }
2158            Some(Tok::DecimalLit(text)) => {
2159                self.bump();
2160                Some(Expr::Lit {
2161                    kind: LitKind::Decimal,
2162                    text,
2163                    pos,
2164                    span: self.node_span(start_i),
2165                })
2166            }
2167            Some(Tok::StringLit(text)) => {
2168                self.bump();
2169                Some(Expr::Lit {
2170                    kind: LitKind::Text,
2171                    text,
2172                    pos,
2173                    span: self.node_span(start_i),
2174                })
2175            }
2176            Some(Tok::CharLit(text)) => {
2177                self.bump();
2178                Some(Expr::Lit {
2179                    kind: LitKind::Char,
2180                    text,
2181                    pos,
2182                    span: self.node_span(start_i),
2183                })
2184            }
2185            Some(Tok::Op(o)) if o == "\\" => self.lambda_expr(),
2186            Some(Tok::LParen) => self.paren_expr(),
2187            Some(Tok::LBracket) => self.list_expr(),
2188            _ => None,
2189        }
2190    }
2191
2192    fn if_expr(&mut self) -> Option<Expr> {
2193        let pos = self.pos();
2194        let start_i = self.i;
2195        self.bump(); // if
2196        let cond = self.expr();
2197        self.eat(&Tok::VSemi); // DoAndIfThenElse style
2198        if !self.eat_keyword("then") {
2199            self.diag("expected 'then'");
2200            return Some(Expr::Error {
2201                raw: format!("if {}", cond.render()),
2202                pos,
2203                span: self.node_span(start_i),
2204            });
2205        }
2206        let then_branch = self.expr();
2207        self.eat(&Tok::VSemi);
2208        if !self.eat_keyword("else") {
2209            self.diag("expected 'else'");
2210            return Some(Expr::Error {
2211                raw: format!("if {} then {}", cond.render(), then_branch.render()),
2212                pos,
2213                span: self.node_span(start_i),
2214            });
2215        }
2216        let else_branch = self.expr();
2217        Some(Expr::If {
2218            cond: Box::new(cond),
2219            then_branch: Box::new(then_branch),
2220            else_branch: Box::new(else_branch),
2221            pos,
2222            span: self.node_span(start_i),
2223        })
2224    }
2225
2226    fn case_expr(&mut self) -> Option<Expr> {
2227        let pos = self.pos();
2228        let start_i = self.i;
2229        self.bump(); // case
2230        let scrutinee = self.expr_no_do();
2231        if !self.eat_keyword("of") {
2232            self.diag("expected 'of' in case expression");
2233            return Some(Expr::Error {
2234                raw: format!("case {}", scrutinee.render()),
2235                pos,
2236                span: self.node_span(start_i),
2237            });
2238        }
2239        let mut alts = Vec::new();
2240        if self.eat(&Tok::VLBrace) || self.eat(&Tok::LBrace) {
2241            loop {
2242                while self.eat(&Tok::VSemi) || self.eat(&Tok::Semi) {}
2243                match self.peek() {
2244                    None => break,
2245                    Some(Tok::VRBrace) | Some(Tok::RBrace) => {
2246                        self.bump();
2247                        break;
2248                    }
2249                    // Stray closer: discard so the loop always progresses.
2250                    Some(Tok::RParen) | Some(Tok::RBracket) => {
2251                        self.bump();
2252                        continue;
2253                    }
2254                    _ => {}
2255                }
2256                // An alternative can carry a `where` block for its body.
2257                if self.eat_keyword("where") {
2258                    let _ = self.binding_block();
2259                    continue;
2260                }
2261                match self.case_alt() {
2262                    Some(a) => alts.push(a),
2263                    None => self.skip_to_item_end(),
2264                }
2265            }
2266        }
2267        Some(Expr::Case {
2268            scrutinee: Box::new(scrutinee),
2269            alts,
2270            pos,
2271            span: self.node_span(start_i),
2272        })
2273    }
2274
2275    fn case_alt(&mut self) -> Option<Alt> {
2276        let pos = self.pos();
2277        let start_i = self.i;
2278        let pat = self.pattern()?;
2279        if self.at_op("|") {
2280            // Guarded alternative(s): take the first body, consume all.
2281            // Each guard is comma-separated qualifiers, each a boolean
2282            // expression or a pattern guard `pat <- expr`.
2283            let mut first: Option<Expr> = None;
2284            while self.eat_op("|") {
2285                loop {
2286                    let _guard = self.expr();
2287                    if self.eat_op("<-") {
2288                        let _ = self.expr();
2289                    }
2290                    if !self.eat(&Tok::Comma) {
2291                        break;
2292                    }
2293                }
2294                if !self.eat_op("->") {
2295                    self.diag("expected '->' in guarded case alternative");
2296                    return None;
2297                }
2298                let body = self.expr();
2299                if first.is_none() {
2300                    first = Some(body);
2301                }
2302            }
2303            return Some(Alt {
2304                pat,
2305                body: first?,
2306                pos,
2307                span: self.node_span(start_i),
2308            });
2309        }
2310        if !self.eat_op("->") {
2311            self.diag("expected '->' in case alternative");
2312            return None;
2313        }
2314        let body = self.expr();
2315        Some(Alt {
2316            pat,
2317            body,
2318            pos,
2319            span: self.node_span(start_i),
2320        })
2321    }
2322
2323    fn do_expr(&mut self) -> Option<Expr> {
2324        let pos = self.pos();
2325        let start_i = self.i;
2326        self.bump(); // do
2327        let mut stmts = Vec::new();
2328        if self.eat(&Tok::VLBrace) || self.eat(&Tok::LBrace) {
2329            loop {
2330                while self.eat(&Tok::VSemi) || self.eat(&Tok::Semi) {}
2331                match self.peek() {
2332                    None => break,
2333                    Some(Tok::VRBrace) | Some(Tok::RBrace) => {
2334                        self.bump();
2335                        break;
2336                    }
2337                    // Stray closer: discard so the loop always progresses.
2338                    Some(Tok::RParen) | Some(Tok::RBracket) => {
2339                        self.bump();
2340                        continue;
2341                    }
2342                    _ => {}
2343                }
2344                stmts.push(self.do_stmt());
2345            }
2346        }
2347        Some(Expr::Do {
2348            stmts,
2349            pos,
2350            span: self.node_span(start_i),
2351        })
2352    }
2353
2354    fn do_stmt(&mut self) -> DoStmt {
2355        let pos = self.pos();
2356        let start_i = self.i;
2357        if self.at_keyword("let") {
2358            self.bump();
2359            let bindings = self.binding_block();
2360            // `let ... in body` as a statement is an expression.
2361            if self.eat_keyword("in") {
2362                let body = self.expr();
2363                return DoStmt::Expr {
2364                    expr: Expr::LetIn {
2365                        bindings,
2366                        body: Box::new(body),
2367                        pos,
2368                        span: self.node_span(start_i),
2369                    },
2370                    pos,
2371                    span: self.node_span(start_i),
2372                };
2373            }
2374            return DoStmt::Let {
2375                bindings,
2376                pos,
2377                span: self.node_span(start_i),
2378            };
2379        }
2380        // Try `pat <- expr` with rollback.
2381        let snapshot = self.i;
2382        if let Some(pat) = self.try_bind_pattern() {
2383            if self.at_op("<-") {
2384                self.bump();
2385                let expr = self.expr();
2386                return DoStmt::Bind {
2387                    pat,
2388                    expr,
2389                    pos,
2390                    span: self.node_span(start_i),
2391                };
2392            }
2393        }
2394        self.i = snapshot;
2395        let expr = self.expr();
2396        DoStmt::Expr {
2397            expr,
2398            pos,
2399            span: self.node_span(start_i),
2400        }
2401    }
2402
2403    /// Pattern attempt for `pat <- ...`; restores nothing itself (caller
2404    /// rolls back on failure).
2405    fn try_bind_pattern(&mut self) -> Option<Pat> {
2406        self.pattern()
2407    }
2408
2409    fn let_expr(&mut self) -> Option<Expr> {
2410        let pos = self.pos();
2411        let start_i = self.i;
2412        self.bump(); // let
2413        let bindings = self.binding_block();
2414        if self.eat_keyword("in") {
2415            let body = self.expr();
2416            return Some(Expr::LetIn {
2417                bindings,
2418                body: Box::new(body),
2419                pos,
2420                span: self.node_span(start_i),
2421            });
2422        }
2423        // `let` without `in` outside a do block — degrade gracefully.
2424        Some(Expr::LetIn {
2425            bindings,
2426            body: Box::new(Expr::Error {
2427                raw: String::new(),
2428                pos,
2429                span: self.node_span(start_i),
2430            }),
2431            pos,
2432            span: self.node_span(start_i),
2433        })
2434    }
2435
2436    fn try_expr(&mut self) -> Option<Expr> {
2437        let pos = self.pos();
2438        let start_i = self.i;
2439        self.bump(); // try
2440        let body = self.expr();
2441        let mut handlers = Vec::new();
2442        self.eat(&Tok::VSemi);
2443        if self.eat_keyword("catch") {
2444            if self.eat(&Tok::VLBrace) || self.eat(&Tok::LBrace) {
2445                loop {
2446                    while self.eat(&Tok::VSemi) || self.eat(&Tok::Semi) {}
2447                    match self.peek() {
2448                        None => break,
2449                        Some(Tok::VRBrace) | Some(Tok::RBrace) => {
2450                            self.bump();
2451                            break;
2452                        }
2453                        // Stray closer: discard so the loop always progresses.
2454                        Some(Tok::RParen) | Some(Tok::RBracket) => {
2455                            self.bump();
2456                            continue;
2457                        }
2458                        _ => {}
2459                    }
2460                    match self.case_alt() {
2461                        Some(a) => handlers.push(a),
2462                        None => self.skip_to_item_end(),
2463                    }
2464                }
2465            } else if let Some(a) = self.case_alt() {
2466                // Single-alternative catch on the same line.
2467                handlers.push(a);
2468            }
2469        }
2470        Some(Expr::Try {
2471            body: Box::new(body),
2472            handlers,
2473            pos,
2474            span: self.node_span(start_i),
2475        })
2476    }
2477
2478    fn lambda_expr(&mut self) -> Option<Expr> {
2479        let pos = self.pos();
2480        let start_i = self.i;
2481        self.bump(); // backslash
2482                     // `\case` — lambda-case: one implicit argument matched by the alts.
2483        if self.eat_keyword("case") {
2484            let mut alts = Vec::new();
2485            if self.eat(&Tok::VLBrace) || self.eat(&Tok::LBrace) {
2486                loop {
2487                    while self.eat(&Tok::VSemi) || self.eat(&Tok::Semi) {}
2488                    match self.peek() {
2489                        None => break,
2490                        Some(Tok::VRBrace) | Some(Tok::RBrace) => {
2491                            self.bump();
2492                            break;
2493                        }
2494                        Some(Tok::RParen) | Some(Tok::RBracket) => {
2495                            self.bump();
2496                            continue;
2497                        }
2498                        _ => {}
2499                    }
2500                    match self.case_alt() {
2501                        Some(a) => alts.push(a),
2502                        None => self.skip_to_item_end(),
2503                    }
2504                }
2505            }
2506            return Some(Expr::Lambda {
2507                params: vec![Pat::Var {
2508                    name: "_".to_string(),
2509                    pos,
2510                    span: Span::new(self.byte_at(start_i), self.byte_at(start_i)),
2511                }],
2512                body: Box::new(Expr::Case {
2513                    scrutinee: Box::new(Expr::Var {
2514                        qualifier: None,
2515                        name: "_".to_string(),
2516                        pos,
2517                        span: Span::new(self.byte_at(start_i), self.byte_at(start_i)),
2518                    }),
2519                    alts,
2520                    pos,
2521                    span: self.node_span(start_i),
2522                }),
2523                pos,
2524                span: self.node_span(start_i),
2525            });
2526        }
2527        let mut params = Vec::new();
2528        while !self.at_op("->") {
2529            match self.pattern_atom() {
2530                Some(p) => params.push(p),
2531                None => {
2532                    self.diag("bad lambda parameter");
2533                    let start = self.i;
2534                    self.skip_to_item_end();
2535                    return Some(Expr::Error {
2536                        raw: format!("\\{}", self.slice_text(start)),
2537                        pos,
2538                        span: self.node_span(start_i),
2539                    });
2540                }
2541            }
2542        }
2543        self.bump(); // ->
2544        let body = self.expr();
2545        Some(Expr::Lambda {
2546            params,
2547            body: Box::new(body),
2548            pos,
2549            span: self.node_span(start_i),
2550        })
2551    }
2552
2553    fn paren_expr(&mut self) -> Option<Expr> {
2554        let pos = self.pos();
2555        let start_i = self.i;
2556        self.bump(); // (
2557        if self.eat(&Tok::RParen) {
2558            return Some(Expr::Con {
2559                qualifier: None,
2560                name: "()".to_string(),
2561                pos,
2562                span: self.node_span(start_i),
2563            });
2564        }
2565        // Operator section / operator reference: `(+)`, `(+ 1)`.
2566        if let Some(Tok::Op(o)) = self.peek().cloned() {
2567            if !is_reserved_op(&o) && o != "\\" && o != "-" {
2568                self.bump();
2569                if self.eat(&Tok::RParen) {
2570                    return Some(Expr::Section {
2571                        op: o,
2572                        operand: None,
2573                        left: false,
2574                        pos,
2575                        span: self.node_span(start_i),
2576                    });
2577                }
2578                let operand = self.expr();
2579                self.eat(&Tok::RParen);
2580                return Some(Expr::Section {
2581                    op: o,
2582                    operand: Some(Box::new(operand)),
2583                    left: false,
2584                    pos,
2585                    span: self.node_span(start_i),
2586                });
2587            }
2588        }
2589        let first = self.expr();
2590        if self.at(&Tok::Comma) {
2591            let mut items = vec![first];
2592            while self.eat(&Tok::Comma) {
2593                items.push(self.expr());
2594            }
2595            self.eat(&Tok::RParen);
2596            return Some(Expr::Tuple {
2597                items,
2598                pos,
2599                span: self.node_span(start_i),
2600            });
2601        }
2602        // Left section: `(x +)`.
2603        if let Some(Tok::Op(o)) = self.peek().cloned() {
2604            if !is_reserved_op(&o) && self.peek_at(1) == Some(&Tok::RParen) {
2605                self.bump();
2606                self.bump();
2607                return Some(Expr::Section {
2608                    op: o,
2609                    operand: Some(Box::new(first)),
2610                    left: true,
2611                    pos,
2612                    span: self.node_span(start_i),
2613                });
2614            }
2615        }
2616        self.eat(&Tok::RParen);
2617        Some(first)
2618    }
2619
2620    fn list_expr(&mut self) -> Option<Expr> {
2621        let pos = self.pos();
2622        let start_i = self.i;
2623        self.bump(); // [
2624        let mut items = Vec::new();
2625        if self.eat(&Tok::RBracket) {
2626            return Some(Expr::List {
2627                items,
2628                pos,
2629                span: self.node_span(start_i),
2630            });
2631        }
2632        loop {
2633            let e = self.expr();
2634            // Range: `[a .. b]` / `[a ..]`.
2635            if self.at_op("..") {
2636                self.bump();
2637                let hi = if self.at(&Tok::RBracket) {
2638                    Expr::Error {
2639                        raw: String::new(),
2640                        pos,
2641                        span: self.node_span(start_i),
2642                    }
2643                } else {
2644                    self.expr()
2645                };
2646                self.eat(&Tok::RBracket);
2647                return Some(Expr::BinOp {
2648                    op: "..".to_string(),
2649                    lhs: Box::new(e),
2650                    rhs: Box::new(hi),
2651                    pos,
2652                    span: self.node_span(start_i),
2653                });
2654            }
2655            // List comprehension: degrade the qualifier part to raw text.
2656            if self.at_op("|") {
2657                let start = self.i;
2658                let mut brackets = 1usize;
2659                while let Some(t) = self.peek() {
2660                    match t {
2661                        Tok::LBracket => brackets += 1,
2662                        Tok::RBracket => {
2663                            brackets -= 1;
2664                            if brackets == 0 {
2665                                break;
2666                            }
2667                        }
2668                        Tok::VSemi | Tok::VRBrace => break,
2669                        _ => {}
2670                    }
2671                    self.i += 1;
2672                }
2673                let raw = self.slice_text(start);
2674                self.eat(&Tok::RBracket);
2675                return Some(Expr::App {
2676                    func: Box::new(e),
2677                    args: vec![Expr::Error {
2678                        raw,
2679                        pos,
2680                        span: self.node_span(start_i),
2681                    }],
2682                    pos,
2683                    span: self.node_span(start_i),
2684                });
2685            }
2686            items.push(e);
2687            if !self.eat(&Tok::Comma) {
2688                break;
2689            }
2690        }
2691        self.eat(&Tok::RBracket);
2692        Some(Expr::List {
2693            items,
2694            pos,
2695            span: self.node_span(start_i),
2696        })
2697    }
2698}
2699
2700/// Operators that structure declarations and can never be expression infix
2701/// operators.
2702fn is_reserved_op(op: &str) -> bool {
2703    matches!(op, "=" | "<-" | "->" | "|" | ":" | "=>" | "@" | "\\" | "..")
2704}
2705
2706/// (precedence, right-assoc) — Haskell defaults; unknown operators get
2707/// infixl 9.
2708fn fixity(op: &str) -> (u8, bool) {
2709    match op {
2710        "$" | "$!" => (1, true),
2711        ">>=" | ">>" | "=<<" | "<&>" => (2, false),
2712        "||" => (3, true),
2713        "&&" => (4, true),
2714        "==" | "/=" | "<" | "<=" | ">" | ">=" => (5, false),
2715        "::" | "++" | "<>" => (6, true),
2716        "+" | "-" => (7, false),
2717        "*" | "/" => (8, false),
2718        "^" | "**" => (9, true),
2719        "." | "!!" => (10, true),
2720        _ => (9, false),
2721    }
2722}
2723
2724/// Merge type signatures and successive equations of the same function into
2725/// one `Decl::Function`, preserving first-seen order.
2726/// Bounding span of a function's equations (their first start to last end).
2727/// `None` for a signature-only function (no equations yet).
2728fn equations_extent(eqs: &[Equation]) -> Option<Span> {
2729    let mut it = eqs.iter();
2730    let first = it.next()?;
2731    let mut s = first.span;
2732    for e in it {
2733        s.start = s.start.min(e.span.start);
2734        s.end = s.end.max(e.span.end);
2735    }
2736    Some(s)
2737}
2738
2739fn merge_functions(decls: &mut Vec<Decl>) {
2740    let mut out: Vec<Decl> = Vec::with_capacity(decls.len());
2741    for decl in decls.drain(..) {
2742        match decl {
2743            Decl::Function(f) => {
2744                let existing = out.iter_mut().find_map(|d| match d {
2745                    Decl::Function(g) if g.name == f.name => Some(g),
2746                    _ => None,
2747                });
2748                match existing {
2749                    Some(g) => {
2750                        if g.ty.is_none() {
2751                            g.ty = f.ty.clone();
2752                        }
2753                        if g.sig_span.is_none() {
2754                            g.sig_span = f.sig_span;
2755                        }
2756                        // The function's reported position is its first
2757                        // equation, not its type signature.
2758                        if g.equations.is_empty() && !f.equations.is_empty() {
2759                            g.pos = f.pos;
2760                        }
2761                        g.equations.extend(f.equations);
2762                        // A function's span is the extent of its equations,
2763                        // which are contiguous in well-formed source. A type
2764                        // signature can sit apart (with unrelated decls in
2765                        // between), so it is tracked in `sig_span` and never
2766                        // folded into `span` — doing so could straddle a
2767                        // sibling decl and break the nesting invariant.
2768                        g.span = equations_extent(&g.equations)
2769                            .or(g.sig_span)
2770                            .unwrap_or(g.span);
2771                    }
2772                    None => out.push(Decl::Function(f)),
2773                }
2774            }
2775            other => out.push(other),
2776        }
2777    }
2778    *decls = out;
2779}
2780
2781/// Parse a type from a slice of the type's tokens (e.g. the tokens between a
2782/// field's `:` and the item end). PURE: it never touches the main parser cursor
2783/// and never affects any span, so it is invisible to daml-fmt. Returns `None`
2784/// when the whole slice does not parse cleanly as a type.
2785///
2786/// Grammar (precedence low → high): constraint `C => T`, function `a -> b`
2787/// (right-assoc), application `head arg...` (left-assoc), then atoms (`Con`,
2788/// `Var`, `[T]`, `()`, `(T)`, `(a, b)`).
2789pub(crate) fn parse_type_from_tokens(tokens: &[Token]) -> Option<Type> {
2790    // Virtual layout tokens carry no type meaning; drop them so a stray VSemi
2791    // in the slice can't sink an otherwise-clean parse.
2792    let real_tokens: Vec<&Token> = tokens.iter().filter(|t| !t.is_virtual()).collect();
2793    if real_tokens.is_empty() {
2794        return None;
2795    }
2796    let mut parser = TypeTokenParser {
2797        tokens: &real_tokens,
2798        cursor: 0,
2799    };
2800    let ty = parser.parse_type()?;
2801    // Require the whole slice to be consumed: a partial parse means the type
2802    // had a shape we don't model, so report unknown rather than a half-truth.
2803    if parser.cursor == real_tokens.len() {
2804        Some(ty)
2805    } else {
2806        None
2807    }
2808}
2809
2810struct TypeTokenParser<'a> {
2811    tokens: &'a [&'a Token],
2812    cursor: usize,
2813}
2814
2815/// Result of parsing one atom: a real type, or a dropped type-level nat literal
2816/// (`Numeric 10` — not a type, so it never enters the App arg list).
2817enum TypeAtom {
2818    ParsedType(Type),
2819    DroppedLiteral(Span),
2820}
2821
2822impl<'a> TypeTokenParser<'a> {
2823    fn peek(&self) -> Option<&'a Token> {
2824        self.tokens.get(self.cursor).copied()
2825    }
2826
2827    fn eat_op(&mut self, op: &str) -> bool {
2828        if self.peek().is_some_and(|t| t.tok.is_op(op)) {
2829            self.cursor += 1;
2830            true
2831        } else {
2832            false
2833        }
2834    }
2835
2836    /// Full type: a constraint context `=> body`, or a function `a -> b`, or a
2837    /// bare application.
2838    fn parse_type(&mut self) -> Option<Type> {
2839        let lhs = self.parse_application_type()?;
2840        if self.eat_op("=>") {
2841            // `lhs` was the constraint context; drop it, keep the body.
2842            let body = self.parse_type()?;
2843            let span = Span::new(lhs.span().start, body.span().end);
2844            return Some(Type::Constrained(Box::new(body), span));
2845        }
2846        if self.eat_op("->") {
2847            let rhs = self.parse_type()?;
2848            let span = Span::new(lhs.span().start, rhs.span().end);
2849            return Some(Type::Fun(Box::new(lhs), Box::new(rhs), span));
2850        }
2851        Some(lhs)
2852    }
2853
2854    /// Application spine: one head atom applied to zero or more argument atoms.
2855    fn parse_application_type(&mut self) -> Option<Type> {
2856        let head = match self.parse_atom()? {
2857            TypeAtom::ParsedType(t) => t,
2858            // A bare nat literal is not a type.
2859            TypeAtom::DroppedLiteral(_) => return None,
2860        };
2861        let mut args = Vec::new();
2862        let start = head.span().start;
2863        let mut end = head.span().end;
2864        loop {
2865            // Only continue the spine if the next token can START an atom; an
2866            // operator (`->`, `=>`) or closer ends it.
2867            if !self.is_at_type_atom_start() {
2868                break;
2869            }
2870            match self.parse_atom()? {
2871                TypeAtom::ParsedType(t) => {
2872                    end = t.span().end;
2873                    args.push(t);
2874                }
2875                TypeAtom::DroppedLiteral(span) => {
2876                    // `Numeric 10` — drop the `10` as structure but keep it in
2877                    // the enclosing type span.
2878                    end = span.end;
2879                }
2880            }
2881        }
2882        let span = Span::new(start, end);
2883        if args.is_empty() {
2884            Some(head.with_span(span))
2885        } else {
2886            Some(Type::App(Box::new(head), args, span))
2887        }
2888    }
2889
2890    /// True if the current token can begin an atom (so the application spine
2891    /// should keep going).
2892    fn is_at_type_atom_start(&self) -> bool {
2893        matches!(
2894            self.peek().map(|t| &t.tok),
2895            Some(Tok::UpperId { .. })
2896                | Some(Tok::LowerId { .. })
2897                | Some(Tok::IntLit(_))
2898                | Some(Tok::DecimalLit(_))
2899                | Some(Tok::LBracket)
2900                | Some(Tok::LParen)
2901        )
2902    }
2903
2904    fn parse_atom(&mut self) -> Option<TypeAtom> {
2905        let tok = self.peek()?;
2906        match &tok.tok {
2907            Tok::UpperId { qualifier, name } => {
2908                let con = Type::Con {
2909                    qualifier: qualifier.clone(),
2910                    name: name.clone(),
2911                    span: Span::new(tok.start, tok.end),
2912                };
2913                self.cursor += 1;
2914                Some(TypeAtom::ParsedType(con))
2915            }
2916            Tok::LowerId { name, .. } => {
2917                // Type variable (`a`, `n`). Qualified lowercase never appears in
2918                // a real type position; treat the name as the variable.
2919                let var = Type::Var(name.clone(), Span::new(tok.start, tok.end));
2920                self.cursor += 1;
2921                Some(TypeAtom::ParsedType(var))
2922            }
2923            Tok::IntLit(_) | Tok::DecimalLit(_) => {
2924                // Type-level nat literal (`Numeric 10`): consumed, but dropped.
2925                self.cursor += 1;
2926                Some(TypeAtom::DroppedLiteral(Span::new(tok.start, tok.end)))
2927            }
2928            Tok::LBracket => {
2929                let start = tok.start;
2930                self.cursor += 1;
2931                let inner = self.parse_type()?;
2932                self.eat_token(Tok::RBracket).map(|end| {
2933                    TypeAtom::ParsedType(Type::List(Box::new(inner), Span::new(start, end.end)))
2934                })
2935            }
2936            Tok::LParen => {
2937                let start = tok.start;
2938                self.cursor += 1;
2939                if let Some(end) = self.eat_token(Tok::RParen) {
2940                    // ()
2941                    return Some(TypeAtom::ParsedType(Type::Unit(Span::new(start, end.end))));
2942                }
2943                let first = self.parse_type()?;
2944                if self.peek().map(|t| &t.tok) == Some(&Tok::Comma) {
2945                    let mut items = vec![first];
2946                    while self.eat_token(Tok::Comma).is_some() {
2947                        items.push(self.parse_type()?);
2948                    }
2949                    self.eat_token(Tok::RParen).map(|end| {
2950                        TypeAtom::ParsedType(Type::Tuple(items, Span::new(start, end.end)))
2951                    })
2952                } else {
2953                    self.eat_token(Tok::RParen).map(|end| {
2954                        // Grouping parens.
2955                        TypeAtom::ParsedType(first.with_span(Span::new(start, end.end)))
2956                    })
2957                }
2958            }
2959            _ => None,
2960        }
2961    }
2962
2963    fn eat_token(&mut self, tok: Tok) -> Option<&'a Token> {
2964        if self.peek().is_some_and(|t| t.tok == tok) {
2965            let t = self.peek();
2966            self.cursor += 1;
2967            t
2968        } else {
2969            None
2970        }
2971    }
2972}
2973
2974fn render_token_slice(tokens: &[Token]) -> String {
2975    let mut s = String::new();
2976    let mut prev_no_space_after = true;
2977    for t in tokens {
2978        let (text, no_space_before, no_space_after): (String, bool, bool) = match &t.tok {
2979            Tok::LowerId { qualifier, name } | Tok::UpperId { qualifier, name } => (
2980                qualifier
2981                    .as_ref()
2982                    .map_or_else(|| name.clone(), |q| format!("{}.{}", q, name)),
2983                false,
2984                false,
2985            ),
2986            Tok::Op(o) => (o.clone(), false, false),
2987            Tok::IntLit(n) | Tok::DecimalLit(n) => (n.clone(), false, false),
2988            Tok::StringLit(v) => (format!("{:?}", v), false, false),
2989            Tok::CharLit(v) => (format!("'{}'", v), false, false),
2990            Tok::LParen => ("(".to_string(), false, true),
2991            Tok::RParen => (")".to_string(), true, false),
2992            Tok::LBracket => ("[".to_string(), false, true),
2993            Tok::RBracket => ("]".to_string(), true, false),
2994            Tok::LBrace => ("{".to_string(), false, true),
2995            Tok::RBrace => ("}".to_string(), true, false),
2996            Tok::Comma => (",".to_string(), true, false),
2997            Tok::Semi | Tok::VSemi => (";".to_string(), true, false),
2998            Tok::Backtick => ("`".to_string(), false, false),
2999            Tok::VLBrace | Tok::VRBrace => continue,
3000        };
3001        if !s.is_empty() && !no_space_before && !prev_no_space_after {
3002            s.push(' ');
3003        }
3004        s.push_str(&text);
3005        prev_no_space_after = no_space_after;
3006    }
3007    s
3008}
3009
3010#[cfg(test)]
3011mod type_tests {
3012    use super::*;
3013    use crate::lexer::lex;
3014
3015    /// Parse a bare type string straight through the lexer. A single-line type
3016    /// has no layout-significant newlines, so no virtual tokens appear — this
3017    /// exercises the type grammar in isolation.
3018    fn ty(s: &str) -> Option<Type> {
3019        let (toks, errs) = lex(s);
3020        assert!(errs.is_empty(), "lex errors for {s:?}: {errs:?}");
3021        parse_type_from_tokens(&toks)
3022    }
3023
3024    fn con(name: &str) -> Type {
3025        Type::Con {
3026            qualifier: None,
3027            name: name.to_string(),
3028            span: Span::default(),
3029        }
3030    }
3031
3032    fn qualified_con(qualifier: &str, name: &str) -> Type {
3033        Type::Con {
3034            qualifier: Some(qualifier.to_string()),
3035            name: name.to_string(),
3036            span: Span::default(),
3037        }
3038    }
3039
3040    fn app(head: Type, args: Vec<Type>) -> Type {
3041        Type::App(Box::new(head), args, Span::default())
3042    }
3043
3044    fn list(inner: Type) -> Type {
3045        Type::List(Box::new(inner), Span::default())
3046    }
3047
3048    fn tuple(items: Vec<Type>) -> Type {
3049        Type::Tuple(items, Span::default())
3050    }
3051
3052    fn fun(param: Type, result: Type) -> Type {
3053        Type::Fun(Box::new(param), Box::new(result), Span::default())
3054    }
3055
3056    fn var(name: &str) -> Type {
3057        Type::Var(name.to_string(), Span::default())
3058    }
3059
3060    fn unit() -> Type {
3061        Type::Unit(Span::default())
3062    }
3063
3064    fn constrained(body: Type) -> Type {
3065        Type::Constrained(Box::new(body), Span::default())
3066    }
3067
3068    #[test]
3069    fn atoms() {
3070        assert_eq!(ty("Party"), Some(con("Party")));
3071        assert_eq!(ty("Decimal"), Some(con("Decimal")));
3072        assert_eq!(ty("a"), Some(var("a")));
3073        assert_eq!(ty("()"), Some(unit()));
3074    }
3075
3076    #[test]
3077    fn application_vs_constructor() {
3078        // The whole point of the new model: `ContractId Foo` is an APPLICATION,
3079        // not one opaque name.
3080        assert_eq!(
3081            ty("ContractId Foo"),
3082            Some(app(con("ContractId"), vec![con("Foo")]))
3083        );
3084        assert_eq!(
3085            ty("Optional (ContractId Foo)"),
3086            Some(app(
3087                con("Optional"),
3088                vec![app(con("ContractId"), vec![con("Foo")])]
3089            ))
3090        );
3091        assert_eq!(
3092            ty("Map Text Int"),
3093            Some(app(con("Map"), vec![con("Text"), con("Int")]))
3094        );
3095    }
3096
3097    #[test]
3098    fn qualified_constructor_keeps_qualifier() {
3099        assert_eq!(
3100            ty("DA.Map.Map Text Int"),
3101            Some(app(
3102                qualified_con("DA.Map", "Map"),
3103                vec![con("Text"), con("Int")]
3104            ))
3105        );
3106    }
3107
3108    #[test]
3109    fn list_and_tuple() {
3110        assert_eq!(ty("[Text]"), Some(list(con("Text"))));
3111        assert_eq!(
3112            ty("(Int, Text)"),
3113            Some(tuple(vec![con("Int"), con("Text")]))
3114        );
3115        // A tuple is NOT a grouping paren — must stay a Tuple, never collapse.
3116        assert_eq!(
3117            ty("(a, b, c)"),
3118            Some(tuple(vec![var("a"), var("b"), var("c")]))
3119        );
3120        // Single grouping paren unwraps.
3121        assert_eq!(ty("(Text)"), Some(con("Text")));
3122    }
3123
3124    #[test]
3125    fn function_types_are_arrows_not_names() {
3126        // These are exactly the corpus strings the old matcher swallowed into
3127        // one opaque `Named`.
3128        assert_eq!(ty("Int -> Int"), Some(fun(con("Int"), con("Int"))));
3129        // Right associativity: `a -> b -> c` == `a -> (b -> c)`.
3130        assert_eq!(
3131            ty("Int -> Text -> Bool"),
3132            Some(fun(con("Int"), fun(con("Text"), con("Bool"))))
3133        );
3134        assert_eq!(
3135            ty("Party -> Script ()"),
3136            Some(fun(con("Party"), app(con("Script"), vec![unit()])))
3137        );
3138    }
3139
3140    #[test]
3141    fn script_application() {
3142        // `Script ()` ×147 in the corpus — an application flattened to `Named`
3143        // before. Now a real App.
3144        assert_eq!(ty("Script ()"), Some(app(con("Script"), vec![unit()])));
3145    }
3146
3147    #[test]
3148    fn numeric_nat_literal_is_dropped() {
3149        // `Numeric 10`: the `10` is a type-level nat, not a type, so it drops
3150        // and the head Con stands alone. `Numeric n` keeps the type variable.
3151        assert_eq!(ty("Numeric 10"), Some(con("Numeric")));
3152        assert_eq!(ty("Numeric n"), Some(app(con("Numeric"), vec![var("n")])));
3153    }
3154
3155    #[test]
3156    fn constraint_context_is_dropped_body_kept() {
3157        // `NumericScale n => Numeric 37 -> Numeric n` — a constrained function.
3158        // Context dropped; body (the arrow) kept.
3159        assert_eq!(
3160            ty("NumericScale n => Numeric 37 -> Numeric n"),
3161            Some(constrained(fun(
3162                con("Numeric"),
3163                app(con("Numeric"), vec![var("n")])
3164            )))
3165        );
3166        // Tuple context `(Eq a, Show a) => a` also drops cleanly.
3167        assert_eq!(ty("(Eq a, Show a) => a"), Some(constrained(var("a"))));
3168    }
3169
3170    #[test]
3171    fn unparseable_is_none() {
3172        // A trailing arrow with no body is not a clean type → unknown (None),
3173        // never a half-parse.
3174        assert_eq!(ty("Int ->"), None);
3175        assert_eq!(ty("-> Int"), None);
3176    }
3177
3178    #[test]
3179    fn ty_is_populated_through_real_parse() {
3180        // End-to-end: the wiring actually fills `ty` on template fields and the
3181        // choice return type, from the real token stream.
3182        let src = r#"module M where
3183template T
3184  with
3185    owner : Party
3186    held : ContractId Asset
3187  where
3188    signatory owner
3189    choice Go : Optional (ContractId Asset)
3190      controller owner
3191      do
3192        pure None
3193"#;
3194        let (m, _) = parse_module(src);
3195        let t = match &m.decls[0] {
3196            Decl::Template(t) => t,
3197            other => panic!("expected template, got {other:?}"),
3198        };
3199        assert_eq!(t.fields[0].ty, Some(con("Party")));
3200        assert_eq!(
3201            t.fields[1].ty,
3202            Some(app(con("ContractId"), vec![con("Asset")]))
3203        );
3204        let choice = match &t
3205            .body
3206            .iter()
3207            .find(|d| matches!(d, TemplateBodyDecl::Choice(_)))
3208        {
3209            Some(TemplateBodyDecl::Choice(c)) => (*c).clone(),
3210            _ => panic!("expected choice"),
3211        };
3212        assert_eq!(
3213            choice.return_ty,
3214            Some(app(
3215                con("Optional"),
3216                vec![app(con("ContractId"), vec![con("Asset")])]
3217            ))
3218        );
3219    }
3220
3221    #[test]
3222    fn ty_is_populated_on_key_and_interface_method() {
3223        // The other two type-bearing nodes: a template `key ... : T` and an
3224        // interface method signature both fill `ty` from the token stream.
3225        let src = r#"module M where
3226template T
3227  with
3228    owner : Party
3229  where
3230    signatory owner
3231    key owner : Party
3232    maintainer owner
3233
3234interface I where
3235  getAmount : Numeric 10
3236"#;
3237        let (m, _) = parse_module(src);
3238        let t = match &m.decls[0] {
3239            Decl::Template(t) => t,
3240            other => panic!("expected template, got {other:?}"),
3241        };
3242        let key_ty = t.body.iter().find_map(|d| match d {
3243            TemplateBodyDecl::Key { ty, .. } => Some(ty.clone()),
3244            _ => None,
3245        });
3246        assert_eq!(key_ty, Some(Some(con("Party"))));
3247
3248        let iface = match &m.decls[1] {
3249            Decl::Interface(i) => i,
3250            other => panic!("expected interface, got {other:?}"),
3251        };
3252        // `Numeric 10` — the nat literal is dropped, leaving the bare head Con.
3253        assert_eq!(iface.methods[0].ty, Some(con("Numeric")));
3254    }
3255}