use crate::syntax::ast::{BinOp, Expr, ExprKind, FieldInit, Ident, IndexArg, ModulePath, UnaryOp};
use crate::syntax::names::{DeclName, FieldName, IndexVariantName, NamePath, ScopedName};
use crate::syntax::span::{Span, Spanned};
use crate::syntax::token::Token;
use super::{ParseError, Parser};
pub(super) const fn token_to_comparison_op(token: Token) -> Option<BinOp> {
match token {
Token::EqEq => Some(BinOp::Eq),
Token::BangEq => Some(BinOp::Ne),
Token::Lt => Some(BinOp::Lt),
Token::Gt => Some(BinOp::Gt),
Token::LtEq => Some(BinOp::Le),
Token::GtEq => Some(BinOp::Ge),
_ => None,
}
}
impl Parser<'_> {
pub(crate) fn parse_expr(&mut self) -> Result<Expr, ParseError> {
self.with_depth(Self::parse_convert)
}
fn parse_arg_list(&mut self) -> Result<Vec<Expr>, ParseError> {
self.parse_comma_separated(Token::RParen, Self::parse_expr)
}
pub(super) fn parse_named_field_init(&mut self) -> Result<FieldInit, ParseError> {
let ident = self.parse_any_ident()?;
let name = Spanned::new(FieldName::new(&ident.name), ident.span);
self.expect(Token::Colon)?;
let value = self.parse_expr()?;
Ok(FieldInit { name, value })
}
fn parse_convert(&mut self) -> Result<Expr, ParseError> {
let expr = self.parse_conditional()?;
if self.lexer.peek() == Some(&Token::Arrow) {
self.lexer.next_token();
if self.lexer.peek() == Some(&Token::StringLiteral) {
let (_, tz_span) = self.advance()?;
let raw = self.lexer.slice_at(tz_span);
let timezone = raw[1..raw.len() - 1].to_string();
let span = expr.span.merge(tz_span);
return Ok(Expr::new(
ExprKind::DisplayTimezone {
expr: Box::new(expr),
timezone,
},
span,
));
}
let target = self.parse_unit_expr()?;
let span = expr.span.merge(target.span);
Ok(Expr::new(
ExprKind::Convert {
expr: Box::new(expr),
target,
},
span,
))
} else {
Ok(expr)
}
}
fn parse_conditional(&mut self) -> Result<Expr, ParseError> {
if self.lexer.peek() == Some(&Token::If) {
let (_, if_span) = self.advance()?;
let condition = self.parse_expr()?;
self.expect(Token::LBrace)?;
let then_branch = self.parse_expr()?;
self.expect(Token::RBrace)?;
self.expect(Token::Else)?;
self.expect(Token::LBrace)?;
let else_branch = self.parse_expr()?;
let (_, rbrace_span) = self.expect(Token::RBrace)?;
let span = if_span.merge(rbrace_span);
Ok(Expr::new(
ExprKind::If {
condition: Box::new(condition),
then_branch: Box::new(then_branch),
else_branch: Box::new(else_branch),
},
span,
))
} else {
self.parse_or()
}
}
fn parse_or(&mut self) -> Result<Expr, ParseError> {
let mut lhs = self.parse_and()?;
while self.lexer.peek() == Some(&Token::PipePipe) {
self.lexer.next_token();
let rhs = self.parse_and()?;
let span = lhs.span.merge(rhs.span);
lhs = Expr::new(
ExprKind::BinOp {
op: BinOp::Or,
lhs: Box::new(lhs),
rhs: Box::new(rhs),
},
span,
);
}
Ok(lhs)
}
fn parse_and(&mut self) -> Result<Expr, ParseError> {
let mut lhs = self.parse_comparison()?;
while self.lexer.peek() == Some(&Token::AmpAmp) {
self.lexer.next_token();
let rhs = self.parse_comparison()?;
let span = lhs.span.merge(rhs.span);
lhs = Expr::new(
ExprKind::BinOp {
op: BinOp::And,
lhs: Box::new(lhs),
rhs: Box::new(rhs),
},
span,
);
}
Ok(lhs)
}
fn parse_comparison(&mut self) -> Result<Expr, ParseError> {
let lhs = self.parse_add()?;
let op = self.lexer.peek().copied().and_then(token_to_comparison_op);
if let Some(op) = op {
self.lexer.next_token();
let rhs = self.parse_add()?;
let span = lhs.span.merge(rhs.span);
Ok(Expr::new(
ExprKind::BinOp {
op,
lhs: Box::new(lhs),
rhs: Box::new(rhs),
},
span,
))
} else {
Ok(lhs)
}
}
fn parse_add(&mut self) -> Result<Expr, ParseError> {
let mut lhs = self.parse_mul()?;
loop {
let op = match self.lexer.peek() {
Some(Token::Plus) => BinOp::Add,
Some(Token::Minus) => BinOp::Sub,
_ => break,
};
self.lexer.next_token();
let rhs = self.parse_mul()?;
let span = lhs.span.merge(rhs.span);
lhs = Expr::new(
ExprKind::BinOp {
op,
lhs: Box::new(lhs),
rhs: Box::new(rhs),
},
span,
);
}
Ok(lhs)
}
fn parse_mul(&mut self) -> Result<Expr, ParseError> {
let mut lhs = self.parse_unary()?;
loop {
let op = match self.lexer.peek() {
Some(Token::Star) => BinOp::Mul,
Some(Token::Slash) => BinOp::Div,
Some(Token::Percent) => BinOp::Mod,
_ => break,
};
self.lexer.next_token();
let rhs = self.parse_unary()?;
let span = lhs.span.merge(rhs.span);
lhs = Expr::new(
ExprKind::BinOp {
op,
lhs: Box::new(lhs),
rhs: Box::new(rhs),
},
span,
);
}
Ok(lhs)
}
pub(super) fn parse_unary(&mut self) -> Result<Expr, ParseError> {
self.with_depth(Self::parse_unary_inner)
}
fn parse_unary_inner(&mut self) -> Result<Expr, ParseError> {
match self.lexer.peek() {
Some(Token::Minus) => {
let (_, op_span) = self.advance()?;
let operand = self.parse_unary()?;
let span = op_span.merge(operand.span);
Ok(Expr::new(
ExprKind::UnaryOp {
op: UnaryOp::Neg,
operand: Box::new(operand),
},
span,
))
}
Some(Token::Bang) => {
let (_, op_span) = self.advance()?;
let operand = self.parse_unary()?;
let span = op_span.merge(operand.span);
Ok(Expr::new(
ExprKind::UnaryOp {
op: UnaryOp::Not,
operand: Box::new(operand),
},
span,
))
}
_ => self.parse_power(),
}
}
fn parse_power(&mut self) -> Result<Expr, ParseError> {
let base = self.parse_postfix()?;
if self.lexer.peek() == Some(&Token::Caret) {
self.lexer.next_token();
let exp = self.parse_unary()?;
let span = base.span.merge(exp.span);
Ok(Expr::new(
ExprKind::BinOp {
op: BinOp::Pow,
lhs: Box::new(base),
rhs: Box::new(exp),
},
span,
))
} else {
Ok(base)
}
}
pub(super) fn apply_postfix(&mut self, mut expr: Expr) -> Result<Expr, ParseError> {
loop {
match self.lexer.peek() {
Some(Token::Dot) => {
self.lexer.next_token(); let field_ident = self.parse_any_ident()?;
let span = expr.span.merge(field_ident.span);
expr = Expr::new(
ExprKind::FieldAccess {
expr: Box::new(expr),
field: field_ident.into_spanned::<FieldName>(),
},
span,
);
}
Some(Token::LBracket) => {
self.lexer.next_token(); let args =
self.parse_comma_separated(Token::RBracket, Self::parse_index_arg)?;
let (_, end_span) = self.expect(Token::RBracket)?;
let span = expr.span.merge(end_span);
expr = Expr::new(
ExprKind::IndexAccess {
expr: Box::new(expr),
args,
},
span,
);
}
_ => break,
}
}
Ok(expr)
}
fn parse_postfix(&mut self) -> Result<Expr, ParseError> {
let expr = self.parse_atom()?;
self.apply_postfix(expr)
}
fn parse_atom(&mut self) -> Result<Expr, ParseError> {
match self.lexer.peek() {
Some(Token::Number) => self.parse_number_expr(),
Some(Token::True) => {
let (_, span) = self.advance()?;
Ok(Expr::new(ExprKind::Bool(true), span))
}
Some(Token::False) => {
let (_, span) = self.advance()?;
Ok(Expr::new(ExprKind::Bool(false), span))
}
Some(Token::StringLiteral) => {
let (_, span) = self.advance()?;
let raw = self.lexer.slice_at(span);
let text = raw[1..raw.len() - 1].to_string();
Ok(Expr::new(ExprKind::StringLiteral(text), span))
}
Some(Token::At) => self.parse_at_expr(),
Some(Token::Scan) => {
let (_, span) = self.advance()?;
self.parse_scan(span)
}
Some(Token::Unfold) => {
let (_, span) = self.advance()?;
self.parse_unfold(span)
}
Some(Token::Ident) => self.parse_identifier_expr(),
Some(Token::For) => {
self.parse_for_comp()
}
Some(Token::LBrace) => self.parse_brace_expr(),
Some(Token::Table) => self.parse_table_expr(),
Some(Token::Match) => self.parse_match_expr(),
Some(Token::LParen) => {
self.lexer.next_token();
let expr = self.parse_expr()?;
self.expect(Token::RParen)?;
Ok(expr)
}
Some(_) => {
let (tok, span) = self.advance()?;
Err(self.unexpected_token("expression", &tok.to_string(), span))
}
None => Err(self.unexpected_eof("expression")),
}
}
fn parse_at_expr(&mut self) -> Result<Expr, ParseError> {
let (_, at_span) = self.advance()?; let first_seg = self.parse_any_ident()?;
if self.lexer.peek() == Some(&Token::LParen) {
return self.finish_inline_dag_call(at_span, vec![first_seg]);
}
if self.lexer.peek() != Some(&Token::Dot) {
let span = at_span.merge(first_seg.span);
return Ok(Expr::new(
ExprKind::GraphRef(first_seg.into_spanned::<ScopedName>()),
span,
));
}
let mut segments = vec![first_seg];
while self.lexer.peek() == Some(&Token::Dot)
&& self.lexer.peek_second() == Some(&Token::Ident)
{
self.advance()?; let seg = self.parse_any_ident()?;
segments.push(seg);
if self.lexer.peek() == Some(&Token::LParen) {
return self.finish_inline_dag_call(at_span, segments);
}
}
let mut iter = segments.into_iter();
#[expect(
clippy::expect_used,
reason = "loop seeded with first_seg, so segments is non-empty"
)]
let head = iter.next().expect("path always has at least one segment");
let head_span = head.span;
let mut expr = Expr::new(
ExprKind::GraphRef(head.into_spanned::<ScopedName>()),
at_span.merge(head_span),
);
for seg in iter {
let seg_span = seg.span;
let span = expr.span.merge(seg_span);
expr = Expr::new(
ExprKind::FieldAccess {
expr: Box::new(expr),
field: seg.into_spanned::<FieldName>(),
},
span,
);
}
Ok(expr)
}
fn finish_inline_dag_call(
&mut self,
at_span: crate::syntax::span::Span,
segments: Vec<Ident>,
) -> Result<Expr, ParseError> {
#[expect(
clippy::expect_used,
reason = "callers seed inline DAG paths with at least one segment"
)]
let segments = crate::syntax::non_empty::NonEmpty::try_from_vec(segments)
.expect("inline dag call must have at least one path segment");
let path_start = at_span.merge(segments.first().span);
let path_end = segments.last().span;
let path = ModulePath {
segments,
span: path_start.merge(path_end),
};
let args = self.parse_import_param_bindings()?;
match self.lexer.peek_with_span() {
Some((Token::Dot, _)) => {
self.lexer.next_token();
}
Some((_, span)) => {
return Err(ParseError::InlineDagCallMissingProjection {
src: self.named_source(),
span: span.into(),
});
}
None => {
return Err(self.unexpected_eof("`.<out>` projection"));
}
}
let output = self.parse_any_ident()?;
let span = at_span.merge(output.span);
Ok(Expr::new(
ExprKind::InlineDagRef {
path,
args,
output: output.into_spanned::<DeclName>(),
},
span,
))
}
fn parse_number_expr(&mut self) -> Result<Expr, ParseError> {
let (_, span) = self.advance()?;
let text = self.lexer.slice_at(span).replace('_', "");
let is_integer = !text.contains('.') && !text.contains('e') && !text.contains('E');
if is_integer {
if self.lexer.peek() == Some(&Token::Ident) {
return Err(ParseError::InvalidNumber {
reason: format!("integer literal cannot have units; write `{text}.0` instead"),
src: self.named_source(),
span: span.into(),
});
}
let value: i64 =
text.parse()
.map_err(|e: std::num::ParseIntError| ParseError::InvalidNumber {
reason: e.to_string(),
src: self.named_source(),
span: span.into(),
})?;
Ok(Expr::new(ExprKind::Integer(value), span))
} else {
let value = self.parse_finite_f64_literal(&text, span)?;
if self.lexer.peek() == Some(&Token::Ident) {
let unit_expr = self.parse_unit_expr()?;
let full_span = span.merge(unit_expr.span);
Ok(Expr::new(
ExprKind::UnitLiteral {
value,
unit: unit_expr,
},
full_span,
))
} else {
Ok(Expr::new(ExprKind::Number(value), span))
}
}
}
fn parse_identifier_expr(&mut self) -> Result<Expr, ParseError> {
let path = self.parse_ident_path()?;
if self.lexer.peek() == Some(&Token::LParen)
|| (self.lexer.peek() == Some(&Token::Lt) && self.is_type_args_followed_by_paren())
{
let generic_args = if self.lexer.peek() == Some(&Token::Lt) {
self.parse_generic_arg_list()?
} else {
vec![]
};
if self.is_named_arg_call() {
self.lexer.next_token(); let fields =
self.parse_comma_separated(Token::RParen, Self::parse_named_field_init)?;
let (_, rparen_span) = self.expect(Token::RParen)?;
let call_span = path.span().merge(rparen_span);
return Ok(Expr::new(
ExprKind::ConstructorCall {
callee: path,
generic_args,
fields,
},
call_span,
));
}
self.lexer.next_token(); let args = self.parse_arg_list()?;
let (_, rparen_span) = self.expect(Token::RParen)?;
let call_span = path.span().merge(rparen_span);
Ok(Expr::new(
ExprKind::FnCall {
callee: path,
type_args: generic_args,
args,
},
call_span,
))
} else {
Ok(Expr::new(
ExprKind::UnresolvedRef(crate::syntax::ast::UnresolvedRef::Path(path.clone())),
path.span(),
))
}
}
fn parse_brace_expr(&mut self) -> Result<Expr, ParseError> {
let (_, start_span) = self.advance()?;
if let Some((Token::Ident, ident_span)) = self.lexer.peek_with_span() {
let saved_text = self.lexer.slice_at(ident_span).to_string();
if self.lexer.peek_second() == Some(&Token::Dot) {
let (index, variant, _) = self.parse_index_variant_path()?;
if self.lexer.peek() == Some(&Token::Colon) {
self.parse_map_literal_after_first_entry(start_span, index, variant)
} else {
let found = self
.lexer
.peek()
.map_or_else(|| "EOF".to_string(), std::string::ToString::to_string);
Err(self.unexpected_token(
"`:` after variant in map literal",
&found,
start_span,
))
}
} else {
Err(self.unexpected_token(
"map literal (`{ Index.Variant: expr, ... }`)",
&saved_text,
start_span,
))
}
} else if self.lexer.peek() == Some(&Token::LParen) {
self.parse_tuple_key_map_literal(start_span)
} else {
let found = self
.lexer
.peek()
.map_or_else(|| "EOF".to_string(), std::string::ToString::to_string);
Err(self.unexpected_token(
"map literal (`{ Index.Variant: expr, ... }`)",
&found,
start_span,
))
}
}
pub(super) fn index_name_path_from_segments(index_segments: &[Ident]) -> Spanned<NamePath> {
let index_ident = &index_segments[index_segments.len() - 1];
let span = index_segments
.first()
.map_or(index_ident.span, |first| first.span.merge(index_ident.span));
let qualifier = index_segments[..index_segments.len().saturating_sub(1)]
.iter()
.map(|ident| ident.name.clone());
Spanned::new(
NamePath::qualified_path(qualifier, index_ident.name.clone()),
span,
)
}
pub(super) fn parse_index_variant_path(
&mut self,
) -> Result<(Spanned<NamePath>, Spanned<IndexVariantName>, Span), ParseError> {
let first = self.parse_any_ident()?;
let start_span = first.span;
self.expect(Token::Dot)?;
let second = self.parse_any_ident()?;
let mut segments = vec![first, second];
while self.lexer.peek() == Some(&Token::Dot) {
self.lexer.next_token();
segments.push(self.parse_any_ident()?);
}
let variant_ident = segments.remove(segments.len() - 1);
let full_span = start_span.merge(variant_ident.span);
let index = Self::index_name_path_from_segments(&segments);
let variant = Spanned::new(
IndexVariantName::new(variant_ident.name),
variant_ident.span,
);
Ok((index, variant, full_span))
}
pub(super) fn parse_index_arg(&mut self) -> Result<IndexArg, ParseError> {
if self.lexer.peek() == Some(&Token::Ident)
&& self.lexer.peek_second() == Some(&Token::Dot)
&& self.lexer.peek_third() == Some(&Token::Ident)
{
let (index, variant, _) = self.parse_index_variant_path()?;
return Ok(IndexArg::Variant { index, variant });
}
let expr = self.parse_expr()?;
match expr.kind {
ExprKind::UnresolvedRef(crate::syntax::ast::UnresolvedRef::Path(path)) => {
match path.into_bare() {
Ok(ident) => Ok(IndexArg::Var(ident)),
Err(path) => Ok(IndexArg::Expr(Box::new(Expr::new(
ExprKind::UnresolvedRef(crate::syntax::ast::UnresolvedRef::Path(path)),
expr.span,
)))),
}
}
other => Ok(IndexArg::Expr(Box::new(Expr::new(other, expr.span)))),
}
}
fn is_type_args_followed_by(&mut self, expected: u8) -> bool {
let Some((&Token::Lt, lt_span)) = self.lexer.peek_with_span() else {
return false;
};
let bytes = self.source.as_bytes();
let mut pos = lt_span.offset() + lt_span.len(); let mut depth: usize = 1;
while pos < bytes.len() {
match bytes[pos] {
b'<' => depth += 1,
b'>' => {
depth -= 1;
if depth == 0 {
let mut p = pos + 1;
while p < bytes.len() && bytes[p].is_ascii_whitespace() {
p += 1;
}
return p < bytes.len() && bytes[p] == expected;
}
}
b'/' if bytes.get(pos + 1) == Some(&b'/') => {
while pos < bytes.len() && bytes[pos] != b'\n' {
pos += 1;
}
continue;
}
b'/' if bytes.get(pos + 1) == Some(&b'*') => {
pos += 2;
while pos + 1 < bytes.len() && !(bytes[pos] == b'*' && bytes[pos + 1] == b'/') {
pos += 1;
}
pos += 2;
continue;
}
b'&' | b'|' | b';' | b'=' | b'{' | b'}' | b'"' | b'@' | b'!' => return false,
_ => {}
}
pos += 1;
}
false
}
pub(super) fn is_tuple_key_sugar(&mut self) -> bool {
let Some((&Token::LParen, lp_span)) = self.lexer.peek_with_span() else {
return false;
};
let bytes = self.source.as_bytes();
let mut pos = lp_span.offset() + lp_span.len();
loop {
while pos < bytes.len() && bytes[pos].is_ascii_whitespace() {
pos += 1;
}
if pos >= bytes.len() {
return false;
}
if bytes[pos] == b')' {
pos += 1;
while pos < bytes.len() && bytes[pos].is_ascii_whitespace() {
pos += 1;
}
return pos + 1 < bytes.len() && bytes[pos] == b'=' && bytes[pos + 1] == b'>';
}
if !bytes[pos].is_ascii_alphabetic() && bytes[pos] != b'_' {
return false;
}
while pos < bytes.len() && (bytes[pos].is_ascii_alphanumeric() || bytes[pos] == b'_') {
pos += 1;
}
while pos < bytes.len() && bytes[pos].is_ascii_whitespace() {
pos += 1;
}
if pos >= bytes.len() {
return false;
}
if bytes[pos] == b',' {
pos += 1; } else if bytes[pos] != b')' {
return false;
} else {
}
}
}
pub(super) fn is_type_args_followed_by_paren(&mut self) -> bool {
self.is_type_args_followed_by(b'(')
}
pub(super) fn is_named_arg_call(&mut self) -> bool {
let Some((&Token::LParen, lp_span)) = self.lexer.peek_with_span() else {
return false;
};
let bytes = self.source.as_bytes();
let mut pos = lp_span.offset() + lp_span.len();
loop {
while pos < bytes.len() && bytes[pos].is_ascii_whitespace() {
pos += 1;
}
if pos + 1 < bytes.len() && bytes[pos] == b'/' && bytes[pos + 1] == b'/' {
while pos < bytes.len() && bytes[pos] != b'\n' {
pos += 1;
}
continue;
}
break;
}
if pos >= bytes.len() {
return false;
}
if !bytes[pos].is_ascii_alphabetic() && bytes[pos] != b'_' {
return false;
}
while pos < bytes.len() && (bytes[pos].is_ascii_alphanumeric() || bytes[pos] == b'_') {
pos += 1;
}
while pos < bytes.len() && bytes[pos].is_ascii_whitespace() {
pos += 1;
}
if pos >= bytes.len() {
return false;
}
bytes[pos] == b':' && bytes.get(pos + 1).is_none_or(|c| *c != b':')
}
pub(super) fn parse_generic_arg_list(
&mut self,
) -> Result<Vec<crate::syntax::ast::GenericArg>, ParseError> {
self.expect(Token::Lt)?;
let args = self.parse_comma_separated(Token::Gt, Self::parse_generic_arg)?;
self.expect(Token::Gt)?;
Ok(args)
}
fn parse_generic_arg(&mut self) -> Result<crate::syntax::ast::GenericArg, ParseError> {
use crate::syntax::ast::{GenericArg, NatExpr};
if let Some((&Token::Number, _)) = self.lexer.peek_with_span() {
let (_, lit_span) = self.advance()?;
let text = self.lexer.slice_at(lit_span);
let value: u64 = text.parse().map_err(|_| {
self.unexpected_token("a valid non-negative integer", text, lit_span)
})?;
Ok(GenericArg::Nat(NatExpr::Literal(value, lit_span)))
} else {
let te = self.parse_type_expr()?;
Ok(GenericArg::Type(te))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::syntax::ast::{BinOp, DeclKind, ExprKind, UnaryOp};
fn parse_node_expr(input: &str) -> crate::syntax::ast::Expr {
let full = format!("node x: Dimensionless = {input};");
let file = Parser::new(&full).parse_file().unwrap();
match file.declarations.into_iter().next().unwrap().kind {
DeclKind::Node(n) => n.value,
_ => panic!("expected node"),
}
}
#[test]
fn deeply_nested_parens_error_instead_of_stack_overflow() {
let depth = 100_000;
let src = format!(
"node x: Dimensionless = {}1.0{};",
"(".repeat(depth),
")".repeat(depth)
);
let err = Parser::new(&src).parse_file().unwrap_err();
assert!(matches!(err, ParseError::TooDeeplyNested { .. }), "{err:?}");
}
#[test]
fn deeply_nested_unary_chain_errors_instead_of_stack_overflow() {
let src = format!("node x: Dimensionless = {}1.0;", "-".repeat(100_000));
let err = Parser::new(&src).parse_file().unwrap_err();
assert!(matches!(err, ParseError::TooDeeplyNested { .. }), "{err:?}");
}
#[test]
fn reasonable_nesting_stays_below_the_depth_limit() {
let depth = 60;
let src = format!(
"node x: Dimensionless = {}1.0{};",
"(".repeat(depth),
")".repeat(depth)
);
Parser::new(&src).parse_file().unwrap();
}
#[test]
fn long_operator_chains_are_not_depth_limited() {
let src = format!(
"node x: Dimensionless = {};",
vec!["1.0"; 10_000].join(" + ")
);
Parser::new(&src).parse_file().unwrap();
}
#[test]
fn parse_unit_literal() {
let file = Parser::new("param alt: Length = 400.0 km;")
.parse_file()
.unwrap();
match &file.declarations[0].kind {
DeclKind::Param(p) => match &p.value.as_ref().unwrap().kind {
ExprKind::UnitLiteral { value, unit } => {
assert!((value - 400.0).abs() < f64::EPSILON);
assert_eq!(unit.terms.len(), 1);
assert_eq!(unit.terms[0].name.value.to_string(), "km");
}
_ => panic!("expected UnitLiteral"),
},
_ => panic!("expected param"),
}
}
#[test]
fn parse_compound_unit_literal() {
let file = Parser::new("const node g0: Acceleration = 9.80665 m/s^2;")
.parse_file()
.unwrap();
match &file.declarations[0].kind {
DeclKind::ConstNode(c) => match &c.value.kind {
ExprKind::UnitLiteral { value, unit } => {
assert!((value - 9.80665).abs() < f64::EPSILON);
assert_eq!(unit.terms.len(), 2);
assert_eq!(unit.terms[0].name.value.to_string(), "m");
assert_eq!(unit.terms[1].op, crate::syntax::ast::MulDivOp::Div);
assert_eq!(unit.terms[1].name.value.to_string(), "s");
assert_eq!(
unit.terms[1].power,
Some(crate::syntax::dimension::Rational::from_int(2))
);
}
_ => panic!("expected UnitLiteral"),
},
_ => panic!("expected const"),
}
}
#[test]
fn parse_conversion() {
let file = Parser::new("node speed_kmh: Velocity = @speed -> km/hour;")
.parse_file()
.unwrap();
match &file.declarations[0].kind {
DeclKind::Node(n) => match &n.value.kind {
ExprKind::Convert { expr, target } => {
assert!(
matches!(&expr.kind, ExprKind::GraphRef(id) if id.value.member() == "speed")
);
assert_eq!(target.terms.len(), 2);
assert_eq!(target.terms[0].name.value.to_string(), "km");
assert_eq!(target.terms[1].op, crate::syntax::ast::MulDivOp::Div);
assert_eq!(target.terms[1].name.value.to_string(), "hour");
}
_ => panic!("expected Convert"),
},
_ => panic!("expected node"),
}
}
#[test]
fn parse_conversion_with_qualified_unit() {
let file = Parser::new("node b: Length = @a -> u.mile;")
.parse_file()
.unwrap();
match &file.declarations[0].kind {
DeclKind::Node(n) => match &n.value.kind {
ExprKind::Convert { target, .. } => {
assert_eq!(target.terms.len(), 1);
let unit_ref = &target.terms[0].name.value;
assert_eq!(
unit_ref.qualifier().map(ToString::to_string).as_deref(),
Some("u")
);
assert_eq!(unit_ref.name().as_str(), "mile");
}
_ => panic!("expected Convert"),
},
_ => panic!("expected node"),
}
}
#[test]
fn parse_qualified_unit_literal() {
let file = Parser::new("node d: Length = 2.0 u.mile;")
.parse_file()
.unwrap();
match &file.declarations[0].kind {
DeclKind::Node(n) => match &n.value.kind {
ExprKind::UnitLiteral { unit, .. } => {
assert_eq!(unit.terms[0].name.value.to_string(), "u.mile");
}
_ => panic!("expected UnitLiteral"),
},
_ => panic!("expected node"),
}
}
#[test]
fn parse_unit_path_deeper_than_alias_is_rejected() {
let err = Parser::new("node b: Length = @a -> app.units.mile;")
.parse_file()
.unwrap_err();
assert!(
matches!(err, super::ParseError::UnitReferenceTooDeep { .. }),
"expected UnitReferenceTooDeep, got {err:?}"
);
}
#[test]
fn parse_convert_binds_loosely() {
let file = Parser::new("node x: Length = @a + @b -> km;")
.parse_file()
.unwrap();
match &file.declarations[0].kind {
DeclKind::Node(n) => match &n.value.kind {
ExprKind::Convert { expr, target } => {
assert!(matches!(expr.kind, ExprKind::BinOp { op: BinOp::Add, .. }));
assert_eq!(target.terms[0].name.value.to_string(), "km");
}
_ => panic!("expected Convert"),
},
_ => panic!("expected node"),
}
}
#[test]
fn parse_arithmetic_precedence() {
let expr = parse_node_expr("1.0 + 2.0 * 3.0");
assert!(matches!(expr.kind, ExprKind::BinOp { op: BinOp::Add, .. }));
if let ExprKind::BinOp { rhs, .. } = &expr.kind {
assert!(matches!(rhs.kind, ExprKind::BinOp { op: BinOp::Mul, .. }));
}
}
#[test]
fn parse_left_associative_add() {
let expr = parse_node_expr("1.0 - 2.0 - 3.0");
if let ExprKind::BinOp { op, lhs, .. } = &expr.kind {
assert_eq!(*op, BinOp::Sub);
assert!(matches!(lhs.kind, ExprKind::BinOp { op: BinOp::Sub, .. }));
} else {
panic!("expected BinOp");
}
}
#[test]
fn parse_power_right_assoc() {
let expr = parse_node_expr("2.0 ^ 3.0 ^ 2.0");
if let ExprKind::BinOp { op, rhs, .. } = &expr.kind {
assert_eq!(*op, BinOp::Pow);
assert!(matches!(rhs.kind, ExprKind::BinOp { op: BinOp::Pow, .. }));
} else {
panic!("expected Pow");
}
}
#[test]
fn parse_neg_power_precedence() {
let expr = parse_node_expr("-@x ^ 2.0");
if let ExprKind::UnaryOp {
op: UnaryOp::Neg,
operand,
} = &expr.kind
{
assert!(matches!(
operand.kind,
ExprKind::BinOp { op: BinOp::Pow, .. }
));
} else {
panic!("expected Neg(Pow(...))");
}
}
#[test]
fn parse_graph_ref() {
let expr = parse_node_expr("@x + 1.0");
if let ExprKind::BinOp { lhs, .. } = &expr.kind {
assert!(matches!(&lhs.kind, ExprKind::GraphRef(id) if id.value.member() == "x"));
} else {
panic!("expected BinOp");
}
}
#[test]
fn parse_name_ref() {
let expr = parse_node_expr("PI * 2.0");
if let ExprKind::BinOp { lhs, .. } = &expr.kind {
assert!(matches!(
&lhs.kind,
ExprKind::UnresolvedRef(crate::syntax::ast::UnresolvedRef::Path(path))
if path.as_bare().is_some_and(|id| id.name.as_str() == "PI")
));
} else {
panic!("expected BinOp");
}
}
#[test]
fn parse_function_call_one_arg() {
let expr = parse_node_expr("sqrt(@x)");
if let ExprKind::FnCall { callee, args, .. } = &expr.kind {
assert_eq!(callee.as_bare().unwrap().name, "sqrt");
assert_eq!(args.len(), 1);
assert!(matches!(&args[0].kind, ExprKind::GraphRef(id) if id.value.member() == "x"));
} else {
panic!("expected FnCall");
}
}
#[test]
fn parse_qualified_function_call_preserves_callee_path() {
let expr = parse_node_expr("module.sqrt(@x)");
if let ExprKind::FnCall { callee, args, .. } = &expr.kind {
assert_eq!(callee.segments.len(), 2);
assert_eq!(callee.segments[0].name, "module");
assert_eq!(callee.segments[1].name, "sqrt");
assert_eq!(args.len(), 1);
} else {
panic!("expected FnCall");
}
}
#[test]
fn parse_function_call_two_args() {
let expr = parse_node_expr("atan2(@a, @b)");
if let ExprKind::FnCall { callee, args, .. } = &expr.kind {
assert_eq!(callee.as_bare().unwrap().name, "atan2");
assert_eq!(args.len(), 2);
} else {
panic!("expected FnCall");
}
}
#[test]
fn parse_function_call_zero_args() {
let expr = parse_node_expr("foo()");
if let ExprKind::FnCall { callee, args, .. } = &expr.kind {
assert_eq!(callee.as_bare().unwrap().name, "foo");
assert_eq!(args.len(), 0);
} else {
panic!("expected FnCall");
}
}
#[test]
fn parse_turbofish_nat_arg() {
let expr = parse_node_expr("eye<3>()");
if let ExprKind::FnCall {
callee,
type_args,
args,
} = &expr.kind
{
assert_eq!(callee.as_bare().unwrap().name, "eye");
assert_eq!(type_args.len(), 1);
assert!(matches!(
&type_args[0],
crate::syntax::ast::GenericArg::Nat(crate::syntax::ast::NatExpr::Literal(3, _))
));
assert_eq!(args.len(), 0);
} else {
panic!("expected FnCall, got {:?}", expr.kind);
}
}
#[test]
fn parse_turbofish_type_arg() {
let expr = parse_node_expr("make<Length>(@x)");
if let ExprKind::FnCall {
callee,
type_args,
args,
} = &expr.kind
{
assert_eq!(callee.as_bare().unwrap().name, "make");
assert_eq!(type_args.len(), 1);
assert!(matches!(
&type_args[0],
crate::syntax::ast::GenericArg::Type(_)
));
assert_eq!(args.len(), 1);
} else {
panic!("expected FnCall, got {:?}", expr.kind);
}
}
#[test]
fn parse_turbofish_multiple_args() {
let expr = parse_node_expr("foo<3, Length>(@x)");
if let ExprKind::FnCall {
callee,
type_args,
args,
} = &expr.kind
{
assert_eq!(callee.as_bare().unwrap().name, "foo");
assert_eq!(type_args.len(), 2);
assert!(matches!(
&type_args[0],
crate::syntax::ast::GenericArg::Nat(crate::syntax::ast::NatExpr::Literal(3, _))
));
assert!(matches!(
&type_args[1],
crate::syntax::ast::GenericArg::Type(_)
));
assert_eq!(args.len(), 1);
} else {
panic!("expected FnCall, got {:?}", expr.kind);
}
}
#[test]
fn parse_comparison_not_turbofish() {
let expr = parse_node_expr("@a < @b");
assert!(
matches!(&expr.kind, ExprKind::BinOp { op: BinOp::Lt, .. }),
"expected comparison, got {:?}",
expr.kind
);
}
#[test]
fn comparison_with_and_then_gt_paren_is_not_turbofish() {
let expr = parse_node_expr("@limit < @threshold && @other > (1.0)");
assert!(
matches!(&expr.kind, ExprKind::BinOp { op: BinOp::And, .. }),
"expected `&&` at the top, got {:?}",
expr.kind
);
}
#[test]
fn comparison_with_comment_containing_gt_paren_is_not_turbofish() {
let expr = parse_node_expr("@a < @b // note: > (\n || @c");
assert!(
matches!(&expr.kind, ExprKind::BinOp { op: BinOp::Or, .. }),
"expected `||` at the top, got {:?}",
expr.kind
);
}
#[test]
fn parse_if_else() {
let expr = parse_node_expr("if @x > 0.0 { @x } else { 0.0 }");
if let ExprKind::If {
condition,
then_branch,
else_branch,
} = &expr.kind
{
assert!(matches!(
condition.kind,
ExprKind::BinOp { op: BinOp::Gt, .. }
));
assert!(matches!(
&then_branch.kind,
ExprKind::GraphRef(id) if id.value.member() == "x"
));
assert!(matches!(else_branch.kind, ExprKind::Number(_)));
} else {
panic!("expected If");
}
}
#[test]
fn parse_nested_parens() {
let expr = parse_node_expr("(1.0 + 2.0) * 3.0");
if let ExprKind::BinOp { op, lhs, .. } = &expr.kind {
assert_eq!(*op, BinOp::Mul);
assert!(matches!(lhs.kind, ExprKind::BinOp { op: BinOp::Add, .. }));
} else {
panic!("expected Mul");
}
}
#[test]
fn parse_boolean_and() {
let expr = parse_node_expr("@a > 0.0 && @b > 0.0");
if let ExprKind::BinOp { op, lhs, rhs } = &expr.kind {
assert_eq!(*op, BinOp::And);
assert!(matches!(lhs.kind, ExprKind::BinOp { op: BinOp::Gt, .. }));
assert!(matches!(rhs.kind, ExprKind::BinOp { op: BinOp::Gt, .. }));
} else {
panic!("expected And");
}
}
#[test]
fn parse_boolean_or() {
let expr = parse_node_expr("@a > 0.0 || @b > 0.0");
assert!(matches!(expr.kind, ExprKind::BinOp { op: BinOp::Or, .. }));
}
#[test]
fn parse_unary_neg() {
let expr = parse_node_expr("-1.0");
assert!(matches!(
expr.kind,
ExprKind::UnaryOp {
op: UnaryOp::Neg,
..
}
));
}
#[test]
fn parse_unary_not() {
let expr = parse_node_expr("!true");
assert!(matches!(
expr.kind,
ExprKind::UnaryOp {
op: UnaryOp::Not,
..
}
));
}
#[test]
fn parse_complex_expression() {
let expr = parse_node_expr("@v_exhaust * ln(@mass_ratio)");
if let ExprKind::BinOp { op, lhs, rhs } = &expr.kind {
assert_eq!(*op, BinOp::Mul);
assert!(
matches!(&lhs.kind, ExprKind::GraphRef(id) if id.value.member() == "v_exhaust")
);
assert!(
matches!(&rhs.kind, ExprKind::FnCall { callee, .. } if callee.as_bare().is_some_and(|name| name.name == "ln"))
);
} else {
panic!("expected Mul");
}
}
#[test]
fn parse_comparison_eq() {
let expr = parse_node_expr("@x == 1.0");
assert!(matches!(expr.kind, ExprKind::BinOp { op: BinOp::Eq, .. }));
}
#[test]
fn parse_comparison_ne() {
let expr = parse_node_expr("@x != 1.0");
assert!(matches!(expr.kind, ExprKind::BinOp { op: BinOp::Ne, .. }));
}
#[test]
fn parse_field_access() {
let source = "node x: Dimensionless = @transfer.dv1;";
let file = Parser::new(source).parse_file().unwrap();
match &file.declarations[0].kind {
DeclKind::Node(n) => match &n.value.kind {
ExprKind::FieldAccess { expr, field } => {
assert!(
matches!(&expr.kind, ExprKind::GraphRef(ident) if ident.value.member() == "transfer")
);
assert_eq!(field.value.as_str(), "dv1");
}
other => panic!("expected FieldAccess, got {other:?}"),
},
_ => panic!("expected node"),
}
}
#[test]
fn parse_chained_field_access() {
let source = "node x: Dimensionless = @mission.transfer.dv1;";
let file = Parser::new(source).parse_file().unwrap();
match &file.declarations[0].kind {
DeclKind::Node(n) => match &n.value.kind {
ExprKind::FieldAccess { expr, field } => {
assert_eq!(field.value.as_str(), "dv1");
match &expr.kind {
ExprKind::FieldAccess {
expr: inner,
field: mid_field,
} => {
assert_eq!(mid_field.value.as_str(), "transfer");
assert!(
matches!(&inner.kind, ExprKind::GraphRef(ident) if ident.value.member() == "mission")
);
}
other => panic!("expected inner FieldAccess, got {other:?}"),
}
}
other => panic!("expected FieldAccess, got {other:?}"),
},
_ => panic!("expected node"),
}
}
#[test]
fn parse_at_with_field_access_parses_as_field_chain() {
let file = Parser::new("node x: Dimensionless = @stage.delta_v;")
.parse_file()
.unwrap();
let decl = &file.declarations[0].kind;
let DeclKind::Node(node) = decl else {
panic!("expected Node");
};
match &node.value.kind {
ExprKind::FieldAccess { expr: inner, field } => {
assert!(
matches!(&inner.kind, ExprKind::GraphRef(id) if id.value.member() == "stage")
);
assert_eq!(field.value.as_str(), "delta_v");
}
other => panic!("expected FieldAccess on GraphRef, got {other:?}"),
}
}
#[test]
fn parse_inline_dag_ref_basic() {
let file = Parser::new("node y: Length = @clamp(x: @p).result;")
.parse_file()
.unwrap();
let decl = &file.declarations[0].kind;
let DeclKind::Node(node) = decl else {
panic!("expected Node");
};
match &node.value.kind {
ExprKind::InlineDagRef { path, args, output } => {
assert_eq!(path.segments.len(), 1);
assert_eq!(path.segments[0].name, "clamp");
assert_eq!(args.len(), 1);
assert_eq!(args[0].name.name, "x");
assert!(
matches!(&args[0].value.kind, ExprKind::GraphRef(id) if id.value.member() == "p")
);
assert_eq!(output.value.as_str(), "result");
}
other => panic!("expected InlineDagRef, got {other:?}"),
}
}
#[test]
fn parse_inline_dag_ref_multi_arg() {
let file = Parser::new("node y: Velocity = @scale(factor: 2.0, v: @speed).out;")
.parse_file()
.unwrap();
let decl = &file.declarations[0].kind;
let DeclKind::Node(node) = decl else {
panic!("expected Node");
};
match &node.value.kind {
ExprKind::InlineDagRef { path, args, output } => {
assert_eq!(path.segments.len(), 1);
assert_eq!(path.segments[0].name, "scale");
assert_eq!(args.len(), 2);
assert_eq!(args[0].name.name, "factor");
assert_eq!(args[1].name.name, "v");
assert_eq!(output.value.as_str(), "out");
}
other => panic!("expected InlineDagRef, got {other:?}"),
}
}
#[test]
fn parse_inline_dag_ref_qualified_accepted() {
let file = Parser::new("node y: Length = @geom.clamp(x: @p).result;")
.parse_file()
.expect("qualified inline DAG call should parse");
let decl = &file.declarations[0].kind;
let DeclKind::Node(node) = decl else {
panic!("expected Node");
};
match &node.value.kind {
ExprKind::InlineDagRef { path, args, output } => {
assert_eq!(path.segments.len(), 2);
assert_eq!(path.segments[0].name, "geom");
assert_eq!(path.segments[1].name, "clamp");
assert_eq!(path.display_path(), "geom.clamp");
assert_eq!(args.len(), 1);
assert_eq!(args[0].name.name, "x");
assert_eq!(output.value.as_str(), "result");
}
other => panic!("expected InlineDagRef, got {other:?}"),
}
}
#[test]
fn parse_at_field_access_still_works() {
let file = Parser::new("node y: Length = @orbit.altitude;")
.parse_file()
.expect("graph-ref field access should parse");
let decl = &file.declarations[0].kind;
let DeclKind::Node(node) = decl else {
panic!("expected Node");
};
match &node.value.kind {
ExprKind::FieldAccess { expr, field } => {
assert_eq!(field.value.as_str(), "altitude");
match &expr.kind {
ExprKind::GraphRef(name) => assert_eq!(name.value.member(), "orbit"),
other => panic!("expected inner GraphRef, got {other:?}"),
}
}
other => panic!("expected FieldAccess, got {other:?}"),
}
}
#[test]
fn parse_at_field_access_chain_still_works() {
let file = Parser::new("node y: Length = @a.b.c;")
.parse_file()
.expect("graph-ref field access chain should parse");
let decl = &file.declarations[0].kind;
let DeclKind::Node(node) = decl else {
panic!("expected Node");
};
let ExprKind::FieldAccess {
expr: outer_inner,
field: c,
} = &node.value.kind
else {
panic!("expected outer FieldAccess");
};
assert_eq!(c.value.as_str(), "c");
let ExprKind::FieldAccess {
expr: inner,
field: b,
} = &outer_inner.kind
else {
panic!("expected inner FieldAccess");
};
assert_eq!(b.value.as_str(), "b");
let ExprKind::GraphRef(a) = &inner.kind else {
panic!("expected innermost GraphRef");
};
assert_eq!(a.value.member(), "a");
}
#[test]
fn parse_inline_dag_ref_no_projection_rejected() {
let result = Parser::new("node y: Length = @dag(x: @p);").parse_file();
assert!(
result.is_err(),
"@dag(args) without .<out> projection must be rejected"
);
}
#[test]
fn parse_inline_dag_ref_qualified_no_projection_rejected() {
let result = Parser::new("node y: Length = @geom.clamp(x: @p);").parse_file();
assert!(
result.is_err(),
"@module.dag(args) without .<out> projection must be rejected"
);
}
#[test]
fn parse_inline_dag_call_basic_fixture() {
let source =
include_str!("../../../../../tests/fixtures/valid/inline_dag_call_basic/main.gcl");
let file = Parser::new(source)
.parse_file()
.expect("fixture should parse");
let node = file
.declarations
.iter()
.find_map(|d| match &d.kind {
DeclKind::Node(n) if n.name.value.as_str() == "doubled_result" => Some(n),
_ => None,
})
.expect("doubled_result node");
assert!(matches!(&node.value.kind, ExprKind::InlineDagRef { .. }));
}
#[test]
fn parse_dotted_identifier_path_ref() {
let file = Parser::new("node x: Dimensionless = constants.physics.G0;")
.parse_file()
.unwrap();
let decl = &file.declarations[0].kind;
let DeclKind::Node(node) = decl else {
panic!("expected Node");
};
match &node.value.kind {
ExprKind::UnresolvedRef(crate::syntax::ast::UnresolvedRef::Path(path)) => {
let names = path
.segments
.iter()
.map(|segment| segment.name.as_str())
.collect::<Vec<_>>();
assert_eq!(names, vec!["constants", "physics", "G0"]);
}
other => panic!("expected unresolved path, got {other:?}"),
}
}
#[test]
fn single_expr_unit_literal() {
let expr = Parser::new("450.0 s").parse_single_expr().unwrap();
assert!(matches!(expr.kind, ExprKind::UnitLiteral { .. }));
}
#[test]
fn single_expr_integer_with_unit_errors() {
let result = Parser::new("450 s").parse_single_expr();
assert!(
result.is_err(),
"integer literal with unit should be an error"
);
}
#[test]
fn single_expr_number() {
let expr = Parser::new("3.0").parse_single_expr().unwrap();
assert!(matches!(expr.kind, ExprKind::Number(n) if (n - 3.0).abs() < f64::EPSILON));
}
#[test]
fn single_expr_compound_unit() {
let expr = Parser::new("9.80665 m/s^2").parse_single_expr().unwrap();
assert!(matches!(expr.kind, ExprKind::UnitLiteral { .. }));
}
#[test]
fn single_expr_arithmetic_with_const() {
let expr = Parser::new("2.0 * PI").parse_single_expr().unwrap();
assert!(matches!(expr.kind, ExprKind::BinOp { .. }));
}
#[test]
fn single_expr_trailing_tokens_error() {
let result = Parser::new("450.0 s; extra").parse_single_expr();
assert!(result.is_err());
}
}