use rowan::{GreenNode, GreenNodeBuilder, TextRange, TextSize};
use crate::keywords::{self, Profile};
use crate::lexer::{Token, lex};
use crate::syntax_kind::{SyntaxKind, SyntaxNode};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SyntaxError {
pub message: String,
pub code: &'static str,
pub range: TextRange,
}
#[derive(Debug, Clone)]
pub struct Parse {
green: GreenNode,
errors: Vec<SyntaxError>,
}
impl Parse {
pub fn syntax(&self) -> SyntaxNode {
SyntaxNode::new_root(self.green.clone())
}
pub fn errors(&self) -> &[SyntaxError] {
&self.errors
}
}
impl PartialEq for Parse {
fn eq(&self, other: &Self) -> bool {
self.green == other.green
}
}
impl Eq for Parse {}
pub fn parse(input: &str, profile: Profile) -> Parse {
let mut parser = Parser::new(lex(input, profile), profile);
parser.source_file();
parser.finish()
}
fn is_value_token(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::IntNumber
| SyntaxKind::FloatNumber
| SyntaxKind::String
| SyntaxKind::TrueKw
| SyntaxKind::FalseKw
| SyntaxKind::Regex
| SyntaxKind::Ident
)
}
fn ungrammatical_reserved_noun(kind: SyntaxKind) -> &'static str {
match kind {
SyntaxKind::FloatNumber => "a floating-point literal",
SyntaxKind::String => "a string literal",
SyntaxKind::TrueKw | SyntaxKind::FalseKw => "a boolean literal",
SyntaxKind::Regex => "a regex literal",
SyntaxKind::Ident => "a constant reference",
_ => "a literal of that shape",
}
}
fn is_top_level_start(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::PackageKw
| SyntaxKind::ImportKw
| SyntaxKind::InternalKw
| SyntaxKind::ErrorKw
| SyntaxKind::TypeKw
| SyntaxKind::ConstKw
| SyntaxKind::StructKw
| SyntaxKind::EnumKw
| SyntaxKind::EnumsetKw
| SyntaxKind::UnionKw
| SyntaxKind::InterfaceKw
| SyntaxKind::ServiceKw
| SyntaxKind::SystemKw
| SyntaxKind::ComponentKw
| SyntaxKind::DistributionKw
| SyntaxKind::DeploymentKw
)
}
fn is_rsdl_declaration_start(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::SystemKw
| SyntaxKind::ComponentKw
| SyntaxKind::DistributionKw
| SyntaxKind::DeploymentKw
| SyntaxKind::MachineKw
)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum RsdlBody {
Members,
ComponentLines,
Machines,
}
impl RsdlBody {
fn starts_line(self, kind: SyntaxKind) -> bool {
match self {
Self::Members => matches!(
kind,
SyntaxKind::Ident
| SyntaxKind::BooleanKw
| SyntaxKind::IntegerKw
| SyntaxKind::FloatKw
| SyntaxKind::StringKw
| SyntaxKind::BytesKw
),
Self::ComponentLines => matches!(kind, SyntaxKind::OffersKw | SyntaxKind::RequiresKw),
Self::Machines => kind == SyntaxKind::MachineKw,
}
}
}
fn rsdl_foreign_declaration_message(word: &str) -> String {
format!(
"`{word}` starts a declaration of another profile, and an `.rsdl` file declares \
`system`, `component`, `distribution` and `deployment` only — a type is declared in \
a `.typl` file, an interface or a service in a `.ridl` file (rsdl reference §2)"
)
}
fn is_interaction_start(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::SignalKw
| SyntaxKind::EventKw
| SyntaxKind::CommandKw
| SyntaxKind::QueryKw
| SyntaxKind::FixedKw
)
}
fn is_return_sync(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::RBrace | SyntaxKind::Comma | SyntaxKind::ReservedKw | SyntaxKind::At
) || is_interaction_start(kind)
|| is_top_level_start(kind)
}
fn is_typl_definition_start(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::TypeKw
| SyntaxKind::ConstKw
| SyntaxKind::StructKw
| SyntaxKind::EnumKw
| SyntaxKind::EnumsetKw
| SyntaxKind::UnionKw
)
}
const MAX_TYPE_DEPTH: usize = 128;
struct Parser<'a> {
tokens: Vec<Token<'a>>,
offsets: Vec<usize>,
pos: usize,
depth: usize,
expr_refused: bool,
profile: Profile,
builder: GreenNodeBuilder<'static>,
errors: Vec<SyntaxError>,
}
impl<'a> Parser<'a> {
fn new(tokens: Vec<Token<'a>>, profile: Profile) -> Self {
let mut offsets = Vec::with_capacity(tokens.len() + 1);
let mut acc = 0;
for token in &tokens {
offsets.push(acc);
acc += token.text.len();
}
offsets.push(acc);
Self {
tokens,
offsets,
pos: 0,
depth: 0,
expr_refused: false,
profile,
builder: GreenNodeBuilder::new(),
errors: Vec::new(),
}
}
fn finish(self) -> Parse {
Parse {
green: self.builder.finish(),
errors: self.errors,
}
}
fn significant_pos(&self) -> usize {
let mut i = self.pos;
while self.tokens.get(i).is_some_and(|t| t.kind.is_trivia()) {
i += 1;
}
i
}
fn current(&self) -> Option<SyntaxKind> {
self.tokens.get(self.significant_pos()).map(|t| t.kind)
}
fn nth(&self, n: usize) -> Option<SyntaxKind> {
let mut i = self.pos;
let mut seen = 0;
loop {
let token = self.tokens.get(i)?;
if !token.kind.is_trivia() {
if seen == n {
return Some(token.kind);
}
seen += 1;
}
i += 1;
}
}
fn at(&self, kind: SyntaxKind) -> bool {
self.current() == Some(kind)
}
fn current_text(&self) -> &'a str {
self.tokens
.get(self.significant_pos())
.map_or("", |token| token.text)
}
fn eat_trivia(&mut self) {
while let Some(token) = self.tokens.get(self.pos) {
if token.kind.is_trivia() {
self.builder.token(token.kind.into(), token.text);
self.pos += 1;
} else {
break;
}
}
}
fn start(&mut self, kind: SyntaxKind) {
self.eat_trivia();
self.builder.start_node(kind.into());
}
fn bump(&mut self) {
self.eat_trivia();
if let Some(token) = self.tokens.get(self.pos) {
self.builder.token(token.kind.into(), token.text);
self.pos += 1;
}
}
fn expect(&mut self, kind: SyntaxKind) {
if self.at(kind) {
self.bump();
} else {
self.error_at_current("FORM-101", format!("expected {kind:?}"));
}
}
fn error_at_current(&mut self, code: &'static str, message: String) {
let i = self.significant_pos();
let start = TextSize::new(self.offsets[i] as u32);
if code != "TYPL-302"
&& self
.errors
.last()
.is_some_and(|last| last.range.start() == start)
{
return;
}
let end = self
.tokens
.get(i)
.map_or(start, |t| start + TextSize::of(t.text));
self.errors.push(SyntaxError {
message,
code,
range: TextRange::new(start, end),
});
}
fn unexpected_token_diag(&self, context: &str) -> (&'static str, String) {
if self.profile == Profile::Typl {
match self.current() {
Some(SyntaxKind::Duration) => {
return ("TYPL-302", "duration literal in typl context".to_string());
}
Some(SyntaxKind::At) => {
return ("TYPL-302", "timing annotation in typl context".to_string());
}
_ => {}
}
}
("FORM-102", format!("unexpected token {context}"))
}
fn err_and_recover(&mut self, context: &str, is_sync: impl Fn(SyntaxKind) -> bool) {
let (code, message) = self.unexpected_token_diag(context);
self.error_at_current(code, message);
self.recover(is_sync);
}
fn recover(&mut self, is_sync: impl Fn(SyntaxKind) -> bool) {
self.start(SyntaxKind::ErrorNode);
self.bump();
while let Some(kind) = self.current() {
if is_sync(kind) {
break;
}
self.bump();
}
self.builder.finish_node();
}
fn source_file(&mut self) {
self.builder.start_node(SyntaxKind::SourceFile.into());
self.eat_trivia();
if self.at(SyntaxKind::PackageKw) {
self.package_decl();
} else {
self.error_at_current("FORM-104", "missing `package` declaration".to_string());
}
loop {
self.eat_trivia();
match self.current() {
None => break,
Some(SyntaxKind::PackageKw) => self.package_decl(),
Some(SyntaxKind::ImportKw) => self.import(),
Some(
SyntaxKind::InternalKw
| SyntaxKind::ErrorKw
| SyntaxKind::TypeKw
| SyntaxKind::ConstKw
| SyntaxKind::StructKw
| SyntaxKind::EnumKw
| SyntaxKind::EnumsetKw
| SyntaxKind::UnionKw
| SyntaxKind::InterfaceKw,
) => {
if self.profile == Profile::Rsdl {
self.rsdl_typl_word_at_top_level();
} else {
self.definition();
}
}
Some(SyntaxKind::ServiceKw) => self.service_def(),
Some(SyntaxKind::SystemKw) => {
self.rsdl_container(SyntaxKind::SystemDef, RsdlBody::Members);
}
Some(SyntaxKind::ComponentKw) => {
self.rsdl_container(SyntaxKind::ComponentDef, RsdlBody::ComponentLines);
}
Some(SyntaxKind::DistributionKw) => {
self.rsdl_container(SyntaxKind::DistributionDef, RsdlBody::Members);
}
Some(SyntaxKind::DeploymentKw) => self.deployment_def(),
Some(SyntaxKind::ReservedWord) => {
if !self.profile_boundary_at_top_level() {
self.err_and_recover("at top level", is_top_level_start);
}
}
Some(_) => self.err_and_recover("at top level", is_top_level_start),
}
}
self.builder.finish_node();
}
fn profile_boundary_at_top_level(&mut self) -> bool {
let text = self.tokens[self.significant_pos()].text;
let (code, message) = match self.profile {
Profile::Typl => match keywords::ridl_keyword(text) {
None => return false,
Some(SyntaxKind::RequireKw | SyntaxKind::EnsureKw) => (
"TYPL-303",
"require/ensure attribute in typl context".to_string(),
),
Some(_) => (
"TYPL-304",
format!("interaction declaration in typl context: `{text}`"),
),
},
Profile::Ridl => (
"RIDL-403",
format!(
"`{text}` is reserved by another profile of the family, so it cannot start a \
declaration here — a `.ridl` file declares `interface` and `service`. Move \
the declaration to the profile that owns the word, or, if `{text}` was meant \
as a name, choose one the family does not reserve (ridl reference §16.4)"
),
),
Profile::Rsdl => {
self.rsdl_foreign_declaration(0);
return true;
}
};
self.error_at_current(code, message);
self.recover(is_top_level_start);
true
}
fn rsdl_typl_word_at_top_level(&mut self) {
let mut modifiers = 0;
while matches!(
self.nth(modifiers),
Some(SyntaxKind::InternalKw | SyntaxKind::ErrorKw)
) {
modifiers += 1;
}
if modifiers > 0 && self.nth(modifiers).is_some_and(is_rsdl_declaration_start) {
for _ in 0..modifiers {
let word = self.current_text();
self.error_at_current(
"FORM-102",
format!(
"`{word}` cannot modify an rsdl declaration — the closure is \
workspace-wide, so every declaration it names is visible from the \
system's package (rsdl reference §2)"
),
);
self.start(SyntaxKind::ErrorNode);
self.bump();
self.builder.finish_node();
}
return;
}
self.rsdl_foreign_declaration(modifiers);
}
fn rsdl_foreign_declaration(&mut self, modifiers: usize) {
let keyword = self.nth_text(modifiers);
self.error_at_current("RSDL-604", rsdl_foreign_declaration_message(keyword));
self.start(SyntaxKind::ErrorNode);
for _ in 0..=modifiers {
self.bump();
}
let mut depth = 0usize;
while let Some(kind) = self.current() {
match kind {
SyntaxKind::LBrace => depth += 1,
SyntaxKind::RBrace if depth > 0 => depth -= 1,
_ if depth == 0
&& (is_top_level_start(kind) || kind == SyntaxKind::ReservedWord) =>
{
break;
}
_ => {}
}
self.bump();
}
self.builder.finish_node();
}
fn nth_text(&self, n: usize) -> &'a str {
self.tokens[self.pos..]
.iter()
.filter(|token| !token.kind.is_trivia())
.nth(n)
.map_or("", |token| token.text)
}
fn rsdl_container(&mut self, kind: SyntaxKind, body: RsdlBody) {
self.start(kind);
self.bump(); self.name();
if self.at(SyntaxKind::LBracket) {
self.attr_block();
}
self.rsdl_body(body);
self.builder.finish_node();
}
fn deployment_def(&mut self) {
self.start(SyntaxKind::DeploymentDef);
self.bump(); self.name();
if self.at(SyntaxKind::ForKw) {
self.bump(); self.reference();
} else {
self.error_at_current(
"FORM-101",
"expected `for` and the system this deployment places".to_string(),
);
}
if self.at(SyntaxKind::LBracket) {
self.attr_block();
}
self.rsdl_body(RsdlBody::Machines);
self.builder.finish_node();
}
fn rsdl_body(&mut self, body: RsdlBody) {
if !self.block_open() {
return;
}
loop {
self.eat_trivia();
match self.current() {
None => {
self.error_at_current("FORM-103", "unclosed `{`".to_string());
break;
}
Some(SyntaxKind::RBrace) => {
self.bump();
break;
}
Some(SyntaxKind::Comma) => self.bump(),
Some(kind) if body.starts_line(kind) => match body {
RsdlBody::Members => self.member_line(),
RsdlBody::ComponentLines => self.component_line(),
RsdlBody::Machines => {
self.rsdl_container(SyntaxKind::MachineDef, RsdlBody::Members);
}
},
Some(kind) if is_top_level_start(kind) => {
self.error_at_current("FORM-103", "unclosed `{`".to_string());
break;
}
Some(_) => {
let sync = move |kind: SyntaxKind| {
matches!(kind, SyntaxKind::RBrace | SyntaxKind::Comma)
|| body.starts_line(kind)
|| is_top_level_start(kind)
};
if body == RsdlBody::ComponentLines && self.at_path_segment() {
self.error_at_current(
"FORM-102",
format!(
"`{}` is a second reference — a component line takes one \
reference, so write its own `offers` or `requires` before it \
(rsdl reference §3.2)",
self.current_text()
),
);
self.recover(sync);
} else {
let context = match body {
RsdlBody::Members => "in a member list",
RsdlBody::ComponentLines => "in a component body",
RsdlBody::Machines => "in a deployment body",
};
self.err_and_recover(context, sync);
}
}
}
}
}
fn member_line(&mut self) {
self.start(SyntaxKind::MemberLine);
self.reference();
if self.at(SyntaxKind::LBracket) {
self.attr_block();
}
self.builder.finish_node();
}
fn component_line(&mut self) {
self.start(SyntaxKind::ComponentLine);
self.bump(); self.reference();
if self.at(SyntaxKind::LBracket) {
self.attr_block();
}
self.builder.finish_node();
}
fn reference(&mut self) {
if !self.at_path_segment() {
self.error_at_current("FORM-101", "expected a reference".to_string());
return;
}
self.start(SyntaxKind::Reference);
self.qualified_name();
self.builder.finish_node();
}
fn package_decl(&mut self) {
self.start(SyntaxKind::PackageDecl);
self.bump(); self.qualified_name();
self.builder.finish_node();
}
fn import(&mut self) {
self.start(SyntaxKind::Import);
self.bump(); self.qualified_name();
if self.at(SyntaxKind::AsKw) {
self.bump();
self.name();
}
self.builder.finish_node();
}
fn definition(&mut self) {
let mut n = 0;
while matches!(
self.nth(n),
Some(SyntaxKind::InternalKw | SyntaxKind::ErrorKw)
) {
n += 1;
}
match self.nth(n) {
Some(SyntaxKind::TypeKw) => self.type_def(),
Some(SyntaxKind::ConstKw) => self.const_def(),
Some(SyntaxKind::StructKw) => self.struct_def(),
Some(SyntaxKind::EnumKw) => self.enum_def(),
Some(SyntaxKind::EnumsetKw) => self.enum_set_def(),
Some(SyntaxKind::UnionKw) => self.union_def(),
Some(SyntaxKind::InterfaceKw) => self.interface_def(),
_ => self.err_and_recover("in a definition", is_top_level_start),
}
}
fn modifiers(&mut self) {
while matches!(
self.current(),
Some(SyntaxKind::InternalKw | SyntaxKind::ErrorKw)
) {
self.bump();
}
}
fn type_def(&mut self) {
self.start(SyntaxKind::TypeDef);
self.modifiers();
self.bump(); self.name();
self.expect(SyntaxKind::Colon);
let stringish = self.backing();
if self.at(SyntaxKind::LBracket) {
self.constraint(stringish);
}
self.init_value_opt();
self.builder.finish_node();
}
fn backing(&mut self) -> bool {
match self.current() {
Some(
kw @ (SyntaxKind::BooleanKw
| SyntaxKind::IntegerKw
| SyntaxKind::FloatKw
| SyntaxKind::StringKw
| SyntaxKind::BytesKw),
) => {
self.start(SyntaxKind::PrimitiveType);
self.bump();
self.builder.finish_node();
matches!(kw, SyntaxKind::StringKw | SyntaxKind::BytesKw)
}
Some(SyntaxKind::Ident | SyntaxKind::Slash | SyntaxKind::Dot | SyntaxKind::Percent) => {
self.start(SyntaxKind::UnitExpr);
while matches!(
self.current(),
Some(
SyntaxKind::Ident
| SyntaxKind::Slash
| SyntaxKind::Dot
| SyntaxKind::Percent
)
) {
self.bump();
}
self.builder.finish_node();
false
}
_ => {
self.error_at_current("FORM-101", "expected a backing type".to_string());
false
}
}
}
fn const_def(&mut self) {
self.start(SyntaxKind::ConstDef);
self.modifiers();
self.bump(); self.name();
if self.at(SyntaxKind::Colon) {
self.bump();
self.path_type();
}
self.expect(SyntaxKind::Eq);
self.literal();
self.builder.finish_node();
}
fn struct_def(&mut self) {
self.start(SyntaxKind::StructDef);
self.modifiers();
self.bump(); self.name();
self.block_body("in a struct body", true, Self::field_def);
self.builder.finish_node();
}
fn enum_def(&mut self) {
self.start(SyntaxKind::EnumDef);
self.modifiers();
self.bump(); self.name();
self.block_body("in an enum body", true, Self::enum_value);
self.builder.finish_node();
}
fn enum_set_def(&mut self) {
self.start(SyntaxKind::EnumSetDef);
self.modifiers();
self.bump(); self.name();
if self.at(SyntaxKind::Colon) {
self.bump();
self.path_type();
} else {
self.block_body("in an enumset body", false, Self::enum_set_bit);
}
self.builder.finish_node();
}
fn union_def(&mut self) {
self.start(SyntaxKind::UnionDef);
self.modifiers();
self.bump(); self.name();
self.block_body("in a union body", true, Self::union_arm);
self.builder.finish_node();
}
fn union_arm(&mut self) {
self.start(SyntaxKind::UnionArm);
self.name();
self.expect(SyntaxKind::Colon);
self.path_type();
self.builder.finish_node();
}
fn interface_def(&mut self) {
self.start(SyntaxKind::InterfaceDef);
self.modifiers();
self.bump(); self.name();
if !self.block_open() {
self.builder.finish_node();
return;
}
self.interface_body("an interface", "interface");
self.builder.finish_node();
}
fn interface_body(&mut self, noun: &str, shape: &str) {
loop {
self.eat_trivia();
match self.current() {
None => {
self.error_at_current("FORM-103", "unclosed `{`".to_string());
break;
}
Some(SyntaxKind::RBrace) => {
self.bump();
break;
}
Some(SyntaxKind::Comma) => self.bump(),
Some(SyntaxKind::ReservedKw) => self.reserved_entry(),
Some(SyntaxKind::SignalKw) => self.value_interaction(SyntaxKind::SignalDef),
Some(SyntaxKind::EventKw) => self.value_interaction(SyntaxKind::EventDef),
Some(SyntaxKind::CommandKw) => self.callable_interaction(SyntaxKind::CommandDef),
Some(SyntaxKind::QueryKw) => self.callable_interaction(SyntaxKind::QueryDef),
Some(SyntaxKind::FixedKw) => self.value_interaction(SyntaxKind::FixedDef),
Some(kind)
if is_typl_definition_start(kind) && self.interface_body_close_ahead() =>
{
self.error_at_current(
"RIDL-107",
format!(
"`{}` starts a typl declaration, and {noun} body holds interactions \
and `reserved` tombstones only — move the declaration to package \
level, beside the {shape} (ridl reference §14.1)",
self.current_text(),
),
);
self.recover_definition_in_body();
}
Some(kind) if is_top_level_start(kind) => {
self.error_at_current("FORM-103", "unclosed `{`".to_string());
break;
}
Some(_) => self.err_and_recover("in an interface body", |kind| {
matches!(
kind,
SyntaxKind::RBrace | SyntaxKind::Comma | SyntaxKind::ReservedKw
) || is_interaction_start(kind)
|| is_top_level_start(kind)
}),
}
}
}
fn service_def(&mut self) {
self.start(SyntaxKind::ServiceDef);
self.bump(); self.dotted_name();
match self.current() {
Some(SyntaxKind::Colon) => {
self.bump(); self.service_shape_list();
}
Some(SyntaxKind::LBrace) => {
self.bump(); self.interface_body("a service", "service");
}
_ => self.error_at_current(
"FORM-101",
"expected `:` and an interface list, or `{` and an inline shape".to_string(),
),
}
self.builder.finish_node();
}
fn service_shape_list(&mut self) {
self.path_type();
while self.at(SyntaxKind::Comma) {
self.bump(); if !self.at_path_segment() {
break;
}
self.path_type();
}
}
fn dotted_name(&mut self) {
self.start(SyntaxKind::DottedName);
if self.at(SyntaxKind::Ident) {
self.bump();
while self.at(SyntaxKind::Dot) {
self.bump(); if self.at(SyntaxKind::Ident) {
self.bump();
} else {
self.error_at_current(
"FORM-101",
"expected a name segment after `.`".to_string(),
);
break;
}
}
} else {
self.error_at_current("FORM-101", "expected a service name".to_string());
}
self.builder.finish_node();
}
fn interface_body_close_ahead(&self) -> bool {
let mut depth = 0usize;
for token in self.tokens[self.significant_pos()..]
.iter()
.filter(|token| !token.kind.is_trivia())
{
match token.kind {
SyntaxKind::LBrace => depth += 1,
SyntaxKind::RBrace if depth == 0 => return true,
SyntaxKind::RBrace => depth -= 1,
SyntaxKind::PackageKw
| SyntaxKind::ImportKw
| SyntaxKind::InterfaceKw
| SyntaxKind::ServiceKw
if depth == 0 =>
{
return false;
}
_ => {}
}
}
false
}
fn recover_definition_in_body(&mut self) {
self.start(SyntaxKind::ErrorNode);
self.bump(); let mut depth = 0usize;
while let Some(kind) = self.current() {
match kind {
SyntaxKind::LBrace => depth += 1,
SyntaxKind::RBrace if depth > 0 => depth -= 1,
_ if depth == 0
&& (matches!(
kind,
SyntaxKind::RBrace | SyntaxKind::Comma | SyntaxKind::ReservedKw
) || is_interaction_start(kind)
|| is_top_level_start(kind)) =>
{
break;
}
_ => {}
}
self.bump();
}
self.builder.finish_node();
}
fn value_interaction(&mut self, kind: SyntaxKind) {
self.start(kind);
self.bump(); self.name();
self.expect(SyntaxKind::Colon);
self.field_type();
self.init_value_opt();
self.interaction_annotations();
self.builder.finish_node();
}
fn callable_interaction(&mut self, kind: SyntaxKind) {
self.start(kind);
self.bump(); self.name();
self.param_list();
if self.at(SyntaxKind::Colon) {
self.bump();
self.return_type();
} else if kind == SyntaxKind::QueryDef {
self.error_at_current("FORM-101", "expected `:` and a return type".to_string());
}
self.interaction_annotations();
self.builder.finish_node();
}
fn param_list(&mut self) {
if !self.at(SyntaxKind::LParen) {
self.error_at_current("FORM-101", "expected `(`".to_string());
return;
}
self.start(SyntaxKind::ParamList);
self.bump(); loop {
self.eat_trivia();
match self.current() {
None => {
self.error_at_current("FORM-103", "unclosed `(`".to_string());
break;
}
Some(SyntaxKind::RParen) => {
self.bump();
break;
}
Some(SyntaxKind::Comma) => self.bump(),
Some(SyntaxKind::Ident) => self.param(),
Some(kind)
if kind == SyntaxKind::RBrace
|| is_interaction_start(kind)
|| is_top_level_start(kind) =>
{
self.error_at_current("FORM-103", "unclosed `(`".to_string());
break;
}
Some(_) => self.err_and_recover("in a parameter list", |kind| {
matches!(
kind,
SyntaxKind::RParen
| SyntaxKind::Comma
| SyntaxKind::Ident
| SyntaxKind::RBrace
) || is_interaction_start(kind)
|| is_top_level_start(kind)
}),
}
}
self.builder.finish_node();
}
fn param(&mut self) {
self.start(SyntaxKind::Param);
self.name();
self.expect(SyntaxKind::Colon);
self.field_type();
self.builder.finish_node();
}
fn return_type(&mut self) {
self.eat_trivia();
match self.current() {
Some(SyntaxKind::LParen) => {
self.start(SyntaxKind::ReturnType);
self.tuple_type();
self.builder.finish_node();
}
Some(SyntaxKind::Lt) => {
self.start(SyntaxKind::ReturnType);
self.stream_type();
self.builder.finish_node();
}
_ if self.at_path_segment() => {
self.start(SyntaxKind::ReturnType);
let checkpoint = self.builder.checkpoint();
self.path_type();
if self.at(SyntaxKind::Pipe) {
self.builder
.start_node_at(checkpoint, SyntaxKind::FallibleType.into());
self.bump(); self.path_type();
self.builder.finish_node();
}
self.builder.finish_node();
}
_ => {
self.error_at_current(
"FORM-101",
"a return type must be a named type, a named-field tuple `(a: A, b: B)`, a \
stream `<T>`, or a fallible `T | E` (ridl reference §7.1)"
.to_string(),
);
if self.current().is_some_and(|kind| !is_return_sync(kind)) {
self.recover(is_return_sync);
}
}
}
}
fn stream_type(&mut self) {
self.start(SyntaxKind::StreamType);
self.bump(); if matches!(
self.current(),
Some(SyntaxKind::StringKw | SyntaxKind::BytesKw)
) && self.nth(1) == Some(SyntaxKind::Gt)
{
self.bump(); } else if self.at_path_segment() {
self.path_type();
} else {
self.error_at_current("FORM-101", "expected an element type".to_string());
}
self.expect(SyntaxKind::Gt);
self.builder.finish_node();
}
fn interaction_annotations(&mut self) {
let mut seen_timing = false;
let mut seen_attrs = false;
loop {
match self.current() {
Some(SyntaxKind::At) if !seen_timing => {
seen_timing = true;
self.timing();
}
Some(SyntaxKind::LBracket) if !seen_attrs => {
seen_attrs = true;
self.attr_block();
}
_ => break,
}
}
}
fn timing(&mut self) {
self.start(SyntaxKind::Timing);
self.bump(); if self.at(SyntaxKind::Duration) {
self.bump();
} else if self.at(SyntaxKind::LBracket) {
self.bump();
self.timing_range();
self.expect(SyntaxKind::RBracket);
} else {
self.error_at_current("FORM-101", "expected a duration or `[`".to_string());
}
self.builder.finish_node();
}
fn timing_range(&mut self) {
self.start(SyntaxKind::TimingRange);
let has_min = self.at(SyntaxKind::Duration);
if has_min {
self.bump();
}
self.expect(SyntaxKind::DotDot);
let has_max = self.at(SyntaxKind::Duration);
if has_max {
self.bump();
}
if !has_min && !has_max {
self.error_at_current("FORM-101", "expected a duration".to_string());
}
self.builder.finish_node();
}
fn attr_block(&mut self) {
self.start(SyntaxKind::AttrBlock);
self.bump(); let mut saw_attribute = false;
loop {
match self.current() {
None => {
self.error_at_current("FORM-103", "unclosed `[`".to_string());
break;
}
Some(SyntaxKind::RBracket) => {
if !saw_attribute {
self.error_at_current("FORM-101", "expected an attribute".to_string());
}
self.bump();
break;
}
Some(SyntaxKind::Comma) => self.bump(),
Some(
SyntaxKind::Ident
| SyntaxKind::RequireKw
| SyntaxKind::EnsureKw
| SyntaxKind::ReservedWord,
) => {
saw_attribute = true;
self.attribute();
}
Some(kind)
if kind == SyntaxKind::RBrace
|| is_interaction_start(kind)
|| is_top_level_start(kind) =>
{
self.error_at_current("FORM-103", "unclosed `[`".to_string());
break;
}
Some(_) => self.err_and_recover("in an attribute block", |kind| {
matches!(
kind,
SyntaxKind::RBracket
| SyntaxKind::Comma
| SyntaxKind::Ident
| SyntaxKind::RequireKw
| SyntaxKind::EnsureKw
| SyntaxKind::ReservedWord
| SyntaxKind::RBrace
) || is_interaction_start(kind)
|| is_top_level_start(kind)
}),
}
}
self.builder.finish_node();
}
fn attribute(&mut self) {
self.start(SyntaxKind::Attribute);
match self.current() {
Some(SyntaxKind::RequireKw | SyntaxKind::EnsureKw) => {
self.bump(); self.expr();
}
_ => {
self.name(); if self.profile == Profile::Rsdl && self.at(SyntaxKind::Dot) {
self.bump(); self.name();
}
if self.at(SyntaxKind::Eq) {
self.bump();
self.attr_value();
}
}
}
self.builder.finish_node();
}
fn attr_value(&mut self) {
if self.depth >= MAX_TYPE_DEPTH {
self.error_at_current(
"FORM-102",
format!("value nesting deeper than {MAX_TYPE_DEPTH} levels"),
);
self.start(SyntaxKind::ErrorNode);
self.bump();
self.builder.finish_node();
return;
}
self.depth += 1;
self.attr_value_inner();
self.depth -= 1;
}
fn attr_value_inner(&mut self) {
self.eat_trivia();
if self.at(SyntaxKind::LParen) {
self.start(SyntaxKind::AttrValue);
self.bump(); loop {
self.eat_trivia();
match self.current() {
None => {
self.error_at_current("FORM-103", "unclosed `(`".to_string());
break;
}
Some(SyntaxKind::RParen) => {
self.bump();
break;
}
Some(SyntaxKind::Comma) => self.bump(),
Some(SyntaxKind::LParen) => self.attr_value(),
Some(kind)
if kind == SyntaxKind::RBracket
|| kind == SyntaxKind::RBrace
|| is_interaction_start(kind)
|| is_top_level_start(kind) =>
{
self.error_at_current("FORM-103", "unclosed `(`".to_string());
break;
}
_ if self.literal_len_at(0) > 0 => self.attr_value(),
Some(_) => self.err_and_recover("in an attribute value", |kind| {
matches!(
kind,
SyntaxKind::RParen
| SyntaxKind::Comma
| SyntaxKind::LParen
| SyntaxKind::RBracket
| SyntaxKind::RBrace
) || is_interaction_start(kind)
|| is_top_level_start(kind)
}),
}
}
self.builder.finish_node();
} else if self.literal_len_at(0) > 0 {
self.start(SyntaxKind::AttrValue);
self.literal();
self.builder.finish_node();
} else {
self.error_at_current("FORM-101", "expected a value".to_string());
}
}
fn expr(&mut self) {
self.expr_refused = false;
self.or_expr();
}
fn expr_fits(&mut self, height: usize) -> bool {
if self.depth + height <= MAX_TYPE_DEPTH {
return true;
}
if !self.expr_refused {
self.expr_refused = true;
self.error_at_current(
"FORM-102",
format!(
"expression nesting deeper than {MAX_TYPE_DEPTH} levels (each operator, \
member access, parenthesised group and prefix nests one level)"
),
);
}
false
}
fn skip_expr_rest(&mut self) {
self.start(SyntaxKind::ErrorNode);
self.skip_expr_tokens();
self.builder.finish_node();
}
fn skip_expr_tokens(&mut self) {
let mut open = 0usize;
let mut after_operand = false;
while let Some(kind) = self.current() {
match kind {
SyntaxKind::RBracket
| SyntaxKind::Comma
| SyntaxKind::LBrace
| SyntaxKind::RBrace
| SyntaxKind::RequireKw
| SyntaxKind::EnsureKw
| SyntaxKind::ReservedWord => break,
SyntaxKind::RParen if open == 0 => break,
SyntaxKind::Ident if after_operand => break,
_ if is_interaction_start(kind) || is_top_level_start(kind) => break,
SyntaxKind::RParen => open -= 1,
SyntaxKind::LParen => open += 1,
_ => {}
}
after_operand = matches!(
kind,
SyntaxKind::Ident
| SyntaxKind::IntNumber
| SyntaxKind::FloatNumber
| SyntaxKind::String
| SyntaxKind::TrueKw
| SyntaxKind::FalseKw
| SyntaxKind::Duration
| SyntaxKind::RParen
);
self.bump();
}
}
fn chain(&mut self, is_op: fn(SyntaxKind) -> bool, operand: fn(&mut Self) -> usize) -> usize {
self.eat_trivia();
let checkpoint = self.builder.checkpoint();
let mut height = operand(self);
while self.current().is_some_and(is_op) {
if !self.expr_fits(height.max(1) + 1) {
self.skip_expr_rest();
break;
}
self.builder
.start_node_at(checkpoint, SyntaxKind::BinaryExpr.into());
self.bump();
self.depth += 1;
let rhs = operand(self);
self.depth -= 1;
self.builder.finish_node();
height = 1 + height.max(rhs);
}
height
}
fn or_expr(&mut self) -> usize {
self.chain(|kind| kind == SyntaxKind::PipePipe, Self::and_expr)
}
fn and_expr(&mut self) -> usize {
self.chain(|kind| kind == SyntaxKind::AmpAmp, Self::cmp_expr)
}
fn cmp_expr(&mut self) -> usize {
self.eat_trivia();
let checkpoint = self.builder.checkpoint();
let mut height = self.add_expr();
if matches!(
self.current(),
Some(
SyntaxKind::EqEq
| SyntaxKind::Neq
| SyntaxKind::Lt
| SyntaxKind::Le
| SyntaxKind::Gt
| SyntaxKind::Ge
)
) {
if !self.expr_fits(height.max(1) + 1) {
self.skip_expr_rest();
return height;
}
self.builder
.start_node_at(checkpoint, SyntaxKind::BinaryExpr.into());
self.bump();
self.depth += 1;
let rhs = self.add_expr();
self.depth -= 1;
self.builder.finish_node();
height = 1 + height.max(rhs);
}
height
}
fn add_expr(&mut self) -> usize {
self.chain(
|kind| matches!(kind, SyntaxKind::Plus | SyntaxKind::Minus),
Self::mul_expr,
)
}
fn mul_expr(&mut self) -> usize {
self.chain(
|kind| {
matches!(
kind,
SyntaxKind::Star | SyntaxKind::Slash | SyntaxKind::Percent
)
},
Self::unary_expr,
)
}
fn unary_expr(&mut self) -> usize {
if matches!(self.current(), Some(SyntaxKind::Bang | SyntaxKind::Minus)) {
if !self.expr_fits(2) {
self.skip_expr_rest();
return 1;
}
self.start(SyntaxKind::PrefixExpr);
self.bump();
self.depth += 1;
let operand = self.postfix_expr();
self.depth -= 1;
self.builder.finish_node();
1 + operand
} else {
self.postfix_expr()
}
}
fn postfix_expr(&mut self) -> usize {
self.eat_trivia();
let checkpoint = self.builder.checkpoint();
let mut height = self.primary();
while self.at(SyntaxKind::Dot) {
if !self.expr_fits(height + 1) {
self.skip_expr_rest();
break;
}
self.builder
.start_node_at(checkpoint, SyntaxKind::MemberExpr.into());
self.bump(); if self.at(SyntaxKind::Ident) {
self.bump();
} else {
self.error_at_current("FORM-101", "expected a member name".to_string());
}
self.builder.finish_node();
height += 1;
}
height
}
fn primary(&mut self) -> usize {
match self.current() {
Some(
SyntaxKind::IntNumber
| SyntaxKind::FloatNumber
| SyntaxKind::String
| SyntaxKind::TrueKw
| SyntaxKind::FalseKw
| SyntaxKind::Duration,
) => {
self.start(SyntaxKind::LiteralExpr);
if self.at(SyntaxKind::IntNumber) {
self.flag_leading_zeros();
}
self.bump();
self.builder.finish_node();
1
}
Some(SyntaxKind::Ident) => {
self.start(SyntaxKind::PathExpr);
self.bump();
self.builder.finish_node();
1
}
Some(SyntaxKind::LParen) => {
if !self.expr_fits(2) {
self.start(SyntaxKind::ErrorNode);
self.bump(); self.skip_expr_tokens();
if self.at(SyntaxKind::RParen) {
self.bump();
}
self.builder.finish_node();
return 1;
}
self.start(SyntaxKind::ParenExpr);
self.bump();
self.depth += 1;
let inner = self.or_expr();
self.depth -= 1;
self.expect(SyntaxKind::RParen);
self.builder.finish_node();
1 + inner
}
_ => {
self.error_at_current("FORM-101", "expected an expression".to_string());
0
}
}
}
fn enum_value(&mut self) {
self.value_assignment(SyntaxKind::EnumValue);
}
fn enum_set_bit(&mut self) {
self.value_assignment(SyntaxKind::EnumSetBit);
}
fn value_assignment(&mut self, kind: SyntaxKind) {
self.start(kind);
self.name();
self.expect(SyntaxKind::Eq);
self.literal();
self.builder.finish_node();
}
fn block_body(&mut self, context: &str, allow_reserved: bool, member: fn(&mut Self)) {
if !self.block_open() {
return;
}
loop {
self.eat_trivia();
match self.current() {
None => {
self.error_at_current("FORM-103", "unclosed `{`".to_string());
break;
}
Some(SyntaxKind::RBrace) => {
self.bump();
break;
}
Some(SyntaxKind::Comma) => self.bump(),
Some(SyntaxKind::ReservedKw) if allow_reserved => self.reserved_entry(),
Some(SyntaxKind::Ident) => member(self),
Some(kind) if is_top_level_start(kind) => {
self.error_at_current("FORM-103", "unclosed `{`".to_string());
break;
}
Some(_) => self.err_and_recover(context, |kind| {
matches!(
kind,
SyntaxKind::RBrace | SyntaxKind::Comma | SyntaxKind::Ident
) || (allow_reserved && kind == SyntaxKind::ReservedKw)
|| is_top_level_start(kind)
}),
}
}
}
fn block_open(&mut self) -> bool {
if self.at(SyntaxKind::LBrace) {
self.bump();
true
} else {
self.error_at_current("FORM-101", "expected `{`".to_string());
false
}
}
fn field_def(&mut self) {
self.start(SyntaxKind::FieldDef);
self.name();
self.expect(SyntaxKind::Colon);
self.field_type();
self.init_value_opt();
self.builder.finish_node();
}
fn reserved_entry(&mut self) {
self.start(SyntaxKind::ReservedEntry);
self.bump(); if self.at(SyntaxKind::Ident) {
self.name();
} else if let Some(kind) = self.literal_value_kind_at(0) {
if kind == SyntaxKind::IntNumber {
self.literal();
} else {
self.start(SyntaxKind::ErrorNode);
if self.at(SyntaxKind::Minus) {
self.bump();
}
self.error_at_current(
"FORM-102",
format!(
"a `reserved` tombstone takes a retired member name or a retired integer value, not {}",
ungrammatical_reserved_noun(kind),
),
);
self.bump();
self.builder.finish_node();
}
} else {
self.error_at_current(
"FORM-101",
"expected a name or an integer value".to_string(),
);
}
self.builder.finish_node();
}
fn field_type(&mut self) {
if self.depth >= MAX_TYPE_DEPTH {
self.error_at_current(
"FORM-102",
format!("type nesting deeper than {MAX_TYPE_DEPTH} levels"),
);
self.start(SyntaxKind::ErrorNode);
self.bump();
self.builder.finish_node();
return;
}
self.depth += 1;
self.field_type_inner();
self.depth -= 1;
}
fn field_type_inner(&mut self) {
self.eat_trivia();
let checkpoint = self.builder.checkpoint();
match self.current() {
Some(
kw @ (SyntaxKind::BooleanKw
| SyntaxKind::IntegerKw
| SyntaxKind::FloatKw
| SyntaxKind::StringKw
| SyntaxKind::BytesKw),
) => {
self.start(SyntaxKind::PrimitiveType);
self.bump();
if self.at(SyntaxKind::LBracket) {
let stringish = matches!(kw, SyntaxKind::StringKw | SyntaxKind::BytesKw);
self.constraint(stringish);
}
self.builder.finish_node();
}
Some(SyntaxKind::Ident) => self.path_type(),
Some(SyntaxKind::LParen) => self.tuple_type(),
Some(SyntaxKind::LBracket) => self.array_or_map(checkpoint),
Some(SyntaxKind::Lt) => {
if self.profile == Profile::Typl {
self.error_at_current("TYPL-301", "stream type in typl context".to_string());
}
self.stream_type();
}
_ => {
self.error_at_current("FORM-101", "expected a type".to_string());
return;
}
}
let mut wraps = 0;
while self.at(SyntaxKind::Question) {
if wraps >= MAX_TYPE_DEPTH {
self.error_at_current(
"FORM-102",
format!("type nesting deeper than {MAX_TYPE_DEPTH} levels"),
);
self.start(SyntaxKind::ErrorNode);
while self.at(SyntaxKind::Question) {
self.bump();
}
self.builder.finish_node();
break;
}
self.builder
.start_node_at(checkpoint, SyntaxKind::OptionalType.into());
self.bump();
self.builder.finish_node();
wraps += 1;
}
}
fn tuple_type(&mut self) {
self.start(SyntaxKind::TupleType);
self.bump(); loop {
self.eat_trivia();
match self.current() {
None => {
self.error_at_current("FORM-103", "unclosed `(`".to_string());
break;
}
Some(SyntaxKind::RParen) => {
self.bump();
break;
}
Some(SyntaxKind::Comma) => self.bump(),
Some(SyntaxKind::Ident) => self.tuple_field(),
Some(kind) if kind == SyntaxKind::RBrace || is_top_level_start(kind) => {
self.error_at_current("FORM-103", "unclosed `(`".to_string());
break;
}
Some(_) => self.err_and_recover("in a tuple type", |kind| {
matches!(
kind,
SyntaxKind::RParen
| SyntaxKind::Comma
| SyntaxKind::Ident
| SyntaxKind::RBrace
) || is_top_level_start(kind)
}),
}
}
self.builder.finish_node();
}
fn tuple_field(&mut self) {
self.start(SyntaxKind::TupleField);
self.name();
self.expect(SyntaxKind::Colon);
self.field_type();
self.builder.finish_node();
}
fn array_or_map(&mut self, checkpoint: rowan::Checkpoint) {
self.bump(); self.field_type(); if self.at(SyntaxKind::Colon) {
self.builder
.start_node_at(checkpoint, SyntaxKind::MapType.into());
self.bump(); self.field_type(); } else {
self.builder
.start_node_at(checkpoint, SyntaxKind::ArrayType.into());
}
self.expect(SyntaxKind::Semicolon);
self.bound();
self.expect(SyntaxKind::RBracket);
self.builder.finish_node();
}
fn constraint(&mut self, stringish: bool) {
self.start(SyntaxKind::Constraint);
self.bump(); self.constraint_body(stringish);
self.expect(SyntaxKind::RBracket);
self.builder.finish_node();
}
fn constraint_body(&mut self, stringish: bool) {
match self.current() {
Some(SyntaxKind::MatchKw) => {
self.bump();
self.literal();
}
Some(SyntaxKind::DotDot) => {
self.bump();
self.literal();
self.step_opt();
}
_ if self.literal_len_at(0) > 0 => self.constraint_led_by_literal(stringish),
_ => self.error_at_current("FORM-101", "expected a constraint".to_string()),
}
}
fn constraint_led_by_literal(&mut self, stringish: bool) {
let first_len = self.literal_len_at(0);
match self.nth(first_len) {
Some(SyntaxKind::RBracket | SyntaxKind::MatchKw) | None => {
self.bound();
self.match_opt();
}
Some(SyntaxKind::DotDot) => {
let second_len = self.literal_len_at(first_len + 1);
let after_second = self.nth(first_len + 1 + second_len);
let bound_shape = stringish
&& second_len > 0
&& !self.literal_is_float_at(0)
&& !self.literal_is_float_at(first_len + 1)
&& matches!(
after_second,
Some(SyntaxKind::RBracket | SyntaxKind::MatchKw) | None
);
if bound_shape {
self.bound();
self.match_opt();
} else {
self.literal(); self.bump(); if self.literal_len_at(0) > 0 {
self.literal(); }
self.step_opt();
}
}
_ => {
self.bound();
self.match_opt();
}
}
}
fn bound(&mut self) {
if self.literal_len_at(0) == 0 {
self.error_at_current("FORM-101", "expected a bound".to_string());
return;
}
self.start(SyntaxKind::Bound);
self.literal();
if self.at(SyntaxKind::DotDot) {
self.bump();
self.literal();
}
self.builder.finish_node();
}
fn step_opt(&mut self) {
if self.at(SyntaxKind::StepKw) {
self.bump();
self.literal();
}
}
fn match_opt(&mut self) {
if self.at(SyntaxKind::MatchKw) {
self.bump();
self.literal();
}
}
fn init_value_opt(&mut self) {
if self.at(SyntaxKind::Eq) {
self.start(SyntaxKind::InitValue);
self.bump();
self.literal();
self.builder.finish_node();
}
}
fn literal_len_at(&self, n: usize) -> usize {
match self.nth(n) {
Some(SyntaxKind::Minus) => match self.nth(n + 1) {
Some(kind) if is_value_token(kind) => 2,
_ => 0,
},
Some(kind) if is_value_token(kind) => 1,
_ => 0,
}
}
fn literal_value_kind_at(&self, n: usize) -> Option<SyntaxKind> {
match self.nth(n) {
Some(SyntaxKind::Minus) => self.nth(n + 1).filter(|kind| is_value_token(*kind)),
Some(kind) if is_value_token(kind) => Some(kind),
_ => None,
}
}
fn literal_is_float_at(&self, n: usize) -> bool {
match self.nth(n) {
Some(SyntaxKind::FloatNumber) => true,
Some(SyntaxKind::Minus) => matches!(self.nth(n + 1), Some(SyntaxKind::FloatNumber)),
_ => false,
}
}
fn literal(&mut self) {
if self.profile == Profile::Typl && self.at(SyntaxKind::Duration) {
self.error_at_current("TYPL-302", "duration literal in typl context".to_string());
self.start(SyntaxKind::ErrorNode);
self.bump();
self.builder.finish_node();
return;
}
if self.literal_len_at(0) == 0 {
self.error_at_current("FORM-101", "expected a value".to_string());
return;
}
self.start(SyntaxKind::Literal);
if self.at(SyntaxKind::Minus) {
self.bump();
}
if self.at(SyntaxKind::IntNumber) {
self.flag_leading_zeros();
}
self.bump();
self.builder.finish_node();
}
fn flag_leading_zeros(&mut self) {
let i = self.significant_pos();
let text = self.tokens[i].text;
if text.len() > 1 && text.starts_with('0') {
self.error_at_current("FORM-005", "integer literal has leading zeros".to_string());
}
}
fn name(&mut self) {
if self.at(SyntaxKind::Ident) {
self.start(SyntaxKind::Name);
self.bump();
self.builder.finish_node();
} else if self.at(SyntaxKind::ReservedWord) {
let message = format!(
"reserved word `{}` used as identifier",
self.tokens[self.significant_pos()].text,
);
self.error_at_current("FORM-105", message);
self.start(SyntaxKind::ErrorNode);
self.bump();
self.builder.finish_node();
} else {
self.error_at_current("FORM-101", "expected a name".to_string());
}
}
fn at_path_segment(&self) -> bool {
matches!(
self.current(),
Some(
SyntaxKind::Ident
| SyntaxKind::BooleanKw
| SyntaxKind::IntegerKw
| SyntaxKind::FloatKw
| SyntaxKind::StringKw
| SyntaxKind::BytesKw
)
)
}
fn path_type(&mut self) {
if !self.at_path_segment() {
self.error_at_current("FORM-101", "expected a type name".to_string());
return;
}
self.start(SyntaxKind::PathType);
self.qualified_name();
self.builder.finish_node();
}
fn qualified_name(&mut self) {
if !self.at_path_segment() {
self.error_at_current("FORM-101", "expected a name".to_string());
return;
}
self.start(SyntaxKind::QualifiedName);
self.bump();
while self.at(SyntaxKind::Dot) {
self.bump();
if self.at_path_segment() {
self.bump();
} else {
self.error_at_current("FORM-101", "expected a name after `.`".to_string());
break;
}
}
self.builder.finish_node();
}
}
#[cfg(test)]
mod tests {
use super::*;
const FIXTURE: &str = include_str!("../fixtures/walking_skeleton.typl");
#[test]
fn fixture_round_trips_losslessly() {
let parse = parse(FIXTURE, Profile::Typl);
assert_eq!(
parse.syntax().text().to_string(),
FIXTURE,
"the tree text must reproduce the source byte for byte",
);
assert!(
parse.errors().is_empty(),
"the fixture must parse without errors, got: {:?}",
parse.errors(),
);
}
#[test]
fn mangled_input_round_trips_and_reports_errors() {
let input = "type 123 :: [";
let parse = parse(input, Profile::Typl);
assert_eq!(
parse.syntax().text().to_string(),
input,
"recovery must not drop any text",
);
assert!(
!parse.errors().is_empty(),
"broken input must produce at least one error",
);
}
#[test]
fn parse_equality_is_green_node_identity() {
assert_eq!(
parse(FIXTURE, Profile::Typl),
parse(FIXTURE, Profile::Typl),
"parsing identical input twice must compare equal",
);
assert_ne!(
parse(FIXTURE, Profile::Typl),
parse("package fixtures\ntype X: m", Profile::Typl),
"parsing different input must not compare equal",
);
}
fn error_codes(input: &str) -> Vec<&'static str> {
parse(input, Profile::Typl)
.errors()
.iter()
.map(|e| e.code)
.collect()
}
#[test]
fn missing_package_flags_form_104() {
let codes = error_codes("type Speed: km/h\n");
assert_eq!(codes, vec!["FORM-104"]);
}
#[test]
fn leading_zero_integer_flags_form_005() {
let codes = error_codes("package p\nconst BAD = 042\n");
assert_eq!(codes, vec!["FORM-005"]);
}
#[test]
fn duration_literal_flags_typl_302() {
let parse = parse("package p\nconst BAD = 10ms\n", Profile::Typl);
assert_eq!(
parse.errors().iter().map(|e| e.code).collect::<Vec<_>>(),
vec!["TYPL-302"],
);
assert_eq!(
parse.syntax().text().to_string(),
"package p\nconst BAD = 10ms\n",
"the rejected duration token must stay in the tree",
);
}
#[test]
fn stray_at_sigil_flags_typl_302() {
let codes = error_codes("package p\n@\n");
assert_eq!(codes, vec!["TYPL-302"]);
}
#[test]
fn duration_and_at_draw_no_typl_302_under_ridl() {
let parsed = parse("package p\nconst BAD = 10ms\n", Profile::Ridl);
assert!(
parsed.errors().iter().all(|e| e.code != "TYPL-302"),
"no TYPL-302 under Ridl, got: {:?}",
parsed.errors(),
);
let parsed = parse("package p\n@\n", Profile::Ridl);
assert_eq!(
parsed.errors().iter().map(|e| e.code).collect::<Vec<_>>(),
vec!["FORM-102"],
"a stray `@` under Ridl is a generic unexpected token",
);
}
#[test]
fn interaction_keyword_in_typl_flags_typl_304_and_recovers() {
let input = "package p\ninterface X {}\ntype Fine: m\n";
let parsed = parse(input, Profile::Typl);
assert_eq!(
parsed.syntax().text().to_string(),
input,
"TYPL-304 recovery must stay lossless",
);
assert_eq!(
parsed.errors().iter().map(|e| e.code).collect::<Vec<_>>(),
vec!["TYPL-304"],
);
assert_eq!(def_names_in(Profile::Typl, input), vec!["Fine"]);
let codes: Vec<_> = parse("package p\nsignal speed: Speed\n", Profile::Typl)
.errors()
.iter()
.map(|e| e.code)
.collect();
assert_eq!(codes, vec!["TYPL-304"]);
}
#[test]
fn stream_type_in_typl_flags_typl_301_and_keeps_parsing() {
let input = "package p\nstruct S {\n f : <Frame>\n g : integer [0..1]\n}\n";
let parsed = parse(input, Profile::Typl);
assert_eq!(
parsed.syntax().text().to_string(),
input,
"the TYPL-301 boundary must stay lossless",
);
assert_eq!(
parsed.errors().iter().map(|e| e.code).collect::<Vec<_>>(),
vec!["TYPL-301"],
);
let has_stream = parsed
.syntax()
.descendants()
.any(|node| node.kind() == SyntaxKind::StreamType);
assert!(has_stream, "the stream still parses into a StreamType node");
let parsed = parse(input, Profile::Ridl);
assert!(
parsed.errors().is_empty(),
"no parser error under Ridl, got: {:?}",
parsed.errors(),
);
}
#[test]
fn reserved_word_in_ridl_flags_ridl_403_and_recovers() {
let input = "package p\nmodel X {}\ntype Fine: m\n";
let parsed = parse(input, Profile::Ridl);
assert_eq!(
parsed.syntax().text().to_string(),
input,
"RIDL-403 recovery must stay lossless",
);
assert_eq!(
parsed.errors().iter().map(|e| e.code).collect::<Vec<_>>(),
vec!["RIDL-403"],
);
assert_eq!(def_names_in(Profile::Ridl, input), vec!["Fine"]);
}
#[test]
fn rsdl_declaration_in_typl_and_ridl_draws_their_existing_codes() {
let input = "package p\ndistribution Adas { Cruise }\ntype Fine: m\n";
for (profile, code) in [(Profile::Typl, "FORM-102"), (Profile::Ridl, "RIDL-403")] {
let parsed = parse(input, profile);
assert_eq!(parsed.syntax().text().to_string(), input);
assert_eq!(
parsed.errors().iter().map(|e| e.code).collect::<Vec<_>>(),
vec![code],
"under {profile:?}",
);
assert_eq!(
def_names_in(profile, input),
vec!["Fine"],
"under {profile:?}"
);
}
}
#[test]
fn rsdl_profile_parses_its_declarations_and_rejects_the_others() {
let clean = "package p\nsystem Vehicle { Cruise, veh.diag.access }\n";
assert_eq!(parse(clean, Profile::Rsdl).errors(), &[]);
for foreign in [
"type Speed: km/h",
"internal type Speed: km/h",
"struct S { a: integer [0..1] }",
"interface I { signal s: S @10ms }",
"service veh.a.b : I",
] {
let input = format!("package p\n{foreign}\nsystem Vehicle {{ Cruise }}\n");
let parsed = parse(&input, Profile::Rsdl);
assert_eq!(parsed.syntax().text().to_string(), input);
assert_eq!(
parsed.errors().iter().map(|e| e.code).collect::<Vec<_>>(),
vec!["RSDL-604"],
"`{foreign}`",
);
assert!(
parsed
.syntax()
.children()
.any(|node| node.kind() == SyntaxKind::SystemDef),
"the system after `{foreign}` still parses",
);
}
}
#[test]
fn a_backend_key_parses_under_rsdl_only() {
let rsdl = "package p\nsystem V [ linux.cpuset = (2, 3), linux.realtime ] { A }\n";
assert_eq!(parse(rsdl, Profile::Rsdl).errors(), &[]);
let ridl = "package p\ninterface I {\n command c() [ linux.cpuset = 1 ]\n}\n";
assert_eq!(
parse(ridl, Profile::Ridl)
.errors()
.iter()
.map(|e| e.code)
.collect::<Vec<_>>(),
vec!["FORM-102"],
);
}
#[test]
fn a_rejected_return_type_never_consumes_the_construct_after_it() {
for (sync, body, survivor) in [
(
"the next interaction keyword",
" query broken() :
signal speed : Speed @10ms
",
SyntaxKind::SignalDef,
),
(
"a `reserved` tombstone",
" query broken() :
reserved legacyTemp
",
SyntaxKind::ReservedEntry,
),
(
"the body's own closing brace",
" query broken() :
",
SyntaxKind::InterfaceDef,
),
] {
let input = format!(
"package p
interface I {{
{body}}}
"
);
let parsed = parse(&input, Profile::Ridl);
assert_eq!(
parsed.syntax().text().to_string(),
input,
"{sync}: recovery must stay lossless",
);
assert_eq!(
parsed.errors().iter().map(|e| e.code).collect::<Vec<_>>(),
vec!["FORM-101"],
"{sync}: the broken return type is the only diagnostic — an unclosed `{{` here means the `}}` was consumed",
);
assert!(
parsed
.syntax()
.descendants()
.any(|node| node.kind() == survivor),
"{sync}: the {survivor:?} after the broken return type is gone",
);
}
}
#[test]
fn pathological_type_nesting_does_not_panic_or_drop_text() {
let input = format!("package p\nstruct S {{ f : {} }}\n", "[".repeat(300));
let parse = parse(&input, Profile::Typl);
assert_eq!(
parse.syntax().text().to_string(),
input,
"deep nesting must stay lossless",
);
assert!(!parse.errors().is_empty());
}
#[test]
fn pathological_question_repetition_does_not_panic_or_drop_text() {
let input = format!("package p\nstruct S {{ f : T{} }}\n", "?".repeat(30_000));
let parse = parse(&input, Profile::Typl);
assert_eq!(
parse.syntax().text().to_string(),
input,
"the capped optional run must stay lossless",
);
assert_eq!(
parse.errors().iter().map(|e| e.code).collect::<Vec<_>>(),
vec!["FORM-102"],
"the cap must report exactly one error",
);
}
#[test]
fn misplaced_error_modifier_parses_clean() {
let codes = error_codes("package p\nerror type Legacy : integer [0..1]\n");
assert_eq!(codes, Vec::<&str>::new());
}
#[test]
fn error_ranges_cover_the_offending_token() {
let parse = parse("package p\nconst BAD = 042\n", Profile::Typl);
let error = &parse.errors()[0];
assert_eq!(error.code, "FORM-005");
assert_eq!(
error.range,
TextRange::new(TextSize::new(22), TextSize::new(25)),
);
}
fn def_names(input: &str) -> Vec<String> {
def_names_in(Profile::Typl, input)
}
fn def_names_in(profile: Profile, input: &str) -> Vec<String> {
use crate::ast::{AstNode, HasName, SourceFile};
let file = SourceFile::cast(parse(input, profile).syntax()).expect("root is a SourceFile");
file.definitions()
.filter_map(|def| Some(def.name()?.ident_token()?.text().to_string()))
.collect()
}
#[test]
fn top_level_garbage_resyncs_to_the_next_declaration() {
let input = "package p\ntype Good: km/h\n] } garbage 9\ntype AlsoGood: integer [0..1]\n";
let parse = parse(input, Profile::Typl);
assert_eq!(
parse.syntax().text().to_string(),
input,
"resync must stay lossless",
);
assert_eq!(def_names(input), vec!["Good", "AlsoGood"]);
assert_eq!(error_codes(input), vec!["FORM-102"]);
}
#[test]
fn unclosed_brace_resyncs_to_the_next_declaration() {
let input = "package p\nstruct Broken {\n gear: integer\n\ntype After: km/h\n";
let parse = parse(input, Profile::Typl);
assert_eq!(
parse.syntax().text().to_string(),
input,
"unclosed-brace recovery must stay lossless",
);
assert_eq!(def_names(input), vec!["Broken", "After"]);
assert_eq!(error_codes(input), vec!["FORM-103"]);
}
#[test]
fn reserved_word_as_name_flags_form_105_and_keeps_parsing() {
let input = "package p\ntype signal: integer [0..1]\ntype Fine: integer [0..1]\n";
assert_eq!(error_codes(input), vec!["FORM-105"]);
assert_eq!(def_names(input), vec!["Fine"]);
}
#[test]
fn unclosed_brackets_do_not_flood_diagnostics() {
let input = format!("package p\nstruct S {{ f : {} }}\n", "[".repeat(300));
let parse = parse(&input, Profile::Typl);
assert_eq!(
parse.syntax().text().to_string(),
input,
"the unclosed-bracket flood must stay lossless",
);
assert!(
parse.errors().len() <= 4,
"expected a bounded diagnostic count, got {}: {:?}",
parse.errors().len(),
parse.errors(),
);
}
#[test]
fn duration_in_constraint_reports_both_form_101_and_typl_302() {
assert_eq!(
error_codes("package p\ntype X: integer [0..10ms]\n"),
vec!["FORM-101", "TYPL-302"],
);
assert_eq!(
error_codes("package p\ntype X: integer [10ms]\n"),
vec!["FORM-101", "TYPL-302"],
);
assert_eq!(
error_codes("package p\ntype X: integer [0..5 @ 3]\n"),
vec!["FORM-101", "TYPL-302"],
);
}
#[test]
fn closed_deep_nesting_does_not_flood_diagnostics() {
let deep = format!("{}integer{}", "[".repeat(200), ";1]".repeat(200));
let input = format!("package p\nstruct S {{ f : {deep} }}\n");
let parse = parse(&input, Profile::Typl);
assert_eq!(
parse.syntax().text().to_string(),
input,
"the deep-nesting flood must stay lossless",
);
assert!(
parse.errors().len() <= 4,
"expected a bounded diagnostic count, got {}: {:?}",
parse.errors().len(),
parse.errors(),
);
}
#[test]
fn ungrammatical_reserved_literal_flags_form_102_in_every_body() {
let bodies = [
(
Profile::Typl,
"struct S {\n a : X\n reserved {}\n b : X\n}",
),
(
Profile::Typl,
"enum E {\n A = 1\n reserved {}\n B = 2\n}",
),
(
Profile::Typl,
"union U {\n a : X\n reserved {}\n b : X\n}",
),
(
Profile::Ridl,
"interface I {\n signal a : X\n reserved {}\n signal b : X\n}",
),
(
Profile::Ridl,
"service p.svc {\n signal a : X\n reserved {}\n signal b : X\n}",
),
];
for (profile, shape) in bodies {
for literal in ["\"oldName\"", "true", "false", "1.5", "-1.5", "-MAX"] {
let input = format!("package p\n{}\n", shape.replace("{}", literal));
let parsed = parse(&input, profile);
assert_eq!(
parsed.syntax().text().to_string(),
input,
"refusing `reserved {literal}` must stay lossless in {shape}",
);
assert_eq!(
parsed.errors().iter().map(|e| e.code).collect::<Vec<_>>(),
vec!["FORM-102"],
"`reserved {literal}` in {shape} must draw exactly one FORM-102",
);
assert!(
parsed.errors()[0].message.starts_with(
"a `reserved` tombstone takes a retired member name or a retired \
integer value, not "
),
"the message must lead with what a tombstone does take, got {:?}",
parsed.errors()[0].message,
);
let entry = parsed
.syntax()
.descendants()
.find(|node| node.kind() == SyntaxKind::ReservedEntry)
.expect("the tombstone still forms a ReservedEntry");
assert!(
entry
.children()
.any(|child| child.kind() == SyntaxKind::ErrorNode),
"the refused literal is held in an ErrorNode, got {entry:#?}",
);
assert!(
entry
.children()
.all(|child| child.kind() != SyntaxKind::Literal),
"the refused literal must not form a Literal, got {entry:#?}",
);
}
}
}
#[test]
fn grammatical_reserved_forms_still_parse_in_every_body() {
let bodies = [
(
Profile::Typl,
"struct S {\n a : X\n reserved {}\n b : X\n}",
),
(
Profile::Typl,
"enum E {\n A = 1\n reserved {}\n B = 2\n}",
),
(
Profile::Typl,
"union U {\n a : X\n reserved {}\n b : X\n}",
),
(
Profile::Ridl,
"interface I {\n signal a : X\n reserved {}\n signal b : X\n}",
),
(
Profile::Ridl,
"service p.svc {\n signal a : X\n reserved {}\n signal b : X\n}",
),
];
for (profile, shape) in bodies {
for literal in ["legacyChecksum", "3", "-3"] {
let input = format!("package p\n{}\n", shape.replace("{}", literal));
let parsed = parse(&input, profile);
assert!(
parsed.errors().is_empty(),
"`reserved {literal}` in {shape} must parse clean, got {:?}",
parsed.errors(),
);
let entry = parsed
.syntax()
.descendants()
.find(|node| node.kind() == SyntaxKind::ReservedEntry)
.expect("the tombstone forms a ReservedEntry");
let kept = entry
.children()
.any(|child| matches!(child.kind(), SyntaxKind::Name | SyntaxKind::Literal));
assert!(
kept,
"`reserved {literal}` must keep the retired identity, got {entry:#?}",
);
}
}
}
#[test]
fn a_missing_comma_ends_the_shape_list_at_the_next_identifier() {
let input = "package p\nservice p.svc : DoorControl\nStruct Foo {\n a : X\n}\n";
let parsed = parse(input, Profile::Ridl);
assert_eq!(
parsed.syntax().text().to_string(),
input,
"the ended list must stay lossless",
);
let service = parsed
.syntax()
.descendants()
.find(|node| node.kind() == SyntaxKind::ServiceDef)
.expect("the service parses");
assert_eq!(
service
.children()
.filter(|child| child.kind() == SyntaxKind::PathType)
.count(),
1,
"`Struct` must not be absorbed as a second shape, got {service:#?}",
);
let struct_offset = input.find("Struct").expect("the fixture spells it") as u32;
let error = &parsed.errors()[0];
assert_eq!(
u32::from(error.range.start()),
struct_offset,
"the diagnostic lands on the identifier, got: {:?}",
parsed.errors(),
);
}
#[test]
fn a_shape_list_admits_a_trailing_comma_and_ends_before_reserved() {
let input = "package p\nservice p.svc : DoorControl, MotorControl,\n";
let parsed = parse(input, Profile::Ridl);
assert!(
parsed.errors().is_empty(),
"the list parses clean, got: {:?}",
parsed.errors(),
);
assert_eq!(
shape_kinds(&parsed),
[SyntaxKind::PathType, SyntaxKind::PathType]
);
let input = "package p\nservice p.svc : DoorControl, reserved LegacyDiag, MotorControl,\n";
let parsed = parse(input, Profile::Ridl);
assert!(
!parsed.errors().is_empty(),
"`reserved` in a service's list is a parse error",
);
assert_eq!(
shape_kinds(&parsed),
[SyntaxKind::PathType],
"the list ends before the stray keyword",
);
}
fn shape_kinds(parsed: &Parse) -> Vec<SyntaxKind> {
parsed
.syntax()
.descendants()
.find(|node| node.kind() == SyntaxKind::ServiceDef)
.expect("the service parses")
.children()
.filter(|child| {
matches!(
child.kind(),
SyntaxKind::PathType | SyntaxKind::ReservedEntry
)
})
.map(|child| child.kind())
.collect()
}
#[test]
fn reserved_with_no_target_flags_form_101() {
let parsed = parse("package p\nstruct S {\n reserved\n}\n", Profile::Typl);
assert_eq!(
parsed.errors().iter().map(|e| e.code).collect::<Vec<_>>(),
vec!["FORM-101"],
);
assert_eq!(
parsed.errors()[0].message,
"expected a name or an integer value",
);
}
fn contract_file(expr: &str) -> String {
format!(
"package app\ntype N : integer [0..10]\ninterface I {{\n command c(a: N) [ require {expr} ] @[..50ms]\n}}\n"
)
}
fn flat_chain(op: &str, terms: usize) -> String {
match op {
"||" | "&&" => vec!["a == 1"; terms].join(&format!(" {op} ")),
"." => format!("a{} == 1", ".b".repeat(terms - 1)),
_ => format!("{} == 1", vec!["a"; terms].join(&format!(" {op} "))),
}
}
fn tree_depth(root: &SyntaxNode) -> usize {
let (mut depth, mut deepest) = (0usize, 0usize);
for event in root.preorder() {
match event {
rowan::WalkEvent::Enter(_) => {
depth += 1;
deepest = deepest.max(depth);
}
rowan::WalkEvent::Leave(_) => depth -= 1,
}
}
deepest
}
fn on_a_large_stack(body: impl FnOnce() + Send + 'static) {
std::thread::Builder::new()
.stack_size(64 << 20)
.spawn(body)
.expect("spawn the test thread")
.join()
.expect("the test thread must not panic");
}
const TREE_DEPTH_BOUND: usize = 2 * MAX_TYPE_DEPTH;
#[test]
fn a_flat_operator_chain_past_the_depth_limit_draws_one_form_102() {
on_a_large_stack(|| {
for op in ["||", "&&", "+", "-", "*", "/", "%", "."] {
let input = contract_file(&flat_chain(op, 1000));
let parsed = parse(&input, Profile::Ridl);
let form_102 = parsed
.errors()
.iter()
.filter(|error| error.code == "FORM-102")
.count();
assert_eq!(
form_102,
1,
"a 1000-term `{op}` chain draws exactly one FORM-102: {:?}",
parsed.errors(),
);
assert_eq!(
parsed.syntax().text().to_string(),
input,
"the refused `{op}` chain must stay lossless",
);
let depth = tree_depth(&parsed.syntax());
assert!(
depth <= TREE_DEPTH_BOUND,
"a 1000-term `{op}` chain builds a tree {depth} levels deep",
);
}
});
}
fn expr_height(root: &SyntaxNode) -> usize {
let is_expr = |kind: SyntaxKind| {
matches!(
kind,
SyntaxKind::BinaryExpr
| SyntaxKind::PrefixExpr
| SyntaxKind::MemberExpr
| SyntaxKind::PathExpr
| SyntaxKind::ParenExpr
| SyntaxKind::LiteralExpr
)
};
let (mut depth, mut deepest) = (0usize, 0usize);
for event in root.preorder() {
match event {
rowan::WalkEvent::Enter(node) if is_expr(node.kind()) => {
depth += 1;
deepest = deepest.max(depth);
}
rowan::WalkEvent::Leave(node) if is_expr(node.kind()) => depth -= 1,
_ => {}
}
}
deepest
}
#[test]
fn the_expression_height_bound_holds_for_every_shape() {
type Shape = (&'static str, fn(usize) -> String, fn(usize) -> usize);
let shapes: [Shape; 15] = [
("left chain", |n| vec!["a"; n].join(" + "), |n| n),
(
"`||` chain of comparisons",
|n| vec!["a == 1"; n].join(" || "),
|n| n + 1,
),
(
"member steps",
|n| format!("a{}", ".b".repeat(n - 1)),
|n| n,
),
(
"groups around a leaf",
|n| format!("{}a{}", "(".repeat(n - 1), ")".repeat(n - 1)),
|n| n,
),
(
"groups around a negated leaf",
|n| format!("{}-a{}", "(".repeat(n - 1), ")".repeat(n - 1)),
|n| n + 1,
),
(
"groups around a `!` leaf",
|n| format!("{}!a{}", "(".repeat(n - 1), ")".repeat(n - 1)),
|n| n + 1,
),
(
"right-nested chain",
|n| {
let mut expr = "a".to_string();
for _ in 1..n {
expr = format!("a + ({expr})");
}
expr
},
|n| 2 * n - 1,
),
(
"comparison with a grouped chain on the right",
|n| format!("a == ({})", vec!["a"; n].join(" + ")),
|n| n + 2,
),
(
"`!(` tower",
|n| format!("{}a{}", "!(".repeat(n - 1), ")".repeat(n - 1)),
|n| 2 * n - 1,
),
(
"`-(` tower",
|n| format!("{}a{}", "-(".repeat(n - 1), ")".repeat(n - 1)),
|n| 2 * n - 1,
),
(
"prefixed operands in a chain",
|n| vec!["-a"; n].join(" * "),
|n| n + 1,
),
(
"grouped member steps as a right operand",
|n| format!("a + (a{})", ".b".repeat(n - 1)),
|n| n + 2,
),
(
"`||` right-nested",
|n| {
let mut expr = "a == 1".to_string();
for _ in 1..n {
expr = format!("a == 1 || ({expr})");
}
expr
},
|n| 2 * n,
),
(
"a deep right operand, then a long left chain",
|n| {
format!(
"a + ({}a{}){}",
"(".repeat(60),
")".repeat(60),
" + a".repeat(n - 1)
)
},
|n| n + 62,
),
(
"a prefix tower, then a chain",
|n| {
format!(
"{}a == 1{}{}",
"!(".repeat(30),
")".repeat(30),
" && a == 1".repeat(n - 1)
)
},
|n| n + 61,
),
];
for (shape, build, complete_height) in shapes {
let mut sizes: Vec<usize> = (1..)
.take_while(|&n| complete_height(n) <= MAX_TYPE_DEPTH + 8)
.filter(|&n| n <= 3 || complete_height(n) + 8 >= MAX_TYPE_DEPTH)
.collect();
sizes.push(300);
for n in sizes {
let input = contract_file(&build(n));
let parsed = parse(&input, Profile::Ridl);
let complete = complete_height(n);
let built = expr_height(&parsed.syntax());
let form_102 = parsed
.errors()
.iter()
.filter(|error| error.code == "FORM-102")
.count();
assert!(
built <= MAX_TYPE_DEPTH,
"the {shape} at n = {n} builds a tree {built} high",
);
assert_eq!(
parsed.syntax().text().to_string(),
input,
"{shape} at n = {n}"
);
if complete <= MAX_TYPE_DEPTH {
assert!(
parsed.errors().is_empty(),
"the {shape} at n = {n} fits in {complete} levels: {:?}",
parsed.errors(),
);
assert_eq!(
built, complete,
"the {shape} at n = {n} builds its complete tree"
);
} else {
assert_eq!(
form_102,
1,
"the {shape} at n = {n} would be {complete} high: {:?}",
parsed.errors(),
);
}
}
}
}
#[test]
fn a_refused_group_draws_only_form_102_and_the_next_interaction_parses() {
use crate::ast::AstNode;
let groups = MAX_TYPE_DEPTH + 2;
let input = format!(
"package app\ntype N : integer [0..10]\ninterface I {{\n \
command c(a: N) [ require {}(a) + (a){} + a + a ] @[..50ms]\n \
command d(a: N) [ require a == 1 ] @[..50ms]\n}}\n",
"(".repeat(groups),
")".repeat(groups),
);
let parsed = parse(&input, Profile::Ridl);
let codes: Vec<&str> = parsed.errors().iter().map(|error| error.code).collect();
assert_eq!(codes, vec!["FORM-102"]);
assert_eq!(parsed.syntax().text().to_string(), input);
let root = parsed.syntax();
assert!(expr_height(&root) <= MAX_TYPE_DEPTH);
assert_eq!(
root.descendants()
.filter(|node| node.kind() == SyntaxKind::CommandDef)
.count(),
2,
);
let last = root
.descendants()
.filter_map(crate::ast::Attribute::cast)
.last()
.expect("the second command's attribute");
assert_eq!(
last.expr().expect("its expression").syntax().text(),
"a == 1",
);
}
#[test]
fn a_missing_left_operand_keeps_the_bound() {
for inner in ["+ a", "== a", "|| a == 1", "+ a == a"] {
let groups = MAX_TYPE_DEPTH - 1;
let input = contract_file(&format!(
"{}{inner}{}",
"(".repeat(groups),
")".repeat(groups)
));
let parsed = parse(&input, Profile::Ridl);
let built = expr_height(&parsed.syntax());
assert!(
built <= MAX_TYPE_DEPTH,
"`{inner}` under {groups} groups builds a tree {built} high: {:?}",
parsed.errors(),
);
assert!(
parsed.errors().iter().any(|error| error.code == "FORM-101"),
"the missing operand is reported: {:?}",
parsed.errors(),
);
assert_eq!(parsed.syntax().text().to_string(), input);
}
}
#[test]
fn a_refused_expression_does_not_swallow_what_follows_it() {
use crate::ast::AstNode;
let long = flat_chain("+", 200);
let input = format!(
"package app\ntype N : integer [0..10]\ninterface I {{\n \
command c(a: N) [ require {long}, require a == 1 ] @[..50ms]\n \
command d(a: N) [ require {long}\n key = 1\n require a == 1 ] @[..50ms]\n \
command e(a: N) [ require a == ] @[..50ms]\n \
command f(a: N) [ require {long} ensure a == 1 ] @[..50ms]\n}}\n"
);
let parsed = parse(&input, Profile::Ridl);
assert_eq!(parsed.syntax().text().to_string(), input);
let codes: Vec<&str> = parsed.errors().iter().map(|error| error.code).collect();
assert_eq!(
codes.iter().filter(|code| **code == "FORM-102").count(),
3,
"one FORM-102 per overlong expression: {codes:?}",
);
assert!(
codes.contains(&"FORM-101"),
"the syntax error in the later declaration is still reported: {codes:?}",
);
let spans: Vec<_> = parsed
.errors()
.iter()
.filter(|error| error.code == "FORM-102")
.map(|error| error.range.start())
.collect();
assert!(
spans.windows(2).all(|pair| pair[0] < pair[1]),
"each FORM-102 sits at its own expression: {spans:?}",
);
let root = parsed.syntax();
assert_eq!(
root.descendants()
.filter(|node| node.kind() == SyntaxKind::CommandDef)
.count(),
4,
);
let attributes: Vec<crate::ast::Attribute> = root
.descendants()
.filter_map(crate::ast::Attribute::cast)
.collect();
assert_eq!(attributes.len(), 8, "{attributes:?}");
assert_eq!(
attributes
.iter()
.filter(|attribute| attribute
.expr()
.is_some_and(|expr| expr.syntax().text() == "a == 1"))
.count(),
3,
"the `a == 1` after each refused chain is its own attribute",
);
assert!(
attributes.iter().any(|attribute| attribute
.syntax()
.text()
.to_string()
.starts_with("key")),
"the key after the refused chain is its own attribute",
);
}
#[test]
fn a_refused_chain_stops_at_every_kind_of_next_attribute() {
use crate::ast::AstNode;
let numbers = flat_chain("+", 200);
let names = vec!["a"; 200].join(" + ");
let groups = vec!["(a)"; 200].join(" + ");
let cases = [
("a `require`", format!("{numbers}\n require a == 1")),
("a reserved word", format!("{numbers}\n view = 1")),
("a key after a number", format!("{numbers}\n key = 1")),
("a key after a name", format!("{names}\n key = 1")),
("a key after a group", format!("{groups}\n key = 1")),
];
for (case, body) in cases {
let input = format!(
"package app\ntype N : integer [0..10]\ninterface I {{\n \
command c(a: N) [ require {body} ] @[..50ms]\n}}\n"
);
let parsed = parse(&input, Profile::Ridl);
assert_eq!(parsed.syntax().text().to_string(), input, "{case}");
let codes: Vec<&str> = parsed.errors().iter().map(|error| error.code).collect();
let expected: &[&str] = if case == "a reserved word" {
&["FORM-102", "FORM-105"]
} else {
&["FORM-102"]
};
assert_eq!(codes, expected, "{case}");
let attributes: Vec<String> = parsed
.syntax()
.descendants()
.filter_map(crate::ast::Attribute::cast)
.map(|attribute| attribute.syntax().text().to_string())
.collect();
assert_eq!(
attributes.len(),
2,
"{case}: the next attribute is its own node: {attributes:?}",
);
let next = attributes[1].trim();
let starts = match case {
"a `require`" => "require a == 1",
"a reserved word" => "view = 1",
_ => "key = 1",
};
assert_eq!(next, starts, "{case}");
}
}
#[test]
fn a_long_chain_under_the_depth_limit_parses_clean() {
for op in ["||", "&&", "+", "-", "*", "/", "%", "."] {
let input = contract_file(&flat_chain(op, 100));
let parsed = parse(&input, Profile::Ridl);
assert!(
parsed.errors().is_empty(),
"a 100-term `{op}` chain is accepted: {:?}",
parsed.errors(),
);
}
}
#[test]
fn chains_stacked_through_parentheses_and_precedence_stay_bounded() {
on_a_large_stack(|| {
let mut nested = "a".to_string();
for _ in 0..120 {
nested = format!("({nested}{})", " + a".repeat(119));
}
let ladder = format!(
"a{}{}{} == a{}{}",
".b".repeat(119),
" * a".repeat(119),
" + a".repeat(119),
" && a == a".repeat(119),
" || a == a".repeat(119),
);
for (shape, expr) in [("nested", format!("{nested} == 1")), ("ladder", ladder)] {
let input = contract_file(&expr);
let parsed = parse(&input, Profile::Ridl);
assert!(
parsed.errors().iter().any(|error| error.code == "FORM-102"),
"the {shape} shape draws FORM-102: {:?}",
parsed.errors(),
);
assert!(
parsed.errors().len() <= 4,
"the {shape} shape floods {} diagnostics",
parsed.errors().len(),
);
assert_eq!(parsed.syntax().text().to_string(), input);
let depth = tree_depth(&parsed.syntax());
assert!(
depth <= TREE_DEPTH_BOUND,
"the {shape} shape builds a tree {depth} levels deep",
);
}
});
}
}