Skip to main content

miden_assembly_syntax_cst/
parser.rs

1use alloc::{borrow::ToOwned, string::String, sync::Arc, vec::Vec};
2
3use miden_debug_types::{SourceFile, SourceId, SourceLanguage, SourceSpan, Uri};
4use rowan::{GreenNodeBuilder, NodeOrToken, TextRange};
5
6use crate::{
7    MasmLanguage,
8    ast::AstNode,
9    diagnostics::{LabeledSpan, Severity, diagnostic, miette::MietteDiagnostic as Diagnostic},
10    lexer::{Token, tokenize},
11    syntax::{SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken},
12};
13
14/// The result of parsing a MASM source file into a lossless CST.
15///
16/// This type owns the green tree, retains the originating [`SourceFile`], and exposes both
17/// diagnostics and span helpers for later lowering.
18#[derive(Debug, Clone)]
19pub struct Parse {
20    source: Arc<SourceFile>,
21    green_node: rowan::GreenNode,
22    diagnostics: Vec<Diagnostic>,
23}
24
25impl Parse {
26    /// Returns the raw rowan syntax tree rooted at [`SyntaxKind::SourceFile`].
27    pub fn syntax(&self) -> SyntaxNode {
28        SyntaxNode::new_root(self.green_node.clone())
29    }
30
31    /// Returns the typed root node for this parse.
32    pub fn root(&self) -> crate::ast::SourceFile {
33        crate::ast::SourceFile::cast(self.syntax())
34            .expect("parse root kind should always be SourceFile")
35    }
36
37    /// Returns any syntax diagnostics emitted while building the CST.
38    pub fn diagnostics(&self) -> &[Diagnostic] {
39        &self.diagnostics
40    }
41
42    /// Removes and returns any syntax diagnostics emitted while building the CST.
43    pub fn take_diagnostics(&mut self) -> Vec<Diagnostic> {
44        core::mem::take(&mut self.diagnostics)
45    }
46
47    /// Returns the source file used to produce this parse result.
48    pub fn source_file(&self) -> Arc<SourceFile> {
49        Arc::clone(&self.source)
50    }
51
52    /// Returns the source file used to produce this parse result by shared reference.
53    pub fn source(&self) -> &SourceFile {
54        self.source.as_ref()
55    }
56
57    /// Returns `true` when the parse emitted at least one syntax diagnostic.
58    pub fn has_errors(&self) -> bool {
59        !self.diagnostics.is_empty()
60    }
61
62    /// Maps a rowan node back to a [`SourceSpan`] in the originating source file.
63    pub fn span_for_ast_node<T>(&self, node: &T) -> SourceSpan
64    where
65        T: AstNode<Language = MasmLanguage>,
66    {
67        self.span_for_node(node.syntax())
68    }
69
70    /// Maps a rowan node back to a [`SourceSpan`] in the originating source file.
71    pub fn span_for_node(&self, node: &SyntaxNode) -> SourceSpan {
72        self.span_for_range(node.text_range())
73    }
74
75    /// Maps a rowan token back to a [`SourceSpan`] in the originating source file.
76    pub fn span_for_token(&self, token: &SyntaxToken) -> SourceSpan {
77        self.span_for_range(token.text_range())
78    }
79
80    /// Maps a rowan element back to a [`SourceSpan`] in the originating source file.
81    pub fn span_for_element(&self, element: &SyntaxElement) -> SourceSpan {
82        match element {
83            NodeOrToken::Node(node) => self.span_for_node(node),
84            NodeOrToken::Token(token) => self.span_for_token(token),
85        }
86    }
87
88    /// Converts a rowan [`TextRange`] to a [`SourceSpan`] in the originating source file.
89    pub fn span_for_range(&self, range: TextRange) -> SourceSpan {
90        source_span_from_text_range(self.source.id(), range)
91    }
92}
93
94/// Parses a source-managed MASM file into a lossless CST.
95pub fn parse_source_file(source: Arc<SourceFile>) -> Parse {
96    let parser_source = Arc::clone(&source);
97    Parser::new(parser_source.as_ref()).parse(source)
98}
99
100/// Parses raw MASM text into a detached CST with [`SourceId::UNKNOWN`] spans.
101///
102/// This is primarily intended for tests and ad hoc helpers. Production callers should prefer
103/// [`parse_source_file`] so diagnostics and spans remain attached to a real [`SourceFile`].
104pub fn parse_text(input: &str) -> Parse {
105    parse_source_file(detached_source_file(input))
106}
107
108/// Parses a inline MASM from a subset of a source-managed file into a lossless CST.
109///
110/// Content of an inline MASM block is parsed like the body block of a procedure - it is not
111/// supported to define top-level items in an inline MASM block
112pub fn parse_inline_masm(source: Arc<SourceFile>, bounds: Option<SourceSpan>) -> Parse {
113    let parser_source = Arc::clone(&source);
114    if let Some(bounds) = bounds {
115        Parser::new_bounded(parser_source.as_ref(), bounds).parse_inline_masm(source)
116    } else {
117        Parser::new(parser_source.as_ref()).parse_inline_masm(source)
118    }
119}
120
121/// Parses raw MASM text as inline MASM, this is like `parse_text` for `parse_inline_masm`.
122///
123/// This is primarily intended for tests and ad hoc helpers. Production callers should prefer
124/// [`parse_source_file`] so diagnostics and spans remain attached to a real [`SourceFile`].
125pub fn parse_inline_masm_text(input: &str, bounds: Option<core::ops::Range<usize>>) -> Parse {
126    let file = detached_source_file(input);
127    let bounds = bounds.map(|range| {
128        SourceSpan::try_from_range(file.id(), range).expect("invalid inline masm bounds")
129    });
130    parse_inline_masm(file, bounds)
131}
132
133struct Parser<'input> {
134    tokens: Vec<Token<'input>>,
135    pos: usize,
136    builder: GreenNodeBuilder<'static>,
137    diagnostics: Vec<Diagnostic>,
138    eof_span: SourceSpan,
139}
140
141#[derive(Default, Debug, Clone, Copy)]
142struct Nesting {
143    parens: usize,
144    brackets: usize,
145    braces: usize,
146}
147
148impl Nesting {
149    fn is_root(self) -> bool {
150        self.parens == 0 && self.brackets == 0 && self.braces == 0
151    }
152
153    fn bump(self, kind: SyntaxKind) -> Result<Self, SyntaxKind> {
154        let mut next = self;
155        match kind {
156            SyntaxKind::LParen => next.parens += 1,
157            SyntaxKind::LBracket => next.brackets += 1,
158            SyntaxKind::LBrace => next.braces += 1,
159            SyntaxKind::RParen => {
160                next.parens = next.parens.checked_sub(1).ok_or(kind)?;
161            },
162            SyntaxKind::RBracket => {
163                next.brackets = next.brackets.checked_sub(1).ok_or(kind)?;
164            },
165            SyntaxKind::RBrace => {
166                next.braces = next.braces.checked_sub(1).ok_or(kind)?;
167            },
168            _ => (),
169        }
170        Ok(next)
171    }
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175enum BlockOwner {
176    Begin,
177    Procedure,
178    If,
179    While,
180    DoWhileBody,
181    DoWhile,
182    Repeat,
183}
184
185impl BlockOwner {
186    fn missing_end_message(self) -> &'static str {
187        match self {
188            Self::Begin => "expected `end` to close `begin` block",
189            Self::Procedure => "expected `end` to close procedure",
190            Self::If => "expected `end` to close `if`",
191            Self::While => "expected `end` to close `while`",
192            Self::DoWhileBody => "expected `while` to close `do` block",
193            Self::DoWhile => "expected `end` to close `do`..`while` loop",
194            Self::Repeat => "expected `end` to close `repeat`",
195        }
196    }
197
198    fn recovery_message(self, boundary: BlockRecoveryBoundary) -> String {
199        match boundary {
200            BlockRecoveryBoundary::Else => {
201                format!("{} before `else`", self.missing_end_message())
202            },
203            BlockRecoveryBoundary::TopLevelItem => {
204                format!("{} before top-level item", self.missing_end_message())
205            },
206        }
207    }
208}
209
210#[derive(Debug, Clone, Copy, PartialEq, Eq)]
211enum BlockRecoveryBoundary {
212    Else,
213    TopLevelItem,
214}
215
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217enum BlockParseOutcome {
218    FoundTerminator,
219    RecoveredImplicitEnd,
220    ReachedEof,
221}
222
223impl<'input> Parser<'input> {
224    fn new(source: &'input SourceFile) -> Self {
225        let eof_span = eof_anchor_span(source, None);
226        Self {
227            tokens: tokenize(source),
228            pos: 0,
229            builder: GreenNodeBuilder::new(),
230            diagnostics: Vec::new(),
231            eof_span,
232        }
233    }
234
235    fn new_bounded(source: &'input SourceFile, bounds: SourceSpan) -> Self {
236        assert_eq!(source.id(), bounds.source_id());
237
238        let eof_span = eof_anchor_span(source, Some(bounds.into_slice_index()));
239        Self {
240            tokens: tokenize(source),
241            pos: 0,
242            builder: GreenNodeBuilder::new(),
243            diagnostics: Vec::new(),
244            eof_span,
245        }
246    }
247
248    fn parse(mut self, source: Arc<SourceFile>) -> Parse {
249        self.start_node(SyntaxKind::SourceFile);
250        while !self.eof() {
251            self.parse_source_item();
252        }
253        self.finish_node();
254
255        Parse {
256            source,
257            green_node: self.builder.finish(),
258            diagnostics: self.diagnostics,
259        }
260    }
261
262    fn parse_inline_masm(mut self, source: Arc<SourceFile>) -> Parse {
263        match self.parse_block_unterminated() {
264            BlockParseOutcome::ReachedEof => (),
265            BlockParseOutcome::FoundTerminator => self.error_here("unexpected 'end'"),
266            BlockParseOutcome::RecoveredImplicitEnd => {
267                self.error_here("unclosed nested block: expected 'end' but reached eof")
268            },
269        }
270        Parse {
271            source,
272            green_node: self.builder.finish(),
273            diagnostics: self.diagnostics,
274        }
275    }
276
277    fn parse_source_item(&mut self) {
278        if self.at_kind(SyntaxKind::DocComment) {
279            self.parse_doc_form();
280            return;
281        }
282
283        if self.at_regular_trivia() {
284            self.bump();
285            return;
286        }
287
288        if self.at_keyword("namespace") {
289            self.parse_namespace();
290            return;
291        }
292
293        if self.at_prefixed_keyword("extern", "package") {
294            self.parse_extern_package();
295            return;
296        }
297
298        if self.at_keyword("mod") || self.at_prefixed_keyword("pub", "mod") {
299            self.parse_submodule();
300            return;
301        }
302
303        if self.at_kind(SyntaxKind::At)
304            || self.at_keyword("proc")
305            || self.at_prefixed_keyword("pub", "proc")
306        {
307            self.parse_procedure();
308            return;
309        }
310
311        if self.at_keyword("begin") {
312            self.parse_begin_block();
313            return;
314        }
315
316        if self.at_keyword("use") || self.at_prefixed_keyword("pub", "use") {
317            self.parse_import();
318            return;
319        }
320
321        if self.at_keyword("const") || self.at_prefixed_keyword("pub", "const") {
322            self.parse_constant();
323            return;
324        }
325
326        if self.at_keyword("type")
327            || self.at_keyword("enum")
328            || self.at_prefixed_keyword("pub", "type")
329            || self.at_prefixed_keyword("pub", "enum")
330        {
331            self.parse_type_decl();
332            return;
333        }
334
335        if self.at_keyword("adv_map") {
336            self.parse_advice_map();
337            return;
338        }
339
340        self.start_node(SyntaxKind::Error);
341        self.error_here("unexpected top-level token");
342        self.bump();
343        self.finish_node();
344    }
345
346    fn parse_doc_form(&mut self) {
347        self.start_node(SyntaxKind::Doc);
348        self.bump();
349        self.finish_node();
350    }
351
352    fn parse_namespace(&mut self) {
353        self.start_node(SyntaxKind::Namespace);
354        self.expect_keyword("namespace", "expected `namespace` in namespace declaration");
355        self.bump_regular_trivia();
356        self.parse_path_with_message("expected a namespace path");
357        self.parse_line_tail();
358        self.finish_node();
359    }
360
361    fn parse_extern_package(&mut self) {
362        self.start_node(SyntaxKind::ExternPackage);
363        self.expect_keyword("extern", "expected `extern` in extern package declaration");
364        self.expect_keyword("package", "expected `package` in extern package declaration");
365        self.bump_regular_trivia();
366
367        if self.at_package_name_like() {
368            self.bump();
369        } else {
370            self.error_here("expected a package name");
371        }
372
373        self.parse_line_tail();
374        self.finish_node();
375    }
376
377    fn parse_submodule(&mut self) {
378        self.start_node(SyntaxKind::Submodule);
379
380        if self.at_keyword("pub") {
381            self.parse_visibility();
382        }
383
384        self.expect_keyword("mod", "expected `mod` in submodule declaration");
385        self.bump_regular_trivia();
386        if self.at_name_like() {
387            self.bump();
388        } else {
389            self.error_here("expected a submodule name");
390        }
391
392        self.parse_line_tail();
393        self.finish_node();
394    }
395
396    fn parse_import(&mut self) {
397        self.start_node(SyntaxKind::Import);
398
399        let is_public = if self.at_keyword("pub") {
400            self.parse_visibility();
401            true
402        } else {
403            false
404        };
405
406        self.expect_keyword("use", "expected `use` in import declaration");
407        self.bump_inline_whitespace();
408
409        if self.at_kind(SyntaxKind::LBrace) {
410            self.parse_import_list();
411            self.parse_item_import_module_path();
412            self.parse_rejected_old_import_alias();
413        } else if self.at_kind(SyntaxKind::Star) {
414            self.parse_rejected_wildcard_import();
415        } else if self.at_kind(SyntaxKind::Number) {
416            self.parse_rejected_digest_import_target();
417        } else {
418            self.parse_module_import(is_public);
419        }
420
421        self.parse_line_tail();
422        self.finish_node();
423    }
424
425    fn parse_module_import(&mut self, is_public: bool) {
426        let path_start = self.current().map(Token::span).unwrap_or(self.eof_span);
427        if self.at_kind(SyntaxKind::Newline) || self.eof() {
428            self.error_here("expected an import path");
429            return;
430        }
431
432        self.parse_path_with_message("expected an import path");
433
434        if is_public {
435            self.error_at_span(path_start, "`pub use` is only supported for braced item imports");
436        }
437
438        self.parse_optional_module_alias();
439        self.parse_rejected_old_import_alias();
440    }
441
442    fn parse_optional_module_alias(&mut self) {
443        if !self.peek_contextual_keyword_after_non_comment_trivia("as") {
444            return;
445        }
446
447        self.bump_non_comment_trivia();
448        self.bump();
449        self.bump_inline_whitespace();
450        if self.at_name_like() {
451            self.bump();
452        } else {
453            self.error_here("expected an alias name after `as`");
454        }
455    }
456
457    fn parse_import_list(&mut self) {
458        self.start_node(SyntaxKind::ImportList);
459        let _ = self.expect_kind(SyntaxKind::LBrace, "expected `{` to start import list");
460
461        let mut saw_specifier = false;
462        loop {
463            self.bump_regular_trivia();
464
465            if self.eof() {
466                self.error_at_eof("expected `}` to close import list");
467                break;
468            }
469
470            if self.at_kind(SyntaxKind::RBrace) {
471                if !saw_specifier {
472                    self.error_here("import lists must contain at least one item");
473                }
474                self.bump();
475                break;
476            }
477
478            self.parse_import_specifier();
479            saw_specifier = true;
480            self.bump_regular_trivia();
481
482            if self.at_kind(SyntaxKind::Comma) {
483                self.bump();
484                continue;
485            }
486
487            if self.at_kind(SyntaxKind::RBrace) {
488                self.bump();
489                break;
490            }
491
492            self.error_here("expected `,` or `}` in import list");
493            if !self.at_import_list_recovery_boundary() {
494                self.bump();
495            }
496        }
497
498        self.finish_node();
499    }
500
501    fn parse_import_specifier(&mut self) {
502        self.start_node(SyntaxKind::ImportSpecifier);
503        self.bump_regular_trivia();
504
505        if self.at_kind(SyntaxKind::Star) {
506            self.error_here("wildcard imports are not supported");
507            self.bump();
508            self.finish_node();
509            return;
510        }
511
512        if self.at_name_like() {
513            self.bump();
514        } else {
515            self.error_here("expected an imported item name");
516            if !self.at_import_list_recovery_boundary() {
517                self.bump();
518            }
519            self.finish_node();
520            return;
521        }
522
523        if self.peek_contextual_keyword_after_non_comment_trivia("as") {
524            self.bump_non_comment_trivia();
525            self.bump();
526            self.bump_inline_whitespace();
527            if self.at_name_like() {
528                self.bump();
529            } else {
530                self.error_here("expected an alias name after `as`");
531            }
532        }
533
534        self.parse_rejected_old_import_alias();
535        self.finish_node();
536    }
537
538    fn parse_item_import_module_path(&mut self) {
539        if !self.peek_contextual_keyword_after_non_comment_trivia("from") {
540            self.error_here("expected `from` after import list");
541            return;
542        }
543
544        self.bump_non_comment_trivia();
545        self.bump();
546        self.bump_inline_whitespace();
547
548        if self.at_kind(SyntaxKind::Newline) || self.eof() {
549            self.error_here("expected a module path after `from`");
550            return;
551        }
552
553        if self.at_kind(SyntaxKind::Number) {
554            self.parse_rejected_digest_import_target();
555        } else {
556            self.parse_path_with_message("expected a module path after `from`");
557        }
558    }
559
560    fn parse_rejected_old_import_alias(&mut self) {
561        if self.peek_after_non_comment_trivia() != Some(SyntaxKind::RArrow) {
562            return;
563        }
564
565        self.start_node(SyntaxKind::Error);
566        self.bump_non_comment_trivia();
567        self.error_here("import aliases use `as`; `->` is no longer supported");
568        self.bump();
569        self.bump_non_comment_trivia();
570        if self.at_name_like() {
571            self.bump();
572        } else {
573            self.error_here("expected an alias name after `->`");
574        }
575        self.finish_node();
576    }
577
578    fn parse_rejected_digest_import_target(&mut self) {
579        self.start_node(SyntaxKind::Error);
580        self.error_here("digest imports are not supported in source `use` declarations");
581        self.bump();
582        self.parse_optional_module_alias();
583        self.parse_rejected_old_import_alias();
584        self.finish_node();
585    }
586
587    fn parse_rejected_wildcard_import(&mut self) {
588        self.start_node(SyntaxKind::Error);
589        self.error_here("wildcard imports are not supported");
590        self.bump();
591
592        if self.peek_contextual_keyword_after_non_comment_trivia("from") {
593            self.bump_non_comment_trivia();
594            self.bump();
595            self.bump_regular_trivia();
596            self.parse_path_with_message("expected a module path after `from`");
597        }
598
599        self.finish_node();
600    }
601
602    fn parse_constant(&mut self) {
603        self.start_node(SyntaxKind::Constant);
604
605        if self.at_keyword("pub") {
606            self.parse_visibility();
607        }
608
609        self.expect_keyword("const", "expected `const` in constant declaration");
610        self.bump_regular_trivia();
611        if self.at_name_like() {
612            self.bump();
613        } else {
614            self.error_here("expected a constant name");
615        }
616
617        self.expect_kind(SyntaxKind::Equal, "expected `=` in constant declaration");
618        self.parse_expr_until_line_end();
619        self.parse_line_tail();
620        self.finish_node();
621    }
622
623    fn parse_type_decl(&mut self) {
624        self.start_node(SyntaxKind::TypeDecl);
625
626        if self.at_keyword("pub") {
627            self.parse_visibility();
628        }
629
630        self.bump_regular_trivia();
631        if self.at_keyword("type") || self.at_keyword("enum") {
632            self.bump();
633        } else {
634            self.error_here("expected `type` or `enum`");
635            self.finish_node();
636            return;
637        }
638
639        self.bump_regular_trivia();
640        if self.at_name_like() {
641            self.bump();
642        } else {
643            self.error_here("expected a type name");
644        }
645
646        self.bump_regular_trivia();
647        if self.at_kind(SyntaxKind::Equal) || self.at_kind(SyntaxKind::Colon) {
648            self.parse_type_body();
649        } else {
650            self.error_here("expected `=` or `:` in type declaration");
651        }
652
653        self.finish_node();
654    }
655
656    fn parse_advice_map(&mut self) {
657        self.start_node(SyntaxKind::AdviceMap);
658        self.expect_keyword("adv_map", "expected `adv_map`");
659        self.bump_regular_trivia();
660        if self.at_name_like() {
661            self.bump();
662        } else {
663            self.error_here("expected an advice-map name");
664        }
665
666        self.bump_inline_whitespace();
667        if self.at_kind(SyntaxKind::LParen) {
668            self.parse_balanced_group(
669                SyntaxKind::LParen,
670                SyntaxKind::RParen,
671                "expected `)` to close advice-map key",
672            );
673        }
674
675        self.expect_kind(SyntaxKind::Equal, "expected `=` in advice-map declaration");
676        self.parse_expr_until_line_end();
677        self.parse_line_tail();
678        self.finish_node();
679    }
680
681    fn parse_path_with_message(&mut self, message: &'static str) {
682        self.start_node(SyntaxKind::Path);
683        if self.at_kind(SyntaxKind::ColonColon) {
684            self.bump();
685        }
686
687        self.bump_non_comment_trivia();
688        if self.at_name_like() {
689            self.bump();
690        } else {
691            self.error_here(message);
692            self.finish_node();
693            return;
694        }
695
696        loop {
697            if self.peek_after_non_comment_trivia() != Some(SyntaxKind::ColonColon) {
698                break;
699            }
700
701            self.bump_non_comment_trivia();
702            self.bump();
703            self.bump_non_comment_trivia();
704            if self.at_name_like() {
705                self.bump();
706            } else {
707                self.error_here("expected a path segment after `::`");
708                break;
709            }
710        }
711
712        self.finish_node();
713    }
714
715    fn parse_expr_until_line_end(&mut self) {
716        self.bump_regular_trivia();
717        self.start_node(SyntaxKind::Expr);
718
719        let mut nesting = Nesting::default();
720        let mut saw_significant = false;
721        while !self.eof() {
722            let kind = self.current_kind().expect("not eof");
723            if nesting.is_root() && matches!(kind, SyntaxKind::Comment | SyntaxKind::DocComment) {
724                break;
725            }
726            if nesting.is_root() && kind == SyntaxKind::Newline {
727                break;
728            }
729            if nesting.is_root()
730                && saw_significant
731                && kind == SyntaxKind::Whitespace
732                && self
733                    .next_relevant_top_level_token(self.pos + 1)
734                    .is_some_and(|index| self.is_top_level_starter(index))
735            {
736                break;
737            }
738
739            saw_significant |= !kind.is_trivia();
740            self.bump_nesting(&mut nesting, kind);
741            self.bump();
742        }
743
744        self.finish_node();
745    }
746
747    fn parse_type_body(&mut self) {
748        self.start_node(SyntaxKind::TypeBody);
749
750        let mut nesting = Nesting::default();
751        while !self.eof() {
752            if self.at_kind(SyntaxKind::Newline)
753                && nesting.is_root()
754                && self.line_break_starts_new_top_level_item()
755            {
756                break;
757            }
758
759            let current = self.current_kind().expect("not eof");
760            self.bump_nesting(&mut nesting, current);
761            self.bump();
762        }
763
764        self.finish_node();
765    }
766
767    fn parse_line_tail(&mut self) {
768        loop {
769            match self.current_kind() {
770                Some(SyntaxKind::Whitespace) => self.bump(),
771                Some(SyntaxKind::Comment) => {
772                    self.bump();
773                    break;
774                },
775                _ => break,
776            }
777        }
778    }
779
780    fn parse_begin_block(&mut self) {
781        self.start_node(SyntaxKind::BeginBlock);
782        self.expect_keyword("begin", "expected `begin`");
783        self.parse_line_tail();
784        if self.parse_block(BlockOwner::Begin, &["end"]) == BlockParseOutcome::FoundTerminator {
785            self.expect_keyword("end", BlockOwner::Begin.missing_end_message());
786        }
787        self.finish_node();
788    }
789
790    fn parse_procedure(&mut self) {
791        self.start_node(SyntaxKind::Procedure);
792
793        loop {
794            self.bump_regular_trivia();
795            if !self.at_kind(SyntaxKind::At) {
796                break;
797            }
798            self.parse_attribute();
799        }
800
801        self.bump_regular_trivia();
802        if self.at_keyword("pub") {
803            self.parse_visibility();
804        }
805
806        self.bump_regular_trivia();
807        if !self.expect_keyword("proc", "expected `proc` in procedure declaration") {
808            self.finish_node();
809            return;
810        }
811
812        self.bump_regular_trivia();
813        if self.at_name_like() {
814            self.bump();
815        } else {
816            self.error_here("expected a procedure name");
817        }
818
819        self.bump_regular_trivia();
820        if self.at_kind(SyntaxKind::LParen) {
821            self.parse_signature();
822        }
823
824        self.parse_line_tail();
825        if self.parse_block(BlockOwner::Procedure, &["end"]) == BlockParseOutcome::FoundTerminator {
826            self.expect_keyword("end", BlockOwner::Procedure.missing_end_message());
827        }
828        self.finish_node();
829    }
830
831    fn parse_attribute(&mut self) {
832        self.start_node(SyntaxKind::Attribute);
833        let _ = self.expect_kind(SyntaxKind::At, "expected `@`");
834        self.bump_regular_trivia();
835        if self.at_name_like() {
836            self.bump();
837        } else {
838            self.error_here("expected an attribute name");
839        }
840
841        self.bump_inline_whitespace();
842        if self.at_kind(SyntaxKind::LParen) {
843            self.parse_balanced_group(
844                SyntaxKind::LParen,
845                SyntaxKind::RParen,
846                "expected `)` to close attribute arguments",
847            );
848        }
849        self.finish_node();
850    }
851
852    fn parse_visibility(&mut self) {
853        self.start_node(SyntaxKind::Visibility);
854        let _ = self.expect_keyword("pub", "expected `pub`");
855        self.finish_node();
856    }
857
858    fn parse_signature(&mut self) {
859        self.start_node(SyntaxKind::Signature);
860        self.parse_balanced_group(
861            SyntaxKind::LParen,
862            SyntaxKind::RParen,
863            "expected `)` to close procedure parameters",
864        );
865
866        if self.peek_after_non_comment_trivia() == Some(SyntaxKind::RArrow) {
867            self.bump_non_comment_trivia();
868            self.bump();
869            self.bump_non_comment_trivia();
870            if self.at_kind(SyntaxKind::LParen) {
871                self.parse_balanced_group(
872                    SyntaxKind::LParen,
873                    SyntaxKind::RParen,
874                    "expected `)` to close procedure results",
875                );
876            } else {
877                self.parse_signature_result_until_line_end();
878            }
879        }
880        self.finish_node();
881    }
882
883    fn parse_signature_result_until_line_end(&mut self) {
884        let start = self.pos;
885        let mut nesting = Nesting::default();
886        while !self.eof() {
887            let kind = self.current_kind().expect("not eof");
888            if nesting.is_root() && matches!(kind, SyntaxKind::Comment | SyntaxKind::DocComment) {
889                break;
890            }
891            if nesting.is_root() && kind == SyntaxKind::Newline {
892                break;
893            }
894
895            self.bump_nesting(&mut nesting, kind);
896            self.bump();
897        }
898
899        if self.pos == start {
900            self.error_here("expected a result type after `->` in procedure signature");
901        }
902    }
903
904    fn parse_block_unterminated(&mut self) -> BlockParseOutcome {
905        self.start_node(SyntaxKind::Block);
906        while !self.eof() {
907            if self.at_regular_trivia() {
908                self.bump();
909                continue;
910            }
911
912            if self.at_kind(SyntaxKind::DocComment) {
913                self.start_node(SyntaxKind::Error);
914                self.error_here("doc comments are not allowed in inline MASM blocks");
915                self.bump();
916                self.finish_node();
917                continue;
918            }
919
920            if self.at_keyword("if") {
921                if self.parse_if() {
922                    self.finish_node();
923                    return BlockParseOutcome::ReachedEof;
924                }
925            } else if self.at_keyword("do") {
926                if self.parse_do_while() {
927                    self.finish_node();
928                    return BlockParseOutcome::ReachedEof;
929                }
930            } else if self.at_keyword("while") {
931                if self.parse_while() {
932                    self.finish_node();
933                    return BlockParseOutcome::ReachedEof;
934                }
935            } else if self.at_keyword("repeat") {
936                if self.parse_repeat() {
937                    self.finish_node();
938                    return BlockParseOutcome::ReachedEof;
939                }
940            } else if self.can_start_instruction() {
941                self.parse_instruction();
942            } else {
943                self.start_node(SyntaxKind::Error);
944                self.error_here("unexpected token in block");
945                self.bump();
946                self.finish_node();
947            }
948        }
949
950        self.finish_node();
951        BlockParseOutcome::ReachedEof
952    }
953
954    fn parse_block(&mut self, owner: BlockOwner, terminators: &[&str]) -> BlockParseOutcome {
955        self.start_node(SyntaxKind::Block);
956        while !self.eof() {
957            if self.at_terminator(terminators) {
958                self.finish_node();
959                return BlockParseOutcome::FoundTerminator;
960            }
961
962            if self.at_regular_trivia() {
963                self.bump();
964                continue;
965            }
966
967            if let Some(boundary) = self.block_recovery_boundary(terminators) {
968                self.start_node(SyntaxKind::Error);
969                self.error_here(owner.recovery_message(boundary));
970                self.finish_node();
971                self.finish_node();
972                return BlockParseOutcome::RecoveredImplicitEnd;
973            }
974
975            if self.at_kind(SyntaxKind::DocComment) {
976                self.start_node(SyntaxKind::Error);
977                self.error_here("doc comments are only allowed before module-level items");
978                self.bump();
979                self.finish_node();
980                continue;
981            }
982
983            if self.at_keyword("if") {
984                if self.parse_if() {
985                    self.finish_node();
986                    return BlockParseOutcome::ReachedEof;
987                }
988            } else if self.at_keyword("do") {
989                if self.parse_do_while() {
990                    self.finish_node();
991                    return BlockParseOutcome::ReachedEof;
992                }
993            } else if self.at_keyword("while") {
994                if self.parse_while() {
995                    self.finish_node();
996                    return BlockParseOutcome::ReachedEof;
997                }
998            } else if self.at_keyword("repeat") {
999                if self.parse_repeat() {
1000                    self.finish_node();
1001                    return BlockParseOutcome::ReachedEof;
1002                }
1003            } else if self.can_start_instruction() {
1004                self.parse_instruction();
1005            } else {
1006                self.start_node(SyntaxKind::Error);
1007                self.error_here("unexpected token in block");
1008                self.bump();
1009                self.finish_node();
1010            }
1011        }
1012
1013        self.error_at_eof(owner.missing_end_message());
1014        self.finish_node();
1015        BlockParseOutcome::ReachedEof
1016    }
1017
1018    fn parse_if(&mut self) -> bool {
1019        self.start_node(SyntaxKind::IfOp);
1020        self.expect_keyword("if", "expected `if`");
1021        self.parse_structured_header_suffixes();
1022        self.parse_line_tail();
1023        let then_outcome = self.parse_block(BlockOwner::If, &["else", "end"]);
1024        if then_outcome == BlockParseOutcome::ReachedEof {
1025            self.finish_node();
1026            return true;
1027        }
1028        let mut needs_end = then_outcome == BlockParseOutcome::FoundTerminator;
1029        if self.at_keyword("else") {
1030            self.bump();
1031            self.parse_line_tail();
1032            let else_outcome = self.parse_block(BlockOwner::If, &["end"]);
1033            if else_outcome == BlockParseOutcome::ReachedEof {
1034                self.finish_node();
1035                return true;
1036            }
1037            needs_end = else_outcome == BlockParseOutcome::FoundTerminator;
1038        }
1039        if needs_end {
1040            self.expect_keyword("end", BlockOwner::If.missing_end_message());
1041        }
1042        self.finish_node();
1043        false
1044    }
1045
1046    fn parse_while(&mut self) -> bool {
1047        self.start_node(SyntaxKind::WhileOp);
1048        self.expect_keyword("while", "expected `while`");
1049        self.parse_structured_header_suffixes();
1050        self.parse_line_tail();
1051        let outcome = self.parse_block(BlockOwner::While, &["end"]);
1052        if outcome == BlockParseOutcome::ReachedEof {
1053            self.finish_node();
1054            return true;
1055        }
1056        if outcome == BlockParseOutcome::FoundTerminator {
1057            self.expect_keyword("end", BlockOwner::While.missing_end_message());
1058        }
1059        self.finish_node();
1060        false
1061    }
1062
1063    fn parse_do_while(&mut self) -> bool {
1064        self.start_node(SyntaxKind::DoWhileOp);
1065        self.expect_keyword("do", "expected `do`");
1066        self.parse_line_tail();
1067        // The body is terminated by a *bare* `while` (the loop condition). A nested
1068        // `while.true` loop inside the body carries a `.` suffix and is therefore not treated
1069        // as the terminator (see `at_terminator`).
1070        let body_outcome = self.parse_block(BlockOwner::DoWhileBody, &["while"]);
1071        if body_outcome == BlockParseOutcome::ReachedEof {
1072            self.finish_node();
1073            return true;
1074        }
1075        if body_outcome == BlockParseOutcome::FoundTerminator {
1076            self.expect_keyword("while", "expected `while`");
1077            self.parse_line_tail();
1078            let cond_outcome = self.parse_block(BlockOwner::DoWhile, &["end"]);
1079            if cond_outcome == BlockParseOutcome::ReachedEof {
1080                self.finish_node();
1081                return true;
1082            }
1083            if cond_outcome == BlockParseOutcome::FoundTerminator {
1084                self.expect_keyword("end", BlockOwner::DoWhile.missing_end_message());
1085            }
1086        }
1087        self.finish_node();
1088        false
1089    }
1090
1091    fn parse_repeat(&mut self) -> bool {
1092        self.start_node(SyntaxKind::RepeatOp);
1093        self.expect_keyword("repeat", "expected `repeat`");
1094        self.parse_structured_header_suffixes();
1095        self.parse_line_tail();
1096        let outcome = self.parse_block(BlockOwner::Repeat, &["end"]);
1097        if outcome == BlockParseOutcome::ReachedEof {
1098            self.finish_node();
1099            return true;
1100        }
1101        if outcome == BlockParseOutcome::FoundTerminator {
1102            self.expect_keyword("end", BlockOwner::Repeat.missing_end_message());
1103        }
1104        self.finish_node();
1105        false
1106    }
1107
1108    fn parse_structured_header_suffixes(&mut self) {
1109        loop {
1110            self.bump_inline_whitespace();
1111            if !self.at_kind(SyntaxKind::Dot) {
1112                break;
1113            }
1114            self.bump();
1115            self.bump_inline_whitespace();
1116
1117            if self.at_kind(SyntaxKind::LBracket) {
1118                self.parse_balanced_group(
1119                    SyntaxKind::LBracket,
1120                    SyntaxKind::RBracket,
1121                    "expected `]` to close structured operation suffix",
1122                );
1123            } else if self.at_name_like()
1124                || self.at_kind(SyntaxKind::Number)
1125                || self.at_kind(SyntaxKind::QuotedString)
1126            {
1127                self.bump();
1128            } else {
1129                self.error_here("expected a structured operation suffix");
1130                break;
1131            }
1132        }
1133    }
1134
1135    fn parse_instruction(&mut self) {
1136        self.start_node(SyntaxKind::Instruction);
1137
1138        let mut nesting = Nesting::default();
1139        let mut previous_significant = None;
1140        while !self.eof() {
1141            let kind = self.current_kind().expect("not eof");
1142            if kind == SyntaxKind::Whitespace {
1143                if self.should_continue_instruction_after_whitespace(previous_significant, nesting)
1144                {
1145                    self.bump();
1146                    continue;
1147                }
1148                break;
1149            }
1150
1151            if matches!(kind, SyntaxKind::Newline | SyntaxKind::Comment | SyntaxKind::DocComment) {
1152                if nesting.is_root() {
1153                    break;
1154                }
1155                self.bump();
1156                continue;
1157            }
1158
1159            if previous_significant.is_some()
1160                && self.should_stop_instruction_before(kind, previous_significant, nesting)
1161            {
1162                break;
1163            }
1164
1165            previous_significant = Some(kind);
1166            self.bump_nesting(&mut nesting, kind);
1167            self.bump();
1168        }
1169
1170        self.finish_node();
1171    }
1172
1173    fn parse_balanced_group(
1174        &mut self,
1175        open: SyntaxKind,
1176        close: SyntaxKind,
1177        missing_message: &'static str,
1178    ) {
1179        self.bump_regular_trivia();
1180        if !self.expect_kind(open, missing_message) {
1181            return;
1182        }
1183
1184        let mut depth = 1usize;
1185        while !self.eof() {
1186            let kind = self.current_kind().expect("not eof");
1187            if kind == open {
1188                depth += 1;
1189            } else if kind == close {
1190                depth -= 1;
1191            }
1192            self.bump();
1193            if depth == 0 {
1194                return;
1195            }
1196        }
1197
1198        self.error_at_eof(missing_message);
1199    }
1200
1201    fn should_continue_instruction_after_whitespace(
1202        &self,
1203        previous_significant: Option<SyntaxKind>,
1204        nesting: Nesting,
1205    ) -> bool {
1206        if !nesting.is_root() {
1207            return true;
1208        }
1209
1210        let Some(previous_significant) = previous_significant else {
1211            return false;
1212        };
1213        if expects_continuation_operand(previous_significant) {
1214            return true;
1215        }
1216
1217        matches!(
1218            self.peek_after_inline_whitespace(),
1219            Some(
1220                SyntaxKind::Dot
1221                    | SyntaxKind::Equal
1222                    | SyntaxKind::Comma
1223                    | SyntaxKind::DotDot
1224                    | SyntaxKind::Colon
1225                    | SyntaxKind::ColonColon
1226                    | SyntaxKind::RArrow
1227                    | SyntaxKind::Plus
1228                    | SyntaxKind::Minus
1229                    | SyntaxKind::Star
1230                    | SyntaxKind::Slash
1231                    | SyntaxKind::SlashSlash
1232                    | SyntaxKind::RBracket
1233                    | SyntaxKind::RParen
1234                    | SyntaxKind::RBrace
1235            )
1236        )
1237    }
1238
1239    fn should_stop_instruction_before(
1240        &self,
1241        current: SyntaxKind,
1242        previous_significant: Option<SyntaxKind>,
1243        nesting: Nesting,
1244    ) -> bool {
1245        if !nesting.is_root() {
1246            return false;
1247        }
1248
1249        if let Some(previous_significant) = previous_significant
1250            && expects_continuation_operand(previous_significant)
1251        {
1252            return false;
1253        }
1254
1255        if punctuation_continues_instruction(current) {
1256            return false;
1257        }
1258
1259        self.at_terminator(&["else", "end"])
1260            || self.can_start_operation()
1261            || self.at_block_recovery_boundary()
1262    }
1263
1264    fn line_break_starts_new_top_level_item(&self) -> bool {
1265        match self.next_relevant_top_level_token(self.pos + 1) {
1266            Some(index) => self.is_top_level_starter(index),
1267            None => true,
1268        }
1269    }
1270
1271    fn next_relevant_top_level_token(&self, mut index: usize) -> Option<usize> {
1272        while let Some(token) = self.tokens.get(index) {
1273            match token.kind() {
1274                SyntaxKind::Whitespace | SyntaxKind::Newline | SyntaxKind::Comment => index += 1,
1275                _ => return Some(index),
1276            }
1277        }
1278        None
1279    }
1280
1281    fn next_relevant_block_token(&self, mut index: usize) -> Option<usize> {
1282        while let Some(token) = self.tokens.get(index) {
1283            match token.kind() {
1284                SyntaxKind::Whitespace
1285                | SyntaxKind::Newline
1286                | SyntaxKind::Comment
1287                | SyntaxKind::DocComment => index += 1,
1288                _ => return Some(index),
1289            }
1290        }
1291        None
1292    }
1293
1294    fn peek_after_inline_whitespace(&self) -> Option<SyntaxKind> {
1295        let mut index = self.pos;
1296        while let Some(token) = self.tokens.get(index) {
1297            match token.kind() {
1298                SyntaxKind::Whitespace => index += 1,
1299                SyntaxKind::Newline | SyntaxKind::Comment | SyntaxKind::DocComment => return None,
1300                kind => return Some(kind),
1301            }
1302        }
1303        None
1304    }
1305
1306    fn peek_after_non_comment_trivia(&self) -> Option<SyntaxKind> {
1307        self.peek_token_after_non_comment_trivia().map(Token::kind)
1308    }
1309
1310    fn peek_token_after_non_comment_trivia(&self) -> Option<&Token<'input>> {
1311        let mut index = self.pos;
1312        while let Some(token) = self.tokens.get(index) {
1313            match token.kind() {
1314                SyntaxKind::Whitespace | SyntaxKind::Newline => index += 1,
1315                _ => return Some(token),
1316            }
1317        }
1318        None
1319    }
1320
1321    fn peek_contextual_keyword_after_non_comment_trivia(&self, keyword: &str) -> bool {
1322        matches!(
1323            self.peek_token_after_non_comment_trivia(),
1324            Some(token) if token.kind() == SyntaxKind::Ident && token.text() == keyword
1325        )
1326    }
1327
1328    fn is_top_level_starter(&self, index: usize) -> bool {
1329        let Some(token) = self.tokens.get(index) else {
1330            return false;
1331        };
1332
1333        token.kind() == SyntaxKind::DocComment
1334            || token.kind() == SyntaxKind::At
1335            || (token.kind() == SyntaxKind::Ident
1336                && match token.text() {
1337                    "adv_map" | "begin" | "const" | "enum" | "mod" | "namespace" | "proc"
1338                    | "type" | "use" => true,
1339                    "extern" => matches!(
1340                        self.next_relevant_top_level_token(index + 1)
1341                            .and_then(|next| self.tokens.get(next)),
1342                        Some(next) if next.kind() == SyntaxKind::Ident && next.text() == "package"
1343                    ),
1344                    "pub" => matches!(
1345                        self.next_relevant_top_level_token(index + 1)
1346                            .and_then(|next| self.tokens.get(next)),
1347                        Some(next)
1348                            if next.kind() == SyntaxKind::Ident
1349                                && matches!(next.text(), "const" | "enum" | "mod" | "proc" | "type" | "use")
1350                    ),
1351                    _ => false,
1352                })
1353    }
1354
1355    fn can_start_operation(&self) -> bool {
1356        self.can_start_instruction()
1357            || self.at_keyword("if")
1358            || self.at_keyword("do")
1359            || self.at_keyword("while")
1360            || self.at_keyword("repeat")
1361    }
1362
1363    fn can_start_instruction(&self) -> bool {
1364        matches!(
1365            self.current(),
1366            Some(token)
1367                if matches!(
1368                    token.kind(),
1369                    SyntaxKind::Ident | SyntaxKind::SpecialIdent | SyntaxKind::QuotedIdent
1370                ) && (token.kind() != SyntaxKind::Ident
1371                    || !is_reserved_block_keyword(token.text()))
1372        )
1373    }
1374
1375    fn block_recovery_boundary(&self, terminators: &[&str]) -> Option<BlockRecoveryBoundary> {
1376        if self.at_keyword("else") && !terminators.contains(&"else") {
1377            return Some(BlockRecoveryBoundary::Else);
1378        }
1379
1380        if self.at_top_level_form_starter_in_block() {
1381            return Some(BlockRecoveryBoundary::TopLevelItem);
1382        }
1383
1384        None
1385    }
1386
1387    fn at_block_recovery_boundary(&self) -> bool {
1388        self.at_keyword("else") || self.at_top_level_form_starter_in_block()
1389    }
1390
1391    fn at_top_level_form_starter(&self) -> bool {
1392        self.is_top_level_starter(self.pos)
1393    }
1394
1395    fn at_top_level_form_starter_in_block(&self) -> bool {
1396        match self.current() {
1397            Some(token) if token.kind() == SyntaxKind::DocComment => self
1398                .next_relevant_block_token(self.pos + 1)
1399                .is_some_and(|index| self.is_top_level_starter(index)),
1400            _ => self.at_top_level_form_starter(),
1401        }
1402    }
1403
1404    fn at_terminator(&self, terminators: &[&str]) -> bool {
1405        terminators.iter().any(|terminator| {
1406            // A `while` terminator (used to close a `do`..`while` body) matches only a *bare*
1407            // `while`. A `while.true` token sequence opens a nested head-controlled loop and
1408            // must not be mistaken for the terminator.
1409            if *terminator == "while" {
1410                self.at_bare_while()
1411            } else {
1412                self.at_keyword(terminator)
1413            }
1414        })
1415    }
1416
1417    /// Returns `true` if the current token is a `while` keyword that is *not* immediately followed
1418    /// by a `.` suffix (i.e. the `while` that closes a `do`..`while` body, not a nested
1419    /// `while.true`).
1420    ///
1421    /// Only a `.` directly adjacent to the `while` keyword opens a header suffix; any intervening
1422    /// token (e.g. whitespace, as in `while .true`) is treated as a bare `while`, which makes the
1423    /// stray `.true` a syntax error rather than a valid `while.true` spelling.
1424    fn at_bare_while(&self) -> bool {
1425        self.at_keyword("while")
1426            && !matches!(
1427                self.tokens.get(self.pos + 1),
1428                Some(token) if token.kind() == SyntaxKind::Dot
1429            )
1430    }
1431
1432    fn at_name_like(&self) -> bool {
1433        matches!(
1434            self.current_kind(),
1435            Some(SyntaxKind::Ident | SyntaxKind::SpecialIdent | SyntaxKind::QuotedIdent)
1436        )
1437    }
1438
1439    fn at_package_name_like(&self) -> bool {
1440        self.at_name_like() || self.at_kind(SyntaxKind::QuotedString)
1441    }
1442
1443    fn at_import_list_recovery_boundary(&self) -> bool {
1444        self.eof() || matches!(self.current_kind(), Some(SyntaxKind::Comma | SyntaxKind::RBrace))
1445    }
1446
1447    fn at_keyword(&self, keyword: &str) -> bool {
1448        matches!(self.current(), Some(token) if token.kind() == SyntaxKind::Ident && token.text() == keyword)
1449    }
1450
1451    fn at_prefixed_keyword(&self, prefix: &str, keyword: &str) -> bool {
1452        if !self.at_keyword(prefix) {
1453            return false;
1454        }
1455
1456        matches!(
1457            self.next_relevant_top_level_token(self.pos + 1).and_then(|index| self.tokens.get(index)),
1458            Some(token) if token.kind() == SyntaxKind::Ident && token.text() == keyword
1459        )
1460    }
1461
1462    fn at_regular_trivia(&self) -> bool {
1463        matches!(
1464            self.current_kind(),
1465            Some(SyntaxKind::Whitespace | SyntaxKind::Newline | SyntaxKind::Comment)
1466        )
1467    }
1468
1469    fn at_kind(&self, kind: SyntaxKind) -> bool {
1470        self.current_kind() == Some(kind)
1471    }
1472
1473    fn current(&self) -> Option<&Token<'input>> {
1474        self.tokens.get(self.pos)
1475    }
1476
1477    fn current_kind(&self) -> Option<SyntaxKind> {
1478        self.current().map(Token::kind)
1479    }
1480
1481    fn eof(&self) -> bool {
1482        self.pos >= self.tokens.len()
1483    }
1484
1485    fn bump_regular_trivia(&mut self) {
1486        while self.at_regular_trivia() {
1487            self.bump();
1488        }
1489    }
1490
1491    fn bump_non_comment_trivia(&mut self) {
1492        while matches!(self.current_kind(), Some(SyntaxKind::Whitespace | SyntaxKind::Newline)) {
1493            self.bump();
1494        }
1495    }
1496
1497    fn bump_inline_whitespace(&mut self) {
1498        while self.at_kind(SyntaxKind::Whitespace) {
1499            self.bump();
1500        }
1501    }
1502
1503    fn expect_keyword(&mut self, keyword: &str, message: &'static str) -> bool {
1504        self.bump_regular_trivia();
1505        if self.at_keyword(keyword) {
1506            self.bump();
1507            true
1508        } else {
1509            self.error_here(message);
1510            false
1511        }
1512    }
1513
1514    fn expect_kind(&mut self, kind: SyntaxKind, message: &'static str) -> bool {
1515        self.bump_regular_trivia();
1516        if self.at_kind(kind) {
1517            self.bump();
1518            true
1519        } else {
1520            self.error_here(message);
1521            false
1522        }
1523    }
1524
1525    fn start_node(&mut self, kind: SyntaxKind) {
1526        self.builder.start_node(kind.into());
1527    }
1528
1529    fn finish_node(&mut self) {
1530        self.builder.finish_node();
1531    }
1532
1533    fn bump(&mut self) {
1534        if let Some(token) = self.current() {
1535            let kind = token.kind();
1536            let span = token.span();
1537            let text = token.text();
1538            if kind == SyntaxKind::Error {
1539                self.diagnostics.push(diagnostic!(
1540                    severity = Severity::Error,
1541                    labels = vec![LabeledSpan::at(span, format!("unrecognized token `{text}`"))],
1542                    "syntax error"
1543                ));
1544            }
1545            self.builder.token(kind.into(), text);
1546            self.pos += 1;
1547        }
1548    }
1549
1550    fn bump_nesting(&mut self, nesting: &mut Nesting, kind: SyntaxKind) {
1551        match nesting.bump(kind) {
1552            Ok(next) => *nesting = next,
1553            Err(closing) => {
1554                self.error_here(format!(
1555                    "unexpected closing delimiter `{}`",
1556                    closing_delimiter_text(closing)
1557                ));
1558            },
1559        }
1560    }
1561
1562    fn error_here(&mut self, message: impl Into<String>) {
1563        let span = self.current().map(Token::span).unwrap_or(self.eof_span);
1564        self.error_at_span(span, message);
1565    }
1566
1567    fn error_at_span(&mut self, span: SourceSpan, message: impl Into<String>) {
1568        self.diagnostics.push(diagnostic!(
1569            severity = Severity::Error,
1570            labels = vec![LabeledSpan::at(span, message.into())],
1571            "syntax error"
1572        ));
1573    }
1574
1575    fn error_at_eof(&mut self, message: impl Into<String>) {
1576        self.diagnostics.push(diagnostic!(
1577            severity = Severity::Error,
1578            labels = vec![LabeledSpan::at(self.eof_span, message.into())],
1579            "syntax error"
1580        ));
1581    }
1582}
1583
1584fn detached_source_file(input: &str) -> Arc<SourceFile> {
1585    Arc::new(SourceFile::new(
1586        SourceId::UNKNOWN,
1587        SourceLanguage::Masm,
1588        Uri::new("memory:///inline.masm"),
1589        input.to_owned().into_boxed_str(),
1590    ))
1591}
1592
1593fn eof_anchor_span(source: &SourceFile, bounds: Option<core::ops::Range<usize>>) -> SourceSpan {
1594    let content = source.as_str();
1595    let (content, start) = match bounds {
1596        Some(range) => (&content[range.start..range.end], range.start),
1597        None => (content, 0),
1598    };
1599    content
1600        .char_indices()
1601        .last()
1602        .map(|(offset, _)| {
1603            SourceSpan::at(
1604                source.id(),
1605                u32::try_from(offset).expect("source files larger than 4GiB are not supported"),
1606            )
1607        })
1608        .unwrap_or_else(|| {
1609            SourceSpan::try_from_range(source.id(), start..start)
1610                .expect("source files larger than 4GiB are not supported")
1611        })
1612}
1613
1614fn source_span_from_text_range(source_id: SourceId, range: TextRange) -> SourceSpan {
1615    SourceSpan::new(source_id, u32::from(range.start())..u32::from(range.end()))
1616}
1617
1618fn closing_delimiter_text(kind: SyntaxKind) -> &'static str {
1619    match kind {
1620        SyntaxKind::RParen => ")",
1621        SyntaxKind::RBracket => "]",
1622        SyntaxKind::RBrace => "}",
1623        _ => "delimiter",
1624    }
1625}
1626
1627fn expects_continuation_operand(kind: SyntaxKind) -> bool {
1628    matches!(
1629        kind,
1630        SyntaxKind::Dot
1631            | SyntaxKind::Equal
1632            | SyntaxKind::Comma
1633            | SyntaxKind::DotDot
1634            | SyntaxKind::Colon
1635            | SyntaxKind::ColonColon
1636            | SyntaxKind::RArrow
1637            | SyntaxKind::Plus
1638            | SyntaxKind::Minus
1639            | SyntaxKind::Star
1640            | SyntaxKind::Slash
1641            | SyntaxKind::SlashSlash
1642            | SyntaxKind::LBracket
1643            | SyntaxKind::LParen
1644            | SyntaxKind::LBrace
1645    )
1646}
1647
1648fn punctuation_continues_instruction(kind: SyntaxKind) -> bool {
1649    matches!(
1650        kind,
1651        SyntaxKind::Dot
1652            | SyntaxKind::Equal
1653            | SyntaxKind::Comma
1654            | SyntaxKind::DotDot
1655            | SyntaxKind::Colon
1656            | SyntaxKind::ColonColon
1657            | SyntaxKind::RArrow
1658            | SyntaxKind::Plus
1659            | SyntaxKind::Minus
1660            | SyntaxKind::Star
1661            | SyntaxKind::Slash
1662            | SyntaxKind::SlashSlash
1663            | SyntaxKind::RBracket
1664            | SyntaxKind::RParen
1665            | SyntaxKind::RBrace
1666    )
1667}
1668
1669fn is_reserved_block_keyword(text: &str) -> bool {
1670    matches!(
1671        text,
1672        "adv_map"
1673            | "begin"
1674            | "const"
1675            | "do"
1676            | "else"
1677            | "end"
1678            | "enum"
1679            | "extern"
1680            | "if"
1681            | "mod"
1682            | "namespace"
1683            | "proc"
1684            | "pub"
1685            | "repeat"
1686            | "type"
1687            | "use"
1688            | "while"
1689    )
1690}
1691
1692#[cfg(test)]
1693mod tests {
1694    use std::{
1695        fs,
1696        path::{Path, PathBuf},
1697        string::{String, ToString},
1698        sync::Arc,
1699        vec::Vec,
1700    };
1701
1702    use miden_debug_types::{
1703        SourceFile as ManagedSourceFile, SourceId, SourceLanguage, SourceSpan, Uri,
1704    };
1705    use rowan::ast::AstNode;
1706
1707    use crate::{
1708        ast::{ImportKind, Item, SourceFile as AstSourceFile},
1709        parse_source_file, parse_text,
1710        parser::parse_inline_masm_text,
1711        syntax::SyntaxKind,
1712    };
1713
1714    fn repo_root() -> PathBuf {
1715        Path::new(env!("CARGO_MANIFEST_DIR"))
1716            .parent()
1717            .and_then(Path::parent)
1718            .expect("workspace root should be two levels above crates/assembly-syntax-cst")
1719            .to_path_buf()
1720    }
1721
1722    fn checked_in_masm_corpus() -> Vec<PathBuf> {
1723        let root = repo_root();
1724        let mut files = Vec::new();
1725        for relative in [
1726            "crates/lib/core/asm",
1727            "miden-vm/masm-examples",
1728            "miden-vm/tests/integration/cli/data",
1729        ] {
1730            collect_masm_files(&root.join(relative), &mut files);
1731        }
1732        files.sort();
1733        files
1734    }
1735
1736    fn collect_masm_files(dir: &Path, files: &mut Vec<PathBuf>) {
1737        let entries = fs::read_dir(dir)
1738            .unwrap_or_else(|error| panic!("failed to read {}: {error}", dir.display()));
1739        for entry in entries {
1740            let entry = entry.unwrap_or_else(|error| {
1741                panic!("failed to read a directory entry under {}: {error}", dir.display())
1742            });
1743            let path = entry.path();
1744            if path.is_dir() {
1745                collect_masm_files(&path, files);
1746            } else if path.extension().is_some_and(|ext| ext == "masm") {
1747                files.push(path);
1748            }
1749        }
1750    }
1751
1752    fn representative_round_trip_sources() -> &'static [&'static str] {
1753        &[
1754            "",
1755            "# leading comment\n#! module docs\npub const X = [0x01, 0x02]\n",
1756            "\
1757namespace std::math
1758extern package \"miden/base@0.1.0\"
1759mod internal
1760pub mod u64
1761",
1762            "\
1763@inline
1764# keep standalone
1765@locals(1)
1766pub proc foo(a: felt)
1767    # body
1768    push.1
1769end
1770",
1771            "\
1772begin
1773    if.true
1774        repeat.4
1775            swap dup.1 add
1776        end
1777    else
1778        while.true
1779            nop
1780        end
1781    end
1782end
1783",
1784            "\
1785pub type Account = struct { id: felt, vault: ptr<u8, addrspace(byte)> }
1786adv_map TABLE = [
1787    [1, 2],
1788    event(foo(bar, baz)),
1789]
1790",
1791        ]
1792    }
1793
1794    fn assert_lossless_parse(input: &str, label: impl core::fmt::Display) {
1795        let parse = parse_text(input);
1796        assert_eq!(
1797            parse.syntax().text().to_string(),
1798            input,
1799            "CST parse was not lossless for {label}"
1800        );
1801    }
1802
1803    fn diagnostic_labels(parse: &super::Parse) -> Vec<String> {
1804        parse
1805            .diagnostics()
1806            .iter()
1807            .flat_map(|diag| diag.labels.as_deref().unwrap_or(&[]).iter())
1808            .filter_map(|label| label.label())
1809            .map(ToString::to_string)
1810            .collect()
1811    }
1812
1813    fn assert_import_rejected(source: &str, expected_label: &str) {
1814        let parse = parse_text(source);
1815        assert!(parse.has_errors(), "expected {source:?} to be rejected");
1816        let labels = diagnostic_labels(&parse);
1817        assert!(
1818            labels.iter().any(|label| label.contains(expected_label)),
1819            "expected {source:?} to report {expected_label:?}, got {:?}",
1820            parse.diagnostics()
1821        );
1822    }
1823
1824    #[test]
1825    fn parse_text_is_lossless_for_representative_sources() {
1826        for (index, source) in representative_round_trip_sources().iter().enumerate() {
1827            assert_lossless_parse(source, format_args!("representative source {index}"));
1828        }
1829    }
1830
1831    #[test]
1832    fn parse_text_is_lossless_for_checked_in_masm_corpus() {
1833        let files = checked_in_masm_corpus();
1834        assert!(
1835            !files.is_empty(),
1836            "expected the checked-in MASM corpus to contain at least one source file"
1837        );
1838
1839        for path in files {
1840            let source = fs::read_to_string(&path)
1841                .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display()));
1842            assert_lossless_parse(&source, path.display());
1843        }
1844    }
1845
1846    #[test]
1847    fn parse_text_as_inline_masm() {
1848        let source = "\
1849            if.true
1850                repeat.4
1851                    swap dup.1 add
1852                end
1853            else
1854                while.true
1855                    nop
1856                end
1857            end
1858        ";
1859        let parse = parse_inline_masm_text(source, None);
1860        assert!(!parse.has_errors());
1861        let root = parse.syntax();
1862        assert_eq!(root.kind(), SyntaxKind::Block);
1863
1864        let child_kinds = root.children().map(|child| child.kind()).collect::<Vec<_>>();
1865        assert_eq!(child_kinds, vec![SyntaxKind::IfOp,]);
1866
1867        assert!(root.descendants().any(|node| node.kind() == SyntaxKind::IfOp));
1868        assert!(root.descendants().any(|node| node.kind() == SyntaxKind::RepeatOp));
1869        assert!(root.descendants().any(|node| node.kind() == SyntaxKind::WhileOp));
1870    }
1871
1872    #[test]
1873    fn parse_do_while_loop() {
1874        let source = "\
1875            do
1876                push.1
1877                dup.0 neq.0
1878            while
1879                dup.0 lt.10
1880            end
1881        ";
1882        let parse = parse_inline_masm_text(source, None);
1883        let mut parse = parse;
1884        let diagnostics = parse.take_diagnostics();
1885        assert!(diagnostics.is_empty(), "unexpected parse errors: {diagnostics:?}");
1886        let root = parse.syntax();
1887        let child_kinds = root.children().map(|child| child.kind()).collect::<Vec<_>>();
1888        assert_eq!(child_kinds, vec![SyntaxKind::DoWhileOp]);
1889
1890        // A `do`..`while`..`end` op has exactly two block children: the body and the condition.
1891        let do_while = root.children().find(|n| n.kind() == SyntaxKind::DoWhileOp).unwrap();
1892        let block_count = do_while.children().filter(|n| n.kind() == SyntaxKind::Block).count();
1893        assert_eq!(block_count, 2, "expected a body block and a condition block");
1894    }
1895
1896    #[test]
1897    fn parse_do_while_with_nested_while_true_in_body() {
1898        // The bare `while` that closes the `do` body must not be confused with the nested
1899        // head-controlled `while.true` loop inside the body.
1900        let source = "\
1901            do
1902                while.true
1903                    nop
1904                end
1905                push.1
1906            while
1907                eq.0
1908            end
1909        ";
1910        let parse = parse_inline_masm_text(source, None);
1911        let mut parse = parse;
1912        let diagnostics = parse.take_diagnostics();
1913        assert!(diagnostics.is_empty(), "unexpected parse errors: {diagnostics:?}");
1914        let root = parse.syntax();
1915        let child_kinds = root.children().map(|child| child.kind()).collect::<Vec<_>>();
1916        assert_eq!(child_kinds, vec![SyntaxKind::DoWhileOp]);
1917
1918        // The nested `while.true` loop is parsed as a `WhileOp` *inside* the do-while body.
1919        let do_while = root.children().find(|n| n.kind() == SyntaxKind::DoWhileOp).unwrap();
1920        assert!(do_while.descendants().any(|node| node.kind() == SyntaxKind::WhileOp));
1921    }
1922
1923    #[test]
1924    fn do_while_terminator_requires_dot_adjacent_to_while() {
1925        // Only a `.` immediately adjacent to `while` opens a header suffix. With whitespace in
1926        // between, the `while` closes the `do` body and the stray `.true` is a syntax error,
1927        // rather than being matched as a `while.true` spelling.
1928        let source = "\
1929            do
1930                push.1
1931            while .true
1932            end
1933        ";
1934        let mut parse = parse_inline_masm_text(source, None);
1935        let diagnostics = parse.take_diagnostics();
1936        assert!(!diagnostics.is_empty(), "expected a syntax error for the stray `.true`");
1937
1938        // The construct is still recognized as a do-while loop (the body terminated at `while`).
1939        let root = parse.syntax();
1940        assert!(root.descendants().any(|node| node.kind() == SyntaxKind::DoWhileOp));
1941    }
1942
1943    #[test]
1944    fn parses_top_level_forms_and_nested_structured_ops() {
1945        let source = "\
1946#! docs
1947namespace std::math
1948extern package \"miden/base@0.1.0\"
1949mod internal
1950pub mod u64
1951pub use {bar as baz} from foo
1952pub const X = 1
1953pub type FeltAlias = felt
1954adv_map TABLE = [0x01, 0x02]
1955begin
1956    if.true
1957        repeat.4
1958            swap dup.1 add
1959        end
1960    else
1961        while.true
1962            nop
1963        end
1964    end
1965end
1966pub proc foo(a) -> (b)
1967    exec.bar
1968end
1969";
1970        let parse = parse_text(source);
1971        assert!(!parse.has_errors());
1972        let root = parse.syntax();
1973        assert_eq!(root.kind(), SyntaxKind::SourceFile);
1974
1975        let child_kinds = root.children().map(|child| child.kind()).collect::<Vec<_>>();
1976        assert_eq!(
1977            child_kinds,
1978            vec![
1979                SyntaxKind::Doc,
1980                SyntaxKind::Namespace,
1981                SyntaxKind::ExternPackage,
1982                SyntaxKind::Submodule,
1983                SyntaxKind::Submodule,
1984                SyntaxKind::Import,
1985                SyntaxKind::Constant,
1986                SyntaxKind::TypeDecl,
1987                SyntaxKind::AdviceMap,
1988                SyntaxKind::BeginBlock,
1989                SyntaxKind::Procedure,
1990            ]
1991        );
1992
1993        assert!(root.descendants().any(|node| node.kind() == SyntaxKind::IfOp));
1994        assert!(root.descendants().any(|node| node.kind() == SyntaxKind::RepeatOp));
1995        assert!(root.descendants().any(|node| node.kind() == SyntaxKind::WhileOp));
1996    }
1997
1998    #[test]
1999    fn exposes_typed_wrappers_for_structured_top_level_forms() {
2000        let source = "\
2001namespace app::accounts
2002extern package \"miden/base@0.1.0\"
2003mod internal
2004pub mod api
2005use miden::core::mem as memory
2006pub const EVENT = event(\"miden::event\")
2007pub enum Bool : u8 {
2008    FALSE,
2009    TRUE = 1,
2010}
2011adv_map TABLE(0x0200000000000000020000000000000002000000000000000200000000000000) = [0x01, 0x02]
2012";
2013
2014        let parse = parse_text(source);
2015        assert!(!parse.has_errors(), "{:?}", parse.diagnostics());
2016
2017        let source_file = AstSourceFile::cast(parse.syntax()).expect("source file");
2018        let items = source_file.items().collect::<Vec<_>>();
2019        assert_eq!(items.len(), 8);
2020
2021        let Item::Namespace(namespace) = &items[0] else {
2022            panic!("expected namespace, got {:?}", items[0]);
2023        };
2024        assert_eq!(
2025            namespace
2026                .path()
2027                .expect("namespace path")
2028                .segments()
2029                .map(|segment| segment.text().to_string())
2030                .collect::<Vec<_>>(),
2031            vec!["app", "accounts"]
2032        );
2033
2034        let Item::ExternPackage(package) = &items[1] else {
2035            panic!("expected extern package, got {:?}", items[1]);
2036        };
2037        assert_eq!(package.package_token().expect("package name").text(), "\"miden/base@0.1.0\"");
2038
2039        let Item::Submodule(submodule) = &items[2] else {
2040            panic!("expected submodule, got {:?}", items[2]);
2041        };
2042        assert!(submodule.visibility().is_none());
2043        assert_eq!(submodule.name_token().expect("submodule name").text(), "internal");
2044
2045        let Item::Submodule(submodule) = &items[3] else {
2046            panic!("expected public submodule, got {:?}", items[3]);
2047        };
2048        assert!(submodule.visibility().is_some());
2049        assert_eq!(submodule.name_token().expect("submodule name").text(), "api");
2050
2051        let Item::Import(import) = &items[4] else {
2052            panic!("expected import, got {:?}", items[0]);
2053        };
2054        assert_eq!(import.kind(), ImportKind::Module);
2055        assert_eq!(
2056            import
2057                .module_path()
2058                .expect("import path")
2059                .segments()
2060                .map(|segment| segment.text().to_string())
2061                .collect::<Vec<_>>(),
2062            vec!["miden", "core", "mem"]
2063        );
2064        assert_eq!(import.module_alias_token().expect("alias").text(), "memory");
2065
2066        let Item::Constant(constant) = &items[5] else {
2067            panic!("expected constant, got {:?}", items[1]);
2068        };
2069        assert_eq!(constant.name_token().expect("constant name").text(), "EVENT");
2070        assert_eq!(
2071            constant
2072                .expr()
2073                .expect("constant expr")
2074                .significant_tokens()
2075                .map(|token| token.text().to_string())
2076                .collect::<Vec<_>>(),
2077            vec!["event", "(", "\"miden::event\"", ")"]
2078        );
2079
2080        let Item::TypeDecl(type_decl) = &items[6] else {
2081            panic!("expected type declaration, got {:?}", items[2]);
2082        };
2083        assert_eq!(type_decl.keyword_token().expect("type keyword").text(), "enum");
2084        assert_eq!(type_decl.name_token().expect("type name").text(), "Bool");
2085        assert!(
2086            type_decl.body().is_some(),
2087            "expected enum declaration to expose a structured type body"
2088        );
2089
2090        let Item::AdviceMap(advice_map) = &items[7] else {
2091            panic!("expected advice map, got {:?}", items[3]);
2092        };
2093        assert_eq!(advice_map.name_token().expect("advice map name").text(), "TABLE");
2094        assert_eq!(
2095            advice_map
2096                .value_expr()
2097                .expect("advice map value")
2098                .significant_tokens()
2099                .map(|token| token.text().to_string())
2100                .collect::<Vec<_>>(),
2101            vec!["[", "0x01", ",", "0x02", "]"]
2102        );
2103    }
2104
2105    #[test]
2106    fn parses_unparenthesized_procedure_result_types() {
2107        let source = "\
2108pub proc println(message: ptr<u8, addrspace(byte)>) -> ptr<u8, addrspace(byte)>
2109    nop
2110end
2111";
2112
2113        let parse = parse_text(source);
2114        assert!(!parse.has_errors(), "{:?}", parse.diagnostics());
2115
2116        let root = parse.syntax();
2117        let source_file = AstSourceFile::cast(root).expect("source file");
2118        let items = source_file.items().collect::<Vec<_>>();
2119        assert_eq!(items.len(), 1);
2120
2121        let Item::Procedure(procedure) = &items[0] else {
2122            panic!("expected procedure, got {:?}", items[0]);
2123        };
2124        assert!(
2125            procedure.signature().is_some(),
2126            "expected procedure to retain its signature node"
2127        );
2128    }
2129
2130    #[test]
2131    fn no_result_signature_does_not_absorb_body_leading_trivia() {
2132        let source = "\
2133pub proc foo()
2134    # body
2135    nop
2136end
2137";
2138
2139        let parse = parse_text(source);
2140        assert!(!parse.has_errors(), "{:?}", parse.diagnostics());
2141
2142        let source_file = AstSourceFile::cast(parse.syntax()).expect("source file");
2143        let items = source_file.items().collect::<Vec<_>>();
2144        let Item::Procedure(procedure) = &items[0] else {
2145            panic!("expected procedure, got {:?}", items[0]);
2146        };
2147
2148        let signature = procedure.signature().expect("procedure signature");
2149        assert_eq!(signature.syntax().text().to_string(), "()");
2150
2151        let block = procedure.block().expect("procedure block");
2152        assert!(
2153            block.syntax().text().to_string().contains("# body"),
2154            "expected body-leading comment to remain in the block"
2155        );
2156    }
2157
2158    #[test]
2159    fn multiline_result_arrow_stays_in_signature() {
2160        let source = "\
2161pub proc foo()
2162    -> felt
2163    nop
2164end
2165";
2166
2167        let parse = parse_text(source);
2168        assert!(!parse.has_errors(), "{:?}", parse.diagnostics());
2169
2170        let source_file = AstSourceFile::cast(parse.syntax()).expect("source file");
2171        let items = source_file.items().collect::<Vec<_>>();
2172        let Item::Procedure(procedure) = &items[0] else {
2173            panic!("expected procedure, got {:?}", items[0]);
2174        };
2175
2176        let signature = procedure.signature().expect("procedure signature");
2177        assert_eq!(signature.syntax().text().to_string(), "()\n    -> felt");
2178    }
2179
2180    #[test]
2181    fn cst_import_parses_multiline_module_aliases() {
2182        let source = "\
2183use ::miden::core::collections::sorted_array::lowerbound_key_value
2184    as lowerbound_key_value
2185";
2186
2187        let parse = parse_text(source);
2188        assert!(!parse.has_errors(), "{:?}", parse.diagnostics());
2189
2190        let source_file = AstSourceFile::cast(parse.syntax()).expect("source file");
2191        let items = source_file.items().collect::<Vec<_>>();
2192        assert_eq!(items.len(), 1);
2193
2194        let Item::Import(import) = &items[0] else {
2195            panic!("expected import, got {:?}", items[0]);
2196        };
2197        assert_eq!(import.kind(), ImportKind::Module);
2198        assert_eq!(import.module_alias_token().expect("alias").text(), "lowerbound_key_value");
2199    }
2200
2201    #[test]
2202    fn cst_import_parses_module_imports() {
2203        let source = "\
2204use foo
2205use some::module
2206use some::module as sm
2207";
2208
2209        let parse = parse_text(source);
2210        assert!(!parse.has_errors(), "{:?}", parse.diagnostics());
2211
2212        let source_file = AstSourceFile::cast(parse.syntax()).expect("source file");
2213        let imports = source_file
2214            .items()
2215            .map(|item| match item {
2216                Item::Import(import) => import,
2217                other => panic!("expected import, got {other:?}"),
2218            })
2219            .collect::<Vec<_>>();
2220        assert_eq!(imports.len(), 3);
2221
2222        let first = &imports[0];
2223        assert_eq!(first.kind(), ImportKind::Module);
2224        assert!(first.visibility().is_none());
2225        assert_eq!(
2226            first
2227                .module_path()
2228                .expect("module path")
2229                .segments()
2230                .map(|segment| segment.text().to_string())
2231                .collect::<Vec<_>>(),
2232            vec!["foo"]
2233        );
2234        assert!(first.module_alias_token().is_none());
2235
2236        let second = &imports[1];
2237        assert_eq!(second.kind(), ImportKind::Module);
2238        assert_eq!(
2239            second
2240                .module_path()
2241                .expect("module path")
2242                .segments()
2243                .map(|segment| segment.text().to_string())
2244                .collect::<Vec<_>>(),
2245            vec!["some", "module"]
2246        );
2247        assert!(second.module_alias_token().is_none());
2248
2249        let third = &imports[2];
2250        assert_eq!(third.kind(), ImportKind::Module);
2251        assert_eq!(
2252            third
2253                .module_path()
2254                .expect("module path")
2255                .segments()
2256                .map(|segment| segment.text().to_string())
2257                .collect::<Vec<_>>(),
2258            vec!["some", "module"]
2259        );
2260        assert_eq!(third.module_alias_token().expect("alias").text(), "sm");
2261    }
2262
2263    #[test]
2264    fn cst_import_parses_item_imports() {
2265        let source = "\
2266use {foo, bar as baz} from some::module
2267pub use {alpha} from core
2268";
2269
2270        let parse = parse_text(source);
2271        assert!(!parse.has_errors(), "{:?}", parse.diagnostics());
2272
2273        let source_file = AstSourceFile::cast(parse.syntax()).expect("source file");
2274        let imports = source_file
2275            .items()
2276            .map(|item| match item {
2277                Item::Import(import) => import,
2278                other => panic!("expected import, got {other:?}"),
2279            })
2280            .collect::<Vec<_>>();
2281        assert_eq!(imports.len(), 2);
2282
2283        let first = &imports[0];
2284        assert_eq!(first.kind(), ImportKind::Items);
2285        assert!(first.visibility().is_none());
2286        assert!(
2287            first.path().is_none(),
2288            "item imports must not masquerade as legacy path imports"
2289        );
2290        assert!(
2291            first.module_alias_token().is_none(),
2292            "item aliases must not masquerade as module aliases"
2293        );
2294        assert_eq!(
2295            first
2296                .module_path()
2297                .expect("module path")
2298                .segments()
2299                .map(|segment| segment.text().to_string())
2300                .collect::<Vec<_>>(),
2301            vec!["some", "module"]
2302        );
2303        let specs = first.item_specs().collect::<Vec<_>>();
2304        assert_eq!(specs.len(), 2);
2305        assert_eq!(specs[0].name_token().expect("item name").text(), "foo");
2306        assert!(specs[0].alias_token().is_none());
2307        assert_eq!(specs[1].name_token().expect("item name").text(), "bar");
2308        assert_eq!(specs[1].alias_token().expect("item alias").text(), "baz");
2309
2310        let second = &imports[1];
2311        assert_eq!(second.kind(), ImportKind::Items);
2312        assert!(second.visibility().is_some());
2313        assert_eq!(
2314            second
2315                .module_path()
2316                .expect("module path")
2317                .segments()
2318                .map(|segment| segment.text().to_string())
2319                .collect::<Vec<_>>(),
2320            vec!["core"]
2321        );
2322        let specs = second.item_specs().collect::<Vec<_>>();
2323        assert_eq!(specs.len(), 1);
2324        assert_eq!(specs[0].name_token().expect("item name").text(), "alpha");
2325    }
2326
2327    #[test]
2328    fn cst_import_accepts_quoted_special_and_contextual_names() {
2329        let source = "\
2330use \"as\"::\"from\" as \"module\"
2331use {as, from as as, \"as\" as \"from\", $kernel} from \"from\"::\"as\"
2332";
2333
2334        let parse = parse_text(source);
2335        assert!(!parse.has_errors(), "{:?}", parse.diagnostics());
2336
2337        let source_file = AstSourceFile::cast(parse.syntax()).expect("source file");
2338        let imports = source_file
2339            .items()
2340            .map(|item| match item {
2341                Item::Import(import) => import,
2342                other => panic!("expected import, got {other:?}"),
2343            })
2344            .collect::<Vec<_>>();
2345        assert_eq!(imports.len(), 2);
2346
2347        let module = &imports[0];
2348        assert_eq!(
2349            module
2350                .module_path()
2351                .expect("module path")
2352                .segments()
2353                .map(|segment| segment.text().to_string())
2354                .collect::<Vec<_>>(),
2355            vec!["\"as\"", "\"from\""]
2356        );
2357        assert_eq!(module.module_alias_token().expect("module alias").text(), "\"module\"");
2358
2359        let items = &imports[1];
2360        assert_eq!(
2361            items
2362                .module_path()
2363                .expect("module path")
2364                .segments()
2365                .map(|segment| segment.text().to_string())
2366                .collect::<Vec<_>>(),
2367            vec!["\"from\"", "\"as\""]
2368        );
2369        let specs = items.item_specs().collect::<Vec<_>>();
2370        assert_eq!(specs.len(), 4);
2371        assert_eq!(specs[0].name_token().expect("item name").text(), "as");
2372        assert!(specs[0].alias_token().is_none());
2373        assert_eq!(specs[1].name_token().expect("item name").text(), "from");
2374        assert_eq!(specs[1].alias_token().expect("item alias").text(), "as");
2375        assert_eq!(specs[2].name_token().expect("item name").text(), "\"as\"");
2376        assert_eq!(specs[2].alias_token().expect("item alias").text(), "\"from\"");
2377        assert_eq!(specs[3].name_token().expect("item name").text(), "$kernel");
2378    }
2379
2380    #[test]
2381    fn cst_import_rejects_public_module_imports_and_removed_forms() {
2382        let cases = [
2383            ("pub use some::module\n", "`pub use` is only supported for braced item imports"),
2384            ("use foo->bar\n", "import aliases use `as`; `->` is no longer supported"),
2385            ("pub use foo->bar\n", "import aliases use `as`; `->` is no longer supported"),
2386            (
2387                "use {foo->bar} from m\n",
2388                "import aliases use `as`; `->` is no longer supported",
2389            ),
2390            (
2391                "use 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef->foo\n",
2392                "digest imports are not supported",
2393            ),
2394            (
2395                "pub use 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef->foo\n",
2396                "digest imports are not supported",
2397            ),
2398            (
2399                "use {foo} from 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef\n",
2400                "digest imports are not supported",
2401            ),
2402        ];
2403
2404        for (source, expected) in cases {
2405            assert_import_rejected(source, expected);
2406        }
2407    }
2408
2409    #[test]
2410    fn cst_import_does_not_consume_next_item_after_missing_alias_or_from_path() {
2411        let cases = [
2412            ("use\npub const X = 1\n", "expected an import path"),
2413            ("use foo as\npub const X = 1\n", "expected an alias name after `as`"),
2414            ("use {foo} from\npub const X = 1\n", "expected a module path after `from`"),
2415        ];
2416
2417        for (source, expected) in cases {
2418            let parse = parse_text(source);
2419            assert!(parse.has_errors(), "expected {source:?} to be rejected");
2420            let labels = diagnostic_labels(&parse);
2421            assert!(
2422                labels.iter().any(|label| label.contains(expected)),
2423                "expected {source:?} to report {expected:?}, got {:?}",
2424                parse.diagnostics()
2425            );
2426
2427            let source_file = AstSourceFile::cast(parse.syntax()).expect("source file");
2428            let items = source_file.items().collect::<Vec<_>>();
2429            assert_eq!(items.len(), 2, "next top-level item should remain separate");
2430            assert!(matches!(items[0], Item::Import(_)));
2431            let Item::Constant(constant) = &items[1] else {
2432                panic!("expected second item to remain a constant, got {:?}", items[1]);
2433            };
2434            assert!(constant.visibility().is_some(), "`pub` must remain attached to const");
2435            assert_eq!(constant.name_token().expect("constant name").text(), "X");
2436        }
2437    }
2438
2439    #[test]
2440    fn cst_import_rejects_malformed_item_imports() {
2441        let cases = [
2442            ("use {} from m\n", "import lists must contain at least one item"),
2443            ("use {foo} m\n", "expected `from` after import list"),
2444            ("use foo as\n", "expected an alias name after `as`"),
2445            ("use {foo as} from m\n", "expected an alias name after `as`"),
2446        ];
2447
2448        for (source, expected) in cases {
2449            assert_import_rejected(source, expected);
2450        }
2451    }
2452
2453    #[test]
2454    fn cst_import_rejects_wildcard_imports() {
2455        for source in ["use * from m\n", "use {*} from m\n", "use {foo, *} from m\n"] {
2456            assert_import_rejected(source, "wildcard imports are not supported");
2457        }
2458    }
2459
2460    #[test]
2461    fn keeps_header_comments_on_structured_nodes() {
2462        let source = "\
2463use {panic} from ::miden::utils # import
2464pub proc long_name(arg: felt) # proc
2465    nop
2466end
2467begin # begin
2468    if.true # if
2469        nop
2470    else # else
2471        nop
2472    end
2473end
2474";
2475
2476        let parse = parse_text(source);
2477        assert!(!parse.has_errors(), "{:?}", parse.diagnostics());
2478
2479        let root = parse.syntax();
2480        let procedure = root
2481            .descendants()
2482            .find(|node| node.kind() == SyntaxKind::Procedure)
2483            .expect("procedure");
2484        assert!(
2485            procedure
2486                .children_with_tokens()
2487                .filter_map(rowan::NodeOrToken::into_token)
2488                .any(|token| token.kind() == SyntaxKind::Comment && token.text().contains("proc"))
2489        );
2490
2491        let if_node = root
2492            .descendants()
2493            .find(|node| node.kind() == SyntaxKind::IfOp)
2494            .expect("if node");
2495        let if_comments = if_node
2496            .children_with_tokens()
2497            .filter_map(rowan::NodeOrToken::into_token)
2498            .filter(|token| token.kind() == SyntaxKind::Comment)
2499            .map(|token| token.text().to_string())
2500            .collect::<Vec<_>>();
2501        assert!(
2502            if_comments.iter().any(|comment| comment.contains("if")),
2503            "expected header comment on if node, got {if_comments:?}"
2504        );
2505        assert!(
2506            if_comments.iter().any(|comment| comment.contains("else")),
2507            "expected else comment on if node, got {if_comments:?}"
2508        );
2509    }
2510
2511    #[test]
2512    fn rejects_block_local_doc_comments_before_block_keywords() {
2513        let source = "\
2514proc foo
2515    #! mistaken doc comment before if
2516    if.true
2517        nop
2518        #! mistaken doc comment before else
2519    else
2520        #! mistaken doc comment before while
2521        while.true
2522            add
2523        end
2524    end
2525end
2526";
2527
2528        let parse = parse_text(source);
2529        assert!(parse.has_errors());
2530        assert!(
2531            parse
2532                .diagnostics()
2533                .iter()
2534                .flat_map(|diag| diag.labels.as_deref().unwrap_or(&[]).iter())
2535                .filter_map(|label| label.label())
2536                .any(|label| label.contains("doc comments are only allowed")),
2537            "expected block-local doc comments before block keywords to be rejected, got {:?}",
2538            parse.diagnostics()
2539        );
2540
2541        let root = parse.syntax();
2542        assert!(root.descendants().any(|node| node.kind() == SyntaxKind::Procedure));
2543        assert!(root.descendants().any(|node| node.kind() == SyntaxKind::IfOp));
2544        assert!(
2545            root.descendants().filter(|node| node.kind() == SyntaxKind::Instruction).count() >= 2
2546        );
2547    }
2548
2549    #[test]
2550    fn block_local_doc_comments_before_instructions_still_error() {
2551        let source = "\
2552proc foo
2553    #! malformed doc comment before instruction
2554    loc_load.0
2555end
2556";
2557
2558        let parse = parse_text(source);
2559        assert!(parse.has_errors());
2560        assert!(
2561            parse
2562                .diagnostics()
2563                .iter()
2564                .flat_map(|diag| diag.labels.as_deref().unwrap_or(&[]).iter())
2565                .filter_map(|label| label.label())
2566                .any(|label| label.contains("doc comments are only allowed")),
2567            "expected block-local doc comments before instructions to remain invalid, got {:?}",
2568            parse.diagnostics()
2569        );
2570    }
2571
2572    #[test]
2573    fn block_local_doc_comments_still_recover_before_true_top_level_items() {
2574        let source = "\
2575proc foo
2576    #! actual misplaced doc comment
2577    pub const X = 1
2578";
2579
2580        let parse = parse_text(source);
2581        assert!(parse.has_errors());
2582        assert!(
2583            parse
2584                .diagnostics()
2585                .iter()
2586                .flat_map(|diag| diag.labels.as_deref().unwrap_or(&[]).iter())
2587                .filter_map(|label| label.label())
2588                .any(|label| label.contains("before top-level item")),
2589            "expected block recovery before a top-level item, got {:?}",
2590            parse.diagnostics()
2591        );
2592
2593        let root = parse.syntax();
2594        assert!(root.descendants().any(|node| node.kind() == SyntaxKind::Doc));
2595
2596        let source_file = AstSourceFile::cast(root).expect("source file");
2597        let items = source_file.items().collect::<Vec<_>>();
2598        assert_eq!(
2599            items.len(),
2600            3,
2601            "expected recovery to preserve the recovered doc comment and top-level constant"
2602        );
2603        assert!(matches!(items[0], Item::Procedure(_)));
2604        assert!(matches!(items[1], Item::Doc(_)));
2605        assert!(matches!(items[2], Item::Constant(_)));
2606    }
2607
2608    #[test]
2609    fn rejects_stray_closing_delimiters() {
2610        let cases = [
2611            ("const X = )\n", ")"),
2612            ("begin  foo ] end\n", "]"),
2613            ("begin\n    foo}\nend\n", "}"),
2614        ];
2615
2616        for (source, delimiter) in cases {
2617            let parse = parse_text(source);
2618            assert!(parse.has_errors(), "expected {source:?} to be rejected");
2619            let expected = format!("unexpected closing delimiter `{delimiter}`");
2620            assert!(
2621                parse
2622                    .diagnostics()
2623                    .iter()
2624                    .flat_map(|diag| diag.labels.as_deref().unwrap_or(&[]).iter())
2625                    .filter_map(|label| label.label())
2626                    .any(|label| label.contains(&expected)),
2627                "expected {source:?} to report {expected:?}, got {:?}",
2628                parse.diagnostics()
2629            );
2630        }
2631    }
2632
2633    #[test]
2634    fn recovers_from_missing_end_tokens() {
2635        let parse = parse_text("begin\n    if.true\n        add\n");
2636        assert!(parse.has_errors());
2637        let end_labels = parse
2638            .diagnostics()
2639            .iter()
2640            .flat_map(|diag| diag.labels.as_deref().unwrap_or(&[]).iter())
2641            .filter_map(|label| label.label())
2642            .filter(|label| label.contains("expected `end`"))
2643            .collect::<Vec<_>>();
2644        assert_eq!(
2645            end_labels.len(),
2646            1,
2647            "expected exactly one missing-`end` diagnostic, got {:?}",
2648            parse.diagnostics()
2649        );
2650        assert!(
2651            end_labels[0].contains("`if`"),
2652            "expected the innermost unterminated block to own the diagnostic, got {end_labels:?}"
2653        );
2654
2655        let root = parse.syntax();
2656        assert!(root.descendants().any(|node| node.kind() == SyntaxKind::BeginBlock));
2657        assert!(root.descendants().any(|node| node.kind() == SyntaxKind::IfOp));
2658    }
2659
2660    #[test]
2661    fn recovers_before_top_level_items_inside_blocks() {
2662        let source = "\
2663proc foo
2664    if.true
2665        add
2666pub const X = 1
2667";
2668        let parse = parse_text(source);
2669        assert!(parse.has_errors());
2670        assert!(
2671            parse
2672                .diagnostics()
2673                .iter()
2674                .flat_map(|diag| diag.labels.as_deref().unwrap_or(&[]).iter())
2675                .filter_map(|label| label.label())
2676                .any(|label| label.contains("before top-level item")),
2677            "expected block recovery before a top-level item, got {:?}",
2678            parse.diagnostics()
2679        );
2680
2681        let source_file = AstSourceFile::cast(parse.syntax()).expect("source file");
2682        let items = source_file.items().collect::<Vec<_>>();
2683        assert_eq!(items.len(), 2, "expected recovery to preserve the top-level constant");
2684        assert!(matches!(items[0], Item::Procedure(_)));
2685        assert!(matches!(items[1], Item::Constant(_)));
2686        assert!(parse.syntax().descendants().any(|node| node.kind() == SyntaxKind::Error));
2687    }
2688
2689    #[test]
2690    fn else_synchronizes_unterminated_nested_blocks() {
2691        let source = "\
2692proc foo
2693    if.true
2694        while.true
2695            nop
2696    else
2697        nop
2698    end
2699end
2700";
2701        let parse = parse_text(source);
2702        assert!(parse.has_errors());
2703        assert!(
2704            parse
2705                .diagnostics()
2706                .iter()
2707                .flat_map(|diag| diag.labels.as_deref().unwrap_or(&[]).iter())
2708                .filter_map(|label| label.label())
2709                .any(|label| label.contains("close `while` before `else`")),
2710            "expected the nested `while` to recover before `else`, got {:?}",
2711            parse.diagnostics()
2712        );
2713
2714        let if_node = parse
2715            .syntax()
2716            .descendants()
2717            .find(|node| node.kind() == SyntaxKind::IfOp)
2718            .expect("if node");
2719        assert_eq!(
2720            if_node.children().filter(|child| child.kind() == SyntaxKind::Block).count(),
2721            2,
2722            "expected `else` to remain attached to the enclosing `if` after recovery"
2723        );
2724    }
2725
2726    #[test]
2727    fn surfaces_invalid_tokens_as_diagnostics() {
2728        let parse = parse_text("proc foo\n    §\nend\n");
2729        assert!(parse.has_errors());
2730        assert!(parse.diagnostics().iter().any(|diag| diag.labels.as_ref().is_some_and(
2731            |labels| {
2732                labels
2733                    .iter()
2734                    .any(|l| l.label().is_some_and(|label| label.contains("unrecognized token")))
2735            }
2736        )));
2737    }
2738
2739    #[test]
2740    fn rejects_unknown_special_identifiers() {
2741        for source in [
2742            "use $foo::bar\n",
2743            "begin\n    exec.$foo::bar\nend\n",
2744            "begin\n    exec.$execFoo::bar\nend\n",
2745            "begin\n    exec.$kernelFoo::bar\nend\n",
2746        ] {
2747            let parse = parse_text(source);
2748            assert!(parse.has_errors(), "expected {source:?} to reject unknown special ident");
2749            assert!(parse.diagnostics().iter().any(|diag| diag.labels.as_ref().is_some_and(
2750                |labels| {
2751                    labels.iter().any(|l| {
2752                        l.label().is_some_and(|label| label.contains("unrecognized token"))
2753                    })
2754                }
2755            )));
2756        }
2757    }
2758
2759    #[test]
2760    fn parse_source_file_tracks_source_aware_spans() {
2761        let source = Arc::new(ManagedSourceFile::new(
2762            SourceId::new(11),
2763            SourceLanguage::Masm,
2764            Uri::new("memory:///parser-span-test.masm"),
2765            "begin\n    nop\nend\n".to_string().into_boxed_str(),
2766        ));
2767
2768        let parse = parse_source_file(source.clone());
2769        assert!(!parse.has_errors(), "{:?}", parse.diagnostics());
2770        assert_eq!(parse.source_file().id(), source.id());
2771
2772        let nop = parse
2773            .syntax()
2774            .descendants_with_tokens()
2775            .filter_map(rowan::NodeOrToken::into_token)
2776            .find(|token| token.text() == "nop")
2777            .expect("nop token");
2778        let offset = source.as_str().find("nop").expect("nop offset");
2779        let expected = SourceSpan::try_from_range(source.id(), offset..offset + 3).unwrap();
2780        assert_eq!(parse.span_for_token(&nop), expected);
2781    }
2782
2783    #[test]
2784    fn diagnostics_keep_source_ids_from_managed_source_files() {
2785        let source = Arc::new(ManagedSourceFile::new(
2786            SourceId::new(12),
2787            SourceLanguage::Masm,
2788            Uri::new("memory:///parser-diagnostic-span-test.masm"),
2789            "proc foo\n    §\nend\n".to_string().into_boxed_str(),
2790        ));
2791
2792        let parse = parse_source_file(source.clone());
2793        assert!(parse.has_errors());
2794
2795        let diagnostic = parse
2796            .diagnostics()
2797            .iter()
2798            .find(|diag| {
2799                diag.labels.as_ref().is_some_and(|labels| {
2800                    labels.iter().any(|l| {
2801                        l.label().is_some_and(|label| label.contains("unrecognized token"))
2802                    })
2803                })
2804            })
2805            .expect("invalid-token diagnostic");
2806        let offset = source.as_str().find('§').expect("invalid token offset");
2807        let expected =
2808            SourceSpan::try_from_range(source.id(), offset..offset + '§'.len_utf8()).unwrap();
2809        let label_span = diagnostic.labels.as_deref().unwrap()[0].inner();
2810        let actual = SourceSpan::new(
2811            source.id(),
2812            (label_span.offset() as u32)..((label_span.offset() + label_span.len()) as u32),
2813        );
2814        assert_eq!(actual, expected);
2815    }
2816
2817    #[test]
2818    fn eof_diagnostics_anchor_to_the_last_character_offset() {
2819        let source = Arc::new(ManagedSourceFile::new(
2820            SourceId::new(13),
2821            SourceLanguage::Masm,
2822            Uri::new("memory:///parser-eof-span-test.masm"),
2823            "begin\n    if.true\n        add\n".to_string().into_boxed_str(),
2824        ));
2825
2826        let parse = parse_source_file(source.clone());
2827        assert!(parse.has_errors());
2828
2829        let diagnostic = parse
2830            .diagnostics()
2831            .iter()
2832            .find(|diag| {
2833                diag.labels
2834                    .as_deref()
2835                    .unwrap_or(&[])
2836                    .iter()
2837                    .filter_map(|label| label.label())
2838                    .any(|label| label.contains("expected `end`"))
2839            })
2840            .expect("missing-end diagnostic");
2841
2842        let last_char_offset = source
2843            .as_str()
2844            .char_indices()
2845            .last()
2846            .map(|(offset, _)| offset)
2847            .expect("source should be non-empty");
2848        let label_span = diagnostic.labels.as_deref().unwrap()[0].inner();
2849        assert_eq!(label_span.offset(), last_char_offset);
2850        assert_eq!(label_span.len(), 0);
2851    }
2852
2853    #[test]
2854    fn cst_import_spans_do_not_consume_trailing_newlines() {
2855        let source = "\
2856use lib::a
2857use {foo} from lib::b
2858begin end
2859";
2860        let parse = parse_text(source);
2861        let source_file = AstSourceFile::cast(parse.syntax()).expect("source file");
2862        let items = source_file.items().collect::<Vec<_>>();
2863        let Item::Import(module_import) = &items[0] else {
2864            panic!("expected first item to be an import");
2865        };
2866        let module_path = module_import.module_path().expect("module path");
2867        let start = source.find("lib::a").expect("path start") as u32;
2868        let end = start + "lib::a".len() as u32;
2869        let expected = SourceSpan::new(parse.source().id(), start..end);
2870        assert_eq!(parse.span_for_node(module_path.syntax()), expected);
2871
2872        let Item::Import(item_import) = &items[1] else {
2873            panic!("expected second item to be an import");
2874        };
2875        let item_path = item_import.module_path().expect("item import module path");
2876        let start = source.find("lib::b").expect("path start") as u32;
2877        let end = start + "lib::b".len() as u32;
2878        let expected = SourceSpan::new(parse.source().id(), start..end);
2879        assert_eq!(parse.span_for_node(item_path.syntax()), expected);
2880
2881        let spec = item_import.item_specs().next().expect("item specifier");
2882        let name = spec.name_token().expect("item name");
2883        let start = source.find("foo").expect("item start") as u32;
2884        let end = start + "foo".len() as u32;
2885        let expected = SourceSpan::new(parse.source().id(), start..end);
2886        assert_eq!(parse.span_for_token(&name), expected);
2887    }
2888}