use alloc::{borrow::ToOwned, string::String, sync::Arc, vec::Vec};
use miden_debug_types::{SourceFile, SourceId, SourceLanguage, SourceSpan, Uri};
use rowan::{GreenNodeBuilder, NodeOrToken, TextRange};
use crate::{
MasmLanguage,
ast::AstNode,
diagnostics::{LabeledSpan, Severity, diagnostic, miette::MietteDiagnostic as Diagnostic},
lexer::{Token, tokenize},
syntax::{SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken},
};
#[derive(Debug, Clone)]
pub struct Parse {
source: Arc<SourceFile>,
green_node: rowan::GreenNode,
diagnostics: Vec<Diagnostic>,
}
impl Parse {
pub fn syntax(&self) -> SyntaxNode {
SyntaxNode::new_root(self.green_node.clone())
}
pub fn root(&self) -> crate::ast::SourceFile {
crate::ast::SourceFile::cast(self.syntax())
.expect("parse root kind should always be SourceFile")
}
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
pub fn take_diagnostics(&mut self) -> Vec<Diagnostic> {
core::mem::take(&mut self.diagnostics)
}
pub fn source_file(&self) -> Arc<SourceFile> {
Arc::clone(&self.source)
}
pub fn source(&self) -> &SourceFile {
self.source.as_ref()
}
pub fn has_errors(&self) -> bool {
!self.diagnostics.is_empty()
}
pub fn span_for_ast_node<T>(&self, node: &T) -> SourceSpan
where
T: AstNode<Language = MasmLanguage>,
{
self.span_for_node(node.syntax())
}
pub fn span_for_node(&self, node: &SyntaxNode) -> SourceSpan {
self.span_for_range(node.text_range())
}
pub fn span_for_token(&self, token: &SyntaxToken) -> SourceSpan {
self.span_for_range(token.text_range())
}
pub fn span_for_element(&self, element: &SyntaxElement) -> SourceSpan {
match element {
NodeOrToken::Node(node) => self.span_for_node(node),
NodeOrToken::Token(token) => self.span_for_token(token),
}
}
pub fn span_for_range(&self, range: TextRange) -> SourceSpan {
source_span_from_text_range(self.source.id(), range)
}
}
pub fn parse_source_file(source: Arc<SourceFile>) -> Parse {
let parser_source = Arc::clone(&source);
Parser::new(parser_source.as_ref()).parse(source)
}
pub fn parse_text(input: &str) -> Parse {
parse_source_file(detached_source_file(input))
}
pub fn parse_inline_masm(source: Arc<SourceFile>, bounds: Option<SourceSpan>) -> Parse {
let parser_source = Arc::clone(&source);
if let Some(bounds) = bounds {
Parser::new_bounded(parser_source.as_ref(), bounds).parse_inline_masm(source)
} else {
Parser::new(parser_source.as_ref()).parse_inline_masm(source)
}
}
pub fn parse_inline_masm_text(input: &str, bounds: Option<core::ops::Range<usize>>) -> Parse {
let file = detached_source_file(input);
let bounds = bounds.map(|range| {
SourceSpan::try_from_range(file.id(), range).expect("invalid inline masm bounds")
});
parse_inline_masm(file, bounds)
}
struct Parser<'input> {
tokens: Vec<Token<'input>>,
pos: usize,
builder: GreenNodeBuilder<'static>,
diagnostics: Vec<Diagnostic>,
eof_span: SourceSpan,
}
#[derive(Default, Debug, Clone, Copy)]
struct Nesting {
parens: usize,
brackets: usize,
braces: usize,
}
impl Nesting {
fn is_root(self) -> bool {
self.parens == 0 && self.brackets == 0 && self.braces == 0
}
fn bump(self, kind: SyntaxKind) -> Result<Self, SyntaxKind> {
let mut next = self;
match kind {
SyntaxKind::LParen => next.parens += 1,
SyntaxKind::LBracket => next.brackets += 1,
SyntaxKind::LBrace => next.braces += 1,
SyntaxKind::RParen => {
next.parens = next.parens.checked_sub(1).ok_or(kind)?;
},
SyntaxKind::RBracket => {
next.brackets = next.brackets.checked_sub(1).ok_or(kind)?;
},
SyntaxKind::RBrace => {
next.braces = next.braces.checked_sub(1).ok_or(kind)?;
},
_ => (),
}
Ok(next)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BlockOwner {
Begin,
Procedure,
If,
While,
DoWhileBody,
DoWhile,
Repeat,
}
impl BlockOwner {
fn missing_end_message(self) -> &'static str {
match self {
Self::Begin => "expected `end` to close `begin` block",
Self::Procedure => "expected `end` to close procedure",
Self::If => "expected `end` to close `if`",
Self::While => "expected `end` to close `while`",
Self::DoWhileBody => "expected `while` to close `do` block",
Self::DoWhile => "expected `end` to close `do`..`while` loop",
Self::Repeat => "expected `end` to close `repeat`",
}
}
fn recovery_message(self, boundary: BlockRecoveryBoundary) -> String {
match boundary {
BlockRecoveryBoundary::Else => {
format!("{} before `else`", self.missing_end_message())
},
BlockRecoveryBoundary::TopLevelItem => {
format!("{} before top-level item", self.missing_end_message())
},
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BlockRecoveryBoundary {
Else,
TopLevelItem,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BlockParseOutcome {
FoundTerminator,
RecoveredImplicitEnd,
ReachedEof,
}
impl<'input> Parser<'input> {
fn new(source: &'input SourceFile) -> Self {
let eof_span = eof_anchor_span(source, None);
Self {
tokens: tokenize(source),
pos: 0,
builder: GreenNodeBuilder::new(),
diagnostics: Vec::new(),
eof_span,
}
}
fn new_bounded(source: &'input SourceFile, bounds: SourceSpan) -> Self {
assert_eq!(source.id(), bounds.source_id());
let eof_span = eof_anchor_span(source, Some(bounds.into_slice_index()));
Self {
tokens: tokenize(source),
pos: 0,
builder: GreenNodeBuilder::new(),
diagnostics: Vec::new(),
eof_span,
}
}
fn parse(mut self, source: Arc<SourceFile>) -> Parse {
self.start_node(SyntaxKind::SourceFile);
while !self.eof() {
self.parse_source_item();
}
self.finish_node();
Parse {
source,
green_node: self.builder.finish(),
diagnostics: self.diagnostics,
}
}
fn parse_inline_masm(mut self, source: Arc<SourceFile>) -> Parse {
match self.parse_block_unterminated() {
BlockParseOutcome::ReachedEof => (),
BlockParseOutcome::FoundTerminator => self.error_here("unexpected 'end'"),
BlockParseOutcome::RecoveredImplicitEnd => {
self.error_here("unclosed nested block: expected 'end' but reached eof")
},
}
Parse {
source,
green_node: self.builder.finish(),
diagnostics: self.diagnostics,
}
}
fn parse_source_item(&mut self) {
if self.at_kind(SyntaxKind::DocComment) {
self.parse_doc_form();
return;
}
if self.at_regular_trivia() {
self.bump();
return;
}
if self.at_keyword("namespace") {
self.parse_namespace();
return;
}
if self.at_prefixed_keyword("extern", "package") {
self.parse_extern_package();
return;
}
if self.at_keyword("mod") || self.at_prefixed_keyword("pub", "mod") {
self.parse_submodule();
return;
}
if self.at_kind(SyntaxKind::At)
|| self.at_keyword("proc")
|| self.at_prefixed_keyword("pub", "proc")
{
self.parse_procedure();
return;
}
if self.at_keyword("begin") {
self.parse_begin_block();
return;
}
if self.at_keyword("use") || self.at_prefixed_keyword("pub", "use") {
self.parse_import();
return;
}
if self.at_keyword("const") || self.at_prefixed_keyword("pub", "const") {
self.parse_constant();
return;
}
if self.at_keyword("type")
|| self.at_keyword("enum")
|| self.at_prefixed_keyword("pub", "type")
|| self.at_prefixed_keyword("pub", "enum")
{
self.parse_type_decl();
return;
}
if self.at_keyword("adv_map") {
self.parse_advice_map();
return;
}
self.start_node(SyntaxKind::Error);
self.error_here("unexpected top-level token");
self.bump();
self.finish_node();
}
fn parse_doc_form(&mut self) {
self.start_node(SyntaxKind::Doc);
self.bump();
self.finish_node();
}
fn parse_namespace(&mut self) {
self.start_node(SyntaxKind::Namespace);
self.expect_keyword("namespace", "expected `namespace` in namespace declaration");
self.bump_regular_trivia();
self.parse_path_with_message("expected a namespace path");
self.parse_line_tail();
self.finish_node();
}
fn parse_extern_package(&mut self) {
self.start_node(SyntaxKind::ExternPackage);
self.expect_keyword("extern", "expected `extern` in extern package declaration");
self.expect_keyword("package", "expected `package` in extern package declaration");
self.bump_regular_trivia();
if self.at_package_name_like() {
self.bump();
} else {
self.error_here("expected a package name");
}
self.parse_line_tail();
self.finish_node();
}
fn parse_submodule(&mut self) {
self.start_node(SyntaxKind::Submodule);
if self.at_keyword("pub") {
self.parse_visibility();
}
self.expect_keyword("mod", "expected `mod` in submodule declaration");
self.bump_regular_trivia();
if self.at_name_like() {
self.bump();
} else {
self.error_here("expected a submodule name");
}
self.parse_line_tail();
self.finish_node();
}
fn parse_import(&mut self) {
self.start_node(SyntaxKind::Import);
let is_public = if self.at_keyword("pub") {
self.parse_visibility();
true
} else {
false
};
self.expect_keyword("use", "expected `use` in import declaration");
self.bump_inline_whitespace();
if self.at_kind(SyntaxKind::LBrace) {
self.parse_import_list();
self.parse_item_import_module_path();
self.parse_rejected_old_import_alias();
} else if self.at_kind(SyntaxKind::Star) {
self.parse_rejected_wildcard_import();
} else if self.at_kind(SyntaxKind::Number) {
self.parse_rejected_digest_import_target();
} else {
self.parse_module_import(is_public);
}
self.parse_line_tail();
self.finish_node();
}
fn parse_module_import(&mut self, is_public: bool) {
let path_start = self.current().map(Token::span).unwrap_or(self.eof_span);
if self.at_kind(SyntaxKind::Newline) || self.eof() {
self.error_here("expected an import path");
return;
}
self.parse_path_with_message("expected an import path");
if is_public {
self.error_at_span(path_start, "`pub use` is only supported for braced item imports");
}
self.parse_optional_module_alias();
self.parse_rejected_old_import_alias();
}
fn parse_optional_module_alias(&mut self) {
if !self.peek_contextual_keyword_after_non_comment_trivia("as") {
return;
}
self.bump_non_comment_trivia();
self.bump();
self.bump_inline_whitespace();
if self.at_name_like() {
self.bump();
} else {
self.error_here("expected an alias name after `as`");
}
}
fn parse_import_list(&mut self) {
self.start_node(SyntaxKind::ImportList);
let _ = self.expect_kind(SyntaxKind::LBrace, "expected `{` to start import list");
let mut saw_specifier = false;
loop {
self.bump_regular_trivia();
if self.eof() {
self.error_at_eof("expected `}` to close import list");
break;
}
if self.at_kind(SyntaxKind::RBrace) {
if !saw_specifier {
self.error_here("import lists must contain at least one item");
}
self.bump();
break;
}
self.parse_import_specifier();
saw_specifier = true;
self.bump_regular_trivia();
if self.at_kind(SyntaxKind::Comma) {
self.bump();
continue;
}
if self.at_kind(SyntaxKind::RBrace) {
self.bump();
break;
}
self.error_here("expected `,` or `}` in import list");
if !self.at_import_list_recovery_boundary() {
self.bump();
}
}
self.finish_node();
}
fn parse_import_specifier(&mut self) {
self.start_node(SyntaxKind::ImportSpecifier);
self.bump_regular_trivia();
if self.at_kind(SyntaxKind::Star) {
self.error_here("wildcard imports are not supported");
self.bump();
self.finish_node();
return;
}
if self.at_name_like() {
self.bump();
} else {
self.error_here("expected an imported item name");
if !self.at_import_list_recovery_boundary() {
self.bump();
}
self.finish_node();
return;
}
if self.peek_contextual_keyword_after_non_comment_trivia("as") {
self.bump_non_comment_trivia();
self.bump();
self.bump_inline_whitespace();
if self.at_name_like() {
self.bump();
} else {
self.error_here("expected an alias name after `as`");
}
}
self.parse_rejected_old_import_alias();
self.finish_node();
}
fn parse_item_import_module_path(&mut self) {
if !self.peek_contextual_keyword_after_non_comment_trivia("from") {
self.error_here("expected `from` after import list");
return;
}
self.bump_non_comment_trivia();
self.bump();
self.bump_inline_whitespace();
if self.at_kind(SyntaxKind::Newline) || self.eof() {
self.error_here("expected a module path after `from`");
return;
}
if self.at_kind(SyntaxKind::Number) {
self.parse_rejected_digest_import_target();
} else {
self.parse_path_with_message("expected a module path after `from`");
}
}
fn parse_rejected_old_import_alias(&mut self) {
if self.peek_after_non_comment_trivia() != Some(SyntaxKind::RArrow) {
return;
}
self.start_node(SyntaxKind::Error);
self.bump_non_comment_trivia();
self.error_here("import aliases use `as`; `->` is no longer supported");
self.bump();
self.bump_non_comment_trivia();
if self.at_name_like() {
self.bump();
} else {
self.error_here("expected an alias name after `->`");
}
self.finish_node();
}
fn parse_rejected_digest_import_target(&mut self) {
self.start_node(SyntaxKind::Error);
self.error_here("digest imports are not supported in source `use` declarations");
self.bump();
self.parse_optional_module_alias();
self.parse_rejected_old_import_alias();
self.finish_node();
}
fn parse_rejected_wildcard_import(&mut self) {
self.start_node(SyntaxKind::Error);
self.error_here("wildcard imports are not supported");
self.bump();
if self.peek_contextual_keyword_after_non_comment_trivia("from") {
self.bump_non_comment_trivia();
self.bump();
self.bump_regular_trivia();
self.parse_path_with_message("expected a module path after `from`");
}
self.finish_node();
}
fn parse_constant(&mut self) {
self.start_node(SyntaxKind::Constant);
if self.at_keyword("pub") {
self.parse_visibility();
}
self.expect_keyword("const", "expected `const` in constant declaration");
self.bump_regular_trivia();
if self.at_name_like() {
self.bump();
} else {
self.error_here("expected a constant name");
}
self.expect_kind(SyntaxKind::Equal, "expected `=` in constant declaration");
self.parse_expr_until_line_end();
self.parse_line_tail();
self.finish_node();
}
fn parse_type_decl(&mut self) {
self.start_node(SyntaxKind::TypeDecl);
if self.at_keyword("pub") {
self.parse_visibility();
}
self.bump_regular_trivia();
if self.at_keyword("type") || self.at_keyword("enum") {
self.bump();
} else {
self.error_here("expected `type` or `enum`");
self.finish_node();
return;
}
self.bump_regular_trivia();
if self.at_name_like() {
self.bump();
} else {
self.error_here("expected a type name");
}
self.bump_regular_trivia();
if self.at_kind(SyntaxKind::Equal) || self.at_kind(SyntaxKind::Colon) {
self.parse_type_body();
} else {
self.error_here("expected `=` or `:` in type declaration");
}
self.finish_node();
}
fn parse_advice_map(&mut self) {
self.start_node(SyntaxKind::AdviceMap);
self.expect_keyword("adv_map", "expected `adv_map`");
self.bump_regular_trivia();
if self.at_name_like() {
self.bump();
} else {
self.error_here("expected an advice-map name");
}
self.bump_inline_whitespace();
if self.at_kind(SyntaxKind::LParen) {
self.parse_balanced_group(
SyntaxKind::LParen,
SyntaxKind::RParen,
"expected `)` to close advice-map key",
);
}
self.expect_kind(SyntaxKind::Equal, "expected `=` in advice-map declaration");
self.parse_expr_until_line_end();
self.parse_line_tail();
self.finish_node();
}
fn parse_path_with_message(&mut self, message: &'static str) {
self.start_node(SyntaxKind::Path);
if self.at_kind(SyntaxKind::ColonColon) {
self.bump();
}
self.bump_non_comment_trivia();
if self.at_name_like() {
self.bump();
} else {
self.error_here(message);
self.finish_node();
return;
}
loop {
if self.peek_after_non_comment_trivia() != Some(SyntaxKind::ColonColon) {
break;
}
self.bump_non_comment_trivia();
self.bump();
self.bump_non_comment_trivia();
if self.at_name_like() {
self.bump();
} else {
self.error_here("expected a path segment after `::`");
break;
}
}
self.finish_node();
}
fn parse_expr_until_line_end(&mut self) {
self.bump_regular_trivia();
self.start_node(SyntaxKind::Expr);
let mut nesting = Nesting::default();
let mut saw_significant = false;
while !self.eof() {
let kind = self.current_kind().expect("not eof");
if nesting.is_root() && matches!(kind, SyntaxKind::Comment | SyntaxKind::DocComment) {
break;
}
if nesting.is_root() && kind == SyntaxKind::Newline {
break;
}
if nesting.is_root()
&& saw_significant
&& kind == SyntaxKind::Whitespace
&& self
.next_relevant_top_level_token(self.pos + 1)
.is_some_and(|index| self.is_top_level_starter(index))
{
break;
}
saw_significant |= !kind.is_trivia();
self.bump_nesting(&mut nesting, kind);
self.bump();
}
self.finish_node();
}
fn parse_type_body(&mut self) {
self.start_node(SyntaxKind::TypeBody);
let mut nesting = Nesting::default();
while !self.eof() {
if self.at_kind(SyntaxKind::Newline)
&& nesting.is_root()
&& self.line_break_starts_new_top_level_item()
{
break;
}
let current = self.current_kind().expect("not eof");
self.bump_nesting(&mut nesting, current);
self.bump();
}
self.finish_node();
}
fn parse_line_tail(&mut self) {
loop {
match self.current_kind() {
Some(SyntaxKind::Whitespace) => self.bump(),
Some(SyntaxKind::Comment) => {
self.bump();
break;
},
_ => break,
}
}
}
fn parse_begin_block(&mut self) {
self.start_node(SyntaxKind::BeginBlock);
self.expect_keyword("begin", "expected `begin`");
self.parse_line_tail();
if self.parse_block(BlockOwner::Begin, &["end"]) == BlockParseOutcome::FoundTerminator {
self.expect_keyword("end", BlockOwner::Begin.missing_end_message());
}
self.finish_node();
}
fn parse_procedure(&mut self) {
self.start_node(SyntaxKind::Procedure);
loop {
self.bump_regular_trivia();
if !self.at_kind(SyntaxKind::At) {
break;
}
self.parse_attribute();
}
self.bump_regular_trivia();
if self.at_keyword("pub") {
self.parse_visibility();
}
self.bump_regular_trivia();
if !self.expect_keyword("proc", "expected `proc` in procedure declaration") {
self.finish_node();
return;
}
self.bump_regular_trivia();
if self.at_name_like() {
self.bump();
} else {
self.error_here("expected a procedure name");
}
self.bump_regular_trivia();
if self.at_kind(SyntaxKind::LParen) {
self.parse_signature();
}
self.parse_line_tail();
if self.parse_block(BlockOwner::Procedure, &["end"]) == BlockParseOutcome::FoundTerminator {
self.expect_keyword("end", BlockOwner::Procedure.missing_end_message());
}
self.finish_node();
}
fn parse_attribute(&mut self) {
self.start_node(SyntaxKind::Attribute);
let _ = self.expect_kind(SyntaxKind::At, "expected `@`");
self.bump_regular_trivia();
if self.at_name_like() {
self.bump();
} else {
self.error_here("expected an attribute name");
}
self.bump_inline_whitespace();
if self.at_kind(SyntaxKind::LParen) {
self.parse_balanced_group(
SyntaxKind::LParen,
SyntaxKind::RParen,
"expected `)` to close attribute arguments",
);
}
self.finish_node();
}
fn parse_visibility(&mut self) {
self.start_node(SyntaxKind::Visibility);
let _ = self.expect_keyword("pub", "expected `pub`");
self.finish_node();
}
fn parse_signature(&mut self) {
self.start_node(SyntaxKind::Signature);
self.parse_balanced_group(
SyntaxKind::LParen,
SyntaxKind::RParen,
"expected `)` to close procedure parameters",
);
if self.peek_after_non_comment_trivia() == Some(SyntaxKind::RArrow) {
self.bump_non_comment_trivia();
self.bump();
self.bump_non_comment_trivia();
if self.at_kind(SyntaxKind::LParen) {
self.parse_balanced_group(
SyntaxKind::LParen,
SyntaxKind::RParen,
"expected `)` to close procedure results",
);
} else {
self.parse_signature_result_until_line_end();
}
}
self.finish_node();
}
fn parse_signature_result_until_line_end(&mut self) {
let start = self.pos;
let mut nesting = Nesting::default();
while !self.eof() {
let kind = self.current_kind().expect("not eof");
if nesting.is_root() && matches!(kind, SyntaxKind::Comment | SyntaxKind::DocComment) {
break;
}
if nesting.is_root() && kind == SyntaxKind::Newline {
break;
}
self.bump_nesting(&mut nesting, kind);
self.bump();
}
if self.pos == start {
self.error_here("expected a result type after `->` in procedure signature");
}
}
fn parse_block_unterminated(&mut self) -> BlockParseOutcome {
self.start_node(SyntaxKind::Block);
while !self.eof() {
if self.at_regular_trivia() {
self.bump();
continue;
}
if self.at_kind(SyntaxKind::DocComment) {
self.start_node(SyntaxKind::Error);
self.error_here("doc comments are not allowed in inline MASM blocks");
self.bump();
self.finish_node();
continue;
}
if self.at_keyword("if") {
if self.parse_if() {
self.finish_node();
return BlockParseOutcome::ReachedEof;
}
} else if self.at_keyword("do") {
if self.parse_do_while() {
self.finish_node();
return BlockParseOutcome::ReachedEof;
}
} else if self.at_keyword("while") {
if self.parse_while() {
self.finish_node();
return BlockParseOutcome::ReachedEof;
}
} else if self.at_keyword("repeat") {
if self.parse_repeat() {
self.finish_node();
return BlockParseOutcome::ReachedEof;
}
} else if self.can_start_instruction() {
self.parse_instruction();
} else {
self.start_node(SyntaxKind::Error);
self.error_here("unexpected token in block");
self.bump();
self.finish_node();
}
}
self.finish_node();
BlockParseOutcome::ReachedEof
}
fn parse_block(&mut self, owner: BlockOwner, terminators: &[&str]) -> BlockParseOutcome {
self.start_node(SyntaxKind::Block);
while !self.eof() {
if self.at_terminator(terminators) {
self.finish_node();
return BlockParseOutcome::FoundTerminator;
}
if self.at_regular_trivia() {
self.bump();
continue;
}
if let Some(boundary) = self.block_recovery_boundary(terminators) {
self.start_node(SyntaxKind::Error);
self.error_here(owner.recovery_message(boundary));
self.finish_node();
self.finish_node();
return BlockParseOutcome::RecoveredImplicitEnd;
}
if self.at_kind(SyntaxKind::DocComment) {
self.start_node(SyntaxKind::Error);
self.error_here("doc comments are only allowed before module-level items");
self.bump();
self.finish_node();
continue;
}
if self.at_keyword("if") {
if self.parse_if() {
self.finish_node();
return BlockParseOutcome::ReachedEof;
}
} else if self.at_keyword("do") {
if self.parse_do_while() {
self.finish_node();
return BlockParseOutcome::ReachedEof;
}
} else if self.at_keyword("while") {
if self.parse_while() {
self.finish_node();
return BlockParseOutcome::ReachedEof;
}
} else if self.at_keyword("repeat") {
if self.parse_repeat() {
self.finish_node();
return BlockParseOutcome::ReachedEof;
}
} else if self.can_start_instruction() {
self.parse_instruction();
} else {
self.start_node(SyntaxKind::Error);
self.error_here("unexpected token in block");
self.bump();
self.finish_node();
}
}
self.error_at_eof(owner.missing_end_message());
self.finish_node();
BlockParseOutcome::ReachedEof
}
fn parse_if(&mut self) -> bool {
self.start_node(SyntaxKind::IfOp);
self.expect_keyword("if", "expected `if`");
self.parse_structured_header_suffixes();
self.parse_line_tail();
let then_outcome = self.parse_block(BlockOwner::If, &["else", "end"]);
if then_outcome == BlockParseOutcome::ReachedEof {
self.finish_node();
return true;
}
let mut needs_end = then_outcome == BlockParseOutcome::FoundTerminator;
if self.at_keyword("else") {
self.bump();
self.parse_line_tail();
let else_outcome = self.parse_block(BlockOwner::If, &["end"]);
if else_outcome == BlockParseOutcome::ReachedEof {
self.finish_node();
return true;
}
needs_end = else_outcome == BlockParseOutcome::FoundTerminator;
}
if needs_end {
self.expect_keyword("end", BlockOwner::If.missing_end_message());
}
self.finish_node();
false
}
fn parse_while(&mut self) -> bool {
self.start_node(SyntaxKind::WhileOp);
self.expect_keyword("while", "expected `while`");
self.parse_structured_header_suffixes();
self.parse_line_tail();
let outcome = self.parse_block(BlockOwner::While, &["end"]);
if outcome == BlockParseOutcome::ReachedEof {
self.finish_node();
return true;
}
if outcome == BlockParseOutcome::FoundTerminator {
self.expect_keyword("end", BlockOwner::While.missing_end_message());
}
self.finish_node();
false
}
fn parse_do_while(&mut self) -> bool {
self.start_node(SyntaxKind::DoWhileOp);
self.expect_keyword("do", "expected `do`");
self.parse_line_tail();
let body_outcome = self.parse_block(BlockOwner::DoWhileBody, &["while"]);
if body_outcome == BlockParseOutcome::ReachedEof {
self.finish_node();
return true;
}
if body_outcome == BlockParseOutcome::FoundTerminator {
self.expect_keyword("while", "expected `while`");
self.parse_line_tail();
let cond_outcome = self.parse_block(BlockOwner::DoWhile, &["end"]);
if cond_outcome == BlockParseOutcome::ReachedEof {
self.finish_node();
return true;
}
if cond_outcome == BlockParseOutcome::FoundTerminator {
self.expect_keyword("end", BlockOwner::DoWhile.missing_end_message());
}
}
self.finish_node();
false
}
fn parse_repeat(&mut self) -> bool {
self.start_node(SyntaxKind::RepeatOp);
self.expect_keyword("repeat", "expected `repeat`");
self.parse_structured_header_suffixes();
self.parse_line_tail();
let outcome = self.parse_block(BlockOwner::Repeat, &["end"]);
if outcome == BlockParseOutcome::ReachedEof {
self.finish_node();
return true;
}
if outcome == BlockParseOutcome::FoundTerminator {
self.expect_keyword("end", BlockOwner::Repeat.missing_end_message());
}
self.finish_node();
false
}
fn parse_structured_header_suffixes(&mut self) {
loop {
self.bump_inline_whitespace();
if !self.at_kind(SyntaxKind::Dot) {
break;
}
self.bump();
self.bump_inline_whitespace();
if self.at_kind(SyntaxKind::LBracket) {
self.parse_balanced_group(
SyntaxKind::LBracket,
SyntaxKind::RBracket,
"expected `]` to close structured operation suffix",
);
} else if self.at_name_like()
|| self.at_kind(SyntaxKind::Number)
|| self.at_kind(SyntaxKind::QuotedString)
{
self.bump();
} else {
self.error_here("expected a structured operation suffix");
break;
}
}
}
fn parse_instruction(&mut self) {
self.start_node(SyntaxKind::Instruction);
let mut nesting = Nesting::default();
let mut previous_significant = None;
while !self.eof() {
let kind = self.current_kind().expect("not eof");
if kind == SyntaxKind::Whitespace {
if self.should_continue_instruction_after_whitespace(previous_significant, nesting)
{
self.bump();
continue;
}
break;
}
if matches!(kind, SyntaxKind::Newline | SyntaxKind::Comment | SyntaxKind::DocComment) {
if nesting.is_root() {
break;
}
self.bump();
continue;
}
if previous_significant.is_some()
&& self.should_stop_instruction_before(kind, previous_significant, nesting)
{
break;
}
previous_significant = Some(kind);
self.bump_nesting(&mut nesting, kind);
self.bump();
}
self.finish_node();
}
fn parse_balanced_group(
&mut self,
open: SyntaxKind,
close: SyntaxKind,
missing_message: &'static str,
) {
self.bump_regular_trivia();
if !self.expect_kind(open, missing_message) {
return;
}
let mut depth = 1usize;
while !self.eof() {
let kind = self.current_kind().expect("not eof");
if kind == open {
depth += 1;
} else if kind == close {
depth -= 1;
}
self.bump();
if depth == 0 {
return;
}
}
self.error_at_eof(missing_message);
}
fn should_continue_instruction_after_whitespace(
&self,
previous_significant: Option<SyntaxKind>,
nesting: Nesting,
) -> bool {
if !nesting.is_root() {
return true;
}
let Some(previous_significant) = previous_significant else {
return false;
};
if expects_continuation_operand(previous_significant) {
return true;
}
matches!(
self.peek_after_inline_whitespace(),
Some(
SyntaxKind::Dot
| SyntaxKind::Equal
| SyntaxKind::Comma
| SyntaxKind::DotDot
| SyntaxKind::Colon
| SyntaxKind::ColonColon
| SyntaxKind::RArrow
| SyntaxKind::Plus
| SyntaxKind::Minus
| SyntaxKind::Star
| SyntaxKind::Slash
| SyntaxKind::SlashSlash
| SyntaxKind::RBracket
| SyntaxKind::RParen
| SyntaxKind::RBrace
)
)
}
fn should_stop_instruction_before(
&self,
current: SyntaxKind,
previous_significant: Option<SyntaxKind>,
nesting: Nesting,
) -> bool {
if !nesting.is_root() {
return false;
}
if let Some(previous_significant) = previous_significant
&& expects_continuation_operand(previous_significant)
{
return false;
}
if punctuation_continues_instruction(current) {
return false;
}
self.at_terminator(&["else", "end"])
|| self.can_start_operation()
|| self.at_block_recovery_boundary()
}
fn line_break_starts_new_top_level_item(&self) -> bool {
match self.next_relevant_top_level_token(self.pos + 1) {
Some(index) => self.is_top_level_starter(index),
None => true,
}
}
fn next_relevant_top_level_token(&self, mut index: usize) -> Option<usize> {
while let Some(token) = self.tokens.get(index) {
match token.kind() {
SyntaxKind::Whitespace | SyntaxKind::Newline | SyntaxKind::Comment => index += 1,
_ => return Some(index),
}
}
None
}
fn next_relevant_block_token(&self, mut index: usize) -> Option<usize> {
while let Some(token) = self.tokens.get(index) {
match token.kind() {
SyntaxKind::Whitespace
| SyntaxKind::Newline
| SyntaxKind::Comment
| SyntaxKind::DocComment => index += 1,
_ => return Some(index),
}
}
None
}
fn peek_after_inline_whitespace(&self) -> Option<SyntaxKind> {
let mut index = self.pos;
while let Some(token) = self.tokens.get(index) {
match token.kind() {
SyntaxKind::Whitespace => index += 1,
SyntaxKind::Newline | SyntaxKind::Comment | SyntaxKind::DocComment => return None,
kind => return Some(kind),
}
}
None
}
fn peek_after_non_comment_trivia(&self) -> Option<SyntaxKind> {
self.peek_token_after_non_comment_trivia().map(Token::kind)
}
fn peek_token_after_non_comment_trivia(&self) -> Option<&Token<'input>> {
let mut index = self.pos;
while let Some(token) = self.tokens.get(index) {
match token.kind() {
SyntaxKind::Whitespace | SyntaxKind::Newline => index += 1,
_ => return Some(token),
}
}
None
}
fn peek_contextual_keyword_after_non_comment_trivia(&self, keyword: &str) -> bool {
matches!(
self.peek_token_after_non_comment_trivia(),
Some(token) if token.kind() == SyntaxKind::Ident && token.text() == keyword
)
}
fn is_top_level_starter(&self, index: usize) -> bool {
let Some(token) = self.tokens.get(index) else {
return false;
};
token.kind() == SyntaxKind::DocComment
|| token.kind() == SyntaxKind::At
|| (token.kind() == SyntaxKind::Ident
&& match token.text() {
"adv_map" | "begin" | "const" | "enum" | "mod" | "namespace" | "proc"
| "type" | "use" => true,
"extern" => matches!(
self.next_relevant_top_level_token(index + 1)
.and_then(|next| self.tokens.get(next)),
Some(next) if next.kind() == SyntaxKind::Ident && next.text() == "package"
),
"pub" => matches!(
self.next_relevant_top_level_token(index + 1)
.and_then(|next| self.tokens.get(next)),
Some(next)
if next.kind() == SyntaxKind::Ident
&& matches!(next.text(), "const" | "enum" | "mod" | "proc" | "type" | "use")
),
_ => false,
})
}
fn can_start_operation(&self) -> bool {
self.can_start_instruction()
|| self.at_keyword("if")
|| self.at_keyword("do")
|| self.at_keyword("while")
|| self.at_keyword("repeat")
}
fn can_start_instruction(&self) -> bool {
matches!(
self.current(),
Some(token)
if matches!(
token.kind(),
SyntaxKind::Ident | SyntaxKind::SpecialIdent | SyntaxKind::QuotedIdent
) && (token.kind() != SyntaxKind::Ident
|| !is_reserved_block_keyword(token.text()))
)
}
fn block_recovery_boundary(&self, terminators: &[&str]) -> Option<BlockRecoveryBoundary> {
if self.at_keyword("else") && !terminators.contains(&"else") {
return Some(BlockRecoveryBoundary::Else);
}
if self.at_top_level_form_starter_in_block() {
return Some(BlockRecoveryBoundary::TopLevelItem);
}
None
}
fn at_block_recovery_boundary(&self) -> bool {
self.at_keyword("else") || self.at_top_level_form_starter_in_block()
}
fn at_top_level_form_starter(&self) -> bool {
self.is_top_level_starter(self.pos)
}
fn at_top_level_form_starter_in_block(&self) -> bool {
match self.current() {
Some(token) if token.kind() == SyntaxKind::DocComment => self
.next_relevant_block_token(self.pos + 1)
.is_some_and(|index| self.is_top_level_starter(index)),
_ => self.at_top_level_form_starter(),
}
}
fn at_terminator(&self, terminators: &[&str]) -> bool {
terminators.iter().any(|terminator| {
if *terminator == "while" {
self.at_bare_while()
} else {
self.at_keyword(terminator)
}
})
}
fn at_bare_while(&self) -> bool {
self.at_keyword("while")
&& !matches!(
self.tokens.get(self.pos + 1),
Some(token) if token.kind() == SyntaxKind::Dot
)
}
fn at_name_like(&self) -> bool {
matches!(
self.current_kind(),
Some(SyntaxKind::Ident | SyntaxKind::SpecialIdent | SyntaxKind::QuotedIdent)
)
}
fn at_package_name_like(&self) -> bool {
self.at_name_like() || self.at_kind(SyntaxKind::QuotedString)
}
fn at_import_list_recovery_boundary(&self) -> bool {
self.eof() || matches!(self.current_kind(), Some(SyntaxKind::Comma | SyntaxKind::RBrace))
}
fn at_keyword(&self, keyword: &str) -> bool {
matches!(self.current(), Some(token) if token.kind() == SyntaxKind::Ident && token.text() == keyword)
}
fn at_prefixed_keyword(&self, prefix: &str, keyword: &str) -> bool {
if !self.at_keyword(prefix) {
return false;
}
matches!(
self.next_relevant_top_level_token(self.pos + 1).and_then(|index| self.tokens.get(index)),
Some(token) if token.kind() == SyntaxKind::Ident && token.text() == keyword
)
}
fn at_regular_trivia(&self) -> bool {
matches!(
self.current_kind(),
Some(SyntaxKind::Whitespace | SyntaxKind::Newline | SyntaxKind::Comment)
)
}
fn at_kind(&self, kind: SyntaxKind) -> bool {
self.current_kind() == Some(kind)
}
fn current(&self) -> Option<&Token<'input>> {
self.tokens.get(self.pos)
}
fn current_kind(&self) -> Option<SyntaxKind> {
self.current().map(Token::kind)
}
fn eof(&self) -> bool {
self.pos >= self.tokens.len()
}
fn bump_regular_trivia(&mut self) {
while self.at_regular_trivia() {
self.bump();
}
}
fn bump_non_comment_trivia(&mut self) {
while matches!(self.current_kind(), Some(SyntaxKind::Whitespace | SyntaxKind::Newline)) {
self.bump();
}
}
fn bump_inline_whitespace(&mut self) {
while self.at_kind(SyntaxKind::Whitespace) {
self.bump();
}
}
fn expect_keyword(&mut self, keyword: &str, message: &'static str) -> bool {
self.bump_regular_trivia();
if self.at_keyword(keyword) {
self.bump();
true
} else {
self.error_here(message);
false
}
}
fn expect_kind(&mut self, kind: SyntaxKind, message: &'static str) -> bool {
self.bump_regular_trivia();
if self.at_kind(kind) {
self.bump();
true
} else {
self.error_here(message);
false
}
}
fn start_node(&mut self, kind: SyntaxKind) {
self.builder.start_node(kind.into());
}
fn finish_node(&mut self) {
self.builder.finish_node();
}
fn bump(&mut self) {
if let Some(token) = self.current() {
let kind = token.kind();
let span = token.span();
let text = token.text();
if kind == SyntaxKind::Error {
self.diagnostics.push(diagnostic!(
severity = Severity::Error,
labels = vec![LabeledSpan::at(span, format!("unrecognized token `{text}`"))],
"syntax error"
));
}
self.builder.token(kind.into(), text);
self.pos += 1;
}
}
fn bump_nesting(&mut self, nesting: &mut Nesting, kind: SyntaxKind) {
match nesting.bump(kind) {
Ok(next) => *nesting = next,
Err(closing) => {
self.error_here(format!(
"unexpected closing delimiter `{}`",
closing_delimiter_text(closing)
));
},
}
}
fn error_here(&mut self, message: impl Into<String>) {
let span = self.current().map(Token::span).unwrap_or(self.eof_span);
self.error_at_span(span, message);
}
fn error_at_span(&mut self, span: SourceSpan, message: impl Into<String>) {
self.diagnostics.push(diagnostic!(
severity = Severity::Error,
labels = vec![LabeledSpan::at(span, message.into())],
"syntax error"
));
}
fn error_at_eof(&mut self, message: impl Into<String>) {
self.diagnostics.push(diagnostic!(
severity = Severity::Error,
labels = vec![LabeledSpan::at(self.eof_span, message.into())],
"syntax error"
));
}
}
fn detached_source_file(input: &str) -> Arc<SourceFile> {
Arc::new(SourceFile::new(
SourceId::UNKNOWN,
SourceLanguage::Masm,
Uri::new("memory:///inline.masm"),
input.to_owned().into_boxed_str(),
))
}
fn eof_anchor_span(source: &SourceFile, bounds: Option<core::ops::Range<usize>>) -> SourceSpan {
let content = source.as_str();
let (content, start) = match bounds {
Some(range) => (&content[range.start..range.end], range.start),
None => (content, 0),
};
content
.char_indices()
.last()
.map(|(offset, _)| {
SourceSpan::at(
source.id(),
u32::try_from(offset).expect("source files larger than 4GiB are not supported"),
)
})
.unwrap_or_else(|| {
SourceSpan::try_from_range(source.id(), start..start)
.expect("source files larger than 4GiB are not supported")
})
}
fn source_span_from_text_range(source_id: SourceId, range: TextRange) -> SourceSpan {
SourceSpan::new(source_id, u32::from(range.start())..u32::from(range.end()))
}
fn closing_delimiter_text(kind: SyntaxKind) -> &'static str {
match kind {
SyntaxKind::RParen => ")",
SyntaxKind::RBracket => "]",
SyntaxKind::RBrace => "}",
_ => "delimiter",
}
}
fn expects_continuation_operand(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::Dot
| SyntaxKind::Equal
| SyntaxKind::Comma
| SyntaxKind::DotDot
| SyntaxKind::Colon
| SyntaxKind::ColonColon
| SyntaxKind::RArrow
| SyntaxKind::Plus
| SyntaxKind::Minus
| SyntaxKind::Star
| SyntaxKind::Slash
| SyntaxKind::SlashSlash
| SyntaxKind::LBracket
| SyntaxKind::LParen
| SyntaxKind::LBrace
)
}
fn punctuation_continues_instruction(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::Dot
| SyntaxKind::Equal
| SyntaxKind::Comma
| SyntaxKind::DotDot
| SyntaxKind::Colon
| SyntaxKind::ColonColon
| SyntaxKind::RArrow
| SyntaxKind::Plus
| SyntaxKind::Minus
| SyntaxKind::Star
| SyntaxKind::Slash
| SyntaxKind::SlashSlash
| SyntaxKind::RBracket
| SyntaxKind::RParen
| SyntaxKind::RBrace
)
}
fn is_reserved_block_keyword(text: &str) -> bool {
matches!(
text,
"adv_map"
| "begin"
| "const"
| "do"
| "else"
| "end"
| "enum"
| "extern"
| "if"
| "mod"
| "namespace"
| "proc"
| "pub"
| "repeat"
| "type"
| "use"
| "while"
)
}
#[cfg(test)]
mod tests {
use std::{
fs,
path::{Path, PathBuf},
string::{String, ToString},
sync::Arc,
vec::Vec,
};
use miden_debug_types::{
SourceFile as ManagedSourceFile, SourceId, SourceLanguage, SourceSpan, Uri,
};
use rowan::ast::AstNode;
use crate::{
ast::{ImportKind, Item, SourceFile as AstSourceFile},
parse_source_file, parse_text,
parser::parse_inline_masm_text,
syntax::SyntaxKind,
};
fn repo_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(Path::parent)
.expect("workspace root should be two levels above crates/assembly-syntax-cst")
.to_path_buf()
}
fn checked_in_masm_corpus() -> Vec<PathBuf> {
let root = repo_root();
let mut files = Vec::new();
for relative in [
"crates/lib/core/asm",
"miden-vm/masm-examples",
"miden-vm/tests/integration/cli/data",
] {
collect_masm_files(&root.join(relative), &mut files);
}
files.sort();
files
}
fn collect_masm_files(dir: &Path, files: &mut Vec<PathBuf>) {
let entries = fs::read_dir(dir)
.unwrap_or_else(|error| panic!("failed to read {}: {error}", dir.display()));
for entry in entries {
let entry = entry.unwrap_or_else(|error| {
panic!("failed to read a directory entry under {}: {error}", dir.display())
});
let path = entry.path();
if path.is_dir() {
collect_masm_files(&path, files);
} else if path.extension().is_some_and(|ext| ext == "masm") {
files.push(path);
}
}
}
fn representative_round_trip_sources() -> &'static [&'static str] {
&[
"",
"# leading comment\n#! module docs\npub const X = [0x01, 0x02]\n",
"\
namespace std::math
extern package \"miden/base@0.1.0\"
mod internal
pub mod u64
",
"\
@inline
# keep standalone
@locals(1)
pub proc foo(a: felt)
# body
push.1
end
",
"\
begin
if.true
repeat.4
swap dup.1 add
end
else
while.true
nop
end
end
end
",
"\
pub type Account = struct { id: felt, vault: ptr<u8, addrspace(byte)> }
adv_map TABLE = [
[1, 2],
event(foo(bar, baz)),
]
",
]
}
fn assert_lossless_parse(input: &str, label: impl core::fmt::Display) {
let parse = parse_text(input);
assert_eq!(
parse.syntax().text().to_string(),
input,
"CST parse was not lossless for {label}"
);
}
fn diagnostic_labels(parse: &super::Parse) -> Vec<String> {
parse
.diagnostics()
.iter()
.flat_map(|diag| diag.labels.as_deref().unwrap_or(&[]).iter())
.filter_map(|label| label.label())
.map(ToString::to_string)
.collect()
}
fn assert_import_rejected(source: &str, expected_label: &str) {
let parse = parse_text(source);
assert!(parse.has_errors(), "expected {source:?} to be rejected");
let labels = diagnostic_labels(&parse);
assert!(
labels.iter().any(|label| label.contains(expected_label)),
"expected {source:?} to report {expected_label:?}, got {:?}",
parse.diagnostics()
);
}
#[test]
fn parse_text_is_lossless_for_representative_sources() {
for (index, source) in representative_round_trip_sources().iter().enumerate() {
assert_lossless_parse(source, format_args!("representative source {index}"));
}
}
#[test]
fn parse_text_is_lossless_for_checked_in_masm_corpus() {
let files = checked_in_masm_corpus();
assert!(
!files.is_empty(),
"expected the checked-in MASM corpus to contain at least one source file"
);
for path in files {
let source = fs::read_to_string(&path)
.unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display()));
assert_lossless_parse(&source, path.display());
}
}
#[test]
fn parse_text_as_inline_masm() {
let source = "\
if.true
repeat.4
swap dup.1 add
end
else
while.true
nop
end
end
";
let parse = parse_inline_masm_text(source, None);
assert!(!parse.has_errors());
let root = parse.syntax();
assert_eq!(root.kind(), SyntaxKind::Block);
let child_kinds = root.children().map(|child| child.kind()).collect::<Vec<_>>();
assert_eq!(child_kinds, vec![SyntaxKind::IfOp,]);
assert!(root.descendants().any(|node| node.kind() == SyntaxKind::IfOp));
assert!(root.descendants().any(|node| node.kind() == SyntaxKind::RepeatOp));
assert!(root.descendants().any(|node| node.kind() == SyntaxKind::WhileOp));
}
#[test]
fn parse_do_while_loop() {
let source = "\
do
push.1
dup.0 neq.0
while
dup.0 lt.10
end
";
let parse = parse_inline_masm_text(source, None);
let mut parse = parse;
let diagnostics = parse.take_diagnostics();
assert!(diagnostics.is_empty(), "unexpected parse errors: {diagnostics:?}");
let root = parse.syntax();
let child_kinds = root.children().map(|child| child.kind()).collect::<Vec<_>>();
assert_eq!(child_kinds, vec![SyntaxKind::DoWhileOp]);
let do_while = root.children().find(|n| n.kind() == SyntaxKind::DoWhileOp).unwrap();
let block_count = do_while.children().filter(|n| n.kind() == SyntaxKind::Block).count();
assert_eq!(block_count, 2, "expected a body block and a condition block");
}
#[test]
fn parse_do_while_with_nested_while_true_in_body() {
let source = "\
do
while.true
nop
end
push.1
while
eq.0
end
";
let parse = parse_inline_masm_text(source, None);
let mut parse = parse;
let diagnostics = parse.take_diagnostics();
assert!(diagnostics.is_empty(), "unexpected parse errors: {diagnostics:?}");
let root = parse.syntax();
let child_kinds = root.children().map(|child| child.kind()).collect::<Vec<_>>();
assert_eq!(child_kinds, vec![SyntaxKind::DoWhileOp]);
let do_while = root.children().find(|n| n.kind() == SyntaxKind::DoWhileOp).unwrap();
assert!(do_while.descendants().any(|node| node.kind() == SyntaxKind::WhileOp));
}
#[test]
fn do_while_terminator_requires_dot_adjacent_to_while() {
let source = "\
do
push.1
while .true
end
";
let mut parse = parse_inline_masm_text(source, None);
let diagnostics = parse.take_diagnostics();
assert!(!diagnostics.is_empty(), "expected a syntax error for the stray `.true`");
let root = parse.syntax();
assert!(root.descendants().any(|node| node.kind() == SyntaxKind::DoWhileOp));
}
#[test]
fn parses_top_level_forms_and_nested_structured_ops() {
let source = "\
#! docs
namespace std::math
extern package \"miden/base@0.1.0\"
mod internal
pub mod u64
pub use {bar as baz} from foo
pub const X = 1
pub type FeltAlias = felt
adv_map TABLE = [0x01, 0x02]
begin
if.true
repeat.4
swap dup.1 add
end
else
while.true
nop
end
end
end
pub proc foo(a) -> (b)
exec.bar
end
";
let parse = parse_text(source);
assert!(!parse.has_errors());
let root = parse.syntax();
assert_eq!(root.kind(), SyntaxKind::SourceFile);
let child_kinds = root.children().map(|child| child.kind()).collect::<Vec<_>>();
assert_eq!(
child_kinds,
vec![
SyntaxKind::Doc,
SyntaxKind::Namespace,
SyntaxKind::ExternPackage,
SyntaxKind::Submodule,
SyntaxKind::Submodule,
SyntaxKind::Import,
SyntaxKind::Constant,
SyntaxKind::TypeDecl,
SyntaxKind::AdviceMap,
SyntaxKind::BeginBlock,
SyntaxKind::Procedure,
]
);
assert!(root.descendants().any(|node| node.kind() == SyntaxKind::IfOp));
assert!(root.descendants().any(|node| node.kind() == SyntaxKind::RepeatOp));
assert!(root.descendants().any(|node| node.kind() == SyntaxKind::WhileOp));
}
#[test]
fn exposes_typed_wrappers_for_structured_top_level_forms() {
let source = "\
namespace app::accounts
extern package \"miden/base@0.1.0\"
mod internal
pub mod api
use miden::core::mem as memory
pub const EVENT = event(\"miden::event\")
pub enum Bool : u8 {
FALSE,
TRUE = 1,
}
adv_map TABLE(0x0200000000000000020000000000000002000000000000000200000000000000) = [0x01, 0x02]
";
let parse = parse_text(source);
assert!(!parse.has_errors(), "{:?}", parse.diagnostics());
let source_file = AstSourceFile::cast(parse.syntax()).expect("source file");
let items = source_file.items().collect::<Vec<_>>();
assert_eq!(items.len(), 8);
let Item::Namespace(namespace) = &items[0] else {
panic!("expected namespace, got {:?}", items[0]);
};
assert_eq!(
namespace
.path()
.expect("namespace path")
.segments()
.map(|segment| segment.text().to_string())
.collect::<Vec<_>>(),
vec!["app", "accounts"]
);
let Item::ExternPackage(package) = &items[1] else {
panic!("expected extern package, got {:?}", items[1]);
};
assert_eq!(package.package_token().expect("package name").text(), "\"miden/base@0.1.0\"");
let Item::Submodule(submodule) = &items[2] else {
panic!("expected submodule, got {:?}", items[2]);
};
assert!(submodule.visibility().is_none());
assert_eq!(submodule.name_token().expect("submodule name").text(), "internal");
let Item::Submodule(submodule) = &items[3] else {
panic!("expected public submodule, got {:?}", items[3]);
};
assert!(submodule.visibility().is_some());
assert_eq!(submodule.name_token().expect("submodule name").text(), "api");
let Item::Import(import) = &items[4] else {
panic!("expected import, got {:?}", items[0]);
};
assert_eq!(import.kind(), ImportKind::Module);
assert_eq!(
import
.module_path()
.expect("import path")
.segments()
.map(|segment| segment.text().to_string())
.collect::<Vec<_>>(),
vec!["miden", "core", "mem"]
);
assert_eq!(import.module_alias_token().expect("alias").text(), "memory");
let Item::Constant(constant) = &items[5] else {
panic!("expected constant, got {:?}", items[1]);
};
assert_eq!(constant.name_token().expect("constant name").text(), "EVENT");
assert_eq!(
constant
.expr()
.expect("constant expr")
.significant_tokens()
.map(|token| token.text().to_string())
.collect::<Vec<_>>(),
vec!["event", "(", "\"miden::event\"", ")"]
);
let Item::TypeDecl(type_decl) = &items[6] else {
panic!("expected type declaration, got {:?}", items[2]);
};
assert_eq!(type_decl.keyword_token().expect("type keyword").text(), "enum");
assert_eq!(type_decl.name_token().expect("type name").text(), "Bool");
assert!(
type_decl.body().is_some(),
"expected enum declaration to expose a structured type body"
);
let Item::AdviceMap(advice_map) = &items[7] else {
panic!("expected advice map, got {:?}", items[3]);
};
assert_eq!(advice_map.name_token().expect("advice map name").text(), "TABLE");
assert_eq!(
advice_map
.value_expr()
.expect("advice map value")
.significant_tokens()
.map(|token| token.text().to_string())
.collect::<Vec<_>>(),
vec!["[", "0x01", ",", "0x02", "]"]
);
}
#[test]
fn parses_unparenthesized_procedure_result_types() {
let source = "\
pub proc println(message: ptr<u8, addrspace(byte)>) -> ptr<u8, addrspace(byte)>
nop
end
";
let parse = parse_text(source);
assert!(!parse.has_errors(), "{:?}", parse.diagnostics());
let root = parse.syntax();
let source_file = AstSourceFile::cast(root).expect("source file");
let items = source_file.items().collect::<Vec<_>>();
assert_eq!(items.len(), 1);
let Item::Procedure(procedure) = &items[0] else {
panic!("expected procedure, got {:?}", items[0]);
};
assert!(
procedure.signature().is_some(),
"expected procedure to retain its signature node"
);
}
#[test]
fn no_result_signature_does_not_absorb_body_leading_trivia() {
let source = "\
pub proc foo()
# body
nop
end
";
let parse = parse_text(source);
assert!(!parse.has_errors(), "{:?}", parse.diagnostics());
let source_file = AstSourceFile::cast(parse.syntax()).expect("source file");
let items = source_file.items().collect::<Vec<_>>();
let Item::Procedure(procedure) = &items[0] else {
panic!("expected procedure, got {:?}", items[0]);
};
let signature = procedure.signature().expect("procedure signature");
assert_eq!(signature.syntax().text().to_string(), "()");
let block = procedure.block().expect("procedure block");
assert!(
block.syntax().text().to_string().contains("# body"),
"expected body-leading comment to remain in the block"
);
}
#[test]
fn multiline_result_arrow_stays_in_signature() {
let source = "\
pub proc foo()
-> felt
nop
end
";
let parse = parse_text(source);
assert!(!parse.has_errors(), "{:?}", parse.diagnostics());
let source_file = AstSourceFile::cast(parse.syntax()).expect("source file");
let items = source_file.items().collect::<Vec<_>>();
let Item::Procedure(procedure) = &items[0] else {
panic!("expected procedure, got {:?}", items[0]);
};
let signature = procedure.signature().expect("procedure signature");
assert_eq!(signature.syntax().text().to_string(), "()\n -> felt");
}
#[test]
fn cst_import_parses_multiline_module_aliases() {
let source = "\
use ::miden::core::collections::sorted_array::lowerbound_key_value
as lowerbound_key_value
";
let parse = parse_text(source);
assert!(!parse.has_errors(), "{:?}", parse.diagnostics());
let source_file = AstSourceFile::cast(parse.syntax()).expect("source file");
let items = source_file.items().collect::<Vec<_>>();
assert_eq!(items.len(), 1);
let Item::Import(import) = &items[0] else {
panic!("expected import, got {:?}", items[0]);
};
assert_eq!(import.kind(), ImportKind::Module);
assert_eq!(import.module_alias_token().expect("alias").text(), "lowerbound_key_value");
}
#[test]
fn cst_import_parses_module_imports() {
let source = "\
use foo
use some::module
use some::module as sm
";
let parse = parse_text(source);
assert!(!parse.has_errors(), "{:?}", parse.diagnostics());
let source_file = AstSourceFile::cast(parse.syntax()).expect("source file");
let imports = source_file
.items()
.map(|item| match item {
Item::Import(import) => import,
other => panic!("expected import, got {other:?}"),
})
.collect::<Vec<_>>();
assert_eq!(imports.len(), 3);
let first = &imports[0];
assert_eq!(first.kind(), ImportKind::Module);
assert!(first.visibility().is_none());
assert_eq!(
first
.module_path()
.expect("module path")
.segments()
.map(|segment| segment.text().to_string())
.collect::<Vec<_>>(),
vec!["foo"]
);
assert!(first.module_alias_token().is_none());
let second = &imports[1];
assert_eq!(second.kind(), ImportKind::Module);
assert_eq!(
second
.module_path()
.expect("module path")
.segments()
.map(|segment| segment.text().to_string())
.collect::<Vec<_>>(),
vec!["some", "module"]
);
assert!(second.module_alias_token().is_none());
let third = &imports[2];
assert_eq!(third.kind(), ImportKind::Module);
assert_eq!(
third
.module_path()
.expect("module path")
.segments()
.map(|segment| segment.text().to_string())
.collect::<Vec<_>>(),
vec!["some", "module"]
);
assert_eq!(third.module_alias_token().expect("alias").text(), "sm");
}
#[test]
fn cst_import_parses_item_imports() {
let source = "\
use {foo, bar as baz} from some::module
pub use {alpha} from core
";
let parse = parse_text(source);
assert!(!parse.has_errors(), "{:?}", parse.diagnostics());
let source_file = AstSourceFile::cast(parse.syntax()).expect("source file");
let imports = source_file
.items()
.map(|item| match item {
Item::Import(import) => import,
other => panic!("expected import, got {other:?}"),
})
.collect::<Vec<_>>();
assert_eq!(imports.len(), 2);
let first = &imports[0];
assert_eq!(first.kind(), ImportKind::Items);
assert!(first.visibility().is_none());
assert!(
first.path().is_none(),
"item imports must not masquerade as legacy path imports"
);
assert!(
first.module_alias_token().is_none(),
"item aliases must not masquerade as module aliases"
);
assert_eq!(
first
.module_path()
.expect("module path")
.segments()
.map(|segment| segment.text().to_string())
.collect::<Vec<_>>(),
vec!["some", "module"]
);
let specs = first.item_specs().collect::<Vec<_>>();
assert_eq!(specs.len(), 2);
assert_eq!(specs[0].name_token().expect("item name").text(), "foo");
assert!(specs[0].alias_token().is_none());
assert_eq!(specs[1].name_token().expect("item name").text(), "bar");
assert_eq!(specs[1].alias_token().expect("item alias").text(), "baz");
let second = &imports[1];
assert_eq!(second.kind(), ImportKind::Items);
assert!(second.visibility().is_some());
assert_eq!(
second
.module_path()
.expect("module path")
.segments()
.map(|segment| segment.text().to_string())
.collect::<Vec<_>>(),
vec!["core"]
);
let specs = second.item_specs().collect::<Vec<_>>();
assert_eq!(specs.len(), 1);
assert_eq!(specs[0].name_token().expect("item name").text(), "alpha");
}
#[test]
fn cst_import_accepts_quoted_special_and_contextual_names() {
let source = "\
use \"as\"::\"from\" as \"module\"
use {as, from as as, \"as\" as \"from\", $kernel} from \"from\"::\"as\"
";
let parse = parse_text(source);
assert!(!parse.has_errors(), "{:?}", parse.diagnostics());
let source_file = AstSourceFile::cast(parse.syntax()).expect("source file");
let imports = source_file
.items()
.map(|item| match item {
Item::Import(import) => import,
other => panic!("expected import, got {other:?}"),
})
.collect::<Vec<_>>();
assert_eq!(imports.len(), 2);
let module = &imports[0];
assert_eq!(
module
.module_path()
.expect("module path")
.segments()
.map(|segment| segment.text().to_string())
.collect::<Vec<_>>(),
vec!["\"as\"", "\"from\""]
);
assert_eq!(module.module_alias_token().expect("module alias").text(), "\"module\"");
let items = &imports[1];
assert_eq!(
items
.module_path()
.expect("module path")
.segments()
.map(|segment| segment.text().to_string())
.collect::<Vec<_>>(),
vec!["\"from\"", "\"as\""]
);
let specs = items.item_specs().collect::<Vec<_>>();
assert_eq!(specs.len(), 4);
assert_eq!(specs[0].name_token().expect("item name").text(), "as");
assert!(specs[0].alias_token().is_none());
assert_eq!(specs[1].name_token().expect("item name").text(), "from");
assert_eq!(specs[1].alias_token().expect("item alias").text(), "as");
assert_eq!(specs[2].name_token().expect("item name").text(), "\"as\"");
assert_eq!(specs[2].alias_token().expect("item alias").text(), "\"from\"");
assert_eq!(specs[3].name_token().expect("item name").text(), "$kernel");
}
#[test]
fn cst_import_rejects_public_module_imports_and_removed_forms() {
let cases = [
("pub use some::module\n", "`pub use` is only supported for braced item imports"),
("use foo->bar\n", "import aliases use `as`; `->` is no longer supported"),
("pub use foo->bar\n", "import aliases use `as`; `->` is no longer supported"),
(
"use {foo->bar} from m\n",
"import aliases use `as`; `->` is no longer supported",
),
(
"use 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef->foo\n",
"digest imports are not supported",
),
(
"pub use 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef->foo\n",
"digest imports are not supported",
),
(
"use {foo} from 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef\n",
"digest imports are not supported",
),
];
for (source, expected) in cases {
assert_import_rejected(source, expected);
}
}
#[test]
fn cst_import_does_not_consume_next_item_after_missing_alias_or_from_path() {
let cases = [
("use\npub const X = 1\n", "expected an import path"),
("use foo as\npub const X = 1\n", "expected an alias name after `as`"),
("use {foo} from\npub const X = 1\n", "expected a module path after `from`"),
];
for (source, expected) in cases {
let parse = parse_text(source);
assert!(parse.has_errors(), "expected {source:?} to be rejected");
let labels = diagnostic_labels(&parse);
assert!(
labels.iter().any(|label| label.contains(expected)),
"expected {source:?} to report {expected:?}, got {:?}",
parse.diagnostics()
);
let source_file = AstSourceFile::cast(parse.syntax()).expect("source file");
let items = source_file.items().collect::<Vec<_>>();
assert_eq!(items.len(), 2, "next top-level item should remain separate");
assert!(matches!(items[0], Item::Import(_)));
let Item::Constant(constant) = &items[1] else {
panic!("expected second item to remain a constant, got {:?}", items[1]);
};
assert!(constant.visibility().is_some(), "`pub` must remain attached to const");
assert_eq!(constant.name_token().expect("constant name").text(), "X");
}
}
#[test]
fn cst_import_rejects_malformed_item_imports() {
let cases = [
("use {} from m\n", "import lists must contain at least one item"),
("use {foo} m\n", "expected `from` after import list"),
("use foo as\n", "expected an alias name after `as`"),
("use {foo as} from m\n", "expected an alias name after `as`"),
];
for (source, expected) in cases {
assert_import_rejected(source, expected);
}
}
#[test]
fn cst_import_rejects_wildcard_imports() {
for source in ["use * from m\n", "use {*} from m\n", "use {foo, *} from m\n"] {
assert_import_rejected(source, "wildcard imports are not supported");
}
}
#[test]
fn keeps_header_comments_on_structured_nodes() {
let source = "\
use {panic} from ::miden::utils # import
pub proc long_name(arg: felt) # proc
nop
end
begin # begin
if.true # if
nop
else # else
nop
end
end
";
let parse = parse_text(source);
assert!(!parse.has_errors(), "{:?}", parse.diagnostics());
let root = parse.syntax();
let procedure = root
.descendants()
.find(|node| node.kind() == SyntaxKind::Procedure)
.expect("procedure");
assert!(
procedure
.children_with_tokens()
.filter_map(rowan::NodeOrToken::into_token)
.any(|token| token.kind() == SyntaxKind::Comment && token.text().contains("proc"))
);
let if_node = root
.descendants()
.find(|node| node.kind() == SyntaxKind::IfOp)
.expect("if node");
let if_comments = if_node
.children_with_tokens()
.filter_map(rowan::NodeOrToken::into_token)
.filter(|token| token.kind() == SyntaxKind::Comment)
.map(|token| token.text().to_string())
.collect::<Vec<_>>();
assert!(
if_comments.iter().any(|comment| comment.contains("if")),
"expected header comment on if node, got {if_comments:?}"
);
assert!(
if_comments.iter().any(|comment| comment.contains("else")),
"expected else comment on if node, got {if_comments:?}"
);
}
#[test]
fn rejects_block_local_doc_comments_before_block_keywords() {
let source = "\
proc foo
#! mistaken doc comment before if
if.true
nop
#! mistaken doc comment before else
else
#! mistaken doc comment before while
while.true
add
end
end
end
";
let parse = parse_text(source);
assert!(parse.has_errors());
assert!(
parse
.diagnostics()
.iter()
.flat_map(|diag| diag.labels.as_deref().unwrap_or(&[]).iter())
.filter_map(|label| label.label())
.any(|label| label.contains("doc comments are only allowed")),
"expected block-local doc comments before block keywords to be rejected, got {:?}",
parse.diagnostics()
);
let root = parse.syntax();
assert!(root.descendants().any(|node| node.kind() == SyntaxKind::Procedure));
assert!(root.descendants().any(|node| node.kind() == SyntaxKind::IfOp));
assert!(
root.descendants().filter(|node| node.kind() == SyntaxKind::Instruction).count() >= 2
);
}
#[test]
fn block_local_doc_comments_before_instructions_still_error() {
let source = "\
proc foo
#! malformed doc comment before instruction
loc_load.0
end
";
let parse = parse_text(source);
assert!(parse.has_errors());
assert!(
parse
.diagnostics()
.iter()
.flat_map(|diag| diag.labels.as_deref().unwrap_or(&[]).iter())
.filter_map(|label| label.label())
.any(|label| label.contains("doc comments are only allowed")),
"expected block-local doc comments before instructions to remain invalid, got {:?}",
parse.diagnostics()
);
}
#[test]
fn block_local_doc_comments_still_recover_before_true_top_level_items() {
let source = "\
proc foo
#! actual misplaced doc comment
pub const X = 1
";
let parse = parse_text(source);
assert!(parse.has_errors());
assert!(
parse
.diagnostics()
.iter()
.flat_map(|diag| diag.labels.as_deref().unwrap_or(&[]).iter())
.filter_map(|label| label.label())
.any(|label| label.contains("before top-level item")),
"expected block recovery before a top-level item, got {:?}",
parse.diagnostics()
);
let root = parse.syntax();
assert!(root.descendants().any(|node| node.kind() == SyntaxKind::Doc));
let source_file = AstSourceFile::cast(root).expect("source file");
let items = source_file.items().collect::<Vec<_>>();
assert_eq!(
items.len(),
3,
"expected recovery to preserve the recovered doc comment and top-level constant"
);
assert!(matches!(items[0], Item::Procedure(_)));
assert!(matches!(items[1], Item::Doc(_)));
assert!(matches!(items[2], Item::Constant(_)));
}
#[test]
fn rejects_stray_closing_delimiters() {
let cases = [
("const X = )\n", ")"),
("begin foo ] end\n", "]"),
("begin\n foo}\nend\n", "}"),
];
for (source, delimiter) in cases {
let parse = parse_text(source);
assert!(parse.has_errors(), "expected {source:?} to be rejected");
let expected = format!("unexpected closing delimiter `{delimiter}`");
assert!(
parse
.diagnostics()
.iter()
.flat_map(|diag| diag.labels.as_deref().unwrap_or(&[]).iter())
.filter_map(|label| label.label())
.any(|label| label.contains(&expected)),
"expected {source:?} to report {expected:?}, got {:?}",
parse.diagnostics()
);
}
}
#[test]
fn recovers_from_missing_end_tokens() {
let parse = parse_text("begin\n if.true\n add\n");
assert!(parse.has_errors());
let end_labels = parse
.diagnostics()
.iter()
.flat_map(|diag| diag.labels.as_deref().unwrap_or(&[]).iter())
.filter_map(|label| label.label())
.filter(|label| label.contains("expected `end`"))
.collect::<Vec<_>>();
assert_eq!(
end_labels.len(),
1,
"expected exactly one missing-`end` diagnostic, got {:?}",
parse.diagnostics()
);
assert!(
end_labels[0].contains("`if`"),
"expected the innermost unterminated block to own the diagnostic, got {end_labels:?}"
);
let root = parse.syntax();
assert!(root.descendants().any(|node| node.kind() == SyntaxKind::BeginBlock));
assert!(root.descendants().any(|node| node.kind() == SyntaxKind::IfOp));
}
#[test]
fn recovers_before_top_level_items_inside_blocks() {
let source = "\
proc foo
if.true
add
pub const X = 1
";
let parse = parse_text(source);
assert!(parse.has_errors());
assert!(
parse
.diagnostics()
.iter()
.flat_map(|diag| diag.labels.as_deref().unwrap_or(&[]).iter())
.filter_map(|label| label.label())
.any(|label| label.contains("before top-level item")),
"expected block recovery before a top-level item, got {:?}",
parse.diagnostics()
);
let source_file = AstSourceFile::cast(parse.syntax()).expect("source file");
let items = source_file.items().collect::<Vec<_>>();
assert_eq!(items.len(), 2, "expected recovery to preserve the top-level constant");
assert!(matches!(items[0], Item::Procedure(_)));
assert!(matches!(items[1], Item::Constant(_)));
assert!(parse.syntax().descendants().any(|node| node.kind() == SyntaxKind::Error));
}
#[test]
fn else_synchronizes_unterminated_nested_blocks() {
let source = "\
proc foo
if.true
while.true
nop
else
nop
end
end
";
let parse = parse_text(source);
assert!(parse.has_errors());
assert!(
parse
.diagnostics()
.iter()
.flat_map(|diag| diag.labels.as_deref().unwrap_or(&[]).iter())
.filter_map(|label| label.label())
.any(|label| label.contains("close `while` before `else`")),
"expected the nested `while` to recover before `else`, got {:?}",
parse.diagnostics()
);
let if_node = parse
.syntax()
.descendants()
.find(|node| node.kind() == SyntaxKind::IfOp)
.expect("if node");
assert_eq!(
if_node.children().filter(|child| child.kind() == SyntaxKind::Block).count(),
2,
"expected `else` to remain attached to the enclosing `if` after recovery"
);
}
#[test]
fn surfaces_invalid_tokens_as_diagnostics() {
let parse = parse_text("proc foo\n §\nend\n");
assert!(parse.has_errors());
assert!(parse.diagnostics().iter().any(|diag| diag.labels.as_ref().is_some_and(
|labels| {
labels
.iter()
.any(|l| l.label().is_some_and(|label| label.contains("unrecognized token")))
}
)));
}
#[test]
fn rejects_unknown_special_identifiers() {
for source in [
"use $foo::bar\n",
"begin\n exec.$foo::bar\nend\n",
"begin\n exec.$execFoo::bar\nend\n",
"begin\n exec.$kernelFoo::bar\nend\n",
] {
let parse = parse_text(source);
assert!(parse.has_errors(), "expected {source:?} to reject unknown special ident");
assert!(parse.diagnostics().iter().any(|diag| diag.labels.as_ref().is_some_and(
|labels| {
labels.iter().any(|l| {
l.label().is_some_and(|label| label.contains("unrecognized token"))
})
}
)));
}
}
#[test]
fn parse_source_file_tracks_source_aware_spans() {
let source = Arc::new(ManagedSourceFile::new(
SourceId::new(11),
SourceLanguage::Masm,
Uri::new("memory:///parser-span-test.masm"),
"begin\n nop\nend\n".to_string().into_boxed_str(),
));
let parse = parse_source_file(source.clone());
assert!(!parse.has_errors(), "{:?}", parse.diagnostics());
assert_eq!(parse.source_file().id(), source.id());
let nop = parse
.syntax()
.descendants_with_tokens()
.filter_map(rowan::NodeOrToken::into_token)
.find(|token| token.text() == "nop")
.expect("nop token");
let offset = source.as_str().find("nop").expect("nop offset");
let expected = SourceSpan::try_from_range(source.id(), offset..offset + 3).unwrap();
assert_eq!(parse.span_for_token(&nop), expected);
}
#[test]
fn diagnostics_keep_source_ids_from_managed_source_files() {
let source = Arc::new(ManagedSourceFile::new(
SourceId::new(12),
SourceLanguage::Masm,
Uri::new("memory:///parser-diagnostic-span-test.masm"),
"proc foo\n §\nend\n".to_string().into_boxed_str(),
));
let parse = parse_source_file(source.clone());
assert!(parse.has_errors());
let diagnostic = parse
.diagnostics()
.iter()
.find(|diag| {
diag.labels.as_ref().is_some_and(|labels| {
labels.iter().any(|l| {
l.label().is_some_and(|label| label.contains("unrecognized token"))
})
})
})
.expect("invalid-token diagnostic");
let offset = source.as_str().find('§').expect("invalid token offset");
let expected =
SourceSpan::try_from_range(source.id(), offset..offset + '§'.len_utf8()).unwrap();
let label_span = diagnostic.labels.as_deref().unwrap()[0].inner();
let actual = SourceSpan::new(
source.id(),
(label_span.offset() as u32)..((label_span.offset() + label_span.len()) as u32),
);
assert_eq!(actual, expected);
}
#[test]
fn eof_diagnostics_anchor_to_the_last_character_offset() {
let source = Arc::new(ManagedSourceFile::new(
SourceId::new(13),
SourceLanguage::Masm,
Uri::new("memory:///parser-eof-span-test.masm"),
"begin\n if.true\n add\n".to_string().into_boxed_str(),
));
let parse = parse_source_file(source.clone());
assert!(parse.has_errors());
let diagnostic = parse
.diagnostics()
.iter()
.find(|diag| {
diag.labels
.as_deref()
.unwrap_or(&[])
.iter()
.filter_map(|label| label.label())
.any(|label| label.contains("expected `end`"))
})
.expect("missing-end diagnostic");
let last_char_offset = source
.as_str()
.char_indices()
.last()
.map(|(offset, _)| offset)
.expect("source should be non-empty");
let label_span = diagnostic.labels.as_deref().unwrap()[0].inner();
assert_eq!(label_span.offset(), last_char_offset);
assert_eq!(label_span.len(), 0);
}
#[test]
fn cst_import_spans_do_not_consume_trailing_newlines() {
let source = "\
use lib::a
use {foo} from lib::b
begin end
";
let parse = parse_text(source);
let source_file = AstSourceFile::cast(parse.syntax()).expect("source file");
let items = source_file.items().collect::<Vec<_>>();
let Item::Import(module_import) = &items[0] else {
panic!("expected first item to be an import");
};
let module_path = module_import.module_path().expect("module path");
let start = source.find("lib::a").expect("path start") as u32;
let end = start + "lib::a".len() as u32;
let expected = SourceSpan::new(parse.source().id(), start..end);
assert_eq!(parse.span_for_node(module_path.syntax()), expected);
let Item::Import(item_import) = &items[1] else {
panic!("expected second item to be an import");
};
let item_path = item_import.module_path().expect("item import module path");
let start = source.find("lib::b").expect("path start") as u32;
let end = start + "lib::b".len() as u32;
let expected = SourceSpan::new(parse.source().id(), start..end);
assert_eq!(parse.span_for_node(item_path.syntax()), expected);
let spec = item_import.item_specs().next().expect("item specifier");
let name = spec.name_token().expect("item name");
let start = source.find("foo").expect("item start") as u32;
let end = start + "foo".len() as u32;
let expected = SourceSpan::new(parse.source().id(), start..end);
assert_eq!(parse.span_for_token(&name), expected);
}
}