Skip to main content

bynk_syntax/
parser.rs

1//! Hand-written recursive-descent parser for Bynk v0.
2//!
3//! Token grammar in spec §4. The expression parser uses one function per
4//! precedence level (§4.4). Errors carry spans and short fix-oriented
5//! messages; the parser does not currently attempt synchronisation, which
6//! means at most one parse error is reported per compilation.
7
8use crate::ast::*;
9use crate::error::CompileError;
10use crate::lexer::{
11    Token, TokenKind, comment_body, comment_is_legacy, doc_block_content, doc_fence_len,
12    doc_line_is_legacy, doc_line_text, has_blank_line_between, legacy_comment_error,
13    legacy_doc_line_error,
14};
15use crate::span::Span;
16mod declarations;
17mod expressions;
18mod staircase;
19mod statements;
20mod types;
21
22/// Side-channel store for line-comment trivia (v1.1 LSP spec §3.5).
23///
24/// Built once up-front by [`split_trivia`] from the raw lexer token stream.
25/// Comments are removed from the token stream the parser walks; their text
26/// is filed into `leading` (comments on lines preceding a content token)
27/// and `trailing` (a single comment on the same line as a content token).
28/// The parser consumes entries through [`TriviaTable::take_leading`] and
29/// [`TriviaTable::take_trailing`] as it recognises declarations.
30#[derive(Debug, Default)]
31struct TriviaTable {
32    /// `leading[i]` holds the comments that appear immediately before content
33    /// token `i` (zero or more `--` lines, in source order, not separated from
34    /// the token by another content token). A comment on its own line is
35    /// followed by a [`Comment::Blank`] when a blank line comes after it
36    /// (#1884).
37    leading: Vec<Vec<Comment>>,
38    /// `trailing[i]` holds an optional comment on the same source line as
39    /// content token `i`. Only one trailing comment is recorded per token
40    /// because a single `--` consumes the rest of the line. A doc block or an
41    /// opening `{` never has one: see [`split_trivia`].
42    trailing: Vec<Option<String>>,
43    /// `blank_before[i]` is whether a blank line separates content token `i`
44    /// from the previous token, with a comment on its own line between them
45    /// (#2893): the blank line that sets the comment apart from what is above.
46    blank_before: Vec<bool>,
47    /// Any pending leading comments at end-of-file (no content token
48    /// followed), shaped as a `leading` entry. Used to preserve file-trailing
49    /// comments.
50    epilogue: Vec<Comment>,
51    /// #2203: a `bynk.lex.legacy_comment` or `bynk.lex.legacy_doc_line` error
52    /// for each comment or doc line spelled the legacy way. The tokens still
53    /// stand as trivia and docs, so the parse goes on and reports them all;
54    /// [`Parser::new`] files them with the doc errors, and the formatter's
55    /// migrating parse ([`parse_units_migrating`]) drops them.
56    legacy: Vec<CompileError>,
57}
58
59impl TriviaTable {
60    fn take_leading(&mut self, index: usize) -> Vec<Comment> {
61        match self.leading.get_mut(index) {
62            Some(v) => std::mem::take(v),
63            None => Vec::new(),
64        }
65    }
66
67    fn blank_before(&self, index: usize) -> bool {
68        self.blank_before.get(index).copied().unwrap_or(false)
69    }
70
71    fn take_trailing(&mut self, index: usize) -> Option<String> {
72        self.trailing.get_mut(index).and_then(|s| s.take())
73    }
74
75    fn take_epilogue(&mut self) -> Vec<Comment> {
76        std::mem::take(&mut self.epilogue)
77    }
78
79    /// True when every entry has been drained via `take_leading`/
80    /// `take_trailing`/`take_epilogue` — i.e. no comment was silently
81    /// dropped. Each harvest is already a `mem::take`, so anything still
82    /// present here is exactly the set of comments that never reached an
83    /// AST `Trivia` field.
84    ///
85    /// Deliberately **not** wired into a `debug_assert!` in the general parse
86    /// path: expressions carry no per-node trivia (§ "Comment trivia" in the
87    /// 2026-07-27 pipeline review), so an ordinary, valid program with a
88    /// comment inside a `match`/list/record/binop — a common, accepted
89    /// pattern `bynk-fmt`'s own comment-loss guard already handles
90    /// gracefully — would leave `leading`/`trailing` non-empty and trip it on
91    /// every compile, not just on formatting. Instead surfaced through
92    /// [`parse_units_with_drain_check`] (finding #66), whose one caller
93    /// (`bynk-fmt`) *does* care about exactly this signal.
94    /// [`Self::epilogue_is_empty`] is the narrower, safe-to-assert check.
95    fn is_fully_drained(&self) -> bool {
96        self.leading.iter().all(Vec::is_empty)
97            && self.trailing.iter().all(Option::is_none)
98            && self.epilogue.is_empty()
99    }
100
101    /// True when no file-trailing comment was left stranded. Unlike
102    /// [`Self::is_fully_drained`], this is safe to assert unconditionally: a
103    /// clean file's epilogue is empty by construction (nothing pending at
104    /// EOF), and the one shape that legitimately populates it — a top-level
105    /// trailing comment — is drained by every parse path that calls
106    /// `take_epilogue`. A brace-form declaration that forgets to is exactly
107    /// the bug this catches.
108    fn epilogue_is_empty(&self) -> bool {
109        self.epilogue.is_empty()
110    }
111}
112
113/// Remove `Comment` trivia tokens from `tokens` and bin them into a
114/// [`TriviaTable`] keyed against the surviving content tokens. A comment
115/// on the same source line as the preceding content token is recorded as
116/// that token's *trailing* trivia, unless that token is a doc block or an
117/// opening `{`; everything else is *leading* for the next content token.
118///
119/// A blank line after a leading comment on its own line, before the next
120/// comment or content token, is recorded as a [`Comment::Blank`] (#1884). Only
121/// a declaration that canonical style sets apart with blank lines keeps it: see
122/// [`Parser::take_leading_trivia`].
123fn split_trivia(tokens: &[Token], source: &str) -> (Vec<Token>, TriviaTable) {
124    let mut filtered: Vec<Token> = Vec::with_capacity(tokens.len());
125    let mut table = TriviaTable::default();
126    let mut pending_leading: Vec<Comment> = Vec::new();
127    let mut last_content_end: Option<usize> = None;
128    // Where the last comment in `pending_leading` ends, if it has a line to
129    // itself: the gap from there to the next token says whether a blank line
130    // follows it. A comment's span stops before its newline, so a blank line
131    // is two newlines. A comment that leads only because it sits on a `{` line
132    // has no blank line of its own: the one below it was between the `{` and
133    // the first item.
134    let mut last_leading_end: Option<usize> = None;
135    // Whether the first own-line comment now pending follows a blank line.
136    let mut pending_blank_before = false;
137    for tok in tokens {
138        if !pending_leading.is_empty()
139            && let Some(end) = last_leading_end
140            && source[end..tok.span.start].matches('\n').count() >= 2
141        {
142            pending_leading.push(Comment::Blank);
143        }
144        if tok.kind == TokenKind::DocLine && doc_line_is_legacy(source, tok.span) {
145            table.legacy.push(legacy_doc_line_error(source, tok.span));
146        }
147        if tok.kind == TokenKind::Comment {
148            if comment_is_legacy(source, tok.span) {
149                table.legacy.push(legacy_comment_error(tok.span));
150            }
151            let body = comment_body(source, tok.span).into_owned();
152            // If nothing has been buffered as leading for the next token and
153            // there is no newline between the previous content token and
154            // this comment, it trails that token. A doc block never has a
155            // trailing comment: its token ends past the newline after the
156            // closing `---`, so a `--` line directly under it would otherwise
157            // look same-line and be binned where nothing collects it (#1756).
158            // A `--|` doc line (#1888) ends past its newline the same way.
159            // Nor does an opening `{`: no parser collects trivia right after
160            // one, so its comment leads whatever follows instead, the first
161            // item or the `}` of an empty body (#1788). The same goes for a
162            // `[`: a list literal's first element or its `]` (#3067).
163            if pending_leading.is_empty()
164                && filtered.last().is_some_and(|t| {
165                    !matches!(
166                        t.kind,
167                        TokenKind::DocBlock
168                            | TokenKind::DocLine
169                            | TokenKind::LBrace
170                            | TokenKind::LBracket
171                            | TokenKind::LParen
172                    )
173                })
174                && let Some(prev_end) = last_content_end
175                && !source[prev_end..tok.span.start].contains('\n')
176            {
177                let last_idx = filtered.len() - 1;
178                // Only attach if no trailing already recorded (shouldn't
179                // happen because `--` consumes through end-of-line).
180                if table.trailing[last_idx].is_none() {
181                    table.trailing[last_idx] = Some(body);
182                    continue;
183                }
184            }
185            // A doc block's token ends past its closing line's newline, so a
186            // comment under it is on its own line with no newline between. A
187            // `--|` doc line's token does too (#1888).
188            let own_line = !pending_leading.is_empty()
189                || filtered
190                    .last()
191                    .is_none_or(|t| matches!(t.kind, TokenKind::DocBlock | TokenKind::DocLine))
192                || last_content_end.is_some_and(|end| source[end..tok.span.start].contains('\n'));
193            if pending_leading.is_empty() {
194                pending_blank_before = own_line
195                    && last_content_end
196                        .is_some_and(|end| source[end..tok.span.start].matches('\n').count() >= 2);
197            }
198            pending_leading.push(Comment::Line(body));
199            last_leading_end = own_line.then_some(tok.span.end);
200            continue;
201        }
202        filtered.push(*tok);
203        table.leading.push(std::mem::take(&mut pending_leading));
204        table.trailing.push(None);
205        table
206            .blank_before
207            .push(std::mem::take(&mut pending_blank_before));
208        last_content_end = Some(tok.span.end);
209        last_leading_end = None;
210    }
211    table.epilogue = pending_leading;
212    (filtered, table)
213}
214
215/// Parse a token slice into a [`Commons`] AST.
216///
217/// Accepts either form of v0.3 commons file:
218/// - Brace form: `commons name { items... }` (v0–v0.2 compatible).
219/// - Fragment form: `commons name uses... items...` to EOF (v0.3).
220pub fn parse(tokens: &[Token], source: &str) -> Result<Commons, Vec<CompileError>> {
221    parse_with_warnings(tokens, source).map(|(c, _warnings)| c)
222}
223
224/// [`parse`] with the non-fatal diagnostics threaded out alongside the AST
225/// (ADR 0117) — see [`parse_units_with_warnings`].
226pub fn parse_with_warnings(
227    tokens: &[Token],
228    source: &str,
229) -> Result<(Commons, Vec<CompileError>), Vec<CompileError>> {
230    let (unit, warnings) = parse_unit_with_warnings(tokens, source)?;
231    match unit {
232        SourceUnit::Commons(c) => Ok((c, warnings)),
233        SourceUnit::Context(ctx) => Err(vec![
234            CompileError::new(
235                "bynk.parse.unexpected_context",
236                ctx.span,
237                "expected a `commons` declaration but found a `context` declaration",
238            )
239            .with_note(
240                "contexts must be compiled as part of a project — pass the source directory, e.g. `bynk compile --target bundle --output out src`",
241            ),
242        ]),
243        SourceUnit::Suite(t) => Err(vec![
244            CompileError::new(
245                "bynk.parse.unexpected_suite",
246                t.span,
247                "expected a `commons` declaration but found a `suite` declaration",
248            )
249            .with_note(
250                "tests must be compiled as part of a project — pass the source directory, e.g. `bynk compile --target bundle --output out src`",
251            ),
252        ]),
253        SourceUnit::Adapter(a) => Err(vec![
254            CompileError::new(
255                "bynk.parse.unexpected_adapter",
256                a.span,
257                "expected a `commons` declaration but found an `adapter` declaration",
258            )
259            .with_note(
260                "adapters must be compiled as part of a project — pass the source directory, e.g. `bynk compile --target bundle --output out src`",
261            ),
262        ]),
263    }
264}
265
266/// Parse a token slice into a [`SourceUnit`] with error recovery, returning a
267/// best-effort partial AST plus the full list of parse errors and warnings.
268///
269/// Used by the LSP: item-level recovery skips past a malformed declaration to
270/// the next top-level item, so multiple errors are reported per compilation
271/// rather than just the first. Compared to [`parse_unit`], this never bails;
272/// if no SourceUnit could be parsed at all (e.g. the file is empty or the
273/// header itself fails) the returned `Option` is `None`.
274///
275/// Keeps only the *first* unit — v0.113 allows more than one top-level unit
276/// per file (an atomic `commons` + `suite`, DECISION S), and every existing
277/// caller here is keyed on the primary declaration. [`parse_units_with_recovery`]
278/// is the same recovery parse without that narrowing, for the one caller
279/// (finding #29/#30) that needs every unit a file declares.
280pub fn parse_unit_with_recovery(
281    tokens: &[Token],
282    source: &str,
283) -> (Option<SourceUnit>, Vec<CompileError>) {
284    let (units, errors) = parse_units_with_recovery(tokens, source);
285    (units.into_iter().next(), errors)
286}
287
288/// [`parse_unit_with_recovery`], keeping **every** top-level unit instead of
289/// discarding all but the first (finding #29/#30). Used by the IDE's own parse
290/// entry point, which needs to see a trailing `suite` in an atomic
291/// `commons`+`suite` file, not just the primary declaration.
292pub fn parse_units_with_recovery(
293    tokens: &[Token],
294    source: &str,
295) -> (Vec<SourceUnit>, Vec<CompileError>) {
296    let recovered = parse_units_recovering(tokens, source);
297    (recovered.units, recovered.errors)
298}
299
300/// #1663: everything a recovering parse learns — the units it built, every
301/// syntax error, and the names of the top-level declarations it had to skip.
302#[derive(Debug)]
303pub struct Recovered {
304    pub units: Vec<SourceUnit>,
305    pub errors: Vec<CompileError>,
306    /// Declarations recovery dropped, by name (see `Parser::broken_decl_names`).
307    /// A caller keeps references to them from echoing as unknown names.
308    pub broken_decl_names: Vec<String>,
309}
310
311/// #1663: a strict parse's error(s) together with a recovering parse's, each
312/// reported once. The strict errors come first and always survive: some rules
313/// hold only for the strict single-unit parse (`extra_tokens`, a `commons`
314/// expected but a `context` found), which the multi-unit recovering parse
315/// accepts. A recovered error at the same position as one already listed is
316/// the same fault (the two parsers may name it differently) and is dropped.
317pub fn merge_syntax_errors(
318    strict: Vec<CompileError>,
319    recovered: Vec<CompileError>,
320) -> Vec<CompileError> {
321    let mut out = strict;
322    for e in recovered {
323        if !out.iter().any(|o| o.span.start == e.span.start) {
324            out.push(e);
325        }
326    }
327    out
328}
329
330/// [`parse_units_with_recovery`], also returning the names of the declarations
331/// recovery skipped (#1663).
332pub fn parse_units_recovering(tokens: &[Token], source: &str) -> Recovered {
333    parse_units_recovering_from(tokens, source, &mut 0)
334}
335
336/// [`parse_units_recovering`], continuing [`ExprId`] allocation from `next_id`
337/// rather than starting at 0 — see [`parse_unit_with_warnings_from`]. #1710:
338/// the project path checks a recovered file's surviving declarations
339/// alongside every other file's, so its ids must come from the same durable
340/// counter (`bynk_project::parse_cache`), or they would collide.
341pub fn parse_units_recovering_from(tokens: &[Token], source: &str, next_id: &mut u32) -> Recovered {
342    let (filtered, trivia) = split_trivia(tokens, source);
343    let mut warnings = Vec::new();
344    let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
345    p.recover_mode = true;
346    p.next_expr_id = *next_id;
347    let mut units = Vec::new();
348    loop {
349        match p.parse_unit() {
350            Ok(u) => units.push(u),
351            Err(e) => {
352                p.recovered_errors.push(e);
353                break;
354            }
355        }
356        // A genuinely malformed trailing declaration is still surfaced via
357        // recovery — checked *after* each successful parse, matching
358        // `parse_unit`'s own "at least once" attempt on the first unit (an
359        // empty file must still produce its usual unexpected-EOF diagnostic,
360        // not silently yield an empty `units` with no error at all).
361        if p.peek().is_none() {
362            break;
363        }
364    }
365    let broken_decl_names = std::mem::take(&mut p.broken_decl_names);
366    *next_id = p.next_expr_id;
367    let mut all_errors = p.recovered_errors;
368    all_errors.append(&mut p.doc_errors);
369    all_errors.append(&mut warnings);
370    Recovered {
371        units,
372        errors: all_errors,
373        broken_decl_names,
374    }
375}
376
377/// Parse a token slice into a [`SourceUnit`] — either a commons or a context.
378///
379/// Each `.bynk` file is exactly one declaration of one kind.
380pub fn parse_unit(tokens: &[Token], source: &str) -> Result<SourceUnit, Vec<CompileError>> {
381    parse_unit_with_warnings(tokens, source).map(|(unit, _warnings)| unit)
382}
383
384/// [`parse_unit`] with the non-fatal diagnostics threaded out alongside the
385/// AST (ADR 0117) — see [`parse_units_with_warnings`].
386pub fn parse_unit_with_warnings(
387    tokens: &[Token],
388    source: &str,
389) -> Result<(SourceUnit, Vec<CompileError>), Vec<CompileError>> {
390    parse_unit_with_warnings_from(tokens, source, &mut 0)
391}
392
393/// [`parse_unit_with_warnings`], continuing [`ExprId`] allocation from
394/// `next_id` instead of starting at 0, and writing the id one past the last
395/// one this parse handed out back into it. T3.4 (R2.4): every top-level parse
396/// entry point in this file constructs its own `Parser` and therefore its own
397/// zero-based id space; a caller that will check two files' output together
398/// in one pass (a multi-file commons — `bynk-emit`'s `collect_unit_methods`
399/// merges a type's methods from sibling files into the file that declares the
400/// type, before one `check_record` call) must thread one counter across every
401/// file it parses, or two independently-numbered files collide on the same
402/// id in the same `expr_types` map. Every *other* caller (a single buffer, an
403/// LSP hover/completion query, a fixture test) never merges its output with
404/// another file's before checking, so starting at 0 every time is correct —
405/// [`parse_unit_with_warnings`] above is that default, unchanged.
406pub fn parse_unit_with_warnings_from(
407    tokens: &[Token],
408    source: &str,
409    next_id: &mut u32,
410) -> Result<(SourceUnit, Vec<CompileError>), Vec<CompileError>> {
411    let (filtered, trivia) = split_trivia(tokens, source);
412    let mut warnings = Vec::new();
413    let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
414    p.next_expr_id = *next_id;
415    let result = match p.parse_unit() {
416        Ok(u) => {
417            if let Some(extra) = p.peek() {
418                Err(vec![
419                    CompileError::new(
420                        "bynk.parse.extra_tokens",
421                        extra.span,
422                        "unexpected token after top-level declaration",
423                    )
424                    .with_note(
425                        "a `.bynk` file contains exactly one `commons` or `context` declaration",
426                    ),
427                ])
428            } else {
429                Ok(u)
430            }
431        }
432        Err(e) => Err(vec![e]),
433    };
434    // #1885: a doc block that holds code fails the parse, after any error the
435    // parse itself hit.
436    let result = match (result, std::mem::take(&mut p.doc_errors)) {
437        (r, docs) if docs.is_empty() => r,
438        (Ok(_), docs) => Err(docs),
439        (Err(mut errs), mut docs) => {
440            errs.append(&mut docs);
441            Err(errs)
442        }
443    };
444    *next_id = p.next_expr_id;
445    // ADR 0117: warnings (e.g. orphan doc blocks) ride alongside a successful
446    // parse — severity governs gating at the caller, not here.
447    match result {
448        Ok(u) => {
449            // See `parse_units_with_warnings`: a file-trailing comment must
450            // have been drained by `take_epilogue`.
451            debug_assert!(
452                p.trivia.epilogue_is_empty(),
453                "a file-trailing comment was left undrained after a successful parse"
454            );
455            Ok((u, warnings))
456        }
457        Err(mut errs) => {
458            errs.append(&mut warnings);
459            Err(errs)
460        }
461    }
462}
463
464/// Parse a token slice into **all** the top-level [`SourceUnit`]s in one file
465/// (v0.113, testing track slice 1b). A `.bynk` file may hold more than one
466/// top-level declaration — an *atomic* file with `commons`/`context` **and** a
467/// `suite` together (DECISION S) — so the compiler parses a `Vec`, not a single
468/// unit. Test-ness is a property of each declaration, not of the file.
469///
470/// Bails on the first malformed declaration (like [`parse_unit`], not the
471/// recovering LSP path). An empty file is an error.
472pub fn parse_units(tokens: &[Token], source: &str) -> Result<Vec<SourceUnit>, Vec<CompileError>> {
473    parse_units_with_warnings(tokens, source).map(|(units, _warnings)| units)
474}
475
476/// [`parse_units`] with the non-fatal diagnostics threaded out alongside the
477/// AST (ADR 0117): a successful parse returns `Ok((units, warnings))` instead
478/// of hard-failing on a warning-severity diagnostic (an orphan doc block used
479/// to abort file discovery and throw the good AST away). A failed parse still
480/// returns every diagnostic — errors then warnings — in the `Err`.
481pub fn parse_units_with_warnings(
482    tokens: &[Token],
483    source: &str,
484) -> Result<(Vec<SourceUnit>, Vec<CompileError>), Vec<CompileError>> {
485    parse_units_with_drain_check(tokens, source)
486        .map(|(units, warnings, _drained)| (units, warnings))
487}
488
489/// [`parse_units_with_warnings`], continuing [`ExprId`] allocation from
490/// `next_id` rather than starting at 0 — see [`parse_unit_with_warnings_from`]
491/// for why this exists. The one production caller is `bynk-emit`'s per-file
492/// parse loop (`phase_parse`), which owns one counter across every file in a
493/// single project parse so two files whose methods later get merged into one
494/// `check_record` call (a multi-file commons) never collide.
495pub fn parse_units_with_warnings_from(
496    tokens: &[Token],
497    source: &str,
498    next_id: &mut u32,
499) -> Result<(Vec<SourceUnit>, Vec<CompileError>), Vec<CompileError>> {
500    parse_units_with_drain_check_from(tokens, source, next_id)
501        .map(|(units, warnings, _drained)| (units, warnings))
502}
503
504/// [`parse_units_with_warnings`] plus whether every comment's trivia was
505/// drained into the AST (`TriviaTable::is_fully_drained`). Finding #66:
506/// `bynk-fmt`'s comment-preservation guard re-tokenized its own rendered
507/// output just to diff comment bodies against the input — wasted work in the
508/// overwhelming common case where nothing was left behind. That guard uses
509/// this drain signal, computed from the same parse it already needs for
510/// rendering, as a fast-path: `true` means every comment landed in the AST,
511/// so re-checking the output can be skipped outright. No other caller needs
512/// the signal, so it rides its own entry point rather than widening
513/// [`parse_units_with_warnings`].
514pub fn parse_units_with_drain_check(
515    tokens: &[Token],
516    source: &str,
517) -> Result<(Vec<SourceUnit>, Vec<CompileError>, bool), Vec<CompileError>> {
518    parse_units_with_drain_check_from(tokens, source, &mut 0)
519}
520
521/// [`parse_units_with_drain_check`], continuing [`ExprId`] allocation from
522/// `next_id` — see [`parse_unit_with_warnings_from`].
523pub fn parse_units_with_drain_check_from(
524    tokens: &[Token],
525    source: &str,
526    next_id: &mut u32,
527) -> Result<(Vec<SourceUnit>, Vec<CompileError>, bool), Vec<CompileError>> {
528    parse_units_drain(tokens, source, next_id, false)
529}
530
531/// #2203 (DECISION F): [`parse_units_with_drain_check`] for the formatter,
532/// which accepts the legacy `--` comment and `--|` doc line spellings instead
533/// of reporting them, since printing them as `//` and `--- ` is the
534/// migration. Every other error is reported as usual.
535pub fn parse_units_migrating(
536    tokens: &[Token],
537    source: &str,
538) -> Result<(Vec<SourceUnit>, Vec<CompileError>, bool), Vec<CompileError>> {
539    parse_units_drain(tokens, source, &mut 0, true)
540}
541
542fn parse_units_drain(
543    tokens: &[Token],
544    source: &str,
545    next_id: &mut u32,
546    migrate: bool,
547) -> Result<(Vec<SourceUnit>, Vec<CompileError>, bool), Vec<CompileError>> {
548    let (filtered, trivia) = split_trivia(tokens, source);
549    let mut warnings = Vec::new();
550    let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
551    if migrate {
552        p.doc_errors.clear();
553    }
554    p.next_expr_id = *next_id;
555    let mut units = Vec::new();
556    let mut errors: Vec<CompileError> = Vec::new();
557    while p.peek().is_some() {
558        match p.parse_unit() {
559            Ok(u) => units.push(u),
560            Err(e) => {
561                errors.push(e);
562                break;
563            }
564        }
565    }
566    // #1885: a doc block that holds code fails the parse.
567    errors.append(&mut p.doc_errors);
568    *next_id = p.next_expr_id;
569    let eof = p.eof_span();
570    let fully_drained = p.trivia.is_fully_drained();
571    // `p` (and thus its `&mut warnings` borrow) is no longer used past here, so
572    // the local `warnings` are readable again.
573    if !errors.is_empty() {
574        errors.append(&mut warnings);
575        return Err(errors);
576    }
577    if units.is_empty() {
578        return Err(vec![CompileError::new(
579            "bynk.parse.unexpected_eof",
580            eof,
581            "expected `commons`, `context`, or `suite` to start the file, found end of file",
582        )]);
583    }
584    // A file-trailing comment must have been drained by `take_epilogue` — the
585    // one brace-form declarations forgot to call (a live comment-loss bug,
586    // not the fundamentally-unfixed expression-interior case: expressions
587    // carry no trivia at all, so asserting full drainage here would fire on
588    // any ordinary program with a comment inside a `match`/list/record, which
589    // `bynk-fmt`'s own comment-loss guard already handles gracefully rather
590    // than as a hard failure).
591    debug_assert!(
592        p.trivia.epilogue_is_empty(),
593        "a file-trailing comment was left undrained after a successful parse"
594    );
595    Ok((units, warnings, fully_drained))
596}
597
598/// A signed numeric literal in refinement-bound position (v0.21): `InRange`
599/// bounds are either both `Int` or both `Float`.
600enum SignedNumLit {
601    Int(IntBound),
602    Float(FloatBound),
603    Decimal(DecimalBound),
604}
605
606struct Parser<'a> {
607    tokens: &'a [Token],
608    source: &'a str,
609    pos: usize,
610    /// Accumulated non-fatal diagnostics. v0.3 uses this for orphan-doc
611    /// warnings, which are emitted as errors with a distinguishable category.
612    warnings: &'a mut Vec<CompileError>,
613    /// When true, the item-level loops catch errors from individual item
614    /// parses, push them into `recovered_errors`, and skip forward to the
615    /// next top-level item boundary instead of bailing. Used by the LSP via
616    /// [`parse_unit_with_recovery`]; disabled in the normal `parse` path so
617    /// existing single-error behaviour is preserved.
618    recover_mode: bool,
619    /// Errors collected during recovery-mode parsing. Only populated when
620    /// `recover_mode` is true.
621    recovered_errors: Vec<CompileError>,
622    /// #1663: the token index where the current top-level item starts, set by
623    /// each top-level item loop. Read once by [`Self::handle_item_err`] to name
624    /// the declaration a recovery skips.
625    pub(crate) item_start: Option<usize>,
626    /// #1663: the names of top-level declarations recovery skipped (a `type`,
627    /// `fn`, method, `event`, `capability`, `service`, `agent` or `actor`).
628    /// They are known names whose declaration is broken, so a caller can keep
629    /// references to them from echoing as unknown. Recovery mode only.
630    broken_decl_names: Vec<String>,
631    /// Line-comment trivia separated from the token stream. See
632    /// [`TriviaTable`].
633    trivia: TriviaTable,
634    /// Live recursion depth of the three self-recursive parse entry points
635    /// (`parse_expr`, `parse_type_ref`, `parse_pattern`). Incremented on entry
636    /// and decremented on exit by [`Parser::enter_recursion`] so it tracks the
637    /// current stack depth; when it exceeds [`crate::MAX_NESTING_DEPTH`] the
638    /// parser reports a bounded-depth diagnostic instead of overflowing its
639    /// stack (#713).
640    depth: usize,
641    /// When true, a bare `ident {` on the *spine* of the current expression is
642    /// an identifier followed by an unrelated block, never a record
643    /// construction — so an `if`/`match` condition that ends in a bare
644    /// identifier does not swallow the branch/arm block as `Ident { field }`
645    /// (#636). Set only around the condition parse (see [`parse_cond_expr`]);
646    /// `parse_expr` clears it, so the restriction is lifted inside any
647    /// delimited sub-expression (parentheses, call arguments, list, record
648    /// field). Mirrors Rust's `NO_STRUCT_LITERAL` restriction.
649    no_record_literal: bool,
650    /// #2721: set while parsing a match-arm guard's spine, where a `(` is never a
651    /// lambda head: the `=>` after `)` is the arm's own arrow. Cleared by
652    /// `parse_expr` on entry to any delimited sub-expression.
653    in_guard: bool,
654    /// True while parsing the tail of an `is` pattern that sits outside every
655    /// delimiter — `x is P`, `x is A | B`. A record pattern there would be
656    /// ambiguous with the block that follows an `if` condition, so it must be
657    /// parenthesised (`x is (T { .. })`, #2641; the patterns track's Q2 in
658    /// `design/archive/retired-tracks.md`). Cleared by
659    /// [`Parser::parse_pattern`], the entry every delimited (nested) pattern
660    /// goes through.
661    is_pattern_tail: bool,
662    /// Running count of unclosed `{` seen so far — maintained solely by
663    /// [`Self::bump`] (the one primitive that advances `self.pos`), so it
664    /// always reflects the true nesting depth no matter which parse function
665    /// is on the call stack. Finding #27/#30: `recover_to_top_item` reads it
666    /// against [`Self::item_loop_baseline`] to tell "the enclosing item
667    /// loop's own closing brace" apart from a still-unclosed nested
668    /// construct's — without it, a sync scan that started partway through
669    /// such a construct (an error deep inside a function body) stopped at the
670    /// first `}` it saw, however deeply nested, and the enclosing item loop
671    /// mistook that for its own body's end.
672    brace_depth: usize,
673    /// Stack of `brace_depth` snapshots, one per active item-loop body
674    /// (commons/context/adapter/suite) — pushed right after that body's own
675    /// `{` is consumed (or at loop entry, for a brace-free fragment form),
676    /// popped at the loop's normal exit. `recover_to_top_item` treats its top
677    /// entry as the depth an `}` must return to before it counts as the
678    /// enclosing body's own closing brace rather than a nested construct's.
679    item_loop_baseline: Vec<usize>,
680    /// T3.4 (R2.4): next [`ExprId`] to hand out — monotonic, incremented by
681    /// [`Self::alloc_expr_id`], the sole allocation point every `Expr`
682    /// construction site in this parser calls.
683    next_expr_id: u32,
684    /// #1885: `bynk.parse.doc_block_contains_code` errors found by
685    /// [`Self::take_doc_block`], with its `bynk.parse.multiple_docs` (#2203)
686    /// and the legacy-spelling errors [`split_trivia`] found. Kept apart from the item `Result` path so a
687    /// doc block, which every item loop reads before its recovery point, never
688    /// aborts the body it sits in; each entry point turns them into errors.
689    doc_errors: Vec<CompileError>,
690    /// #1885: true for the parser [`doc_content_is_code`] runs over a doc
691    /// block's content. It skips the code check on doc blocks inside that
692    /// content, so the probe stays a plain parse.
693    doc_probe: bool,
694}
695
696impl<'a> Parser<'a> {
697    fn new(
698        tokens: &'a [Token],
699        source: &'a str,
700        mut trivia: TriviaTable,
701        warnings: &'a mut Vec<CompileError>,
702    ) -> Self {
703        let doc_errors = std::mem::take(&mut trivia.legacy);
704        Self {
705            tokens,
706            source,
707            pos: 0,
708            warnings,
709            recover_mode: false,
710            recovered_errors: Vec::new(),
711            item_start: None,
712            broken_decl_names: Vec::new(),
713            trivia,
714            depth: 0,
715            no_record_literal: false,
716            in_guard: false,
717            is_pattern_tail: false,
718            brace_depth: 0,
719            item_loop_baseline: Vec::new(),
720            next_expr_id: 0,
721            doc_errors,
722            doc_probe: false,
723        }
724    }
725
726    /// T3.4 (R2.4): allocate the next [`ExprId`]. The sole allocation point —
727    /// every `Expr { id: self.alloc_expr_id(), .. }` construction in this
728    /// parser calls it exactly once, so two nodes never share an id and every
729    /// id a caller holds was actually handed out here.
730    fn alloc_expr_id(&mut self) -> ExprId {
731        let id = ExprId(self.next_expr_id);
732        self.next_expr_id += 1;
733        id
734    }
735
736    /// Enter a self-recursive parse step, bumping the live recursion depth and
737    /// failing with a bounded-depth diagnostic if it would exceed
738    /// [`crate::MAX_NESTING_DEPTH`]. The caller pairs a successful entry with a
739    /// matching `self.depth -= 1` on the way out (see `parse_expr` /
740    /// `parse_type_ref`); on the error path the depth is restored here so a
741    /// recovering caller is not left mis-counted. `what` names the construct
742    /// for the message (e.g. "this expression", "this type"). See #713.
743    fn enter_recursion(&mut self, what: &str) -> Result<(), CompileError> {
744        self.depth += 1;
745        if self.depth > crate::MAX_NESTING_DEPTH {
746            self.depth -= 1;
747            let span = self
748                .peek()
749                .map(|t| t.span)
750                .unwrap_or_else(|| self.eof_span());
751            return Err(self.nesting_too_deep(span, what));
752        }
753        Ok(())
754    }
755
756    /// The bounded-depth diagnostic shared by [`enter_recursion`] and
757    /// [`enter_chain_fold`].
758    fn nesting_too_deep(&self, span: Span, what: &str) -> CompileError {
759        CompileError::new(
760            "bynk.parse.nesting_too_deep",
761            span,
762            format!(
763                "{what} nests more than {} levels deep",
764                crate::MAX_NESTING_DEPTH
765            ),
766        )
767        .with_note(
768            "deeply nested source is rejected to keep the parser from overflowing its \
769             stack and aborting; flatten or split the construct",
770        )
771    }
772
773    /// The bounded-depth diagnostic for the *iteratively*-built spines —
774    /// associative operator chains ([`enter_chain_fold`]) and postfix receiver
775    /// chains ([`deepen_spine`]). Same code as [`nesting_too_deep`] (one budget,
776    /// one diagnostic) but phrased for a flat chain, which is long rather than
777    /// *nested*, and points at the idiomatic fix.
778    fn expression_too_long(&self, span: Span) -> CompileError {
779        CompileError::new(
780            "bynk.parse.nesting_too_deep",
781            span,
782            format!(
783                "this expression is more than {} levels deep",
784                crate::MAX_NESTING_DEPTH
785            ),
786        )
787        .with_note(
788            "a long operator or member chain is rejected to keep the compiler from overflowing \
789             its stack; split it across `let` bindings, or reduce a sequence with \
790             `.sum()`/`.fold(...)`",
791        )
792    }
793
794    /// Count one more operand folded onto an associative operator chain against
795    /// the same recursion budget as [`enter_recursion`] (#714).
796    ///
797    /// Associative chains (`+`, `*`, `&&`, `||`) are built *iteratively* in the
798    /// precedence ladder, so — unlike parentheses, calls, or `implies` — they
799    /// never re-enter `parse_expr` and thus slip past the `enter_recursion`
800    /// guard. Yet each fold deepens the left-nested `Expr` tree by one level,
801    /// and a long flat chain (`1 + 1 + … + 1`) overflows every *recursive*
802    /// consumer of that tree downstream — the checker's `type_of`, the
803    /// formatter, the emitter, and the AST's own recursive `Drop` — exactly as
804    /// deeply nested source overflows the parser. Counting each fold on the
805    /// shared `depth` budget bounds the whole expression's height, and because
806    /// it is the *same* budget it composes with the ambient nesting depth, so a
807    /// chain buried inside deeply nested source cannot exceed the bound either.
808    ///
809    /// The caller accumulates `folds` and subtracts them from `depth` before it
810    /// returns, so the live count unwinds as a recursive descent would; on the
811    /// overflow path the whole chain's contribution is restored here so a
812    /// recovering caller is not left mis-counted.
813    fn enter_chain_fold(&mut self, folds: &mut usize, span: Span) -> Result<(), CompileError> {
814        self.depth += 1;
815        *folds += 1;
816        if self.depth > crate::MAX_NESTING_DEPTH {
817            self.depth -= *folds;
818            *folds = 0;
819            return Err(self.expression_too_long(span));
820        }
821        Ok(())
822    }
823
824    /// Count one more level of an iteratively-built postfix receiver spine
825    /// (`a.b.c…`, `f()?.g()…`) against the shared budget (#714). Like
826    /// [`enter_chain_fold`], postfix loops rather than recurses, so a long spine
827    /// escapes [`enter_recursion`] yet grows an arbitrarily deep receiver tree
828    /// that the downstream walks recurse through. `parse_postfix` restores
829    /// `depth` wholesale on the way out (its many error paths make a
830    /// save/restore wrapper cleaner than per-fold unwinding), so this only bumps
831    /// and checks.
832    fn deepen_spine(&mut self, span: Span) -> Result<(), CompileError> {
833        self.depth += 1;
834        if self.depth > crate::MAX_NESTING_DEPTH {
835            return Err(self.expression_too_long(span));
836        }
837        Ok(())
838    }
839
840    /// Comments immediately preceding the current peek position. Consumed
841    /// (the table entry is cleared) so the same comments are not attached
842    /// to two nodes.
843    ///
844    /// For a slot in a body: canonical style has no blank lines there, so a
845    /// [`Comment::Blank`] is dropped and the comments stay attached to what
846    /// follows. A declaration set apart by blank lines (one at unit level, or a
847    /// service's or agent's section or handler) keeps it, through
848    /// [`Self::take_unit_leading_trivia`].
849    fn take_leading_trivia(&mut self) -> Vec<Comment> {
850        let mut comments = self.take_unit_leading_trivia();
851        comments.retain(|c| *c != Comment::Blank);
852        comments
853    }
854
855    /// [`Self::take_leading_trivia`] for a slot set apart by blank lines, keeping each
856    /// [`Comment::Blank`] (#1884).
857    fn take_unit_leading_trivia(&mut self) -> Vec<Comment> {
858        self.trivia.take_leading(self.pos)
859    }
860
861    /// The comments after the last token, which close the file at unit level,
862    /// so a [`Comment::Blank`] between them is kept.
863    fn take_epilogue_trivia(&mut self) -> Vec<Comment> {
864        self.trivia.take_epilogue()
865    }
866
867    /// Trailing comment, if any, on the same source line as the most
868    /// recently consumed content token. Call AFTER finishing a declaration
869    /// or statement, while `self.pos` points one past its last token.
870    fn take_trailing_trivia(&mut self) -> Option<String> {
871        if self.pos == 0 {
872            return None;
873        }
874        self.trivia.take_trailing(self.pos - 1)
875    }
876
877    /// Handle a per-item parse error. In recovery mode, record the error and
878    /// advance to the next sync point so the item loop can continue; otherwise
879    /// propagate as a hard failure.
880    fn handle_item_err(&mut self, e: CompileError) -> Result<(), CompileError> {
881        if self.recover_mode {
882            self.recovered_errors.push(e);
883            if let Some(start) = self.item_start.take() {
884                self.broken_decl_names.extend(self.decl_names_at(start));
885            }
886            let before = self.pos;
887            self.recover_to_top_item();
888            // The sync target may be the very token that produced the error —
889            // a context-only keyword (`capability`, `service`, …) at item
890            // position in a commons errors *without consuming it*, and it is
891            // itself a sync point. Recovery must always make progress, or the
892            // item loop re-reports the same error until memory runs out
893            // (found by the `parse` fuzz target on a seed input).
894            // #1663: unless the cursor is on the `}` that closes this item
895            // loop's own body (an error raised at the end of the last item):
896            // consuming it would make the body look unclosed, a follow-on
897            // `unexpected_eof`. The loop ends on that `}` itself. A fragment-
898            // form loop has no closing brace (baseline 0), so a stray `}`
899            // there is still consumed.
900            let baseline = self.item_loop_baseline.last().copied().unwrap_or(0);
901            let closes_body = baseline > 0
902                && self.brace_depth == baseline
903                && self.peek_kind() == Some(TokenKind::RBrace);
904            if self.pos == before && !closes_body {
905                self.bump();
906                // #1663: and skip the rest of the rejected item too (`agent`
907                // in a commons: its name and `{ … }` body). Otherwise the next
908                // loop iteration misreads its name as a malformed item and
909                // reports a second, follow-on syntax error.
910                self.recover_to_top_item();
911            }
912            Ok(())
913        } else {
914            Err(e)
915        }
916    }
917
918    /// #1663: the name(s) a declaration starting at token `start` declares:
919    /// `type T`, `fn f`, `fn T.m` (recorded as `T.m`), `event E`,
920    /// `capability C`, `service S`, `agent A`, `actor A`. Empty when the item
921    /// is not one of those or its name never parsed (`fn ( -> Int`).
922    fn decl_names_at(&self, start: usize) -> Vec<String> {
923        let kind_at = |i: usize| self.tokens.get(i).map(|t| t.kind);
924        let ident_at = |i: usize| {
925            self.tokens
926                .get(i)
927                .filter(|t| t.kind == TokenKind::Ident)
928                .map(|t| self.slice(t.span).to_string())
929        };
930        match kind_at(start) {
931            Some(
932                TokenKind::Type
933                | TokenKind::Event
934                | TokenKind::Capability
935                | TokenKind::Service
936                | TokenKind::Agent
937                | TokenKind::Actor,
938            ) => ident_at(start + 1).into_iter().collect(),
939            Some(TokenKind::Fn) => match (ident_at(start + 1), kind_at(start + 2)) {
940                // A method is recorded qualified, `T.m`, so a broken method
941                // is never mistaken for a free `fn m` of the same name.
942                (Some(owner), Some(TokenKind::Dot)) => match ident_at(start + 3) {
943                    Some(method) => vec![format!("{owner}.{method}")],
944                    None => Vec::new(),
945                },
946                (Some(name), _) => vec![name],
947                (None, _) => Vec::new(),
948            },
949            _ => Vec::new(),
950        }
951    }
952
953    /// Skip forward to the next top-level item boundary: either an
954    /// [`is_item_start`] keyword at the enclosing item loop's own nesting
955    /// depth, a closing brace that returns to that depth, or end-of-input.
956    /// Used only in recovery mode.
957    ///
958    /// Finding #27/#30: brace-depth-gated against
959    /// [`Self::item_loop_baseline`], so a `}` deep inside a still-unclosed
960    /// nested construct (an error partway through a function body, itself
961    /// inside a `match` arm) is skipped over rather than mistaken for the
962    /// enclosing body's own closing brace — the old flat scan stopped at
963    /// literally the first `}` it saw, however deep, handing the item loop a
964    /// brace that did not belong to it and making it return with zero items.
965    fn recover_to_top_item(&mut self) {
966        let baseline = self.item_loop_baseline.last().copied().unwrap_or(0);
967        while let Some(t) = self.peek() {
968            match t.kind {
969                TokenKind::RBrace if self.brace_depth == baseline => return,
970                _ if self.brace_depth == baseline && is_item_start(t.kind) => return,
971                _ => {
972                    self.bump();
973                }
974            }
975        }
976    }
977
978    /// Mark the start of a top-level item loop (`declarations.rs`'s
979    /// `parse_commons_brace`/`_fragment`, `parse_context_brace`/`_fragment`,
980    /// `parse_test_brace`/`_fragment`, `parse_adapter_body`) — called right
981    /// after that body's own `{` is consumed (brace form) or at the loop's
982    /// own entry (fragment form, which has no enclosing brace of its own).
983    /// Paired with [`Self::exit_item_loop`] at the loop's normal exit.
984    fn enter_item_loop(&mut self) {
985        self.item_loop_baseline.push(self.brace_depth);
986    }
987
988    /// Pair of [`Self::enter_item_loop`].
989    fn exit_item_loop(&mut self) {
990        self.item_loop_baseline.pop();
991    }
992
993    fn peek(&self) -> Option<Token> {
994        self.tokens.get(self.pos).copied()
995    }
996
997    fn peek_kind(&self) -> Option<TokenKind> {
998        self.peek().map(|t| t.kind)
999    }
1000
1001    /// #2618: consume a leading `private` modifier at item position and return
1002    /// its span. `private` is contextual, not reserved: it is a modifier only
1003    /// when it is an identifier spelled `private` immediately followed by a
1004    /// declaration keyword, so `@cache(scope: private)` and a binding named
1005    /// `private` keep their meaning.
1006    ///
1007    /// The modifier applies to `fn`, `type`, `messages`, `asset`, `embed` and
1008    /// `const` in a commons or context (`allowed`). On any other declaration, or in an
1009    /// adapter, it is `bynk.visibility.private_not_allowed`: an error in
1010    /// recovery mode that leaves the declaration to parse as if unmodified,
1011    /// and the parse's error otherwise.
1012    fn take_private_modifier(&mut self, allowed: bool) -> Result<Option<Span>, CompileError> {
1013        let Some(t) = self.peek() else {
1014            return Ok(None);
1015        };
1016        if t.kind != TokenKind::Ident || self.slice(t.span) != "private" {
1017            return Ok(None);
1018        }
1019        let Some(next) = self.nth(1) else {
1020            return Ok(None);
1021        };
1022        let modifiable = matches!(
1023            next.kind,
1024            TokenKind::Fn
1025                | TokenKind::Type
1026                | TokenKind::Messages
1027                | TokenKind::Asset
1028                | TokenKind::Embed
1029                | TokenKind::Const
1030        );
1031        let declaration = modifiable
1032            || matches!(
1033                next.kind,
1034                TokenKind::Service
1035                    | TokenKind::Agent
1036                    | TokenKind::Capability
1037                    | TokenKind::Provides
1038                    | TokenKind::Actor
1039                    | TokenKind::Event
1040            );
1041        if !declaration {
1042            return Ok(None);
1043        }
1044        self.bump();
1045        if modifiable && allowed {
1046            return Ok(Some(t.span));
1047        }
1048        let what = self.slice(next.span);
1049        let err = if allowed {
1050            CompileError::new(
1051                "bynk.visibility.private_not_allowed",
1052                t.span,
1053                format!("`private` does not apply to `{what}` declarations"),
1054            )
1055            .with_note(
1056                "`private` applies to `fn`, `type`, `messages`, `asset`, `embed` and `const`; a context's `exports` and `consumes` govern how far its services, agents, capabilities, providers, actors and events reach",
1057            )
1058        } else {
1059            CompileError::new(
1060                "bynk.visibility.private_not_allowed",
1061                t.span,
1062                "`private` applies only to declarations in a commons or context, not in an adapter",
1063            )
1064            .with_note("an adapter's `exports` governs what its consumers see")
1065        };
1066        if self.recover_mode {
1067            self.recovered_errors.push(err);
1068            Ok(None)
1069        } else {
1070            Err(err)
1071        }
1072    }
1073
1074    /// The token `n` positions ahead of the cursor (`nth(0)` == `peek()`).
1075    fn nth(&self, n: usize) -> Option<Token> {
1076        self.tokens.get(self.pos + n).copied()
1077    }
1078
1079    fn nth_kind(&self, n: usize) -> Option<TokenKind> {
1080        self.nth(n).map(|t| t.kind)
1081    }
1082
1083    /// The source text of the token `n` positions ahead, or `""` if none.
1084    fn nth_text(&self, n: usize) -> &'a str {
1085        self.nth(n).map(|t| self.slice(t.span)).unwrap_or("")
1086    }
1087
1088    /// The span of the most recently consumed token (`self.pos - 1`). Falls back
1089    /// to the current token's span when nothing has been consumed yet.
1090    fn prev_span(&self) -> Span {
1091        self.tokens
1092            .get(self.pos.wrapping_sub(1))
1093            .or_else(|| self.peek_ref())
1094            .map(|t| t.span)
1095            .unwrap_or_default()
1096    }
1097
1098    fn peek_ref(&self) -> Option<&Token> {
1099        self.tokens.get(self.pos)
1100    }
1101
1102    fn bump(&mut self) -> Option<Token> {
1103        let t = self.peek();
1104        if let Some(t) = t {
1105            match t.kind {
1106                TokenKind::LBrace => self.brace_depth += 1,
1107                TokenKind::RBrace => self.brace_depth = self.brace_depth.saturating_sub(1),
1108                _ => {}
1109            }
1110            self.pos += 1;
1111        }
1112        t
1113    }
1114
1115    fn eat(&mut self, kind: TokenKind) -> Option<Token> {
1116        if self.peek_kind() == Some(kind) {
1117            self.bump()
1118        } else {
1119            None
1120        }
1121    }
1122
1123    fn slice(&self, span: Span) -> &'a str {
1124        &self.source[span.range()]
1125    }
1126
1127    /// True when the next token sits on a later line than `prev`. Used to
1128    /// keep a `[` that opens a new line out of the postfix type-application
1129    /// form: `f` followed by `[1, 2]` on the next line is an identifier and
1130    /// a list literal, not `f[…]` (v0.20b).
1131    fn next_token_on_new_line(&self, prev: Span) -> bool {
1132        match self.peek() {
1133            Some(t) if prev.end <= t.span.start => {
1134                self.source[prev.end..t.span.start].contains('\n')
1135            }
1136            _ => false,
1137        }
1138    }
1139
1140    /// Span pointing at the end of input — used for "unexpected EOF" reports.
1141    /// The start backs up to the **start of the final char**, not `len - 1`, so
1142    /// the span never splits a multibyte codepoint (an unterminated construct
1143    /// whose last line ends in non-ASCII — e.g. a `--` comment ending in `→`).
1144    fn eof_span(&self) -> Span {
1145        let end = self.source.len();
1146        let start = (0..end)
1147            .rev()
1148            .find(|&i| self.source.is_char_boundary(i))
1149            .unwrap_or(0);
1150        Span::new(start, end)
1151    }
1152
1153    fn expect(&mut self, kind: TokenKind, ctx: &str) -> Result<Token, CompileError> {
1154        match self.peek() {
1155            Some(t) if t.kind == kind => {
1156                self.bump();
1157                Ok(t)
1158            }
1159            Some(t) => Err(CompileError::new(
1160                "bynk.parse.expected_token",
1161                t.span,
1162                format!(
1163                    "expected {} {ctx}, found {}",
1164                    kind.describe(),
1165                    t.kind.describe()
1166                ),
1167            )),
1168            None => Err(CompileError::new(
1169                "bynk.parse.unexpected_eof",
1170                self.eof_span(),
1171                format!("expected {} {ctx}, found end of file", kind.describe()),
1172            )),
1173        }
1174    }
1175
1176    fn expect_ident(&mut self, ctx: &str) -> Result<Ident, CompileError> {
1177        match self.peek() {
1178            Some(t) if t.kind == TokenKind::Ident => {
1179                self.bump();
1180                Ok(Ident {
1181                    name: self.slice(t.span).to_string(),
1182                    span: t.span,
1183                })
1184            }
1185            // v0.5 contextual keyword `on` doubles as an identifier in
1186            // expression / field-access positions so users can name fields and
1187            // parameters using it. It retains its keyword meaning only at
1188            // handler-decl-level (`on call(...)`).
1189            //
1190            // v0.7 / v0.112: `suite` and `case` are contextual too — they
1191            // introduce the suite declaration and its cases, but are perfectly
1192            // valid commons/context/field names otherwise.
1193            //
1194            // The tier is single-sourced in `keywords::RESERVED_CONTEXTUAL`:
1195            // this arm defers to it rather than hardcoding the token kinds, so
1196            // extending that list is enough to admit a new contextual keyword
1197            // here. Each of these words lexes only to its own token, so matching
1198            // the source text is equivalent to matching the kind.
1199            Some(t) if crate::keywords::is_reserved_contextual(self.slice(t.span)) => {
1200                self.bump();
1201                Ok(Ident {
1202                    name: self.slice(t.span).to_string(),
1203                    span: t.span,
1204                })
1205            }
1206            Some(t) if is_reserved_keyword(t.kind) => Err(CompileError::new(
1207                "bynk.parse.reserved_keyword",
1208                t.span,
1209                format!(
1210                    "expected identifier {ctx}, but `{}` is a reserved keyword",
1211                    self.slice(t.span)
1212                ),
1213            )
1214            .with_note("rename the identifier to something that is not a keyword")),
1215            Some(t) => Err(CompileError::new(
1216                "bynk.parse.expected_token",
1217                t.span,
1218                format!("expected identifier {ctx}, found {}", t.kind.describe()),
1219            )),
1220            None => Err(CompileError::new(
1221                "bynk.parse.unexpected_eof",
1222                self.eof_span(),
1223                format!("expected identifier {ctx}, found end of file"),
1224            )),
1225        }
1226    }
1227
1228    /// True when the cursor is on a reserved keyword followed by `:`: a
1229    /// keyword used as a label (#2837). The `:` is what separates a label from
1230    /// a binding, so `type: t` is a field name while `type` alone stays refused.
1231    fn at_keyword_label(&self) -> bool {
1232        matches!(self.peek(), Some(t) if is_reserved_keyword(t.kind))
1233            && self.tokens.get(self.pos + 1).map(|t| t.kind) == Some(TokenKind::Colon)
1234    }
1235
1236    /// Like [`Parser::expect_ident`], but admits any reserved keyword that is
1237    /// followed by `:` (#2837): a field declaration, a construction
1238    /// initialiser, a record or named pattern field and a variant payload
1239    /// field are labels, never bindings. A keyword without the `:` keeps the
1240    /// `reserved_keyword` refusal, which is how shorthand stays an error.
1241    pub(crate) fn expect_label(&mut self, ctx: &str) -> Result<Ident, CompileError> {
1242        if self.at_keyword_label() {
1243            let t = self.bump().expect("peeked a keyword");
1244            return Ok(Ident {
1245                name: self.slice(t.span).to_string(),
1246                span: t.span,
1247            });
1248        }
1249        self.expect_ident(ctx)
1250    }
1251
1252    /// The targeted refusal for a keyword written as field shorthand (#2837):
1253    /// `Account { type, .. }` as a pattern would bind a variable named `type`,
1254    /// and `Account { type }` as a construction would read one. `None` when the
1255    /// cursor is not on such a shorthand, so the caller falls back to
1256    /// [`Parser::expect_ident`]'s plain `reserved_keyword`.
1257    ///
1258    /// A pattern gets the fix `type: type_`. The parser cannot see scope, so the
1259    /// fresh name is the first `type_`, `type_2`, … that no identifier in the
1260    /// file spells: over-cautious, never colliding, and `MaybeIncorrect` all the
1261    /// same. A construction gets a note and no fix: no variable called `type`
1262    /// can exist, so there is no value to suggest.
1263    pub(crate) fn keyword_shorthand_error(&self, pattern: bool) -> Option<CompileError> {
1264        let t = self.peek()?;
1265        if !is_reserved_keyword(t.kind) {
1266            return None;
1267        }
1268        let next = self.tokens.get(self.pos + 1).map(|t| t.kind);
1269        let shorthand = match next {
1270            Some(TokenKind::Comma | TokenKind::RBrace) => true,
1271            Some(TokenKind::DotDot) => pattern,
1272            _ => false,
1273        };
1274        if !shorthand {
1275            return None;
1276        }
1277        let word = self.slice(t.span);
1278        let mut err = CompileError::new(
1279            "bynk.parse.keyword_field_shorthand",
1280            t.span,
1281            format!("`{word}` is a reserved keyword, so it cannot be a field shorthand"),
1282        );
1283        if pattern {
1284            let taken = |name: &str| {
1285                self.tokens
1286                    .iter()
1287                    .any(|k| k.kind == TokenKind::Ident && self.slice(k.span) == name)
1288            };
1289            let mut binding = format!("{word}_");
1290            let mut n = 2;
1291            while taken(&binding) {
1292                binding = format!("{word}_{n}");
1293                n += 1;
1294            }
1295            err = err
1296                .with_note(format!(
1297                    "the shorthand binds a variable named `{word}`; name the binding instead"
1298                ))
1299                .with_suggestion(
1300                    format!("bind the `{word}` field as `{binding}`"),
1301                    vec![(t.span, format!("{word}: {binding}"))],
1302                    crate::error::Applicability::MaybeIncorrect,
1303                );
1304        } else {
1305            err = err.with_note(format!(
1306                "write `{word}: <value>`; a keyword names the field but cannot name a variable"
1307            ));
1308        }
1309        Some(err)
1310    }
1311
1312    /// Like [`Parser::expect_ident`], but admits any reserved keyword on the
1313    /// same line as the `.` before it (#2837, #2778): a field access, a method
1314    /// call or a method declaration. The same-line rule keeps a half-typed
1315    /// `foo.` above a line starting `let` from parsing as `foo.let`.
1316    pub(crate) fn expect_member_name(
1317        &mut self,
1318        ctx: &str,
1319        dot: Span,
1320    ) -> Result<Ident, CompileError> {
1321        if matches!(self.peek(), Some(t) if is_reserved_keyword(t.kind))
1322            && !self.next_token_on_new_line(dot)
1323        {
1324            let t = self.bump().expect("peeked a keyword");
1325            return Ok(Ident {
1326                name: self.slice(t.span).to_string(),
1327                span: t.span,
1328            });
1329        }
1330        self.expect_ident(ctx)
1331    }
1332
1333    // -- top level --
1334
1335    /// Consume an optional doc at the current position, in either form,
1336    /// returning the doc and its span (through the end of its last line).
1337    /// Returns None if the next token is not a doc.
1338    ///
1339    /// #1885: a doc block whose content is code (see [`doc_content_is_code`])
1340    /// records `bynk.parse.doc_block_contains_code` in [`Self::doc_errors`];
1341    /// the block is still returned, so parsing carries on around it. A run of
1342    /// `--|` lines is not probed: each line is marked as a doc on purpose, so
1343    /// it cannot swallow code by accident the way a pair of dividers can.
1344    ///
1345    /// #2203 (DECISION D): a target has one doc. A doc directly followed by
1346    /// another (no blank line between), of either form, records
1347    /// `bynk.parse.multiple_docs` the same way; all are consumed and the first
1348    /// is returned. This replaces #1888's joined `--|` lines and its
1349    /// mixed-forms error.
1350    fn take_doc_block(&mut self) -> Option<(Doc, Span)> {
1351        let first = self.take_one_doc()?;
1352        let mut docs = vec![first.clone()];
1353        while let Some(next) = self.peek()
1354            && matches!(next.kind, TokenKind::DocBlock | TokenKind::DocLine)
1355            && !has_blank_line_between(self.source, docs[docs.len() - 1].1.end, next.span.start)
1356        {
1357            docs.push(self.take_one_doc().expect("a doc token was peeked"));
1358        }
1359        if docs.len() > 1 && !self.doc_probe {
1360            self.doc_errors.push(multiple_docs(self.source, &docs));
1361        }
1362        Some(first)
1363    }
1364
1365    /// One doc: a `---` block or a one-line doc.
1366    fn take_one_doc(&mut self) -> Option<(Doc, Span)> {
1367        match self.peek_kind()? {
1368            TokenKind::DocBlock => {
1369                let t = self.bump().unwrap();
1370                let body = doc_block_content(self.source, t.span);
1371                if !self.doc_probe && doc_content_is_code(&body) {
1372                    self.doc_errors.push(doc_block_contains_code(t.span));
1373                }
1374                Some((Doc::new(body, DocForm::Block), t.span))
1375            }
1376            TokenKind::DocLine => {
1377                let t = self.bump().unwrap();
1378                let text = doc_line_text(self.source, t.span);
1379                Some((Doc::new(text, DocForm::Lines), t.span))
1380            }
1381            _ => None,
1382        }
1383    }
1384
1385    /// Collect all line-comment trivia leading the next declaration plus
1386    /// the optional doc block. Comments may appear both *before* and
1387    /// *between* the doc and the declaration; the spec canonicalises both
1388    /// groups above the doc, so we concatenate them.
1389    ///
1390    /// The doc comes back as a [`DocLead`], which records where it sat among
1391    /// the comments, so an orphan can be kept in place ([`keep_orphan`]).
1392    ///
1393    /// For a declaration set apart by blank lines (a unit-level one, or a
1394    /// service's or agent's section or handler), which keeps a blank line after
1395    /// a comment (#1884); [`Self::collect_body_item_lead`] is the form for a
1396    /// body whose items have no blank lines between them.
1397    fn collect_item_lead(&mut self) -> (Vec<Comment>, Option<DocLead>) {
1398        self.collect_lead(Self::take_unit_leading_trivia)
1399    }
1400
1401    /// [`Self::collect_item_lead`] for a declaration in a body, which drops a
1402    /// [`Comment::Blank`] as [`Self::take_leading_trivia`] does.
1403    fn collect_body_item_lead(&mut self) -> (Vec<Comment>, Option<DocLead>) {
1404        self.collect_lead(Self::take_leading_trivia)
1405    }
1406
1407    fn collect_lead(
1408        &mut self,
1409        take: fn(&mut Self) -> Vec<Comment>,
1410    ) -> (Vec<Comment>, Option<DocLead>) {
1411        let mut leading = take(self);
1412        let doc = self.take_doc_block().map(|(content, span)| DocLead {
1413            content,
1414            span,
1415            at: leading.len(),
1416        });
1417        if doc.is_some() {
1418            leading.extend(take(self));
1419        }
1420        (leading, doc)
1421    }
1422
1423    /// #1888: the comments and doc above a member: a record field, a variant,
1424    /// a variant's payload field or a message entry. As
1425    /// [`Self::collect_body_item_lead`] and [`Self::finalize_doc`] together, so
1426    /// a blank line orphans a member's doc as it does a declaration's, and a
1427    /// [`Comment::Blank`] is dropped, as in any body (#1884). A doc with
1428    /// `close` (the body's closing token) next documents nothing: it is warned
1429    /// about and kept among the comments, which the caller then takes as the
1430    /// body's closing comments.
1431    fn collect_member_lead(&mut self, close: TokenKind) -> (Vec<Comment>, Option<Doc>) {
1432        let (mut leading, doc) = self.collect_body_item_lead();
1433        let Some(doc) = doc else {
1434            return (leading, None);
1435        };
1436        match self.peek() {
1437            Some(t) if t.kind != close => {
1438                let doc = self.finalize_doc(Some(doc), t.span, &mut leading);
1439                (leading, doc)
1440            }
1441            _ => {
1442                self.warnings.push(CompileError::new(
1443                    "bynk.parse.orphan_doc_block",
1444                    doc.span,
1445                    "documentation has no following member to attach to",
1446                ));
1447                keep_orphan(&mut leading, doc);
1448                (leading, None)
1449            }
1450        }
1451    }
1452
1453    /// #1888: the kind of the first token after any doc tokens at the cursor,
1454    /// so a member loop can tell a doc on its next member from a doc on the
1455    /// declaration after the body (a pipe-form sum has no closing token).
1456    fn peek_past_docs(&self) -> Option<TokenKind> {
1457        self.tokens[self.pos.min(self.tokens.len())..]
1458            .iter()
1459            .map(|t| t.kind)
1460            .find(|k| !matches!(k, TokenKind::DocBlock | TokenKind::DocLine))
1461    }
1462
1463    /// Attach a parsed doc block to a following declaration unless a blank
1464    /// line separates them, in which case the doc is orphaned: a warning, and
1465    /// the block is kept in `leading` where it sat (#1756).
1466    fn finalize_doc(
1467        &mut self,
1468        doc: Option<DocLead>,
1469        next_span: Span,
1470        leading: &mut Vec<Comment>,
1471    ) -> Option<Doc> {
1472        let doc = doc?;
1473        let doc_span = doc.span;
1474        // A blank line between the doc and the next decl orphans the doc.
1475        if has_blank_line_between(self.source, doc_span.end, next_span.start) {
1476            self.warnings.push(
1477                CompileError::new(
1478                    "bynk.parse.orphan_doc_block",
1479                    doc_span,
1480                    "documentation block is separated from the following declaration by a blank line; it will not be attached",
1481                )
1482                .with_note(
1483                    "remove the blank line to attach the doc to the next declaration, \
1484                     or remove the doc block if it is not meant to document anything",
1485                ),
1486            );
1487            keep_orphan(leading, doc);
1488            return None;
1489        }
1490        Some(doc.content)
1491    }
1492}
1493
1494/// A doc block read ahead of a declaration, before it is known to attach: its
1495/// normalised content, its span, and its index among the leading comments
1496/// collected with it.
1497pub(crate) struct DocLead {
1498    pub(crate) content: Doc,
1499    pub(crate) span: Span,
1500    pub(crate) at: usize,
1501}
1502
1503/// #1756: keep an orphaned doc block in `leading`, at the place it was written
1504/// among those comments, so the formatter prints it where it was rather than
1505/// dropping it.
1506fn keep_orphan(leading: &mut Vec<Comment>, doc: DocLead) {
1507    let at = doc.at.min(leading.len());
1508    leading.insert(at, Comment::OrphanDoc(doc.content));
1509}
1510
1511/// #2203 (DECISION D): the `bynk.parse.multiple_docs` error for `docs`, two
1512/// or more docs directly above one target. When every one is a `--- text`
1513/// line, the fix joins them into one block, which is what a doc of more than
1514/// one line is.
1515fn multiple_docs(source: &str, docs: &[(Doc, Span)]) -> CompileError {
1516    let (_, first) = docs[0];
1517    let (_, last) = docs[docs.len() - 1];
1518    let mut err = CompileError::new(
1519        "bynk.parse.multiple_docs",
1520        docs[1].1,
1521        "this declaration or member already has a doc; a target has one doc",
1522    )
1523    .with_label(first, "the first doc is here")
1524    .with_note(
1525        "a doc of more than one line is a block: a `---` line, the text, and another `---` line",
1526    );
1527    let one_liners = docs
1528        .iter()
1529        .all(|(d, span)| d.form == DocForm::Lines && !doc_line_is_legacy(source, *span));
1530    if one_liners {
1531        let line_start = source[..first.start].rfind('\n').map_or(0, |i| i + 1);
1532        let indent = &source[line_start..first.start];
1533        let text = docs
1534            .iter()
1535            .map(|(d, _)| d.as_str())
1536            .collect::<Vec<_>>()
1537            .join("\n");
1538        let fence = "-".repeat(doc_fence_len(&text));
1539        let mut block = fence.clone();
1540        for line in text.lines() {
1541            block.push('\n');
1542            if !line.is_empty() {
1543                block.push_str(indent);
1544                block.push_str(line);
1545            }
1546        }
1547        block.push('\n');
1548        block.push_str(indent);
1549        block.push_str(&fence);
1550        let end = source[..last.end].trim_end_matches(['\n', '\r']).len();
1551        err = err.with_suggestion(
1552            "join the docs into one `---` block",
1553            vec![(Span::new_in(first.file, first.start, end), block)],
1554            crate::error::Applicability::MachineApplicable,
1555        );
1556    }
1557    err
1558}
1559
1560/// #1885: the `bynk.parse.doc_block_contains_code` error for the doc block at
1561/// `span`.
1562fn doc_block_contains_code(span: Span) -> CompileError {
1563    CompileError::new(
1564        "bynk.parse.doc_block_contains_code",
1565        span,
1566        "this documentation block contains declarations, so they are not compiled",
1567    )
1568    .with_note(
1569        "a line of three or more dashes opens or closes a doc block, so two dash \
1570         dividers turn the code between them into documentation; for a divider use a \
1571         line comment with text, such as `-- Helpers --`, and to show code in a doc \
1572         block put it in a Markdown code fence (```)",
1573    )
1574}
1575
1576/// The keywords that open a declaration in a unit body. A doc block's content
1577/// is probed as code only when it lexes and holds one of these, so prose (which
1578/// usually fails to lex, or has none) costs one tokenize at most.
1579const DECLARATION_KEYWORDS: &[TokenKind] = &[
1580    TokenKind::Type,
1581    TokenKind::Fn,
1582    TokenKind::Capability,
1583    TokenKind::Provides,
1584    TokenKind::Service,
1585    TokenKind::Agent,
1586    TokenKind::Actor,
1587    TokenKind::Messages,
1588    TokenKind::Event,
1589    TokenKind::Asset,
1590    TokenKind::Embed,
1591    TokenKind::Const,
1592    TokenKind::Uses,
1593    TokenKind::Consumes,
1594    TokenKind::Exports,
1595    TokenKind::Binding,
1596];
1597
1598/// #1885 (ADR: doc-fence-matched-length): true when a doc block's `content`,
1599/// with its Markdown code fences removed, parses as one or more declarations.
1600///
1601/// The probe is an `adapter` body in fragment form: an adapter accepts every
1602/// item a context or commons does, plus `binding`, and leaves placement to the
1603/// checker. Success means the whole content parsed and declared something; a
1604/// content that only holds another doc block (front matter, say) is prose.
1605///
1606/// Nesting is legal since #1885, so code can hide one level down: a doc block
1607/// inside the content lexes to one opaque token, and its content is probed in
1608/// turn (up to [`DOC_PROBE_MAX_DEPTH`] levels), the outer block reporting what
1609/// any level finds. The probe parser itself does not report on those blocks
1610/// (`doc_probe`). Content that does not lex because it holds a lone `---` (a
1611/// Markdown rule) is probed again with its marker lines blanked out.
1612fn doc_content_is_code(content: &str) -> bool {
1613    doc_content_is_code_at(content, 0)
1614}
1615
1616/// How many levels of nested doc blocks [`doc_content_is_code`] looks into.
1617/// Doc blocks nested this deep are written on purpose; the bound only keeps
1618/// adversarial input from costing a parse per level without end.
1619const DOC_PROBE_MAX_DEPTH: usize = 4;
1620
1621fn doc_content_is_code_at(content: &str, depth: usize) -> bool {
1622    let stripped = strip_code_fences(content);
1623    let tokens = match crate::lexer::tokenize(&stripped) {
1624        Ok(tokens) => tokens,
1625        Err(_) => {
1626            // A lone `---` rule leaves the content unlexable; without the
1627            // marker lines it may still be nothing but declarations.
1628            let unmarked = blank_marker_lines(&stripped);
1629            if unmarked == stripped {
1630                return false;
1631            }
1632            return doc_content_is_code_at(&unmarked, depth);
1633        }
1634    };
1635    if depth < DOC_PROBE_MAX_DEPTH
1636        && tokens.iter().any(|t| {
1637            t.kind == TokenKind::DocBlock
1638                && doc_content_is_code_at(&doc_block_content(&stripped, t.span), depth + 1)
1639        })
1640    {
1641        return true;
1642    }
1643    if !tokens
1644        .iter()
1645        .any(|t| DECLARATION_KEYWORDS.contains(&t.kind))
1646    {
1647        return false;
1648    }
1649    let (filtered, trivia) = split_trivia(&tokens, &stripped);
1650    let mut warnings = Vec::new();
1651    let mut p = Parser::new(&filtered, &stripped, trivia, &mut warnings);
1652    p.doc_probe = true;
1653    let at = Span::new(0, 0);
1654    let name = QualifiedName {
1655        parts: Vec::new(),
1656        span: at,
1657    };
1658    match p.parse_adapter_body(at, name, None, false) {
1659        Ok(a) => {
1660            p.peek().is_none()
1661                && (!a.items.is_empty()
1662                    || !a.uses.is_empty()
1663                    || !a.consumes.is_empty()
1664                    || !a.exports.is_empty()
1665                    || a.binding.is_some())
1666        }
1667        Err(_) => false,
1668    }
1669}
1670
1671/// `content` with every doc-block marker line (three or more dashes alone on
1672/// the line) blanked out, line for line.
1673fn blank_marker_lines(content: &str) -> String {
1674    let mut out = String::with_capacity(content.len());
1675    for line in content.lines() {
1676        let trimmed = line.trim();
1677        if !(trimmed.len() >= 3 && trimmed.bytes().all(|b| b == b'-')) {
1678            out.push_str(line);
1679        }
1680        out.push('\n');
1681    }
1682    out
1683}
1684
1685/// `content` with every Markdown fenced code block (```` ``` ```` or `~~~`,
1686/// three or more, closed by a fence of the same character at least as long)
1687/// blanked out, line for line. An unclosed fence runs to the end, as in
1688/// Markdown. Code shown in a fence is an example, not swallowed code.
1689fn strip_code_fences(content: &str) -> String {
1690    let mut out = String::with_capacity(content.len());
1691    let mut open: Option<(char, usize)> = None;
1692    for line in content.lines() {
1693        let trimmed = line.trim();
1694        let fence = ['`', '~'].into_iter().find_map(|c| {
1695            let n = trimmed.chars().take_while(|&x| x == c).count();
1696            (n >= 3).then_some((c, n))
1697        });
1698        match (open, fence) {
1699            (None, Some(f)) => open = Some(f),
1700            (Some((c, n)), Some((fc, fnum)))
1701                if fc == c && fnum >= n && trimmed.len() == fnum * c.len_utf8() =>
1702            {
1703                open = None;
1704            }
1705            (None, None) => out.push_str(line),
1706            (Some(_), _) => {}
1707        }
1708        out.push('\n');
1709    }
1710    out
1711}
1712
1713/// The value of a lexed, hole-free string literal (the lexeme, including its
1714/// delimiters), in any form: plain, raw, or multi-line (#2619).
1715fn parse_string_literal(lexeme: &str, span: Span) -> Result<String, CompileError> {
1716    crate::lexer::decode_string_literal(lexeme, span)
1717}
1718
1719pub fn is_reserved_keyword(kind: TokenKind) -> bool {
1720    use TokenKind::*;
1721    matches!(
1722        kind,
1723        Commons
1724            | Type
1725            | Fn
1726            | Where
1727            | True
1728            | False
1729            | Int
1730            | String
1731            | Bool
1732            | Let
1733            | If
1734            | Else
1735            | Ok
1736            | Err
1737            | Result
1738            | ValidationError
1739            | Enum
1740            | Match
1741            | Option
1742            | Record
1743            | Self_
1744            | Some
1745            | None
1746            | Is
1747            | Opaque
1748            | Uses
1749            | Context
1750            | Consumes
1751            | Exports
1752            | Transparent
1753            | Agent
1754            | As
1755            | Capability
1756            | Effect
1757            | Do
1758            | Given
1759            | On
1760            | Http
1761            | Provides
1762            | Stub
1763            | Service
1764            | Actor
1765            | By
1766            | Expect
1767            | Suite
1768            | Case
1769            | Float
1770            | Duration
1771            | Instant
1772            | Bytes
1773            | Decimal
1774            | JsonError
1775            | Property
1776            | Adapter
1777            | Binding
1778            | Cron
1779            | Queue
1780            | From
1781            | Protocol
1782            | Invariant
1783            | Implies
1784            | Requires
1785            | Ensures
1786            | Transition
1787    )
1788}
1789
1790/// True when `kind` starts a top-level unit (`commons`/`context`/`adapter`/
1791/// `suite`) or an item within one of their bodies — every keyword any of
1792/// `parse_commons_brace`/`_fragment`, `parse_context_brace`/`_fragment`,
1793/// `parse_test_brace`/`_fragment`, or `parse_adapter_body` dispatches on
1794/// (`declarations.rs`). The single set [`Parser::recover_to_top_item`]'s sync
1795/// scan checks against — finding #27/#30: that scan had drifted from what the
1796/// item loops actually recognise (`Property`, `Actor`, `Event`, `Binding`,
1797/// and the `adapter` unit keyword itself were all missing), so an error
1798/// recovery sync could walk past a real item/unit boundary instead of
1799/// stopping there.
1800fn is_item_start(kind: TokenKind) -> bool {
1801    use TokenKind::*;
1802    matches!(
1803        kind,
1804        // Top-level unit keywords.
1805        Commons | Context | Adapter | Suite
1806        // Body items shared across commons/context/adapter.
1807        | Type | Fn | Messages | Event | Asset | Embed | Const | Uses
1808        // Context/adapter-only body items.
1809        | Consumes | Exports | Capability | Provides | Service | Agent | Actor
1810        // Adapter-only.
1811        | Binding
1812        // Suite/test-only body items.
1813        | Stub | Case | Property
1814    )
1815}
1816
1817#[cfg(test)]
1818mod tests {
1819    use super::*;
1820    use crate::lexer::tokenize;
1821
1822    fn parse_str(src: &str) -> Result<Commons, Vec<CompileError>> {
1823        let toks = tokenize(src).map_err(|e| vec![e])?;
1824        parse(&toks, src)
1825    }
1826
1827    fn parse_recover_str(src: &str) -> (Option<SourceUnit>, Vec<CompileError>) {
1828        let toks = match tokenize(src) {
1829            Ok(t) => t,
1830            Err(e) => return (None, vec![e]),
1831        };
1832        parse_unit_with_recovery(&toks, src)
1833    }
1834
1835    /// #2703: a bare name ending a contract clause is the end of the predicate,
1836    /// not the head of a record construction, even with the body's `{` on the
1837    /// next line (and a lone-name body `{ n }`, which looks like `Limit { n }`).
1838    #[test]
1839    fn contract_clause_name_before_next_line_brace_is_not_a_record() {
1840        for src in [
1841            "commons m\n\nconst Limit: Int = 10\n\nfn small(n: Int) -> Int\n  requires small: n <= Limit\n{\n  n\n}\n",
1842            "commons m\n\nfn f(n: Int) -> Int\n  ensures same: result == n\n{\n  n\n}\n",
1843            "commons m\n\nfn f(n: Int) -> Int\n  requires r: n <= Limit {\n  n\n}\n",
1844        ] {
1845            assert!(parse_str(src).is_ok(), "{src}\n{:?}", parse_str(src).err());
1846        }
1847        // A record literal in a clause still works once parenthesised.
1848        let src = "commons m\n\ntype P = { x: Int }\n\nfn f(p: P) -> Int\n  requires r: (p == P { x: 1 })\n{\n  1\n}\n";
1849        assert!(parse_str(src).is_ok(), "{:?}", parse_str(src).err());
1850    }
1851
1852    /// #2703: the flip side — an unparenthesised record literal on a clause's
1853    /// spine is rejected (the `{` opens the body), as in an `if` condition.
1854    #[test]
1855    fn contract_clause_unparenthesised_record_literal_is_rejected() {
1856        let src = "commons m\n\ntype P = { x: Int }\n\nfn f(p: P) -> Int\n  requires r: p == P { x: 1 }\n{\n  1\n}\n";
1857        assert!(parse_str(src).is_err());
1858    }
1859
1860    /// Finding #29/#30: `parse_units_with_recovery` keeps every top-level unit
1861    /// an atomic `commons`+`suite` file declares (v0.113, DECISION S), where
1862    /// `parse_unit_with_recovery` keeps only the first — the defect the IDE's
1863    /// old single-unit parse entry point had (it silently discarded the
1864    /// trailing `suite`).
1865    #[test]
1866    fn parse_units_with_recovery_keeps_every_top_level_unit() {
1867        let src =
1868            "commons m {\n  fn f() -> Int { 1 }\n}\n\nsuite m\n\ncase \"c\" {\n  expect true\n}\n";
1869        let toks = tokenize(src).unwrap();
1870        let (units, errors) = parse_units_with_recovery(&toks, src);
1871        assert!(errors.is_empty(), "{errors:?}");
1872        assert_eq!(units.len(), 2, "expected both units, got {units:?}");
1873        assert!(matches!(units[0], SourceUnit::Commons(_)));
1874        assert!(matches!(units[1], SourceUnit::Suite(_)));
1875
1876        // The singular wrapper still narrows to just the first, unchanged.
1877        let (unit, errors) = parse_unit_with_recovery(&toks, src);
1878        assert!(errors.is_empty(), "{errors:?}");
1879        assert!(matches!(unit, Some(SourceUnit::Commons(_))));
1880    }
1881
1882    /// `parse_unit_with_recovery` always attempts at least one parse, even on
1883    /// empty input — `parse_units_with_recovery`'s loop must preserve that
1884    /// (its `while`-style peek check alone would skip the body entirely and
1885    /// silently return no error), since 16+ existing callers rely on an empty
1886    /// file still producing its usual diagnostic rather than a silent `None`
1887    /// with no error at all.
1888    #[test]
1889    fn empty_input_still_reports_an_error_through_the_plural_entry_point() {
1890        let toks = tokenize("").unwrap();
1891        let (units, errors) = parse_units_with_recovery(&toks, "");
1892        assert!(units.is_empty());
1893        assert!(
1894            !errors.is_empty(),
1895            "an empty file must still produce a diagnostic, not silently no units and no error"
1896        );
1897
1898        let (unit, unit_errors) = parse_unit_with_recovery(&toks, "");
1899        assert!(unit.is_none());
1900        assert_eq!(
1901            errors.len(),
1902            unit_errors.len(),
1903            "the singular wrapper must see the same error(s) as the plural entry point"
1904        );
1905    }
1906
1907    /// Finding #66: `parse_units_with_drain_check`'s `fully_drained` flag is
1908    /// `bynk-fmt`'s signal for whether its comment-loss guard can skip a
1909    /// re-tokenize-and-diff of its own output. It must be `true` for an
1910    /// ordinary file (every comment sits before a declaration/statement or
1911    /// trails one) and `false` the moment a comment sits inside an expression
1912    /// subtree, where `TriviaTable` has no field to attach it to.
1913    #[test]
1914    fn drain_check_reports_expression_interior_comments_as_undrained() {
1915        let ordinary = "commons x {\n// note\ntype T = Int where Positive\n}\n";
1916        let toks = tokenize(ordinary).unwrap();
1917        let (_, _, drained) = parse_units_with_drain_check(&toks, ordinary).unwrap();
1918        assert!(
1919            drained,
1920            "a declaration-leading comment must be fully drained"
1921        );
1922
1923        let lossy = "commons x {\n  fn f() -> Int {\n    1 + // note\n    2\n  }\n}\n";
1924        let toks = tokenize(lossy).unwrap();
1925        let (_, _, drained) = parse_units_with_drain_check(&toks, lossy).unwrap();
1926        assert!(
1927            !drained,
1928            "a comment inside a binop expression must be reported as undrained"
1929        );
1930    }
1931
1932    #[test]
1933    fn eof_span_never_splits_a_multibyte_codepoint() {
1934        // An unterminated construct whose final line ends in a non-ASCII char
1935        // (here a `--` comment ending in `→`) once produced an `unexpected_eof`
1936        // span of `len - 1 .. len`, landing on the arrow's last continuation
1937        // byte. Every reported span must sit on char boundaries.
1938        for src in [
1939            "commons x {\n  // ends with an arrow →",
1940            "agent A {\n  key k: String\n  // note 🦀",
1941            "commons y {\n  type T = é",
1942        ] {
1943            let (_unit, errors) = parse_recover_str(src);
1944            for e in &errors {
1945                assert!(
1946                    src.is_char_boundary(e.span.start) && src.is_char_boundary(e.span.end),
1947                    "span {:?} splits a codepoint in {src:?}",
1948                    e.span,
1949                );
1950            }
1951        }
1952    }
1953
1954    #[test]
1955    fn reserved_contextual_keywords_readable_in_expression_position() {
1956        // Events track, slice 0 (#939): `expect_ident`'s `RESERVED_CONTEXTUAL`
1957        // exemption (`keywords::RESERVED_CONTEXTUAL`: case/event/messages/on/
1958        // suite) covers *declaring* a binding with one of these names — a
1959        // parameter, a `let` — but the primary-expression parser previously
1960        // only admitted plain `TokenKind::Ident` when *reading one back*.
1961        // Latent since messages/on/case/suite shipped (no fixture happened to
1962        // name a binding after one of them and read it back inside the body);
1963        // surfaced concretely when `event` joined the tier and collided with
1964        // `examples/event-log`'s pre-existing `add(event: Event)` handler.
1965        for kw in [
1966            "asset", "case", "const", "embed", "event", "messages", "on", "suite",
1967        ] {
1968            let src = format!("commons x\n\nfn f({kw}: Int) -> Int {{\n  {kw}\n}}\n");
1969            let result = parse_str(&src);
1970            assert!(
1971                result.is_ok(),
1972                "a parameter named `{kw}` must be readable in expression position: {:?}",
1973                result.err()
1974            );
1975        }
1976    }
1977
1978    #[test]
1979    fn recovery_skips_garbage_between_decls() {
1980        // Two `type` declarations separated by garbage. Recovery should
1981        // accept both and report one error for the garbage between them.
1982        let src = "commons x {\n\
1983                   type A = Int where NonNegative\n\
1984                   ??? !!!\n\
1985                   type B = String where NonEmpty\n\
1986                   }";
1987        let (unit, errors) = parse_recover_str(src);
1988        let unit = unit.expect("recovery should produce a partial AST");
1989        let SourceUnit::Commons(c) = unit else {
1990            panic!("expected commons")
1991        };
1992        // Both type decls should have been collected despite the garbage.
1993        let names: Vec<_> = c
1994            .items
1995            .iter()
1996            .map(|i| match i {
1997                CommonsItem::Type(t) => t.name.name.clone(),
1998                _ => panic!("expected only types"),
1999            })
2000            .collect();
2001        assert!(
2002            names.contains(&"A".to_string()) && names.contains(&"B".to_string()),
2003            "expected both A and B; got {names:?}",
2004        );
2005        assert!(!errors.is_empty(), "expected at least one parse error");
2006    }
2007
2008    #[test]
2009    fn recovery_handles_bad_first_decl_then_good_second() {
2010        // First decl is malformed (missing `=`); second is well-formed.
2011        let src = "commons x {\n\
2012                   type A Int where NonNegative\n\
2013                   type B = String where NonEmpty\n\
2014                   }";
2015        let (unit, errors) = parse_recover_str(src);
2016        let unit = unit.expect("recovery should produce a partial AST");
2017        let SourceUnit::Commons(c) = unit else {
2018            panic!("expected commons")
2019        };
2020        let names: Vec<_> = c
2021            .items
2022            .iter()
2023            .filter_map(|i| match i {
2024                CommonsItem::Type(t) => Some(t.name.name.clone()),
2025                _ => None,
2026            })
2027            .collect();
2028        assert!(
2029            names.contains(&"B".to_string()),
2030            "B should be parsed after A's failure; got {names:?}"
2031        );
2032        assert!(!errors.is_empty(), "expected at least one parse error");
2033    }
2034
2035    /// Finding #27/#30: an error two levels deep inside `f`'s body (a
2036    /// `match` arm's own block) used to make `recover_to_top_item`'s flat,
2037    /// depth-blind scan stop at the *first* `}` it saw — the arm block's own,
2038    /// not `f`'s. Two more `}` (the match's, then `f`'s) then got consumed one
2039    /// at a time across repeated recovery re-entries, and the outer item loop
2040    /// eventually mistook the commons's *own* closing `}` for having arrived
2041    /// early, returning zero items and a spurious second
2042    /// `bynk.parse.expected_unit_header` error. With brace-depth tracking, `g`
2043    /// is recovered as the sole item and only `f`'s own error is reported.
2044    /// #1663: an item that ends without its body (`fn f() -> Int` then the
2045    /// commons' own `}`) raises its error *on* that `}`, so recovery makes no
2046    /// progress. The no-progress step must not consume the brace that closes
2047    /// the body (brace form): doing so made the body look unclosed, a
2048    /// follow-on `unexpected_eof`.
2049    #[test]
2050    fn recovery_keeps_the_bodys_closing_brace_after_a_bodiless_item() {
2051        let src = "commons m {\n  fn f() -> Int\n}\n";
2052        let (unit, errors) = parse_recover_str(src);
2053        assert!(unit.is_some(), "recovery should produce a partial AST");
2054        let categories: Vec<_> = errors.iter().map(|e| e.category).collect();
2055        assert_eq!(
2056            categories.len(),
2057            1,
2058            "one syntax error, no follow-on: {categories:?}"
2059        );
2060        assert!(
2061            !categories.contains(&"bynk.parse.unexpected_eof"),
2062            "the commons' closing brace must survive: {categories:?}"
2063        );
2064    }
2065
2066    /// #1663: an item keyword illegal at this position (`agent` in a commons)
2067    /// is skipped *with* its name and `{ … }` body, not one token at a time —
2068    /// otherwise the item loop misreads the agent's name as a malformed item,
2069    /// a second, follow-on `expected_item`. The next item still parses.
2070    #[test]
2071    fn recovery_skips_a_rejected_item_whole() {
2072        let src = "commons m\n\nagent Counter {\n  key id: String\n}\n\nfn g() -> Int { 2 }\n";
2073        let recovered = parse_units_recovering(&crate::lexer::tokenize(src).unwrap(), src);
2074        assert_eq!(
2075            recovered.errors.len(),
2076            1,
2077            "one syntax error, no follow-on: {:?}",
2078            recovered
2079                .errors
2080                .iter()
2081                .map(|e| e.category)
2082                .collect::<Vec<_>>()
2083        );
2084        let Some(SourceUnit::Commons(c)) = recovered.units.first() else {
2085            panic!("expected commons")
2086        };
2087        assert!(
2088            c.items.iter().any(|i| matches!(i, CommonsItem::Fn(_))),
2089            "the `fn g` after the skipped agent must still parse"
2090        );
2091        assert_eq!(recovered.broken_decl_names, ["Counter"]);
2092    }
2093
2094    #[test]
2095    fn recovery_skips_a_nested_blocks_own_closing_brace() {
2096        let src = "commons m {\n  \
2097                   fn f() -> Int {\n    \
2098                   match 1 {\n      \
2099                   is 1 -> { let z = }\n      \
2100                   is _ -> 2\n    \
2101                   }\n  \
2102                   }\n  \
2103                   fn g() -> Int { 2 }\n\
2104                   }\n";
2105        let (unit, errors) = parse_recover_str(src);
2106        let unit = unit.expect("recovery should produce a partial AST");
2107        let SourceUnit::Commons(c) = unit else {
2108            panic!("expected commons")
2109        };
2110        let names: Vec<_> = c
2111            .items
2112            .iter()
2113            .filter_map(|i| match i {
2114                CommonsItem::Fn(f) => match &f.name {
2115                    FnName::Free(id) => Some(id.name.clone()),
2116                    _ => None,
2117                },
2118                _ => None,
2119            })
2120            .collect();
2121        assert_eq!(
2122            names,
2123            vec!["g".to_string()],
2124            "g must still be recovered as an item; got {names:?}"
2125        );
2126        assert!(
2127            !errors
2128                .iter()
2129                .any(|e| e.category == "bynk.parse.expected_unit_header"),
2130            "the outer body's own closing brace must not be mistaken for \
2131             end-of-file: {errors:?}"
2132        );
2133    }
2134
2135    #[test]
2136    fn doc_block_attaches_to_type() {
2137        let c =
2138            parse_str("commons x {\n---\nA descriptive doc.\n---\ntype T = Int where Positive\n}")
2139                .unwrap();
2140        let CommonsItem::Type(t) = &c.items[0] else {
2141            panic!()
2142        };
2143        assert!(t.documentation.is_some());
2144        assert!(
2145            t.documentation
2146                .as_ref()
2147                .unwrap()
2148                .contains("A descriptive doc.")
2149        );
2150    }
2151
2152    #[test]
2153    fn interpolated_string_parses_into_parts() {
2154        // v0.43: `"Hi, \(name)!"` splits into chunk / hole / chunk.
2155        let c = parse_str("commons x\n\nfn f(name: String) -> String {\n  \"Hi, \\(name)!\"\n}\n")
2156            .unwrap();
2157        let CommonsItem::Fn(f) = &c.items[0] else {
2158            panic!("expected fn")
2159        };
2160        let ExprKind::InterpStr(parts) = &f.body.tail.kind else {
2161            panic!("expected InterpStr, got {:?}", f.body.tail.kind)
2162        };
2163        assert_eq!(parts.len(), 3);
2164        assert!(matches!(&parts[0], InterpPart::Chunk(s) if s == "Hi, "));
2165        assert!(
2166            matches!(&parts[1], InterpPart::Hole(h) if matches!(&h.kind, ExprKind::Ident(id) if id.name == "name"))
2167        );
2168        assert!(matches!(&parts[2], InterpPart::Chunk(s) if s == "!"));
2169    }
2170
2171    #[test]
2172    fn interpolated_hole_parses_a_full_expression() {
2173        // A hole holds an arbitrary expression, not just an identifier.
2174        let c =
2175            parse_str("commons x\n\nfn f(a: Int, b: Int) -> String {\n  \"sum = \\(a + b)\"\n}\n")
2176                .unwrap();
2177        let CommonsItem::Fn(f) = &c.items[0] else {
2178            panic!("expected fn")
2179        };
2180        let ExprKind::InterpStr(parts) = &f.body.tail.kind else {
2181            panic!("expected InterpStr")
2182        };
2183        assert!(matches!(&parts[1], InterpPart::Hole(h) if matches!(&h.kind, ExprKind::BinOp(..))));
2184    }
2185
2186    #[test]
2187    fn empty_interpolation_hole_is_rejected() {
2188        let errs = parse_str("commons x\n\nfn f() -> String {\n  \"\\()\"\n}\n").unwrap_err();
2189        assert!(
2190            errs.iter()
2191                .any(|e| e.category == "bynk.parse.empty_interpolation"),
2192            "expected empty_interpolation; got {errs:?}"
2193        );
2194    }
2195
2196    #[test]
2197    fn interpolation_hole_lex_error_span_is_rebased() {
2198        // #716: a lex error inside a `\(…)` hole once carried a span relative to
2199        // the hole substring — never rebased by `hole.start` — so it pointed at
2200        // the file's opening bytes and could split a multibyte char, tripping
2201        // the char-boundary invariant. The error must land on the offending
2202        // bytes within the hole and stay on char boundaries.
2203        let cases = [
2204            // `$` is not a valid token; the error should point at it, not byte 0.
2205            "commons x\n\nfn f() -> String {\n  \"a \\($)\"\n}\n",
2206            // Integer overflow — the reported span must cover the literal itself.
2207            "commons x\n\nfn f() -> String {\n  \"n = \\(99999999999999999999)\"\n}\n",
2208            // A multibyte char before the hole means an un-rebased span could
2209            // land inside the `é`; the rebased span must not.
2210            "commons x\n\nfn f() -> String {\n  \"é \\($)\"\n}\n",
2211        ];
2212        for src in cases {
2213            let errs = parse_str(src).unwrap_err();
2214            assert!(!errs.is_empty(), "expected a lex error for {src:?}");
2215            for e in &errs {
2216                assert!(
2217                    src.is_char_boundary(e.span.start) && src.is_char_boundary(e.span.end),
2218                    "span {:?} splits a codepoint in {src:?}",
2219                    e.span,
2220                );
2221                // The error must point inside the interpolation hole, not at the
2222                // header text that precedes it.
2223                let hole_start = src.find("\\(").expect("case has a hole") + 2;
2224                assert!(
2225                    e.span.start >= hole_start,
2226                    "span {:?} precedes the hole (starts at {hole_start}) in {src:?}",
2227                    e.span,
2228                );
2229            }
2230        }
2231    }
2232
2233    #[test]
2234    fn fragment_form_parses() {
2235        let c = parse_str("commons x.y\n\ntype T = Int where NonNegative\n").unwrap();
2236        assert_eq!(c.form, CommonsForm::Fragment);
2237        assert_eq!(c.items.len(), 1);
2238    }
2239
2240    #[test]
2241    fn uses_parses() {
2242        let c = parse_str("commons x\n\nuses other.lib\n").unwrap();
2243        assert_eq!(c.uses.len(), 1);
2244        assert_eq!(c.uses[0].target.joined(), "other.lib");
2245    }
2246
2247    /// #2804: `%` is a targeted error, with a fix to `.rem` only for simple operands.
2248    #[test]
2249    fn percent_is_a_targeted_error_with_a_fix_for_simple_operands() {
2250        let err = |expr: &str| {
2251            let src = format!("commons m\n\nfn f(n: Int, k: Int) -> Int {{ {expr} }}\n");
2252            let errs = parse_str(&src).unwrap_err();
2253            assert_eq!(
2254                errs[0].category, "bynk.parse.no_remainder_operator",
2255                "{expr}"
2256            );
2257            errs.into_iter().next().unwrap()
2258        };
2259        let fix = err("n % 2");
2260        assert_eq!(fix.suggestions.len(), 1);
2261        assert_eq!(
2262            fix.notes,
2263            [
2264                "use `a.rem(b)` for the C-family remainder, which takes the dividend's sign, \
2265              or `a.mod(b)` for the modulo, which takes the divisor's sign"
2266            ]
2267        );
2268        assert_eq!(fix.suggestions[0].edits[0].1, "n.rem(2)");
2269        assert_eq!(
2270            fix.suggestions[0].applicability,
2271            crate::error::Applicability::MaybeIncorrect
2272        );
2273        assert_eq!(err("n % k").suggestions[0].edits[0].1, "n.rem(k)");
2274        for complex in [
2275            "(n + 1) % k",
2276            "n * 2 % k",
2277            "n % (k + 1)",
2278            "-n % k",
2279            "n % -k",
2280        ] {
2281            assert!(err(complex).suggestions.is_empty(), "{complex}");
2282        }
2283    }
2284
2285    fn parse_unit_str(src: &str) -> Result<SourceUnit, Vec<CompileError>> {
2286        let toks = tokenize(src).map_err(|e| vec![e])?;
2287        parse_unit(&toks, src)
2288    }
2289
2290    #[test]
2291    fn minimal_context_parses() {
2292        let u = parse_unit_str("context commerce.orders {}").unwrap();
2293        let SourceUnit::Context(c) = u else {
2294            panic!("expected context");
2295        };
2296        assert_eq!(c.name.joined(), "commerce.orders");
2297        assert!(c.items.is_empty());
2298    }
2299
2300    #[test]
2301    fn context_consumes_and_exports_parse() {
2302        let src = "context commerce.orders {\n  uses commerce.money\n  consumes commerce.payment\n  exports opaque { OrderId }\n  exports transparent { OrderError }\n  type OrderId = String where Matches(\"ORD-[0-9]+\")\n  type OrderError = enum { CartEmpty, BadInput }\n}";
2303        let u = parse_unit_str(src).unwrap();
2304        let SourceUnit::Context(c) = u else { panic!() };
2305        assert_eq!(c.uses.len(), 1);
2306        assert_eq!(c.consumes.len(), 1);
2307        assert_eq!(c.exports.len(), 2);
2308        assert_eq!(c.exports[0].kind, ExportKind::Type(Visibility::Opaque));
2309        assert_eq!(c.exports[1].kind, ExportKind::Type(Visibility::Transparent));
2310    }
2311
2312    #[test]
2313    fn context_fragment_form_parses() {
2314        let src = "context x.y\n\nuses other.lib\nconsumes other.ctx\nexports opaque { T }\n\ntype T = Int where NonNegative\n";
2315        let u = parse_unit_str(src).unwrap();
2316        let SourceUnit::Context(c) = u else { panic!() };
2317        assert_eq!(c.form, CommonsForm::Fragment);
2318        assert_eq!(c.uses.len(), 1);
2319        assert_eq!(c.consumes.len(), 1);
2320        assert_eq!(c.exports.len(), 1);
2321    }
2322
2323    #[test]
2324    fn opaque_type_parses() {
2325        let c = parse_str("commons x { type T = opaque Int where NonNegative }").unwrap();
2326        let CommonsItem::Type(t) = &c.items[0] else {
2327            panic!()
2328        };
2329        assert!(matches!(t.body, TypeBody::Opaque { .. }));
2330    }
2331
2332    #[test]
2333    fn empty_commons() {
2334        let c = parse_str("commons fitness.units {}").unwrap();
2335        assert_eq!(c.name.joined(), "fitness.units");
2336        assert!(c.items.is_empty());
2337    }
2338
2339    #[test]
2340    fn one_type_decl() {
2341        let c = parse_str("commons x { type Metres = Int where NonNegative }").unwrap();
2342        assert_eq!(c.items.len(), 1);
2343        let CommonsItem::Type(t) = &c.items[0] else {
2344            panic!()
2345        };
2346        assert_eq!(t.name.name, "Metres");
2347        match &t.body {
2348            TypeBody::Refined {
2349                base, refinement, ..
2350            } => {
2351                assert_eq!(*base, BaseType::Int);
2352                assert!(refinement.is_some());
2353            }
2354            _ => panic!("expected refined body"),
2355        }
2356    }
2357
2358    #[test]
2359    fn function_decl() {
2360        let c = parse_str("commons x { fn add(a: Int, b: Int) -> Int { a + b } }").unwrap();
2361        let CommonsItem::Fn(f) = &c.items[0] else {
2362            panic!()
2363        };
2364        assert_eq!(f.name.ident().name, "add");
2365        assert_eq!(f.params.len(), 2);
2366    }
2367
2368    #[test]
2369    fn chained_comparison_is_error() {
2370        let errs = parse_str("commons x { fn f(a: Int, b: Int, c: Int) -> Bool { a < b < c } }")
2371            .unwrap_err();
2372        assert_eq!(errs[0].category, "bynk.parse.non_associative");
2373    }
2374
2375    #[test]
2376    fn chained_equality_is_error() {
2377        let errs = parse_str("commons x { fn f(a: Int, b: Int, c: Int) -> Bool { a == b == c } }")
2378            .unwrap_err();
2379        assert_eq!(errs[0].category, "bynk.parse.non_associative");
2380    }
2381
2382    /// Run `f` on a thread with a generous stack. The depth-guard tests build
2383    /// source that, *without* the guard, overflows — so if the guard ever
2384    /// regressed we want a clean assertion failure, not a `SIGABRT` that takes
2385    /// the whole test binary down. A large stack also absorbs the fat frames a
2386    /// debug build spends per recursion level (production release frames are
2387    /// ~9 KB/level, so `MAX_NESTING_DEPTH = 64` sits well inside a 1 MB stack;
2388    /// a debug frame is several times larger and would overflow libtest's
2389    /// default 2 MB test thread near the limit even though the guard fires).
2390    fn on_big_stack<T: Send + 'static>(f: impl FnOnce() -> T + Send + 'static) -> T {
2391        std::thread::Builder::new()
2392            .stack_size(64 * 1024 * 1024)
2393            .spawn(f)
2394            .unwrap()
2395            .join()
2396            .unwrap()
2397    }
2398
2399    #[test]
2400    fn deeply_nested_parens_are_bounded_not_overflowed() {
2401        // Without a depth guard the parenthesised-expression recursion
2402        // (`parse_primary` -> `parse_expr` -> …) overflows the stack and aborts
2403        // the process (#713). Well past the limit it must instead report a
2404        // bounded-depth diagnostic. The nesting is left open so the guard, not
2405        // a later `)`, is what stops the descent.
2406        let errs = on_big_stack(|| {
2407            let depth = crate::MAX_NESTING_DEPTH + 8;
2408            let src = format!(
2409                "commons x {{ fn f() -> Int {{ {}0{} }} }}",
2410                "(".repeat(depth),
2411                ")".repeat(depth),
2412            );
2413            parse_str(&src).unwrap_err()
2414        });
2415        assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
2416    }
2417
2418    #[test]
2419    fn deeply_nested_types_are_bounded_not_overflowed() {
2420        // The type parser self-recurses through generic type arguments
2421        // (`parse_type_ref` -> `parse_type_atom` -> `parse_type_ref`); the same
2422        // guard bounds it (#713). A right-nested `Result[Int, …]` in parameter
2423        // position drives that recursion.
2424        let errs = on_big_stack(|| {
2425            let depth = crate::MAX_NESTING_DEPTH + 8;
2426            let src = format!(
2427                "commons x {{ fn f(x: {}Int{}) -> Int {{ 0 }} }}",
2428                "Result[Int, ".repeat(depth),
2429                "]".repeat(depth),
2430            );
2431            parse_str(&src).unwrap_err()
2432        });
2433        assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
2434    }
2435
2436    #[test]
2437    fn deeply_nested_patterns_are_bounded_not_overflowed() {
2438        // Variant patterns are a third self-recursive descent (`parse_pattern`
2439        // -> `parse_pattern_binding` -> `parse_pattern`) that routes through
2440        // neither `parse_expr` nor `parse_type_ref`; without its own guard a
2441        // nested `Ok(Ok(…))` match arm reproduces the #713 crash.
2442        let errs = on_big_stack(|| {
2443            let depth = crate::MAX_NESTING_DEPTH + 8;
2444            let src = format!(
2445                "commons x {{ fn f(n: Int) -> Int {{ match n {{ {}n{} => 0 }} }} }}",
2446                "Ok(".repeat(depth),
2447                ")".repeat(depth),
2448            );
2449            parse_str(&src).unwrap_err()
2450        });
2451        assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
2452    }
2453
2454    #[test]
2455    fn nesting_below_the_limit_still_parses() {
2456        // The guard must not reject ordinary well-nested source: a paren-nested
2457        // expression comfortably under the limit still parses cleanly.
2458        let ok = on_big_stack(|| {
2459            let depth = crate::MAX_NESTING_DEPTH - 8;
2460            let src = format!(
2461                "commons x {{ fn f() -> Int {{ {}0{} }} }}",
2462                "(".repeat(depth),
2463                ")".repeat(depth),
2464            );
2465            parse_str(&src).is_ok()
2466        });
2467        assert!(ok, "well-nested source under the limit should parse");
2468    }
2469
2470    #[test]
2471    fn let_statement_parses() {
2472        let c = parse_str("commons x { fn f(n: Int) -> Int { let y = n + 1\n y } }").unwrap();
2473        let CommonsItem::Fn(f) = &c.items[0] else {
2474            panic!()
2475        };
2476        assert_eq!(f.body.statements.len(), 1);
2477        match &f.body.statements[0] {
2478            Statement::Let(l) => {
2479                assert_eq!(l.name.name, "y");
2480                assert!(l.type_annot.is_none());
2481            }
2482            _ => panic!("expected a pure `let` statement"),
2483        }
2484    }
2485
2486    #[test]
2487    fn let_with_annotation() {
2488        let c = parse_str("commons x { fn f(n: Int) -> Int { let y: Int = n\n y } }").unwrap();
2489        let CommonsItem::Fn(f) = &c.items[0] else {
2490            panic!()
2491        };
2492        match &f.body.statements[0] {
2493            Statement::Let(l) => assert!(l.type_annot.is_some()),
2494            _ => panic!("expected a pure `let` statement"),
2495        }
2496    }
2497
2498    #[test]
2499    fn if_else_parses_as_expression() {
2500        let c = parse_str("commons x { fn f(b: Bool) -> Int { if b { 1 } else { 0 } } }").unwrap();
2501        let CommonsItem::Fn(f) = &c.items[0] else {
2502            panic!()
2503        };
2504        assert!(matches!(f.body.tail.kind, ExprKind::If { .. }));
2505    }
2506
2507    #[test]
2508    fn else_if_chain_parses() {
2509        let c = parse_str(
2510            "commons x { fn f(n: Int) -> Int { if n < 0 { -1 } else if n == 0 { 0 } else { 1 } } }",
2511        )
2512        .unwrap();
2513        let CommonsItem::Fn(f) = &c.items[0] else {
2514            panic!()
2515        };
2516        let ExprKind::If { else_block, .. } = &f.body.tail.kind else {
2517            panic!()
2518        };
2519        // The else-branch is a block whose tail is another `If`.
2520        assert!(else_block.statements.is_empty());
2521        assert!(matches!(else_block.tail.kind, ExprKind::If { .. }));
2522    }
2523
2524    #[test]
2525    fn ok_and_err_parse_as_expressions() {
2526        let c = parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Ok(n) } }").unwrap();
2527        let CommonsItem::Fn(f) = &c.items[0] else {
2528            panic!()
2529        };
2530        assert!(matches!(f.body.tail.kind, ExprKind::Ok(_)));
2531
2532        let c =
2533            parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Err(\"x\") } }").unwrap();
2534        let CommonsItem::Fn(f) = &c.items[0] else {
2535            panic!()
2536        };
2537        assert!(matches!(f.body.tail.kind, ExprKind::Err(_)));
2538    }
2539
2540    #[test]
2541    fn question_postfix_parses() {
2542        let c = parse_str(
2543            "commons x { type T = Int where Positive\n fn f(n: Int) -> Result[T, ValidationError] { let x = T.of(n)?\n Ok(x) } }",
2544        )
2545        .unwrap();
2546        let CommonsItem::Fn(f) = &c.items[1] else {
2547            panic!()
2548        };
2549        let Statement::Let(l) = &f.body.statements[0] else {
2550            panic!("expected a pure `let` statement");
2551        };
2552        assert!(matches!(l.value.kind, ExprKind::Question(_)));
2553    }
2554
2555    #[test]
2556    fn constructor_call_parses() {
2557        let c = parse_str(
2558            "commons x { type T = Int where Positive\n fn f(n: Int) -> Result[T, ValidationError] { T.of(n) } }",
2559        )
2560        .unwrap();
2561        let CommonsItem::Fn(f) = &c.items[1] else {
2562            panic!()
2563        };
2564        // v0.2: T.of(n) parses as a MethodCall with receiver Ident("T"); the
2565        // checker reinterprets it as a static call by noticing T is a type.
2566        let ExprKind::MethodCall {
2567            receiver, method, ..
2568        } = &f.body.tail.kind
2569        else {
2570            panic!("expected MethodCall, got {:?}", f.body.tail.kind)
2571        };
2572        let ExprKind::Ident(id) = &receiver.kind else {
2573            panic!("expected receiver Ident");
2574        };
2575        assert_eq!(id.name, "T");
2576        assert_eq!(method.name, "of");
2577    }
2578
2579    #[test]
2580    fn result_type_ref_parses() {
2581        let c = parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Ok(n) } }").unwrap();
2582        let CommonsItem::Fn(f) = &c.items[0] else {
2583            panic!()
2584        };
2585        assert!(matches!(f.return_type, TypeRef::Result(_, _, _)));
2586    }
2587
2588    #[test]
2589    fn result_missing_arg_count_errors() {
2590        let errs = parse_str("commons x { fn f(n: Int) -> Result[Int] { Ok(n) } }").unwrap_err();
2591        assert_eq!(errs[0].category, "bynk.parse.generic_arg_count");
2592    }
2593
2594    #[test]
2595    fn field_access_parses_in_v0_2() {
2596        // v0.2: field access is supported (the type checker validates the
2597        // field exists on the receiver's type). Parser-level acceptance:
2598        let c =
2599            parse_str("commons x { type R = { foo: Int }\n fn f(r: R) -> Int { r.foo } }").unwrap();
2600        let CommonsItem::Fn(f) = &c.items[1] else {
2601            panic!()
2602        };
2603        assert!(matches!(f.body.tail.kind, ExprKind::FieldAccess { .. }));
2604    }
2605
2606    // -- v1.1 trivia attachment --
2607
2608    #[test]
2609    fn leading_line_comment_attaches_to_next_decl() {
2610        let src = "commons x {\n// explain the type\ntype T = Int where NonNegative\n}";
2611        let c = parse_str(src).unwrap();
2612        let CommonsItem::Type(t) = &c.items[0] else {
2613            panic!()
2614        };
2615        assert_eq!(
2616            t.trivia.leading,
2617            vec![Comment::Line(" explain the type".to_string())]
2618        );
2619        assert!(t.trivia.trailing.is_none());
2620    }
2621
2622    #[test]
2623    fn trailing_line_comment_attaches_to_prev_decl() {
2624        let src = "commons x {\ntype T = Int where NonNegative  // trailing note\n}";
2625        let c = parse_str(src).unwrap();
2626        let CommonsItem::Type(t) = &c.items[0] else {
2627            panic!()
2628        };
2629        assert!(t.trivia.leading.is_empty());
2630        assert_eq!(t.trivia.trailing.as_deref(), Some(" trailing note"));
2631    }
2632
2633    #[test]
2634    fn grouped_leading_comments_attach_together() {
2635        let src = "commons x {\n// one\n// two\n// three\ntype T = Int where Positive\n}";
2636        let c = parse_str(src).unwrap();
2637        let CommonsItem::Type(t) = &c.items[0] else {
2638            panic!()
2639        };
2640        assert_eq!(
2641            t.trivia.leading,
2642            vec![
2643                Comment::Line(" one".to_string()),
2644                Comment::Line(" two".to_string()),
2645                Comment::Line(" three".to_string())
2646            ],
2647        );
2648    }
2649
2650    #[test]
2651    fn comment_with_doc_block_keeps_both() {
2652        // Both `-- intro` and the doc block should attach to the type decl.
2653        let src = "commons x {\n// intro\n---\ndocs\n---\ntype T = Int where Positive\n}";
2654        let c = parse_str(src).unwrap();
2655        let CommonsItem::Type(t) = &c.items[0] else {
2656            panic!()
2657        };
2658        assert_eq!(t.trivia.leading, vec![Comment::Line(" intro".to_string())]);
2659        assert_eq!(t.documentation.as_deref(), Some("docs"));
2660    }
2661
2662    #[test]
2663    fn messages_keyword_does_not_collide_with_a_commons_name_segment() {
2664        // `messages` is RESERVED_CONTEXTUAL (like `case`/`on`/`suite`), not a
2665        // hard keyword: `commons app.messages { ... }` — the design's own
2666        // natural naming choice for a bundle commons — must still parse.
2667        // (Caught during slice-1 implementation: a first pass made `messages`
2668        // a plain hard keyword and this exact name broke.)
2669        let src = "commons app.messages {\ntype T = Int where Positive\n}";
2670        let c = parse_str(src).unwrap();
2671        assert_eq!(c.name.joined(), "app.messages");
2672    }
2673
2674    #[test]
2675    fn messages_decl_parses_tag_annotation_and_entries() {
2676        // message-bundles slice 1 (#859): the construct + doc/trivia wiring.
2677        let src = "commons app.messages {\n\
2678                   // intro\n\
2679                   ---\n\
2680                   docs\n\
2681                   ---\n\
2682                   messages \"en\" @reference {\n\
2683                   \"greeting\" => \"Hello, {name}!\"\n\
2684                   \"farewell\" => \"Bye\"\n\
2685                   } // trailing\n\
2686                   }";
2687        let c = parse_str(src).unwrap();
2688        let CommonsItem::Messages(m) = &c.items[0] else {
2689            panic!("expected a messages item, got {:?}", c.items[0]);
2690        };
2691        assert_eq!(m.tag, "en");
2692        assert_eq!(m.annotations.len(), 1);
2693        assert_eq!(m.annotations[0].name.name, "reference");
2694        assert!(m.annotations[0].args.is_empty());
2695        assert_eq!(m.entries.len(), 2);
2696        assert_eq!(m.entries[0].code, "greeting");
2697        assert_eq!(m.entries[0].template, "Hello, {name}!");
2698        assert_eq!(m.entries[1].code, "farewell");
2699        assert_eq!(m.entries[1].template, "Bye");
2700        assert_eq!(m.trivia.leading, vec![Comment::Line(" intro".to_string())]);
2701        assert_eq!(m.documentation.as_deref(), Some("docs"));
2702        assert_eq!(m.trivia.trailing.as_deref(), Some(" trailing"));
2703    }
2704
2705    #[test]
2706    fn messages_decl_parses_with_no_annotation_and_no_entries() {
2707        // The parser stays permissive on annotation cardinality (zero-or-more)
2708        // — "exactly one `@reference`" is a checker concern (validate.rs), not
2709        // a parse error.
2710        let src = "commons app.messages {\nmessages \"en\" {\n}\n}";
2711        let c = parse_str(src).unwrap();
2712        let CommonsItem::Messages(m) = &c.items[0] else {
2713            panic!("expected a messages item, got {:?}", c.items[0]);
2714        };
2715        assert_eq!(m.tag, "en");
2716        assert!(m.annotations.is_empty());
2717        assert!(m.entries.is_empty());
2718    }
2719
2720    #[test]
2721    fn messages_decl_parses_syntactically_inside_a_context_too() {
2722        // Commons-only legality is a checker concern (bynk.messages.outside_commons
2723        // in bynk-emit's project validation), not a parser rejection — mirrors
2724        // how `service`/`agent` already parse syntactically inside `adapter`
2725        // bodies for the same reason.
2726        let src = "context app.svc {\nmessages \"en\" @reference {\n\"a\" => \"b\"\n}\n}";
2727        let toks = tokenize(src).unwrap();
2728        let (unit, errors) = parse_unit_with_recovery(&toks, src);
2729        assert!(errors.is_empty(), "unexpected parse errors: {errors:?}");
2730        let Some(SourceUnit::Context(ctx)) = unit else {
2731            panic!("expected a context")
2732        };
2733        let CommonsItem::Messages(m) = &ctx.items[0] else {
2734            panic!("expected a messages item, got {:?}", ctx.items[0]);
2735        };
2736        assert_eq!(m.tag, "en");
2737    }
2738
2739    #[test]
2740    fn comment_before_let_statement_attaches() {
2741        let src = "commons x {\nfn f(n: Int) -> Int {\n// pick a value\nlet y = n + 1\ny\n}\n}";
2742        let c = parse_str(src).unwrap();
2743        let CommonsItem::Fn(f) = &c.items[0] else {
2744            panic!()
2745        };
2746        let Statement::Let(l) = &f.body.statements[0] else {
2747            panic!()
2748        };
2749        assert_eq!(
2750            l.trivia.leading,
2751            vec![Comment::Line(" pick a value".to_string())]
2752        );
2753    }
2754
2755    #[test]
2756    fn comment_before_tail_attaches_to_block_tail() {
2757        let src = "commons x {\nfn f(n: Int) -> Int {\nlet y = n + 1\n// result\ny\n}\n}";
2758        let c = parse_str(src).unwrap();
2759        let CommonsItem::Fn(f) = &c.items[0] else {
2760            panic!()
2761        };
2762        assert_eq!(
2763            f.body.tail_leading_comments,
2764            vec![Comment::Line(" result".to_string())],
2765        );
2766    }
2767
2768    /// #637 Gap A: the contextual keywords `on` / `suite` / `case` are lexer
2769    /// tokens but `expect_ident` admits them as identifiers outside their one
2770    /// keyword position, so they are valid record-field and parameter names.
2771    /// The keyword reference now renders them as a distinct "contextual" tier
2772    /// rather than claiming (falsely) that they cannot be used as identifiers.
2773    #[test]
2774    fn contextual_keywords_are_valid_identifiers() {
2775        // Record field names.
2776        let c = parse_str("commons demo {\n  type R = { on: Int, suite: String, case: Bool }\n}")
2777            .expect("`on`/`suite`/`case` are valid field names");
2778        let CommonsItem::Type(_) = &c.items[0] else {
2779            panic!("expected a type decl")
2780        };
2781
2782        // Function parameter names (the other `expect_ident` position).
2783        parse_str("commons demo {\n  fn f(on: Int, case: Int) -> Int { 0 }\n}")
2784            .expect("`on`/`case` are valid parameter names");
2785
2786        // `suite` too, as a field name.
2787        parse_str("commons demo {\n  type R = { suite: Int }\n}")
2788            .expect("`suite` is a valid field name");
2789    }
2790
2791    /// #2837: a keyword is a name where the name is a label (followed by `:`)
2792    /// or a member on the `.`'s line, and nowhere it would bind a variable.
2793    #[test]
2794    fn keywords_are_labels_and_members_not_bindings() {
2795        for src in [
2796            "commons demo {\n  type A = { type: String, from: Int, self: Bool }\n}",
2797            "commons demo {\n  type S = | P(from: String, type: String)\n}",
2798            "commons demo {\n  type A = { type: String }\n  fn f(a: A) -> String { a.type }\n}",
2799            "commons demo {\n  type A = { type: String }\n  fn f() -> A { A { type: \"x\" } }\n}",
2800            "commons demo {\n  type A = { type: String }\n  fn f(a: A) -> String { match a { A { type: t, .. } => t } }\n}",
2801            "commons demo {\n  type S = | P(from: String)\n  fn f(s: S) -> String { match s { P(from: x) => x } }\n}",
2802            "commons demo {\n  type S = { n: Int }\n  fn S.from(n: Int) -> S { S { n: n } }\n  fn g() -> S { S.from(1) }\n}",
2803        ] {
2804            parse_str(src).unwrap_or_else(|e| panic!("should parse: {src}\n{e:?}"));
2805        }
2806        for src in [
2807            "commons demo {\n  fn f(type: Int) -> Int { 1 }\n}",
2808            "commons demo {\n  fn f() -> Int { let from = 1\n from }\n}",
2809            "commons demo {\n  type A = { type String }\n}",
2810            // The same-line rule: `a.` then a statement on the next line.
2811            "commons demo {\n  fn f(a: Int) -> Int {\n    a.\n    let x = 1\n    x\n  }\n}",
2812        ] {
2813            let errs = parse_str(src).expect_err(src);
2814            assert_eq!(errs[0].category, "bynk.parse.reserved_keyword", "{src}");
2815        }
2816    }
2817
2818    /// #2837: keyword shorthand is a targeted error. A pattern gets the fix
2819    /// `type: type_` (`MaybeIncorrect`), fresh when `type_` is already spelled;
2820    /// a construction gets a note and no fix.
2821    #[test]
2822    fn keyword_shorthand_is_a_targeted_error() {
2823        let pat = |extra: &str| {
2824            format!(
2825                "commons demo {{\n  type A = {{ type: String }}\n  fn f(a: A) -> String {{ {extra} match a {{ A {{ type, .. }} => \"x\" }} }}\n}}"
2826            )
2827        };
2828        let errs = parse_str(&pat("")).expect_err("pattern shorthand");
2829        assert_eq!(errs[0].category, "bynk.parse.keyword_field_shorthand");
2830        let fix = &errs[0].suggestions[0];
2831        assert_eq!(fix.edits[0].1, "type: type_");
2832        assert!(matches!(
2833            fix.applicability,
2834            crate::error::Applicability::MaybeIncorrect
2835        ));
2836
2837        let errs = parse_str(&pat("let type_ = 1")).expect_err("pattern shorthand");
2838        assert_eq!(errs[0].suggestions[0].edits[0].1, "type: type_2");
2839
2840        let errs = parse_str(
2841            "commons demo {\n  type A = { type: String }\n  fn f() -> A { A { type } }\n}",
2842        )
2843        .expect_err("construction shorthand");
2844        assert_eq!(errs[0].category, "bynk.parse.keyword_field_shorthand");
2845        assert!(errs[0].suggestions.is_empty());
2846    }
2847
2848    /// Drift guard: every alphabetic keyword the lexer declares must be
2849    /// classified by `is_reserved_keyword`, or be one of the *contextual*
2850    /// keywords `expect_ident` deliberately admits as identifiers
2851    /// (`on`/`suite`/`case`). Everything else in this codebase that can
2852    /// drift has a guard; this predicate had silently fallen 17 keywords
2853    /// behind, degrading the reserved-keyword diagnostic to the generic
2854    /// expected-token one.
2855    #[test]
2856    fn is_reserved_keyword_covers_every_lexer_keyword() {
2857        let lexer_src = include_str!("lexer.rs");
2858        let mut words = Vec::new();
2859        for line in lexer_src.lines() {
2860            let t = line.trim();
2861            if let Some(rest) = t.strip_prefix("#[token(\"")
2862                && let Some(word) = rest.split('"').next()
2863                && word.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
2864                && word.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
2865            {
2866                words.push(word.to_string());
2867            }
2868        }
2869        assert!(
2870            words.len() > 30,
2871            "keyword extraction looks broken: only {} words",
2872            words.len()
2873        );
2874        // Contextual keywords double as identifiers (see `expect_ident`); the
2875        // tier is single-sourced in `keywords::RESERVED_CONTEXTUAL`.
2876        use crate::keywords::RESERVED_CONTEXTUAL;
2877        let mut unclassified = Vec::new();
2878        for word in &words {
2879            let tokens = crate::lexer::tokenize(word).expect("keyword lexes");
2880            let kind = tokens.first().expect("keyword yields a token").kind;
2881            if !is_reserved_keyword(kind) && !RESERVED_CONTEXTUAL.contains(&word.as_str()) {
2882                unclassified.push(word.clone());
2883            }
2884        }
2885        assert!(
2886            unclassified.is_empty(),
2887            "keywords missing from is_reserved_keyword (add them, or document \
2888             them as contextual): {unclassified:?}"
2889        );
2890    }
2891
2892    /// Finding #27/#30: pins `is_item_start` to exactly the keyword set
2893    /// `declarations.rs`'s six item loops (`parse_commons_brace`/`_fragment`,
2894    /// `parse_context_brace`/`_fragment`, `parse_test_brace`/`_fragment`) plus
2895    /// `parse_adapter_body` dispatch on, as of this writing — the drift this
2896    /// finding fixed (`Property`, `Actor`, `Event`, `Binding`, and the
2897    /// `adapter` unit keyword itself were all missing from
2898    /// `recover_to_top_item`'s old hand-written sync list, even though every
2899    /// one of them is a real item/unit start). Adding a new item keyword to
2900    /// any of those loops should mean deliberately updating this list too,
2901    /// not silently leaving recovery unable to resync at it.
2902    #[test]
2903    fn is_item_start_matches_the_pinned_keyword_set() {
2904        use TokenKind::*;
2905        let expected_true = [
2906            Commons, Context, Adapter, Suite, Type, Fn, Messages, Event, Asset, Embed, Const, Uses,
2907            Consumes, Exports, Capability, Provides, Service, Agent, Actor, Binding, Stub, Case,
2908            Property,
2909        ];
2910        for kind in expected_true {
2911            assert!(is_item_start(kind), "{kind:?} must be an item start");
2912        }
2913        let expected_false = [
2914            Ident, Plus, Minus, Colon, Dot, Eq, LBrace, RBrace, LParen, RParen, If, Else, Let,
2915            Where, True, False, Match, Is, On, Given,
2916        ];
2917        for kind in expected_false {
2918            assert!(!is_item_start(kind), "{kind:?} must not be an item start");
2919        }
2920    }
2921
2922    /// Fuzz-found (#516): a context-only keyword at item position in a
2923    /// commons errors without consuming the token, and the recovery sync
2924    /// stops at exactly that keyword — without a progress guard the item
2925    /// loop re-reported the same error until memory ran out.
2926    #[test]
2927    fn recovery_makes_progress_on_context_only_keyword_in_commons() {
2928        let src = "commons demo\n\ncapability Logger {\n  fn log(m: String) -> Effect[()]\n}\n";
2929        let tokens = crate::lexer::tokenize(src).unwrap();
2930        let (unit, errors) = parse_unit_with_recovery(&tokens, src);
2931        assert!(unit.is_some(), "the commons header still parses");
2932        assert!(
2933            errors
2934                .iter()
2935                .any(|e| e.category == "bynk.capability.outside_context"),
2936            "the misplaced capability is reported: {errors:?}"
2937        );
2938        // Termination is the real assertion (this used to OOM); a bounded,
2939        // non-repeating error list is the observable proxy.
2940        assert!(errors.len() < 10, "recovery repeated itself: {errors:?}");
2941    }
2942
2943    #[test]
2944    fn trailing_file_comment_becomes_unit_trailing() {
2945        // A comment after the last item but before EOF (fragment form)
2946        // becomes the commons body's trailing comments so the formatter
2947        // can preserve it.
2948        let src = "commons x\n\ntype T = Int where Positive\n// afterword\n";
2949        let c = parse_str(src).unwrap();
2950        assert_eq!(
2951            c.trailing_comments,
2952            vec![Comment::Line(" afterword".to_string())]
2953        );
2954    }
2955
2956    #[test]
2957    fn trailing_file_comment_after_a_brace_form_commons_is_not_dropped() {
2958        // Regression: the brace form's item loop exits on `RBrace`, never
2959        // reaching the fragment form's end-of-input case that drains the
2960        // trivia table's epilogue — so a comment after the closing `}` was
2961        // silently discarded (and, per `epilogue_is_empty`'s debug_assert,
2962        // would panic a debug build instead of round-tripping through
2963        // `bynk-fmt`).
2964        let src = "commons x {\n  type T = Int where Positive\n}\n// afterword\n";
2965        let c = parse_str(src).unwrap();
2966        assert_eq!(
2967            c.trailing_comments,
2968            vec![Comment::Line(" afterword".to_string())]
2969        );
2970    }
2971
2972    #[test]
2973    fn trailing_file_comment_after_a_brace_form_context_is_not_dropped() {
2974        // Same regression as the commons case, for `parse_context_brace`.
2975        let src = "context x {\n  type T = Int where Positive\n}\n// afterword\n";
2976        let SourceUnit::Context(c) = parse_unit_str(src).unwrap() else {
2977            panic!("expected context");
2978        };
2979        assert_eq!(
2980            c.trailing_comments,
2981            vec![Comment::Line(" afterword".to_string())]
2982        );
2983    }
2984
2985    #[test]
2986    fn trailing_file_comment_after_a_brace_form_suite_is_not_dropped() {
2987        // Same regression as the commons case, for `parse_test_brace`.
2988        let src = "suite x {\n  case \"c\" {\n    expect 1 == 1\n  }\n}\n// afterword\n";
2989        let SourceUnit::Suite(s) = parse_unit_str(src).unwrap() else {
2990            panic!("expected suite");
2991        };
2992        assert_eq!(
2993            s.trailing_comments,
2994            vec![Comment::Line(" afterword".to_string())]
2995        );
2996    }
2997
2998    /// Finding #30: unlike the three regressions just above,
2999    /// `parse_adapter_body`'s brace-closing path never called
3000    /// `take_epilogue` at all (not a regression from a shared pattern — it
3001    /// simply never had the call), so a comment after a brace-form adapter's
3002    /// closing `}` was silently dropped. The fragment form (no braces) was
3003    /// already correct.
3004    #[test]
3005    fn trailing_file_comment_after_a_brace_form_adapter_is_not_dropped() {
3006        let src = "adapter x {\n  binding \"./x.ts\"\n}\n// afterword\n";
3007        let SourceUnit::Adapter(a) = parse_unit_str(src).unwrap() else {
3008            panic!("expected adapter");
3009        };
3010        assert_eq!(
3011            a.trailing_comments,
3012            vec![Comment::Line(" afterword".to_string())]
3013        );
3014    }
3015
3016    /// #2321: `includes [ … ]` follows the module, before `requires`; it is a
3017    /// contextual word, so a malformed or unclosed list is an error, not a panic.
3018    #[test]
3019    fn binding_includes_list_parses_before_requires() {
3020        let src = "adapter x {\n  binding \"./x.ts\" includes [\"./a.wasm\", \"b/c.wasm\"] requires { \"p\": \"^1\" }\n}\n";
3021        let SourceUnit::Adapter(a) = parse_unit_str(src).unwrap() else {
3022            panic!("expected adapter");
3023        };
3024        let b = a.binding.unwrap();
3025        let paths: Vec<_> = b.includes.iter().map(|i| i.path.as_str()).collect();
3026        assert_eq!(paths, ["./a.wasm", "b/c.wasm"]);
3027        assert_eq!(b.requires.len(), 1);
3028        assert!(parse_unit_str("adapter x {\n  binding \"./x.ts\" includes [\"a.wasm\"").is_err());
3029        assert!(parse_unit_str("adapter x {\n  binding \"./x.ts\" includes [a]\n}\n").is_err());
3030    }
3031
3032    // -- Six-fold unification (review Part 3): the fragment-only ordering
3033    // restrictions declarations.rs's brace/fragment pairs preserve, now that
3034    // they share one function each behind `brace: bool`. None of these had
3035    // any prior test coverage at all. --
3036
3037    /// Fragment-form commons: `uses` must precede every `type`/`fn`.
3038    #[test]
3039    fn commons_fragment_rejects_uses_after_a_decl() {
3040        let src = "commons x\n\ntype T = Int where Positive\nuses bynk.list\n";
3041        let errs = parse_str(src).unwrap_err();
3042        assert!(
3043            errs.iter()
3044                .any(|e| e.category == "bynk.parse.uses_after_decls"),
3045            "{errs:?}"
3046        );
3047    }
3048
3049    /// The same ordering is NOT enforced in brace form — `uses` may appear
3050    /// anywhere in the body.
3051    #[test]
3052    fn commons_brace_allows_uses_after_a_decl() {
3053        let src = "commons x {\n  type T = Int where Positive\n  uses bynk.list\n}\n";
3054        parse_str(src).expect("brace form must not enforce fragment's uses-ordering rule");
3055    }
3056
3057    /// Fragment-form context: `consumes` must precede every `type`/`fn`/etc.
3058    #[test]
3059    fn context_fragment_rejects_consumes_after_a_decl() {
3060        let src = "context x\n\ntype T = Int where Positive\nconsumes bynk\n";
3061        let errs = parse_unit_str(src).unwrap_err();
3062        assert!(
3063            errs.iter()
3064                .any(|e| e.category == "bynk.parse.consumes_after_decls"),
3065            "{errs:?}"
3066        );
3067    }
3068
3069    /// Fragment-form context: `exports` must precede every `type`/`fn`/etc.
3070    #[test]
3071    fn context_fragment_rejects_exports_after_a_decl() {
3072        let src = "context x\n\ntype T = Int where Positive\nexports opaque { T }\n";
3073        let errs = parse_unit_str(src).unwrap_err();
3074        assert!(
3075            errs.iter()
3076                .any(|e| e.category == "bynk.parse.exports_after_decls"),
3077            "{errs:?}"
3078        );
3079    }
3080
3081    /// Brace-form context enforces none of the three orderings.
3082    #[test]
3083    fn context_brace_allows_consumes_and_exports_after_a_decl() {
3084        let src = "context x {\n  type T = Int where Positive\n  consumes bynk\n  exports opaque { T }\n}\n";
3085        parse_unit_str(src)
3086            .expect("brace form must not enforce fragment's consumes/exports-ordering rules");
3087    }
3088
3089    /// Fragment-form suite/test: `uses` must precede every `stub`/`case`/`property`.
3090    #[test]
3091    fn test_fragment_rejects_uses_after_a_decl() {
3092        let src = "suite m\n\ncase \"c\" {\n  expect true\n}\nuses bynk.list\n";
3093        let errs = parse_unit_str(src).unwrap_err();
3094        assert!(
3095            errs.iter()
3096                .any(|e| e.category == "bynk.parse.uses_after_decls"),
3097            "{errs:?}"
3098        );
3099    }
3100
3101    /// Brace-form suite/test allows `uses` anywhere.
3102    #[test]
3103    fn test_brace_allows_uses_after_a_decl() {
3104        let src = "suite m {\n  case \"c\" {\n    expect true\n  }\n  uses bynk.list\n}\n";
3105        parse_unit_str(src).expect("brace form must not enforce fragment's uses-ordering rule");
3106    }
3107
3108    // ---- #636: `if`/`match` condition vs record construction ----
3109
3110    /// Parse `body` as the tail expression of a fn and return its kind.
3111    fn body_tail(body: &str) -> ExprKind {
3112        let src = format!("commons x\n\nfn f() -> Int {{\n  {body}\n}}\n");
3113        let c = parse_str(&src).unwrap_or_else(|e| panic!("parse failed for {body:?}: {e:?}"));
3114        let CommonsItem::Fn(f) = &c.items[0] else {
3115            panic!("expected fn, got {:?}", c.items[0]);
3116        };
3117        f.body.tail.kind.clone()
3118    }
3119
3120    fn body_err(body: &str) -> Vec<CompileError> {
3121        let src = format!("commons x\n\nfn f() -> Int {{\n  {body}\n}}\n");
3122        parse_str(&src).expect_err(&format!("expected a parse error for {body:?}"))
3123    }
3124
3125    // ---- #2641: record patterns ----
3126
3127    /// Apply a parse error's first suggestion to `src`.
3128    fn apply_fix(src: &str, err: &CompileError) -> String {
3129        let s = &err.suggestions[0];
3130        assert_eq!(
3131            s.applicability,
3132            crate::error::Applicability::MachineApplicable
3133        );
3134        let mut edits = s.edits.clone();
3135        edits.sort_by_key(|(span, _)| std::cmp::Reverse(span.start));
3136        let mut out = src.to_string();
3137        for (span, text) in edits {
3138            out.replace_range(span.start..span.end, &text);
3139        }
3140        out
3141    }
3142
3143    // ---- #2680: list patterns ----
3144
3145    #[test]
3146    fn list_patterns_parse_in_every_shape() {
3147        let ExprKind::Match { arms, .. } = body_tail(
3148            "match xs { [] => 0\n [a] => 1\n [a, b, ..] => 2\n [a, ..rest] => 3\n [..all] => 4\n [..] => 5\n [a, ..,] => 6\n Some([x, ..]) => 7 }",
3149        ) else {
3150            panic!("expected Match");
3151        };
3152        let shape = |i: usize| match &arms[i].pattern {
3153            Pattern::List { elems, rest, .. } => (
3154                elems.len(),
3155                rest.as_ref()
3156                    .map(|r| r.binder.as_ref().map(|b| b.name.clone())),
3157            ),
3158            other => panic!("arm {i}: expected a list pattern, got {other:?}"),
3159        };
3160        assert_eq!(shape(0), (0, None));
3161        assert_eq!(shape(1), (1, None));
3162        assert_eq!(shape(2), (2, Some(None)));
3163        assert_eq!(shape(3), (1, Some(Some("rest".to_string()))));
3164        assert_eq!(shape(4), (0, Some(Some("all".to_string()))));
3165        assert_eq!(shape(5), (0, Some(None)));
3166        assert_eq!(shape(6), (1, Some(None)));
3167        assert!(matches!(&arms[7].pattern, Pattern::Variant { bindings, .. }
3168            if matches!(bindings[0].pattern(), Pattern::List { .. })));
3169        // Only `[..]` and `[..rest]` match every list.
3170        assert!(!arms[0].pattern.is_irrefutable());
3171        assert!(!arms[2].pattern.is_irrefutable());
3172        assert!(arms[4].pattern.is_irrefutable());
3173        assert!(arms[5].pattern.is_irrefutable());
3174    }
3175
3176    #[test]
3177    fn list_pattern_after_is_closes_at_the_bracket() {
3178        for body in [
3179            "if xs is [_, ..] { 1 } else { 2 }",
3180            "if xs is [] { 1 } else { 2 }",
3181            "if xs is [a, ..rest] { 1 } else { 2 }",
3182            "if xs is [S { n: 1, .. }, ..] { 1 } else { 2 }",
3183            "if xs is [] | [_] { 1 } else { 2 }",
3184        ] {
3185            let src = format!("commons x\n\nfn f() -> Int {{\n  {body}\n}}\n");
3186            parse_str(&src).unwrap_or_else(|e| panic!("{body:?}: {e:?}"));
3187        }
3188    }
3189
3190    #[test]
3191    fn list_rest_must_be_last() {
3192        for body in [
3193            "match xs { [.., last] => 1 }",
3194            "match xs { [..r, last] => 1 }",
3195            "match xs { [.., ..] => 1 }",
3196            "match xs { [a, .., b, c] => 1 }",
3197        ] {
3198            let errs = body_err(body);
3199            assert_eq!(errs[0].category, "bynk.parse.list_rest_not_last", "{body}");
3200            assert!(
3201                errs[0].suggestions.is_empty(),
3202                "{body}: only trailing wildcards earn a fix"
3203            );
3204        }
3205    }
3206
3207    #[test]
3208    fn list_rest_followed_by_wildcards_has_a_fix() {
3209        for (body, fixed) in [
3210            ("match xs { [.., _] => 1 }", "match xs { [_, ..] => 1 }"),
3211            (
3212                "match xs { [a, .., _, _] => 1 }",
3213                "match xs { [a, _, _, ..] => 1 }",
3214            ),
3215        ] {
3216            let errs = body_err(body);
3217            assert_eq!(errs[0].category, "bynk.parse.list_rest_not_last");
3218            let src = format!("commons x\n\nfn f() -> Int {{\n  {body}\n}}\n");
3219            let got = apply_fix(&src, &errs[0]);
3220            assert!(got.contains(fixed), "{body}: fixed to {got:?}");
3221            parse_str(&got).unwrap_or_else(|e| panic!("{body:?} fixed to {got:?}: {e:?}"));
3222        }
3223    }
3224
3225    #[test]
3226    fn list_rest_followed_by_an_integer_is_a_range_element() {
3227        // #2699 retired slice 4's reservation: `..` then an integer is a range.
3228        for body in [
3229            "match xs { [..9] => 1 _ => 0 }",
3230            "match xs { [a, ..-1] => 1 _ => 0 }",
3231            "match xs { [0..9, ..rest] => 1 _ => 0 }",
3232            "match xs { [18.., ..] => 1 _ => 0 }",
3233        ] {
3234            let ExprKind::Match { arms, .. } = body_tail(body) else {
3235                panic!("expected Match: {body}");
3236            };
3237            let Pattern::List { elems, .. } = &arms[0].pattern else {
3238                panic!("expected a list pattern: {body}");
3239            };
3240            assert!(
3241                elems.iter().any(|e| matches!(e, Pattern::Range(_))),
3242                "{body}"
3243            );
3244        }
3245        // A string or boolean after `..` is still no form.
3246        for body in ["match xs { [..\"a\"] => 1 }", "match xs { [..true] => 1 }"] {
3247            let errs = body_err(body);
3248            assert_eq!(errs[0].category, "bynk.parse.expected_token", "{body}");
3249        }
3250        // And `.._` is not a form.
3251        let errs = body_err("match xs { [a, .._] => 1 }");
3252        assert_eq!(errs[0].category, "bynk.parse.expected_token");
3253    }
3254
3255    #[test]
3256    fn int_range_patterns_parse_in_every_form() {
3257        let range = |body: &str| {
3258            let ExprKind::Match { arms, .. } = body_tail(body) else {
3259                panic!("expected Match: {body}");
3260            };
3261            match &arms[0].pattern {
3262                Pattern::Range(r) => (r.lo, r.hi),
3263                other => panic!("expected a range: {body}: {other:?}"),
3264            }
3265        };
3266        assert_eq!(range("match n { 0..12 => 1 _ => 0 }"), (Some(0), Some(12)));
3267        assert_eq!(range("match n { 18.. => 1 _ => 0 }"), (Some(18), None));
3268        assert_eq!(range("match n { ..-1 => 1 _ => 0 }"), (None, Some(-1)));
3269        assert_eq!(
3270            range("match n { -5..-1 => 1 _ => 0 }"),
3271            (Some(-5), Some(-1))
3272        );
3273        assert_eq!(
3274            range("match n { 1_000..2_000 => 1 _ => 0 }"),
3275            (Some(1000), Some(2000))
3276        );
3277        // An open range ends its arm: the next arm's literal is not its bound.
3278        let ExprKind::Match { arms, .. } = body_tail("match n { 18.. => 1 5 => 2 _ => 0 }") else {
3279            panic!("expected Match");
3280        };
3281        assert!(matches!(arms[0].pattern, Pattern::Range(_)));
3282        assert!(matches!(arms[1].pattern, Pattern::Literal { .. }));
3283    }
3284
3285    #[test]
3286    fn a_range_is_not_an_event_pattern_value() {
3287        // Only `via schema(...)` takes a range; a payload field's value does not.
3288        let src = "service S from Events(E { n: 1..5 }) { }";
3289        assert!(parse_str(src).is_err());
3290    }
3291
3292    #[test]
3293    fn record_pattern_parses_in_a_match_arm_and_nested() {
3294        let ExprKind::Match { arms, .. } = body_tail(
3295            "match o { Order { status: Paid, total, .. } => 1 Some(Order { .. }) => 2 _ => 3 }",
3296        ) else {
3297            panic!("expected Match");
3298        };
3299        let Pattern::Record {
3300            type_name, fields, ..
3301        } = &arms[0].pattern
3302        else {
3303            panic!("expected a record pattern, got {:?}", arms[0].pattern);
3304        };
3305        assert_eq!(type_name.name, "Order");
3306        assert_eq!(fields.len(), 2);
3307        assert!(fields[0].1.is_some(), "`status: Paid` has a sub-pattern");
3308        assert!(fields[1].1.is_none(), "`total` is shorthand");
3309        let Pattern::Variant { bindings, .. } = &arms[1].pattern else {
3310            panic!("expected Some(..)");
3311        };
3312        assert!(matches!(bindings[0].pattern(), Pattern::Record { .. }));
3313    }
3314
3315    #[test]
3316    fn record_pattern_without_rest_is_an_error_with_a_fix() {
3317        for body in [
3318            "match o { Order { total } => 1 }",
3319            "match o { Order { total, } => 1 }",
3320            "match o { Order { status: Paid, total } => 1 }",
3321            "match o { Order { } => 1 }",
3322        ] {
3323            let errs = body_err(body);
3324            assert_eq!(errs[0].category, "bynk.parse.record_pattern_without_rest");
3325            let src = format!("commons x\n\nfn f() -> Int {{\n  {body}\n}}\n");
3326            let fixed = apply_fix(&src, &errs[0]);
3327            parse_str(&fixed).unwrap_or_else(|e| panic!("{body:?} fixed to {fixed:?}: {e:?}"));
3328        }
3329    }
3330
3331    #[test]
3332    fn record_pattern_after_is_must_be_parenthesised() {
3333        for body in [
3334            "if o is Order { status: Paid, .. } { 1 } else { 2 }",
3335            "if o is None | Order { .. } { 1 } else { 2 }",
3336        ] {
3337            let errs = body_err(body);
3338            assert_eq!(
3339                errs[0].category,
3340                "bynk.parse.record_pattern_in_is_unparenthesised"
3341            );
3342            let src = format!("commons x\n\nfn f() -> Int {{\n  {body}\n}}\n");
3343            if body.contains("None |") {
3344                continue; // the fix wraps only the record alternative
3345            }
3346            let fixed = apply_fix(&src, &errs[0]);
3347            parse_str(&fixed).unwrap_or_else(|e| panic!("{body:?} fixed to {fixed:?}: {e:?}"));
3348        }
3349        // Parenthesised, nested, and a unit variant before the block all parse.
3350        for body in [
3351            "if o is (Order { status: Paid, .. }) { 1 } else { 2 }",
3352            "if o is Some(Order { .. }) { 1 } else { 2 }",
3353            "if o is (None | Order { .. }) { 1 } else { 2 }",
3354            "if o is Paid { 1 } else { 2 }",
3355        ] {
3356            body_tail(body);
3357        }
3358    }
3359
3360    #[test]
3361    fn if_condition_ending_in_ident_does_not_swallow_a_single_ident_branch() {
3362        // #636: `ready { result }` shares its shape with a shorthand-field
3363        // record construction. In condition position the branch must win.
3364        for src in [
3365            "if ready { result } else { fallback }",
3366            "if ready { fallback } else { result }",
3367            "if !ready { result } else { fallback }",
3368            "if a == b { result } else { fallback }",
3369            "if a && b { result } else { fallback }",
3370        ] {
3371            let ExprKind::If {
3372                then_block,
3373                else_block,
3374                ..
3375            } = body_tail(src)
3376            else {
3377                panic!("expected If for {src:?}, got {:?}", body_tail(src));
3378            };
3379            // Both branches carry a bare-identifier tail — proof the `{ … }`
3380            // was read as a block, not consumed as a record by the condition.
3381            assert!(
3382                matches!(&then_block.tail.kind, ExprKind::Ident(_)),
3383                "then-branch tail not an ident for {src:?}: {:?}",
3384                then_block.tail.kind,
3385            );
3386            assert!(
3387                matches!(&else_block.tail.kind, ExprKind::Ident(_)),
3388                "else-branch tail not an ident for {src:?}: {:?}",
3389                else_block.tail.kind,
3390            );
3391        }
3392    }
3393
3394    #[test]
3395    fn else_less_if_with_single_ident_branch_parses() {
3396        // The no-`else` reproduction: previously errored `found `}``.
3397        let ExprKind::If { then_block, .. } = body_tail("if ready { result }") else {
3398            panic!("expected If");
3399        };
3400        assert!(matches!(&then_block.tail.kind, ExprKind::Ident(_)));
3401    }
3402
3403    #[test]
3404    fn record_construction_still_parses_in_value_position() {
3405        // The restriction is confined to condition spines — an ordinary value
3406        // position still constructs records, including the shorthand tail form.
3407        assert!(matches!(
3408            body_tail("Point { x }"),
3409            ExprKind::RecordConstruction { .. }
3410        ));
3411        assert!(matches!(
3412            body_tail("Point { x: 1, y: 2 }"),
3413            ExprKind::RecordConstruction { .. }
3414        ));
3415        assert!(matches!(
3416            body_tail("Empty {}"),
3417            ExprKind::RecordConstruction { .. }
3418        ));
3419    }
3420
3421    #[test]
3422    fn parenthesised_record_is_allowed_in_condition_head() {
3423        // A delimiter lifts the restriction: `(ready { result })` constructs a
3424        // record even in condition position (mirrors Rust's paren escape).
3425        let ExprKind::If { cond, .. } =
3426            body_tail("if (ready { result }) { branch } else { other }")
3427        else {
3428            panic!("expected If");
3429        };
3430        let ExprKind::Paren(inner) = &cond.kind else {
3431            panic!("expected a parenthesised condition, got {:?}", cond.kind);
3432        };
3433        assert!(
3434            matches!(&inner.kind, ExprKind::RecordConstruction { .. }),
3435            "parenthesised record in condition head should still construct: {:?}",
3436            inner.kind,
3437        );
3438    }
3439
3440    #[test]
3441    fn record_in_call_arg_within_condition_still_constructs() {
3442        // The restriction is lifted through a call-argument delimiter, so a
3443        // record literal passed to a predicate in the condition still parses.
3444        let ExprKind::If { cond, .. } = body_tail("if check(Point { x: 1 }) { a } else { b }")
3445        else {
3446            panic!("expected If");
3447        };
3448        let ExprKind::Call { args, .. } = &cond.kind else {
3449            panic!("expected Call in condition, got {:?}", cond.kind);
3450        };
3451        assert!(matches!(&args[0].kind, ExprKind::RecordConstruction { .. }));
3452    }
3453
3454    #[test]
3455    fn safe_condition_shapes_are_unaffected() {
3456        // Cases the issue lists as already-safe must stay safe.
3457        assert!(matches!(
3458            body_tail("if ready == true { result } else { fallback }"),
3459            ExprKind::If { .. }
3460        ));
3461        assert!(matches!(
3462            body_tail("if (ready) { result } else { fallback }"),
3463            ExprKind::If { .. }
3464        ));
3465        assert!(matches!(
3466            body_tail("if ready { \"a\" } else { \"b\" }"),
3467            ExprKind::If { .. }
3468        ));
3469    }
3470
3471    fn body_block(body: &str) -> Block {
3472        let src = format!("commons x\n\nfn f() -> Int {{\n  {body}\n}}\n");
3473        let c = parse_str(&src).unwrap_or_else(|e| panic!("parse failed for {body:?}: {e:?}"));
3474        let CommonsItem::Fn(f) = &c.items[0] else {
3475            panic!("expected fn, got {:?}", c.items[0]);
3476        };
3477        f.body.clone()
3478    }
3479
3480    fn has_error(errs: &[CompileError], code: &str) -> bool {
3481        errs.iter().any(|e| e.category == code)
3482    }
3483
3484    // ---- #2712: record patterns as lambda parameters ----
3485
3486    fn lambda_of(body: &str) -> LambdaExpr {
3487        let b = body_block(body);
3488        let ExprKind::Lambda(l) = &b.tail.kind else {
3489            panic!("expected a lambda tail, got {:?}", b.tail.kind);
3490        };
3491        l.clone()
3492    }
3493
3494    #[test]
3495    fn a_record_pattern_parameter_desugars_to_a_hidden_binder_and_a_let() {
3496        // `(P) => body` is `(__lambdaArg) => { let P = __lambdaArg; body }`.
3497        let l = lambda_of("(Item { qty, .. }) => qty");
3498        assert_eq!(l.params.len(), 1);
3499        let p = &l.params[0];
3500        assert!(p.name.name.starts_with(LAMBDA_ARG_BINDER_PREFIX));
3501        assert!(is_generated_binding(&p.name.name));
3502        assert_eq!(
3503            p.name.span.start, p.name.span.end,
3504            "the binder has no extent"
3505        );
3506        assert!(p.type_ref.is_none());
3507        assert!(matches!(p.pattern, Some(Pattern::Record { .. })));
3508        assert!(!l.block_body);
3509        let ExprKind::Block(b) = &l.body.kind else {
3510            panic!("expected a block body, got {:?}", l.body.kind);
3511        };
3512        let Statement::Let(hidden) = &b.statements[0] else {
3513            panic!("expected the hidden `let`, got {:?}", b.statements[0]);
3514        };
3515        assert_eq!(
3516            hidden.let_else.as_ref().map(|le| le.form),
3517            Some(LetElseForm::Plain)
3518        );
3519        assert!(b.tail.is_let_else_match());
3520        assert!(
3521            matches!(&hidden.value.kind, ExprKind::Ident(i) if i.name == p.name.name),
3522            "the `let` destructures the hidden parameter"
3523        );
3524    }
3525
3526    #[test]
3527    fn name_and_pattern_parameters_mix_and_each_binder_is_distinct() {
3528        let l = lambda_of("(a, Item { qty, .. }, b: Int, Box { v, .. }) => a");
3529        let names: Vec<_> = l.params.iter().map(|p| p.name.name.as_str()).collect();
3530        assert_eq!(names[0], "a");
3531        assert_eq!(names[2], "b");
3532        assert!(names[1].starts_with(LAMBDA_ARG_BINDER_PREFIX));
3533        assert!(names[3].starts_with(LAMBDA_ARG_BINDER_PREFIX));
3534        assert_ne!(names[1], names[3]);
3535        assert!(l.params[0].pattern.is_none() && l.params[2].type_ref.is_some());
3536        // The first pattern's `let` is outermost, the second's inside its arm.
3537        let ExprKind::Block(outer) = &l.body.kind else {
3538            panic!("expected a block body");
3539        };
3540        let ExprKind::Match { arms, .. } = &outer.tail.kind else {
3541            panic!("expected the first pattern's match");
3542        };
3543        let MatchBody::Block(inner) = &arms[0].body else {
3544            panic!("expected the arm's block");
3545        };
3546        assert!(inner.tail.is_let_else_match(), "the second pattern nests");
3547    }
3548
3549    #[test]
3550    fn a_block_body_is_kept_as_the_pattern_arms_block() {
3551        let l = lambda_of("(Item { qty, .. }) => { let x = qty\n  x }");
3552        assert!(l.block_body);
3553    }
3554
3555    #[test]
3556    fn a_lambda_without_pattern_parameters_is_unchanged() {
3557        let l = lambda_of("(a, b: Int) => a");
3558        assert!(l.params.iter().all(|p| p.pattern.is_none()));
3559        assert!(matches!(l.body.kind, ExprKind::Ident(_)));
3560    }
3561
3562    #[test]
3563    fn a_lambda_pattern_parameter_that_is_refutable_names_its_first_failing_field() {
3564        let errs = body_err("(Item { status: Paid, qty, .. }) => qty");
3565        assert_eq!(errs[0].category, "bynk.parse.lambda_pattern_refutable");
3566        assert!(
3567            errs[0].notes.iter().any(|n| n.contains("status: Paid")),
3568            "{:?}",
3569            errs[0].notes
3570        );
3571    }
3572
3573    #[test]
3574    fn a_lambda_pattern_parameter_must_be_a_record_pattern() {
3575        for body in [
3576            "(Some(x)) => x",
3577            "(a, [x, ..]) => x",
3578            "(0..9) => 1",
3579            "(1) => 1",
3580            "(None) => 1",
3581            "(Status.Paid) => 1",
3582        ] {
3583            let errs = body_err(body);
3584            assert_eq!(
3585                errs[0].category, "bynk.parse.lambda_pattern_not_record",
3586                "{body}"
3587            );
3588        }
3589    }
3590
3591    #[test]
3592    fn an_annotated_lambda_pattern_parameter_offers_to_remove_the_annotation() {
3593        let body = "(Item { qty, .. }: Item) => qty";
3594        let errs = body_err(body);
3595        assert_eq!(errs[0].category, "bynk.parse.lambda_pattern_annotation");
3596        let src = format!("commons x\n\nfn f() -> Int {{\n  {body}\n}}\n");
3597        let fixed = apply_fix(&src, &errs[0]);
3598        assert!(fixed.contains("(Item { qty, .. }) => qty"), "{fixed}");
3599        parse_str(&fixed).unwrap_or_else(|e| panic!("fixed to {fixed:?}: {e:?}"));
3600    }
3601
3602    #[test]
3603    fn a_missing_rest_in_a_lambda_pattern_parameter_has_slice_ones_fix() {
3604        let body = "(a, Item { qty }) => qty";
3605        let errs = body_err(body);
3606        assert_eq!(errs[0].category, "bynk.parse.record_pattern_without_rest");
3607        let src = format!("commons x\n\nfn f() -> Int {{\n  {body}\n}}\n");
3608        let fixed = apply_fix(&src, &errs[0]);
3609        parse_str(&fixed).unwrap_or_else(|e| panic!("fixed to {fixed:?}: {e:?}"));
3610    }
3611
3612    #[test]
3613    fn let_else_desugars_to_a_hidden_binding_and_a_match() {
3614        // #2010: `let P = e else E; rest` is `let __v = e` then
3615        // `match __v { P => { rest }, _ => E }`.
3616        let b = body_block("let x = 1\n  let Some(n) = find(x) else { 0 }\n  n + x");
3617        assert_eq!(
3618            b.statements.len(),
3619            2,
3620            "the plain let and the hidden binding"
3621        );
3622        let Statement::Let(hidden) = &b.statements[1] else {
3623            panic!("expected a pure hidden binding, got {:?}", b.statements[1]);
3624        };
3625        assert!(
3626            is_generated_binding(&hidden.name.name),
3627            "{}",
3628            hidden.name.name
3629        );
3630        assert_eq!(
3631            hidden.name.span.start, hidden.name.span.end,
3632            "the binder has no extent"
3633        );
3634        let le = hidden
3635            .let_else
3636            .as_ref()
3637            .expect("the binding records the sugar");
3638        assert_eq!(le.form, LetElseForm::Block);
3639        let ExprKind::Match { discriminant, arms } = &b.tail.kind else {
3640            panic!("expected a match tail, got {:?}", b.tail.kind);
3641        };
3642        assert!(matches!(&discriminant.kind, ExprKind::Ident(i) if i.name == hidden.name.name));
3643        assert_eq!(arms.len(), 2);
3644        assert!(matches!(arms[0].pattern, Pattern::Variant { .. }));
3645        let MatchBody::Block(rest) = &arms[0].body else {
3646            panic!("the pattern's arm runs the rest of the block");
3647        };
3648        assert!(rest.statements.is_empty());
3649        assert!(
3650            !matches!(rest.tail.kind, ExprKind::UnitLit),
3651            "{:?}",
3652            rest.tail.kind
3653        );
3654        assert!(matches!(arms[1].pattern, Pattern::Wildcard(_)));
3655    }
3656
3657    /// #2640: `par { … }` parses to a `Par` holding its statements in order.
3658    #[test]
3659    fn par_block_holds_its_statements_in_source_order() {
3660        let b = body_block(
3661            "par {\n    let a <- one()\n    do two()\n    let b: Int <- three()\n  }\n  a",
3662        );
3663        let Statement::Par(p) = &b.statements[0] else {
3664            panic!("expected a par block, got {:?}", b.statements[0]);
3665        };
3666        assert_eq!(p.statements.len(), 3);
3667        assert!(matches!(&p.statements[0], Statement::EffectLet(l) if l.name.name == "a"));
3668        assert!(matches!(&p.statements[1], Statement::Do(_)));
3669        assert!(
3670            matches!(&p.statements[2], Statement::EffectLet(l) if l.name.name == "b" && l.type_annot.is_some())
3671        );
3672    }
3673
3674    /// #2640: `par` is contextual — an identifier everywhere but before `{`.
3675    #[test]
3676    fn par_is_an_identifier_outside_its_statement_shape() {
3677        let b = body_block("let par = 1\n  let x = par + 1\n  x");
3678        assert_eq!(b.statements.len(), 2);
3679        assert!(matches!(&b.statements[0], Statement::Let(l) if l.name.name == "par"));
3680    }
3681
3682    /// #2640: the parser admits any statement in a block (the checker names the
3683    /// forbidden ones), except those it cannot hold at all.
3684    #[test]
3685    fn par_block_admits_statements_for_the_checker_to_refuse() {
3686        let b = body_block(
3687            "par {\n    let a = 1\n    ~> notify()\n    par {\n      do x()\n    }\n  }\n  1",
3688        );
3689        let Statement::Par(p) = &b.statements[0] else {
3690            panic!("expected a par block");
3691        };
3692        assert!(matches!(&p.statements[0], Statement::Let(_)));
3693        assert!(matches!(&p.statements[1], Statement::Send(_)));
3694        assert!(matches!(&p.statements[2], Statement::Par(_)));
3695    }
3696
3697    #[test]
3698    fn par_block_refuses_a_refutable_let_and_a_stray_expression() {
3699        assert!(has_error(
3700            &body_err("par {\n    let Ok(v) <- load() else { 0 }\n  }\n  1"),
3701            "bynk.par.invalid_statement"
3702        ));
3703        assert!(has_error(
3704            &body_err("par {\n    load()\n  }\n  1"),
3705            "bynk.par.invalid_statement"
3706        ));
3707    }
3708
3709    #[test]
3710    fn effectful_let_else_binds_with_an_arrow() {
3711        let b = body_block("let Ok(v) <- load() else { Effect.pure(0) }\n  v");
3712        assert!(matches!(&b.statements[0], Statement::EffectLet(l) if l.let_else.is_some()));
3713    }
3714
3715    #[test]
3716    fn let_else_chain_nests_each_step_in_the_previous_arm() {
3717        // Three steps in one flat block: each later step lives in the earlier
3718        // one's pattern arm, and the plain `let` between them stays in order.
3719        let b = body_block(
3720            "let Ok(a) = one() else { 1 }\n  let y = a\n  let Ok(b) = two(y) else { 2 }\n  let Ok(c) = three(b) else { 3 }\n  c",
3721        );
3722        let ExprKind::Match { arms, .. } = &b.tail.kind else {
3723            panic!("expected a match tail");
3724        };
3725        let MatchBody::Block(second) = &arms[0].body else {
3726            panic!("expected a block arm");
3727        };
3728        assert_eq!(
3729            second.statements.len(),
3730            2,
3731            "`let y` and the second hidden binding"
3732        );
3733        assert!(matches!(&second.statements[0], Statement::Let(l) if l.name.name == "y"));
3734        let ExprKind::Match { arms, .. } = &second.tail.kind else {
3735            panic!("expected the second step's match");
3736        };
3737        let MatchBody::Block(third) = &arms[0].body else {
3738            panic!("expected a block arm");
3739        };
3740        let ExprKind::Match { arms, .. } = &third.tail.kind else {
3741            panic!("expected the third step's match");
3742        };
3743        let MatchBody::Block(last) = &arms[0].body else {
3744            panic!("expected a block arm");
3745        };
3746        assert!(matches!(&last.tail.kind, ExprKind::Ident(i) if i.name == "c"));
3747    }
3748
3749    #[test]
3750    fn else_match_arms_follow_the_patterns_arm() {
3751        let b = body_block("let Ok(v) = r else match { Err(e) => 0 }\n  v");
3752        let Statement::Let(l) = &b.statements[0] else {
3753            panic!("expected the hidden binding");
3754        };
3755        assert_eq!(l.let_else.as_ref().unwrap().form, LetElseForm::Match);
3756        let ExprKind::Match { arms, .. } = &b.tail.kind else {
3757            panic!("expected a match tail");
3758        };
3759        assert_eq!(arms.len(), 2);
3760        assert!(
3761            matches!(&arms[1].pattern, Pattern::Variant { variant, .. } if variant.name == "Err")
3762        );
3763    }
3764
3765    #[test]
3766    fn an_irrefutable_record_let_needs_no_else() {
3767        // #2654: `let T { .. } = e` is the one-arm `match` of a `let … else`
3768        // with no `else` arm, marked `LetElseForm::Plain`.
3769        for body in [
3770            "let Item { sku, qty: n, .. } = item\n  n",
3771            "let (Item { sku, .. }) = item\n  sku",
3772            "let Item { sku: _, inner: Inner { x, .. }, .. } <- load(1)\n  x",
3773            "let Item { sku, .. } <- load(1) by Admin(\"a\")\n  sku",
3774        ] {
3775            let b = body_block(body);
3776            let (Statement::Let(l) | Statement::EffectLet(l)) = &b.statements[0] else {
3777                panic!("{body:?}: expected the hidden binding");
3778            };
3779            let le = l.let_else.as_ref().expect("the binding records the sugar");
3780            assert_eq!(le.form, LetElseForm::Plain, "{body:?}");
3781            assert_eq!(le.else_span.start, le.else_span.end, "no `else` extent");
3782            let ExprKind::Match { arms, .. } = &b.tail.kind else {
3783                panic!("{body:?}: expected a match tail");
3784            };
3785            assert_eq!(arms.len(), 1, "{body:?}: only the pattern's arm");
3786            assert!(matches!(arms[0].pattern, Pattern::Record { .. }));
3787        }
3788    }
3789
3790    #[test]
3791    fn a_refutable_record_let_without_else_names_the_refutable_field() {
3792        for (body, field) in [
3793            (
3794                "let Order { status: Paid, total, .. } = o\n  total",
3795                "status: Paid",
3796            ),
3797            (
3798                "let Order { total, c: Cust { kind: Gold, .. }, .. } = o\n  total",
3799                "kind: Gold",
3800            ),
3801            ("let Order { id: 1, .. } <- load()\n  1", "id: 1"),
3802        ] {
3803            let errs = body_err(body);
3804            assert_eq!(errs[0].category, "bynk.parse.let_pattern_without_else");
3805            assert!(
3806                errs[0]
3807                    .notes
3808                    .iter()
3809                    .any(|n| n.contains(&format!("`{field}` can fail to match"))),
3810                "{body:?}: {:?}",
3811                errs[0].notes
3812            );
3813        }
3814        // Any other pattern without an `else` is refused as before, with no field named.
3815        for body in [
3816            "let Ok(n) = r\n  n",
3817            "let (Ok(n)) = r\n  n",
3818            "let Item { sku, .. } | Other = r\n  1",
3819        ] {
3820            let errs = body_err(body);
3821            assert_eq!(
3822                errs[0].category, "bynk.parse.let_pattern_without_else",
3823                "{body:?}"
3824            );
3825        }
3826    }
3827
3828    #[test]
3829    fn a_record_let_keeps_an_else_when_one_is_written() {
3830        let b = body_block("let Item { sku, .. } = item else { 0 }\n  1");
3831        let Statement::Let(l) = &b.statements[0] else {
3832            panic!("expected the hidden binding");
3833        };
3834        assert_eq!(l.let_else.as_ref().unwrap().form, LetElseForm::Block);
3835    }
3836
3837    #[test]
3838    fn a_capitalised_binding_is_not_a_record_pattern() {
3839        // `let Name = e` and `let Name: T = e` read `Name` as a binding, as before.
3840        for body in ["let Total = 1\n  Total", "let Total: Int = 1\n  Total"] {
3841            let b = body_block(body);
3842            let Statement::Let(l) = &b.statements[0] else {
3843                panic!("{body:?}: expected a plain let");
3844            };
3845            assert!(l.let_else.is_none(), "{body:?}");
3846        }
3847        // And `let Name {` is no longer an error at the brace: it opens a pattern.
3848        let errs = body_err("let Item { sku } = item\n  1");
3849        assert_eq!(errs[0].category, "bynk.parse.record_pattern_without_rest");
3850    }
3851
3852    #[test]
3853    fn let_else_as_the_last_statement_leaves_a_unit_rest() {
3854        let b = body_block("let Some(n) = find(1) else { () }");
3855        let ExprKind::Match { arms, .. } = &b.tail.kind else {
3856            panic!("expected a match tail");
3857        };
3858        let MatchBody::Block(rest) = &arms[0].body else {
3859            panic!("expected a block arm");
3860        };
3861        assert!(rest.implicit_tail && rest.statements.is_empty());
3862    }
3863
3864    #[test]
3865    fn let_pattern_forms_are_recognised() {
3866        for src in [
3867            "let None = o else { 0 }\n  1",
3868            "let Status.Done = s else { 0 }\n  1",
3869            "let Shipped(at) = s else { 0 }\n  at",
3870            "let 0 = n else { 1 }\n  0",
3871            "let -1 = n else { 1 }\n  0",
3872            "let \"a\" = s else { 1 }\n  0",
3873            "let (Ok(x) | Err(x)) = r else { 1 }\n  x",
3874        ] {
3875            let b = body_block(src);
3876            assert!(
3877                matches!(&b.statements[0], Statement::Let(l) if l.let_else.is_some()),
3878                "{src:?} should parse as a let-else"
3879            );
3880        }
3881        // A bare name, even capitalised, is still a binding.
3882        let b = body_block("let Total = 1\n  Total");
3883        assert!(
3884            matches!(&b.statements[0], Statement::Let(l) if l.let_else.is_none() && l.name.name == "Total")
3885        );
3886    }
3887
3888    #[test]
3889    fn let_pattern_without_else_is_refused() {
3890        assert!(has_error(
3891            &body_err("let Ok(v) = r\n  v"),
3892            "bynk.parse.let_pattern_without_else"
3893        ));
3894        assert!(has_error(
3895            &body_err("let Ok(v) <- r()\n  v"),
3896            "bynk.parse.let_pattern_without_else"
3897        ));
3898    }
3899
3900    #[test]
3901    fn else_on_a_plain_binding_is_refused() {
3902        assert!(has_error(
3903            &body_err("let v = r else { 0 }\n  v"),
3904            "bynk.parse.let_else_on_binding"
3905        ));
3906        assert!(has_error(
3907            &body_err("let v <- r() else { 0 }\n  v"),
3908            "bynk.parse.let_else_on_binding"
3909        ));
3910    }
3911
3912    #[test]
3913    fn annotated_let_pattern_is_refused() {
3914        assert!(has_error(
3915            &body_err("let Ok(v): Int = r else { 0 }\n  v"),
3916            "bynk.parse.let_pattern_annotation"
3917        ));
3918    }
3919
3920    #[test]
3921    fn empty_else_match_is_refused() {
3922        assert!(has_error(
3923            &body_err("let Ok(v) = r else match { }\n  v"),
3924            "bynk.parse.empty_match"
3925        ));
3926    }
3927
3928    #[test]
3929    fn let_else_steps_count_against_the_nesting_budget() {
3930        // Each step nests the rest of the block one level deeper, exactly as
3931        // the hand-written `match` it stands for, so a very long chain is
3932        // refused with the nesting diagnostic rather than overflowing later.
3933        let chain = |n: usize| -> String {
3934            let steps: String = (0..n)
3935                .map(|i| format!("let Ok(v{i}) = step() else {{ {i} }}\n  "))
3936                .collect();
3937            format!("{steps}0")
3938        };
3939        body_block(&chain(40));
3940        let errs = body_err(&chain(400));
3941        assert!(
3942            errs.iter()
3943                .any(|e| e.category == "bynk.parse.nesting_too_deep"),
3944            "{errs:?}"
3945        );
3946    }
3947
3948    #[test]
3949    fn empty_match_reports_its_own_diagnostic() {
3950        // #636: `match result {}` once parsed `result {}` as an empty record,
3951        // masking `bynk.parse.empty_match`. The intended diagnostic is now
3952        // reachable.
3953        let errs = body_err("match result {}");
3954        assert!(
3955            errs.iter().any(|e| e.category == "bynk.parse.empty_match"),
3956            "expected empty_match; got {errs:?}",
3957        );
3958    }
3959
3960    #[test]
3961    fn match_discriminant_ending_in_ident_parses() {
3962        // A `match` over a bare-identifier discriminant reaches its arm list.
3963        assert!(matches!(
3964            body_tail("match ready { x => x }"),
3965            ExprKind::Match { .. }
3966        ));
3967    }
3968
3969    /// #981: an identifier statement immediately followed, on its own line, by
3970    /// a standalone `()` must stay two separate constructs — not merge into a
3971    /// zero-arg call. `status := Paid` / `()` is an Assign statement whose
3972    /// value is the bare identifier `Paid`, then a unit tail; it must never
3973    /// parse as a single `status := Paid()` (a call). The call-parens rule
3974    /// mirrors the v0.20b same-line `[` rule already applied to type
3975    /// arguments: a postfix opener that begins a new line does not continue
3976    /// the previous token.
3977    #[test]
3978    fn identifier_statement_followed_by_unit_tail_does_not_merge_into_a_call() {
3979        let src = "commons c\n\nfn f() -> Int {\n  status := Paid\n  ()\n}\n";
3980        let c = parse_str(src).unwrap_or_else(|e| panic!("parse failed: {e:?}"));
3981        let CommonsItem::Fn(f) = &c.items[0] else {
3982            panic!("expected fn, got {:?}", c.items[0]);
3983        };
3984        assert_eq!(
3985            f.body.statements.len(),
3986            1,
3987            "expected exactly one Assign statement, got {:?}",
3988            f.body.statements
3989        );
3990        let Statement::Assign(a) = &f.body.statements[0] else {
3991            panic!(
3992                "expected an Assign statement, got {:?}",
3993                f.body.statements[0]
3994            );
3995        };
3996        assert!(
3997            matches!(a.value.kind, ExprKind::Ident(_)),
3998            "assign value must stay the bare identifier `Paid`, got {:?}",
3999            a.value.kind
4000        );
4001        assert!(
4002            matches!(f.body.tail.kind, ExprKind::UnitLit),
4003            "the `()` must remain the block's own tail, got {:?}",
4004            f.body.tail.kind
4005        );
4006    }
4007
4008    /// #1858: a variant pattern's `(` must sit on the variant's line too. An
4009    /// `is` test ending in a nullary variant, above a `()` tail on its own
4010    /// line, is a pattern and a unit, not `Paid()`/`None()`: `fmt` writes that
4011    /// tail after a comment, and its reparse must not absorb it.
4012    #[test]
4013    fn variant_pattern_followed_by_unit_tail_does_not_take_it_as_bindings() {
4014        for (name, pat) in [("user variant", "Paid"), ("built-in variant", "None")] {
4015            let src = format!("commons c\n\nfn f() -> () {{\n  let b = r is {pat}\n  ()\n}}\n");
4016            let c = parse_str(&src).unwrap_or_else(|e| panic!("{name}: parse failed: {e:?}"));
4017            let CommonsItem::Fn(f) = &c.items[0] else {
4018                panic!("expected fn, got {:?}", c.items[0]);
4019            };
4020            let Statement::Let(l) = &f.body.statements[0] else {
4021                panic!("{name}: expected a Let, got {:?}", f.body.statements[0]);
4022            };
4023            let ExprKind::Is { pattern, .. } = &l.value.kind else {
4024                panic!("{name}: expected an `is` test, got {:?}", l.value.kind);
4025            };
4026            let Pattern::Variant { variant, span, .. } = pattern.as_ref() else {
4027                panic!("{name}: expected a variant pattern, got {pattern:?}");
4028            };
4029            assert_eq!(
4030                span.end, variant.span.end,
4031                "{name}: the pattern must end at `{pat}`, not take the `()` below it"
4032            );
4033            assert!(
4034                matches!(f.body.tail.kind, ExprKind::UnitLit),
4035                "{name}: the `()` must remain the block's own tail, got {:?}",
4036                f.body.tail.kind
4037            );
4038        }
4039    }
4040
4041    /// #1858's boundary: a `(` on the variant's line still opens its payload
4042    /// list, and the list itself may span lines.
4043    #[test]
4044    fn variant_pattern_payload_on_the_same_line_still_binds() {
4045        for (name, pat) in [
4046            ("user variant", "Paid(\n    x\n  )"),
4047            ("built-in variant", "Some(x)"),
4048        ] {
4049            let src = format!("commons c\n\nfn f() -> Bool {{\n  r is {pat}\n}}\n");
4050            let c = parse_str(&src).unwrap_or_else(|e| panic!("{name}: parse failed: {e:?}"));
4051            let CommonsItem::Fn(f) = &c.items[0] else {
4052                panic!("expected fn, got {:?}", c.items[0]);
4053            };
4054            let ExprKind::Is { pattern, .. } = &f.body.tail.kind else {
4055                panic!("{name}: expected an `is` tail, got {:?}", f.body.tail.kind);
4056            };
4057            assert!(
4058                matches!(pattern.as_ref(), Pattern::Variant { bindings, .. } if bindings.len() == 1),
4059                "{name}: expected one binding, got {pattern:?}"
4060            );
4061        }
4062    }
4063
4064    /// #1858, chosen uniformly: a match arm's pattern is followed only by `if`
4065    /// or `=>`, so a next-line `(` there could not be the next statement, but
4066    /// the same-line rule holds in every pattern position anyway. One lexical
4067    /// rule, wherever a pattern appears, is the one a reader can apply without
4068    /// knowing the position. The arm is rejected, not misparsed.
4069    #[test]
4070    fn match_arm_pattern_payload_on_a_later_line_is_rejected() {
4071        let src = "commons c\n\nfn f() -> Int {\n  match m {\n    Settled\n      (amount) => 1,\n    _ => 0,\n  }\n}\n";
4072        assert!(
4073            parse_str(src).is_err(),
4074            "a next-line `(` after an arm's variant must not open its payload list"
4075        );
4076        let same_line = src.replace("Settled\n      (amount)", "Settled(amount)");
4077        parse_str(&same_line).unwrap_or_else(|e| panic!("same-line form must parse: {e:?}"));
4078    }
4079
4080    /// #981: the same same-line rule extends to a method call's parens — a
4081    /// `.method` immediately followed, on its own line, by a standalone `()`
4082    /// must not merge into `.method()`.
4083    #[test]
4084    fn method_reference_followed_by_unit_tail_does_not_merge_into_a_call() {
4085        let src = "commons c\n\nfn f() -> Int {\n  let y = x.field\n  ()\n}\n";
4086        let c = parse_str(src).unwrap_or_else(|e| panic!("parse failed: {e:?}"));
4087        let CommonsItem::Fn(f) = &c.items[0] else {
4088            panic!("expected fn, got {:?}", c.items[0]);
4089        };
4090        let Statement::Let(l) = &f.body.statements[0] else {
4091            panic!("expected a Let statement, got {:?}", f.body.statements[0]);
4092        };
4093        assert!(
4094            matches!(l.value.kind, ExprKind::FieldAccess { .. }),
4095            "let value must stay a field access, got {:?}",
4096            l.value.kind
4097        );
4098        assert!(
4099            matches!(f.body.tail.kind, ExprKind::UnitLit),
4100            "the `()` must remain the block's own tail, got {:?}",
4101            f.body.tail.kind
4102        );
4103    }
4104
4105    #[test]
4106    fn unparenthesised_record_in_condition_head_now_errors() {
4107        // #636 narrowing (matches Rust): a record literal in condition *head*
4108        // position must be parenthesised. Unparenthesised, `Point` reads as the
4109        // discriminant and `{ x: 1 }` as the arm list, whose first "arm" `x: 1`
4110        // is not an arm — so the parse fails. Pinned so the divergence from the
4111        // (still-accepting) tree-sitter grammar is deliberate, not a bug.
4112        assert!(
4113            !body_err("match Point { x: 1 } { p => p }").is_empty(),
4114            "unparenthesised record discriminant should not parse",
4115        );
4116        // Parenthesised, the record is the discriminant and the match parses.
4117        let ExprKind::Match { discriminant, .. } = body_tail("match (Point { x: 1 }) { p => p }")
4118        else {
4119            panic!("expected Match for the parenthesised form");
4120        };
4121        let ExprKind::Paren(inner) = &discriminant.kind else {
4122            panic!(
4123                "expected a parenthesised discriminant, got {:?}",
4124                discriminant.kind
4125            );
4126        };
4127        assert!(matches!(&inner.kind, ExprKind::RecordConstruction { .. }));
4128    }
4129
4130    /// The codes a strict multi-unit parse (the build's path) reports.
4131    fn strict_codes(src: &str) -> Vec<&'static str> {
4132        let toks = tokenize(src).unwrap();
4133        match parse_units_with_warnings(&toks, src) {
4134            Ok((_, warnings)) => warnings.iter().map(|e| e.category).collect(),
4135            Err(errs) => errs.iter().map(|e| e.category).collect(),
4136        }
4137    }
4138
4139    /// #1885 (DECISION B): a divider pair around a declaration, attached to
4140    /// the next one, swallowed `helper` silently. Every parse entry point now
4141    /// reports it.
4142    #[test]
4143    fn a_divider_pair_around_code_is_doc_block_contains_code() {
4144        let src =
4145            "commons m\n\n--------\nfn helper() -> Int { 1 }\n--------\nfn f() -> Int { 2 }\n";
4146        assert_eq!(strict_codes(src), ["bynk.parse.doc_block_contains_code"]);
4147        let errs = parse_str(src).unwrap_err();
4148        assert_eq!(errs[0].category, "bynk.parse.doc_block_contains_code");
4149        // The span is the whole doc block.
4150        assert_eq!(
4151            &src[errs[0].span.range()],
4152            "--------\nfn helper() -> Int { 1 }\n--------\n"
4153        );
4154        let (unit, errs) = parse_recover_str(src);
4155        assert!(unit.is_some(), "recovery keeps the unit");
4156        assert!(
4157            errs.iter()
4158                .any(|e| e.category == "bynk.parse.doc_block_contains_code"),
4159            "{errs:?}"
4160        );
4161    }
4162
4163    /// The orphan shape (a blank line before `fn f`) is the same error, beside
4164    /// the orphan warning; and so is a pair with nothing after it.
4165    #[test]
4166    fn an_orphaned_or_trailing_divider_pair_is_doc_block_contains_code() {
4167        let orphan = "commons m\n\n---\nfn helper() -> Int { 1 }\n---\n\nfn f() -> Int { 2 }\n";
4168        let codes = strict_codes(orphan);
4169        assert!(
4170            codes.contains(&"bynk.parse.doc_block_contains_code"),
4171            "{codes:?}"
4172        );
4173        let trailing = "commons m\n\nfn f() -> Int { 2 }\n\n---\ntype Id = Int\n---\n";
4174        let codes = strict_codes(trailing);
4175        assert!(
4176            codes.contains(&"bynk.parse.doc_block_contains_code"),
4177            "{codes:?}"
4178        );
4179    }
4180
4181    /// Prose that only starts like a declaration, a doc whose code sits in a
4182    /// Markdown fence, and a doc that holds only a nested doc example are all
4183    /// documentation.
4184    #[test]
4185    fn near_miss_prose_and_fenced_code_are_not_code() {
4186        for doc in [
4187            "type of the thing",
4188            "fn is used here",
4189            "The type Id is an Int.",
4190            "uses the clock",
4191            "Event handler.\nfn helper() -> Int { 1 }",
4192            "Example:\n```bynk\nfn helper() -> Int { 1 }\n```",
4193            "~~~~\ntype Id = Int\n~~~~",
4194            "---\nA nested doc.\n---",
4195            "",
4196        ] {
4197            assert!(!doc_content_is_code(doc), "{doc:?} read as code");
4198        }
4199        for doc in [
4200            "fn helper() -> Int { 1 }",
4201            "type Id = Int\nfn f() -> Int { 1 }",
4202            "```\nan example\n```\nfn helper() -> Int { 1 }",
4203            "---\nA nested doc.\n---\nfn helper() -> Int { 1 }",
4204        ] {
4205            assert!(doc_content_is_code(doc), "{doc:?} not read as code");
4206        }
4207    }
4208
4209    /// #1894 review: code one level down, inside a nested doc block, or
4210    /// beside a lone `---` rule that keeps the content from lexing, is still
4211    /// code; prose at either level is not.
4212    #[test]
4213    fn code_inside_a_nested_doc_or_beside_a_rule_is_code() {
4214        let nested =
4215            "commons m\n\n----\n---\nfn helper() -> Int { 1 }\n---\n----\nfn f() -> Int { 2 }\n";
4216        assert_eq!(strict_codes(nested), ["bynk.parse.doc_block_contains_code"]);
4217        let ruled = "commons m\n\n----\n---\nfn helper() -> Int { 1 }\n----\nfn f() -> Int { 2 }\n";
4218        assert_eq!(strict_codes(ruled), ["bynk.parse.doc_block_contains_code"]);
4219        // Two levels down.
4220        assert!(doc_content_is_code(
4221            "----\n---\nfn helper() -> Int { 1 }\n---\n----"
4222        ));
4223        for doc in [
4224            "---\nAn inner doc.\n---",
4225            "Intro.\n\n---\n\nfn is used here",
4226            "---\n```\nfn helper() -> Int { 1 }\n```\n---",
4227        ] {
4228            assert!(!doc_content_is_code(doc), "{doc:?} read as code");
4229        }
4230        // Deeper than the bound, the probe stops looking.
4231        let mut deep = "fn helper() -> Int { 1 }".to_string();
4232        for n in (3..3 + DOC_PROBE_MAX_DEPTH + 1).rev() {
4233            let fence = "-".repeat(n);
4234            deep = format!("{fence}\n{deep}\n{fence}");
4235        }
4236        assert!(!doc_content_is_code(&deep));
4237    }
4238
4239    /// A `----` doc holding a nested doc example in a code fence attaches to
4240    /// its declaration with no diagnostic.
4241    #[test]
4242    fn a_fenced_nested_doc_example_parses_clean() {
4243        let src = "commons m\n\n----\nShows a doc.\n\n```bynk\n---\nInner.\n---\nfn example() -> Int { 1 }\n```\n----\nfn f() -> Int { 2 }\n";
4244        assert!(strict_codes(src).is_empty(), "{:?}", strict_codes(src));
4245        let c = parse_str(src).unwrap();
4246        let CommonsItem::Fn(f) = &c.items[0] else {
4247            panic!("expected fn")
4248        };
4249        assert!(
4250            f.documentation
4251                .as_deref()
4252                .is_some_and(|d| d.contains("---\nInner.\n---")),
4253            "{:?}",
4254            f.documentation
4255        );
4256    }
4257
4258    /// #2618: `private` before a declaration keyword is the modifier, its span
4259    /// recorded and folded into the declaration's; a doc above it still
4260    /// attaches. Elsewhere `private` is an ordinary identifier.
4261    #[test]
4262    fn private_modifier_is_contextual() {
4263        let src = "commons m\n\n--- Hidden.\nprivate type T = Int\n\nprivate fn T.get(self) -> Int { 1 }\n\nfn f(private: Int) -> Int { private }\n";
4264        let c = parse_str(src).unwrap();
4265        let t = type_at(&c, 0);
4266        let at = src.find("private type").unwrap();
4267        assert_eq!(t.private.map(|s| s.start), Some(at));
4268        assert_eq!(t.span.start, at);
4269        assert_eq!(doc_of(&t.documentation), Some(("Hidden.", DocForm::Lines)));
4270        assert!(c.items[1].is_private());
4271        assert!(!c.items[2].is_private());
4272    }
4273
4274    /// `private` before `const` is the modifier too, its span folded into the
4275    /// constant's.
4276    #[test]
4277    fn private_modifier_on_a_constant() {
4278        let src = "commons m\n\nprivate const Max: Int = 3\n\nconst Min: Int = 1\n";
4279        let c = parse_str(src).unwrap();
4280        let CommonsItem::Const(k) = &c.items[0] else {
4281            panic!("a constant");
4282        };
4283        assert_eq!(
4284            k.private.map(|s| s.start),
4285            Some(src.find("private").unwrap())
4286        );
4287        assert_eq!(k.span.start, src.find("private").unwrap());
4288        assert!(c.items[0].is_private());
4289        assert!(!c.items[1].is_private());
4290    }
4291
4292    /// #2618: `private` on a declaration it does not apply to is rejected.
4293    #[test]
4294    fn private_on_a_service_is_rejected() {
4295        let src = "context m\n\nprivate service s {\n  on call() -> Effect[Int] { 1 }\n}\n";
4296        let tokens = crate::lexer::tokenize(src).unwrap();
4297        let errs = parse_units(&tokens, src).unwrap_err();
4298        assert_eq!(errs[0].category, "bynk.visibility.private_not_allowed");
4299    }
4300
4301    /// The `TypeDecl` at `items[i]`.
4302    fn type_at(c: &Commons, i: usize) -> &TypeDecl {
4303        let CommonsItem::Type(t) = &c.items[i] else {
4304            panic!("expected a type at {i}")
4305        };
4306        t
4307    }
4308
4309    fn doc_of(d: &Option<Doc>) -> Option<(&str, DocForm)> {
4310        // An indented `---` block keeps a trailing newline (its closing
4311        // marker's indent survives the marker trim); it prints the same.
4312        d.as_ref().map(|d| (d.as_str().trim_end(), d.form))
4313    }
4314
4315    /// #2203 (DECISION C): a `--- text` line documents a declaration, with or
4316    /// without its closer.
4317    #[test]
4318    fn doc_lines_document_a_declaration() {
4319        let src = "commons m\n\n--- A commit SHA.\ntype Commit = String\n\n---- First. ----\nfn f() -> Int { 1 }\n";
4320        let c = parse_str(src).unwrap();
4321        assert_eq!(
4322            doc_of(&type_at(&c, 0).documentation),
4323            Some(("A commit SHA.", DocForm::Lines))
4324        );
4325        let CommonsItem::Fn(f) = &c.items[1] else {
4326            panic!("expected fn")
4327        };
4328        assert_eq!(doc_of(&f.documentation), Some(("First.", DocForm::Lines)));
4329        assert!(strict_codes(src).is_empty(), "{:?}", strict_codes(src));
4330    }
4331
4332    /// #2203 (DECISION E): a legacy `--` comment and `--|` doc line still
4333    /// parse as a comment and a doc, and each is reported, all at once, with
4334    /// its fix; the formatter's migrating parse accepts them.
4335    #[test]
4336    fn legacy_spellings_parse_and_are_each_reported() {
4337        let src =
4338            "-- a header\ncommons m\n\n--| A run.\ntype Run = {\n  repo: String, -- trailing\n}\n";
4339        assert_eq!(
4340            strict_codes(src),
4341            [
4342                "bynk.lex.legacy_comment",
4343                "bynk.lex.legacy_doc_line",
4344                "bynk.lex.legacy_comment"
4345            ]
4346        );
4347        let tokens = tokenize(src).unwrap();
4348        let (units, _, _) = parse_units_migrating(&tokens, src).unwrap();
4349        let SourceUnit::Commons(c) = &units[0] else {
4350            panic!("commons")
4351        };
4352        assert_eq!(
4353            doc_of(&type_at(c, 0).documentation),
4354            Some(("A run.", DocForm::Lines))
4355        );
4356    }
4357
4358    /// #2203 (DECISION D): two one-line docs on one target is
4359    /// `bynk.parse.multiple_docs`, whose fix joins them into a block.
4360    #[test]
4361    fn consecutive_doc_lines_are_multiple_docs_with_a_block_fix() {
4362        let src = "commons m\n\ntype R = {\n  --- First.\n  --- Second.\n  a: Int,\n}\n";
4363        let tokens = tokenize(src).unwrap();
4364        let errs = parse_units(&tokens, src).unwrap_err();
4365        assert_eq!(errs.len(), 1, "{errs:?}");
4366        assert_eq!(errs[0].category, "bynk.parse.multiple_docs");
4367        let (span, block) = &errs[0].suggestions[0].edits[0];
4368        assert_eq!(&src[span.range()], "--- First.\n  --- Second.");
4369        assert_eq!(block, "---\n  First.\n  Second.\n  ---");
4370    }
4371
4372    /// #1888 (DECISION D): record fields, both sum forms' variants, payload
4373    /// fields and message entries take a doc in either form; a `//` comment
4374    /// above a field stays a comment.
4375    #[test]
4376    fn members_take_docs_in_either_form() {
4377        let src = "commons m\n\ntype Run = {\n  --- The repository.\n  repo: String,\n  ---\n  The version.\n  ---\n  version: String,\n  // a note\n  sha: String, // trailing\n}\n\ntype Outcome = enum {\n  --- Passed.\n  Pass,\n  Fail,\n}\n\ntype Shape =\n  --- Round.\n  | Circle(\n    --- The radius.\n    r: Int,\n  )\n  | Dot\n\n--- After the sum.\nfn f() -> Int { 1 }\n\nmessages \"en\" @reference {\n  --- Greets.\n  \"hi\" => \"Hello\"\n}\n";
4378        assert!(strict_codes(src).is_empty(), "{:?}", strict_codes(src));
4379        let c = parse_str(src).unwrap();
4380        let TypeBody::Record(r) = &type_at(&c, 0).body else {
4381            panic!("record")
4382        };
4383        assert_eq!(
4384            doc_of(&r.fields[0].documentation),
4385            Some(("The repository.", DocForm::Lines))
4386        );
4387        assert_eq!(
4388            doc_of(&r.fields[1].documentation),
4389            Some(("The version.", DocForm::Block))
4390        );
4391        assert_eq!(r.fields[2].documentation, None);
4392        assert_eq!(
4393            r.fields[2].trivia.leading,
4394            vec![Comment::Line(" a note".into())]
4395        );
4396        assert_eq!(r.fields[2].trivia.trailing.as_deref(), Some(" trailing"));
4397        let TypeBody::Sum(e) = &type_at(&c, 1).body else {
4398            panic!("enum")
4399        };
4400        assert_eq!(
4401            doc_of(&e.variants[0].documentation),
4402            Some(("Passed.", DocForm::Lines))
4403        );
4404        assert_eq!(e.variants[1].documentation, None);
4405        let TypeBody::Sum(p) = &type_at(&c, 2).body else {
4406            panic!("sum")
4407        };
4408        assert_eq!(
4409            doc_of(&p.variants[0].documentation),
4410            Some(("Round.", DocForm::Lines))
4411        );
4412        assert_eq!(
4413            doc_of(&p.variants[0].payload[0].documentation),
4414            Some(("The radius.", DocForm::Lines))
4415        );
4416        assert_eq!(p.variants[1].documentation, None);
4417        // The doc after the last variant is the next declaration's.
4418        let CommonsItem::Fn(f) = &c.items[3] else {
4419            panic!("expected fn")
4420        };
4421        assert_eq!(
4422            doc_of(&f.documentation),
4423            Some(("After the sum.", DocForm::Lines))
4424        );
4425        let CommonsItem::Messages(m) = &c.items[4] else {
4426            panic!("expected messages")
4427        };
4428        assert_eq!(
4429            doc_of(&m.entries[0].documentation),
4430            Some(("Greets.", DocForm::Lines))
4431        );
4432    }
4433
4434    /// #2203 (DECISION D): two docs directly above one target, of either
4435    /// form or both, is `bynk.parse.multiple_docs`, on a declaration and on a
4436    /// member; the recovering parse keeps the unit.
4437    #[test]
4438    fn two_docs_on_one_target_is_multiple_docs() {
4439        for src in [
4440            "commons m\n\n---\nBlock.\n---\n--- Lines.\nfn f() -> Int { 1 }\n",
4441            "commons m\n\n--- Lines.\n---\nBlock.\n---\nfn f() -> Int { 1 }\n",
4442            "commons m\n\n---\nOne.\n---\n---\nTwo.\n---\nfn f() -> Int { 1 }\n",
4443            "commons m\n\n--- One.\n--- Two.\nfn f() -> Int { 1 }\n",
4444            "commons m\n\ntype R = {\n  --- Lines.\n  ---\n  Block.\n  ---\n  a: Int,\n}\n",
4445        ] {
4446            assert_eq!(strict_codes(src), ["bynk.parse.multiple_docs"], "{src}");
4447            let (unit, errs) = parse_recover_str(src);
4448            assert!(unit.is_some());
4449            assert!(
4450                errs.iter()
4451                    .any(|e| e.category == "bynk.parse.multiple_docs")
4452            );
4453        }
4454    }
4455
4456    /// #1888 review: a doc with only a payload's `)` after it is an orphan
4457    /// warning, and the variant still parses with its field.
4458    #[test]
4459    fn a_dangling_doc_before_a_payload_close_is_only_an_orphan() {
4460        let src =
4461            "commons m\n\ntype S =\n  | Circle(\n    r: Int,\n    --- dangling\n  )\n  | Dot\n";
4462        assert_eq!(strict_codes(src), ["bynk.parse.orphan_doc_block"]);
4463        let c = parse_str(src).unwrap();
4464        let TypeBody::Sum(s) = &type_at(&c, 0).body else {
4465            panic!("sum")
4466        };
4467        assert_eq!(s.variants[0].payload.len(), 1);
4468        assert_eq!(s.variants[0].payload[0].documentation, None);
4469    }
4470
4471    /// #1888: a member doc follows the declaration rules: a blank line
4472    /// orphans it (a warning, kept in place), as does the body's end.
4473    #[test]
4474    fn an_unattached_member_doc_is_an_orphan() {
4475        let src = "commons m\n\ntype R = {\n  --- Spaced.\n\n  a: Int,\n  --- Dangling.\n}\n";
4476        assert_eq!(
4477            strict_codes(src),
4478            ["bynk.parse.orphan_doc_block", "bynk.parse.orphan_doc_block"]
4479        );
4480        let c = parse_str(src).unwrap();
4481        let TypeBody::Record(r) = &type_at(&c, 0).body else {
4482            panic!("record")
4483        };
4484        assert_eq!(r.fields[0].documentation, None);
4485        assert!(matches!(
4486            &r.fields[0].trivia.leading[..],
4487            [Comment::OrphanDoc(d)] if d.as_str() == "Spaced." && d.form == DocForm::Lines
4488        ));
4489        assert!(matches!(
4490            &r.trailing_comments[..],
4491            [Comment::OrphanDoc(d)] if d.as_str() == "Dangling."
4492        ));
4493    }
4494}
4495
4496#[cfg(test)]
4497mod paren_guard_2721 {
4498    use super::*;
4499    use crate::lexer::tokenize;
4500
4501    fn ok(src: &str) {
4502        let toks = tokenize(src).unwrap();
4503        parse(&toks, src).unwrap_or_else(|e| panic!("{src}\n{e:?}"));
4504    }
4505
4506    fn wrap(guard: &str) -> String {
4507        format!(
4508            "commons t {{\n  fn f(n: Int, flag: Bool) -> Int {{\n    match n {{\n      n if {guard} => n,\n      _ => 0,\n    }}\n  }}\n}}\n"
4509        )
4510    }
4511
4512    #[test]
4513    fn parenthesised_guards_parse() {
4514        ok(&wrap("(flag)"));
4515        ok(&wrap("(n > 0 && flag)"));
4516        ok(&wrap("(n > 0) && flag"));
4517        ok(&wrap("!(flag)"));
4518        ok(&wrap("()"));
4519    }
4520
4521    #[test]
4522    fn lambda_still_parses_when_parenthesised_in_guard() {
4523        ok(&wrap("pred(((x: Int) => x > 0))"));
4524    }
4525}