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