use crate::ast::*;
use crate::lexer::{Span, Token, TokenKind};
struct Parser {
tokens: Vec<Token>,
pos: usize,
}
impl Parser {
fn new(tokens: Vec<Token>) -> Self {
Self { tokens, pos: 0 }
}
fn peek(&self) -> &TokenKind {
&self.tokens[self.pos].kind
}
fn span(&self) -> Span {
self.tokens[self.pos].span
}
fn advance(&mut self) -> &Token {
let tok = &self.tokens[self.pos];
if self.pos < self.tokens.len() - 1 {
self.pos += 1;
}
tok
}
fn expect(&mut self, expected: &TokenKind) -> Result<&Token, String> {
if self.peek() == expected {
Ok(self.advance())
} else {
Err(format!(
"expected {:?}, got {:?} at line {}, col {}",
expected,
self.peek(),
self.span().line,
self.span().col
))
}
}
fn expect_ident(&mut self) -> Result<String, String> {
match self.peek().clone() {
TokenKind::Ident(name) => {
self.advance();
Ok(name)
}
TokenKind::Input => {
self.advance();
Ok("input".to_string())
}
TokenKind::Cursor => {
self.advance();
Ok("cursor".to_string())
}
TokenKind::Over => {
self.advance();
Ok("over".to_string())
}
_ => Err(format!(
"expected identifier, got {:?} at line {}, col {}",
self.peek(),
self.span().line,
self.span().col
)),
}
}
fn at_eof(&self) -> bool {
matches!(self.peek(), TokenKind::Eof)
}
}
pub fn parse(tokens: Vec<Token>) -> Result<PolydatFile, String> {
let mut parser = Parser::new(tokens);
let mut statements = Vec::new();
while !parser.at_eof() {
parse_statement_into(&mut parser, &mut statements)?;
}
Ok(PolydatFile { statements })
}
pub fn parse_expression(tokens: Vec<Token>) -> Result<Expr, String> {
let mut parser = Parser::new(tokens);
let expr = parse_expr(&mut parser)?;
if !parser.at_eof() {
let span = parser.span();
return Err(format!(
"expected end of expression at line {}, col {}, got {:?}",
span.line,
span.col,
parser.peek()
));
}
Ok(expr)
}
fn parse_statement_into(p: &mut Parser, out: &mut Vec<Statement>) -> Result<(), String> {
match p.peek() {
TokenKind::Pragma => out.push(parse_pragma(p)?),
TokenKind::Input => parse_input_decl(p, out)?,
TokenKind::Extern => out.push(parse_extern_port(p)?),
TokenKind::Cursor => out.push(parse_cursor_decl(p)?),
TokenKind::For(_) => out.push(parse_for_statement(p)?),
TokenKind::Tile => out.push(parse_tile(p)?),
TokenKind::Const | TokenKind::Shared | TokenKind::Volatile => {
out.push(parse_modified_binding(p)?);
}
TokenKind::LParen => out.push(parse_destructuring_binding(p)?),
TokenKind::Ident(_) => {
if is_module_def(p) {
out.push(parse_module_def(p)?);
} else if is_polytile_binding(p) {
out.push(parse_polytile_binding(p)?);
} else {
out.push(parse_cycle_binding(p)?);
}
}
_ => {
return Err(format!(
"unexpected token {:?} at line {}, col {}",
p.peek(),
p.span().line,
p.span().col
));
}
}
Ok(())
}
fn parse_for_statement(p: &mut Parser) -> Result<Statement, String> {
let span = p.span();
let text = match p.peek().clone() {
TokenKind::For(text) => text,
other => {
return Err(format!(
"expected `for`, got {other:?} at line {}, col {}",
span.line, span.col
));
}
};
p.advance();
let source = for_source_from_text(&text, span, true)?;
if !matches!(p.peek(), TokenKind::LBrace) {
return Err(format!(
"`for {text}` at line {}, col {} needs a `{{` block on the same line; \
to bind a producer instead, write `name := for {text}`",
span.line, span.col
));
}
p.advance();
let mut body = Vec::new();
while !matches!(p.peek(), TokenKind::RBrace | TokenKind::Eof) {
parse_statement_into(p, &mut body)?;
}
p.expect(&TokenKind::RBrace)?;
Ok(Statement::For(ForStmt { source, body, span }))
}
fn parse_tile(p: &mut Parser) -> Result<Statement, String> {
let span = p.span();
p.expect(&TokenKind::Tile)?;
let name = p.expect_ident()?;
let encoding = if matches!(p.peek(), TokenKind::Colon) {
p.advance();
Some(p.expect_ident()?)
} else {
None
};
let mut options = TileOptions::default();
if matches!(p.peek(), TokenKind::LParen) {
p.advance();
while !matches!(p.peek(), TokenKind::RParen | TokenKind::Eof) {
let key = p.expect_ident()?;
match key.as_str() {
"delims" => {
options.open = expect_string(p, "delims open")?;
options.close = expect_string(p, "delims close")?;
if options.open.is_empty() || options.close.is_empty() {
return Err(format!(
"tile '{name}' at line {}, col {}: delimiters must not be empty",
span.line, span.col
));
}
}
"sigil" => {
options.sigil = expect_string(p, "sigil")?;
if options.sigil.is_empty() {
return Err(format!(
"tile '{name}' at line {}, col {}: sigil must not be empty",
span.line, span.col
));
}
}
"strict" => options.strict = true,
"instring" => options.in_string = true,
other => {
return Err(format!(
"tile '{name}' at line {}, col {}: unknown option '{other}'; options are delims, sigil, strict, instring",
span.line, span.col
));
}
}
if matches!(p.peek(), TokenKind::Comma) {
p.advance();
}
}
p.expect(&TokenKind::RParen)?;
}
if !matches!(p.peek(), TokenKind::ColonEq) {
return Err(format!(
"tile '{name}' at line {}, col {}: expected `:=` before the body; a tile binds a wire, \
as in `tile {name} : json := {{ ... }}` or `tile {name} := \"...\"`, got {:?}",
span.line,
span.col,
p.peek()
));
}
p.advance();
let (body_kind, body) = match p.peek().clone() {
TokenKind::TileBody(text, kind) => {
p.advance();
(kind, text)
}
TokenKind::StringLit(s) => {
p.advance();
(TileBodyKind::Literal, s)
}
other => {
return Err(format!(
"tile '{name}' at line {}, col {}: expected a body (a `{{ }}` or `[ ]` block, `<<< >>>` heredoc, or string), got {other:?}",
span.line, span.col
));
}
};
if let Some(enc) = &encoding
&& !matches!(enc.as_str(), "json" | "text" | "csv")
{
return Err(format!(
"tile '{name}' at line {}, col {}: unknown encoding '{enc}'; encodings are json, text, csv",
span.line, span.col
));
}
let pieces = super::tile::parse_template(&body, &options, span)
.map_err(|e| format!("tile '{name}': {e}"))?;
Ok(Statement::Tile(TileDef {
name,
encoding,
options,
body_kind,
body,
pieces,
span,
}))
}
fn is_polytile_binding(p: &Parser) -> bool {
p.pos + 3 < p.tokens.len()
&& matches!(&p.tokens[p.pos].kind, TokenKind::Ident(_))
&& matches!(&p.tokens[p.pos + 1].kind, TokenKind::ColonEq)
&& matches!(&p.tokens[p.pos + 2].kind, TokenKind::Ident(f) if f == "polytile" || f == "polytile_json")
&& matches!(&p.tokens[p.pos + 3].kind, TokenKind::LParen)
}
fn parse_polytile_binding(p: &mut Parser) -> Result<Statement, String> {
let span = p.span();
let name = p.expect_ident()?;
p.expect(&TokenKind::ColonEq)?;
let func = p.expect_ident()?;
p.expect(&TokenKind::LParen)?;
let at = |what: &str| {
format!(
"{func} for '{name}' at line {}, col {}: {what}",
span.line, span.col
)
};
let encoding = if func == "polytile" {
let e = expect_string(p, "the encoding").map_err(|m| at(&m))?;
if !matches!(p.peek(), TokenKind::Comma) {
return Err(at("expected `,` and then the template"));
}
p.advance();
Some(e)
} else {
None
};
let body = expect_string(p, "the template body (a string or a `<<< >>>` heredoc)")
.map_err(|m| at(&m))?;
let mut options = TileOptions::default();
while matches!(p.peek(), TokenKind::Comma) {
p.advance();
if matches!(p.peek(), TokenKind::RParen) {
break;
}
let key = p.expect_ident()?;
p.expect(&TokenKind::Colon)
.map_err(|_| at(&format!("option `{key}` needs `: \"value\"`")))?;
match key.as_str() {
"open" => options.open = expect_string(p, "open").map_err(|m| at(&m))?,
"close" => options.close = expect_string(p, "close").map_err(|m| at(&m))?,
"sigil" => options.sigil = expect_string(p, "sigil").map_err(|m| at(&m))?,
"strict" => {
let v = p.expect_ident().map_err(|m| at(&m))?;
options.strict = v == "true";
}
"instring" => {
let v = p.expect_ident().map_err(|m| at(&m))?;
options.in_string = v == "true";
}
other => {
return Err(at(&format!(
"unknown option '{other}'; options are open, close, sigil, strict, instring"
)));
}
}
}
p.expect(&TokenKind::RParen).map_err(|m| at(&m))?;
if options.open.is_empty() || options.close.is_empty() || options.sigil.is_empty() {
return Err(at("delimiters and sigil must not be empty"));
}
let tile = match encoding {
Some(enc) => super::tile_structural::tile_from_text(&name, &enc, &body, &options, span)?,
None => super::tile_structural::tile_from_json_text(&name, &body, &options, span)?,
};
Ok(Statement::Tile(tile))
}
fn expect_string(p: &mut Parser, what: &str) -> Result<String, String> {
match p.peek().clone() {
TokenKind::StringLit(s) => {
p.advance();
Ok(s)
}
other => Err(format!(
"expected a string for {what}, got {other:?} at line {}, col {}",
p.span().line,
p.span().col
)),
}
}
pub fn for_source_from_text(
text: &str,
span: Span,
allow_producer: bool,
) -> Result<ForSource, String> {
if text.is_empty() {
return Err(format!(
"`for` at line {}, col {} has no comprehension",
span.line, span.col
));
}
let is_ident = text.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
&& text
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
if is_ident {
if allow_producer {
return Ok(ForSource {
text: text.to_string(),
kind: ForSourceKind::Producer(text.to_string()),
span,
});
}
return Err(format!(
"`for {text}` at line {}, col {}: a producer expression needs comprehension text such as `k in 1..10`, \
or a derivation such as `{text} where {{k}} > 1` or `{text} order halton/5`",
span.line, span.col
));
}
{
use crate::comprehension::parse::{split_at_order, split_at_where};
let (head, order) = split_at_order(text);
let (base, filter) = split_at_where(&head);
let base = base.trim();
let base_is_ident = !base.is_empty()
&& base.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
&& base
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
if base_is_ident && (filter.is_some() || order.is_some()) {
return Ok(ForSource {
text: text.to_string(),
kind: ForSourceKind::Derived {
base: base.to_string(),
filter,
order,
},
span,
});
}
}
let legacy = crate::comprehension::parse::parse_comprehension_text(text)
.map_err(|e| format!("`for {text}` at line {}, col {}: {e}", span.line, span.col))?;
let algebra = crate::comprehension::spec::legacy_to_algebra(&legacy)
.map_err(|e| format!("`for {text}` at line {}, col {}: {e}", span.line, span.col))?;
Ok(ForSource {
text: text.to_string(),
kind: ForSourceKind::Comprehension(algebra),
span,
})
}
fn parse_pragma(p: &mut Parser) -> Result<Statement, String> {
let span = p.span();
p.expect(&TokenKind::Pragma)?;
let name = p.expect_ident()?;
Ok(Statement::Pragma { name, span })
}
fn is_module_def(p: &Parser) -> bool {
if p.pos + 4 >= p.tokens.len() {
return false;
}
let third_is_param_name = matches!(
&p.tokens[p.pos + 2].kind,
TokenKind::Ident(_) | TokenKind::Input,
);
matches!(&p.tokens[p.pos].kind, TokenKind::Ident(_))
&& matches!(&p.tokens[p.pos + 1].kind, TokenKind::LParen)
&& third_is_param_name
&& matches!(&p.tokens[p.pos + 3].kind, TokenKind::Colon)
}
fn parse_module_def(p: &mut Parser) -> Result<Statement, String> {
let span = p.span();
let name = p.expect_ident()?;
p.expect(&TokenKind::LParen)?;
let mut params = Vec::new();
while !matches!(p.peek(), TokenKind::RParen) {
let pname = p.expect_ident()?;
p.expect(&TokenKind::Colon)?;
let ptype = p.expect_ident()?;
params.push(TypedParam {
name: pname,
typ: ptype,
});
if matches!(p.peek(), TokenKind::Comma) {
p.advance();
}
}
p.expect(&TokenKind::RParen)?;
p.expect(&TokenKind::Arrow)?;
p.expect(&TokenKind::LParen)?;
let mut outputs = Vec::new();
while !matches!(p.peek(), TokenKind::RParen) {
let oname = p.expect_ident()?;
p.expect(&TokenKind::Colon)?;
let otype = p.expect_ident()?;
outputs.push(TypedParam {
name: oname,
typ: otype,
});
if matches!(p.peek(), TokenKind::Comma) {
p.advance();
}
}
p.expect(&TokenKind::RParen)?;
p.expect(&TokenKind::ColonEq)?;
p.expect(&TokenKind::LBrace)?;
let mut body = Vec::new();
while !matches!(p.peek(), TokenKind::RBrace | TokenKind::Eof) {
parse_statement_into(p, &mut body)?;
}
p.expect(&TokenKind::RBrace)?;
Ok(Statement::ModuleDef(ModuleDef {
name,
params,
outputs,
body,
span,
}))
}
fn parse_extern_port(p: &mut Parser) -> Result<Statement, String> {
let span = p.span();
p.advance();
let name = p.expect_ident()?;
p.expect(&TokenKind::Colon)?;
let typ = p.expect_ident()?;
let default = if matches!(p.peek(), TokenKind::Eq) {
p.advance(); Some(parse_expr(p)?)
} else {
None
};
Ok(Statement::ExternPort(ExternPort {
name,
typ,
default,
span,
}))
}
fn parse_input_decl(p: &mut Parser, out: &mut Vec<Statement>) -> Result<(), String> {
let keyword_span = p.span();
p.advance();
if matches!(p.peek(), TokenKind::LParen) {
p.advance(); if matches!(p.peek(), TokenKind::RParen) {
return Err(format!(
"`input ()` is empty; omit the line entirely to declare zero inputs \
(at line {}, col {})",
keyword_span.line, keyword_span.col,
));
}
loop {
let span = p.span();
let name = p.expect_ident()?;
let ty = if matches!(p.peek(), TokenKind::Colon) {
p.advance();
Some(p.expect_ident()?)
} else {
None
};
out.push(Statement::InputDecl(InputDecl { name, ty, span }));
if matches!(p.peek(), TokenKind::Comma) {
p.advance();
} else {
break;
}
}
p.expect(&TokenKind::RParen)?;
} else {
let span = p.span();
let name = p.expect_ident()?;
let ty = if matches!(p.peek(), TokenKind::Colon) {
p.advance();
Some(p.expect_ident()?)
} else {
None
};
out.push(Statement::InputDecl(InputDecl { name, ty, span }));
}
Ok(())
}
fn parse_cursor_decl(p: &mut Parser) -> Result<Statement, String> {
let span = p.span();
p.advance(); let name = p.expect_ident()?;
p.expect(&TokenKind::Eq)?;
let constructor = parse_expr(p)?;
let over = if matches!(p.peek(), TokenKind::Over) {
p.advance();
Some(parse_expr(p)?)
} else {
None
};
Ok(Statement::Cursor(CursorDecl {
name,
constructor,
over,
span,
}))
}
fn parse_modified_binding(p: &mut Parser) -> Result<Statement, String> {
let start_span = p.span();
let mut collected: Vec<WireModifier> = Vec::new();
loop {
let m = match p.peek() {
TokenKind::Const => WireModifier::Const,
TokenKind::Shared => WireModifier::Shared,
TokenKind::Volatile => WireModifier::Volatile,
_ => break,
};
if collected.contains(&m) {
return Err(format!(
"duplicate `{m:?}` modifier at line {}, col {}",
p.span().line,
p.span().col,
));
}
collected.push(m);
p.advance();
}
let modifier = BindingModifier::try_from_iter(collected)
.map_err(|e| format!("{e} at line {}, col {}", start_span.line, start_span.col,))?;
match p.peek() {
TokenKind::Ident(_) | TokenKind::Input | TokenKind::Cursor | TokenKind::Over => {
parse_cycle_binding_with_modifier(p, modifier)
}
_ => Err(format!(
"expected binding name after modifiers at line {}, col {}",
p.span().line,
p.span().col
)),
}
}
fn parse_cycle_binding(p: &mut Parser) -> Result<Statement, String> {
parse_cycle_binding_with_modifier(p, BindingModifier::NONE)
}
fn parse_cycle_binding_with_modifier(
p: &mut Parser,
modifier: BindingModifier,
) -> Result<Statement, String> {
let span = p.span();
let name = p.expect_ident()?;
let type_annotation = if matches!(p.peek(), TokenKind::Colon) {
p.advance(); let typ = p.expect_ident()?;
if !modifier.is_shared() {
return Err(format!(
"type annotation `{name}: {typ}` is only supported on `shared` bindings (it pins the shared cell's type). For a plain typed slot use `extern {name}: {typ} = …` at line {}, col {}",
span.line, span.col,
));
}
Some(typ)
} else {
None
};
p.expect(&TokenKind::ColonEq)?;
let value = parse_expr(p)?;
Ok(Statement::Binding(Binding {
targets: vec![name],
value,
modifier,
type_annotation,
span,
}))
}
fn parse_destructuring_binding(p: &mut Parser) -> Result<Statement, String> {
let span = p.span();
p.advance(); let mut targets = Vec::new();
loop {
targets.push(p.expect_ident()?);
if matches!(p.peek(), TokenKind::Comma) {
p.advance();
} else {
break;
}
}
p.expect(&TokenKind::RParen)?;
p.expect(&TokenKind::ColonEq)?;
let value = parse_expr(p)?;
Ok(Statement::Binding(Binding {
targets,
value,
modifier: BindingModifier::NONE,
type_annotation: None,
span,
}))
}
fn parse_expr(p: &mut Parser) -> Result<Expr, String> {
parse_expr_bp(p, 0)
}
fn parse_expr_bp(p: &mut Parser, min_bp: u8) -> Result<Expr, String> {
let mut lhs = parse_atom(p)?;
lhs = parse_postfix_as(p, lhs)?;
loop {
let op = match p.peek() {
TokenKind::PipePipe => Some((BinOpKind::Or, 1, 2)),
TokenKind::AmpAmp => Some((BinOpKind::And, 3, 4)),
TokenKind::EqEq => Some((BinOpKind::Eq, 5, 6)),
TokenKind::BangEq => Some((BinOpKind::Ne, 5, 6)),
TokenKind::Lt => Some((BinOpKind::Lt, 7, 8)),
TokenKind::Gt => Some((BinOpKind::Gt, 7, 8)),
TokenKind::LtEq => Some((BinOpKind::Le, 7, 8)),
TokenKind::GtEq => Some((BinOpKind::Ge, 7, 8)),
TokenKind::Pipe => Some((BinOpKind::BitOr, 9, 10)),
TokenKind::Caret => Some((BinOpKind::BitXor, 11, 12)),
TokenKind::Ampersand => Some((BinOpKind::BitAnd, 13, 14)),
TokenKind::ShiftLeft => Some((BinOpKind::Shl, 15, 16)),
TokenKind::ShiftRight => Some((BinOpKind::Shr, 15, 16)),
TokenKind::Plus => Some((BinOpKind::Add, 17, 18)),
TokenKind::Minus => Some((BinOpKind::Sub, 17, 18)),
TokenKind::Star => Some((BinOpKind::Mul, 19, 20)),
TokenKind::Slash => Some((BinOpKind::Div, 19, 20)),
TokenKind::Percent => Some((BinOpKind::Mod, 19, 20)),
TokenKind::StarStar => Some((BinOpKind::Pow, 22, 21)), _ => None,
};
let Some((op_kind, l_bp, r_bp)) = op else {
break;
};
if l_bp < min_bp {
break;
}
p.advance(); let rhs = parse_expr_bp(p, r_bp)?;
lhs = Expr::BinOp(Box::new(lhs), op_kind, Box::new(rhs));
}
Ok(lhs)
}
fn parse_postfix_as(p: &mut Parser, mut expr: Expr) -> Result<Expr, String> {
while matches!(p.peek(), TokenKind::Ident(s) if s.as_str() == "as") {
let span = p.span();
p.advance(); let ty_name = match p.peek() {
TokenKind::Ident(name) => name.clone(),
other => return Err(format!("expected a type name after `as`, found {other:?}")),
};
p.advance(); let port_type = crate::PortType::from_keyword(&ty_name)
.ok_or_else(|| format!("unknown type `{ty_name}` in `... as {ty_name}` cast"))?;
expr = Expr::Cast(Box::new(expr), port_type, span);
}
Ok(expr)
}
fn parse_if_block(p: &mut Parser, span: Span) -> Result<Expr, String> {
let cond = parse_expr(p)?;
if !matches!(p.peek(), TokenKind::LBrace) {
return Err(format!(
"expected `{{` to open the then-branch of an `if` expression, got {:?} at line {}, col {}. \
Block form is `if <cond> {{ <then> }} else {{ <else> }}`; the call form `if(cond, a, b)` \
is also accepted.",
p.peek(),
p.span().line,
p.span().col
));
}
p.advance();
let then_expr = parse_expr(p)?;
p.expect(&TokenKind::RBrace)?;
match p.peek().clone() {
TokenKind::Ident(word) if word == "else" => {
p.advance();
}
other => {
return Err(format!(
"expected `else` after the then-branch of an `if` expression, got {:?} at line {}, col {}. \
`else` is required: a Polydat expression always produces a value, so there is no \
result for the false path without it.",
other,
p.span().line,
p.span().col
));
}
}
let else_expr = match p.peek().clone() {
TokenKind::Ident(word) if word == "if" => {
let else_span = p.span();
p.advance();
parse_if_block(p, else_span)?
}
TokenKind::LBrace => {
p.advance();
let e = parse_expr(p)?;
p.expect(&TokenKind::RBrace)?;
e
}
other => {
return Err(format!(
"expected `{{` or `if` after `else`, got {:?} at line {}, col {}",
other,
p.span().line,
p.span().col
));
}
};
Ok(Expr::Call(CallExpr {
func: "if".into(),
args: vec![
Arg::Positional(cond),
Arg::Positional(then_expr),
Arg::Positional(else_expr),
],
span,
}))
}
fn parse_atom(p: &mut Parser) -> Result<Expr, String> {
let span = p.span();
match p.peek().clone() {
TokenKind::Minus => {
p.advance();
let inner = parse_atom(p)?;
Ok(Expr::UnaryNeg(Box::new(inner), span))
}
TokenKind::Bang => {
p.advance();
let inner = parse_atom(p)?;
Ok(Expr::UnaryBitNot(Box::new(inner), span))
}
TokenKind::LParen => {
p.advance(); let inner = parse_expr(p)?;
p.expect(&TokenKind::RParen)?;
Ok(inner)
}
TokenKind::StringLit(s) => {
p.advance();
Ok(parse_interpolated_string(s, span))
}
TokenKind::IntLit(v) => {
p.advance();
Ok(Expr::IntLit(v, span))
}
TokenKind::FloatLit(v) => {
p.advance();
Ok(Expr::FloatLit(v, span))
}
TokenKind::LBracket => parse_array_lit(p),
TokenKind::For(text) => {
p.advance();
let source = for_source_from_text(&text, span, false)?;
Ok(Expr::For(Box::new(source)))
}
TokenKind::Ident(name) => {
p.advance();
if name == "if" && !matches!(p.peek(), TokenKind::LParen) {
parse_if_block(p, span)
} else if matches!(p.peek(), TokenKind::LParen) {
parse_call(p, name, span)
} else if matches!(p.peek(), TokenKind::Dot) {
parse_field_chain(p, name, span)
} else {
Ok(Expr::Ident(name, span))
}
}
TokenKind::Input => {
p.advance();
let name = "input".to_string();
if matches!(p.peek(), TokenKind::Dot) {
parse_field_chain(p, name, span)
} else {
Ok(Expr::Ident(name, span))
}
}
TokenKind::Cursor => {
p.advance();
let name = "cursor".to_string();
if matches!(p.peek(), TokenKind::Dot) {
parse_field_chain(p, name, span)
} else {
Ok(Expr::Ident(name, span))
}
}
TokenKind::Over => {
p.advance();
let name = "over".to_string();
if matches!(p.peek(), TokenKind::Dot) {
parse_field_chain(p, name, span)
} else {
Ok(Expr::Ident(name, span))
}
}
_ => Err(format!(
"expected expression, got {:?} at line {}, col {}",
p.peek(),
span.line,
span.col
)),
}
}
fn parse_interpolated_string(s: String, span: Span) -> Expr {
let segments = match scan_interpolation_segments(&s) {
Some(segs) => segs,
None => return Expr::StringLit(s, span), };
if !segments
.iter()
.any(|seg| matches!(seg, Segment::Placeholder(_)))
{
return Expr::StringLit(s, span);
}
let mut format_str = String::with_capacity(s.len());
let mut placeholder_exprs: Vec<Expr> = Vec::new();
for seg in segments {
match seg {
Segment::Literal(text) => format_str.push_str(&text),
Segment::Placeholder(body) => {
let expr = match parse_placeholder_body(&body, span) {
Ok(e) => e,
Err(_) => return Expr::StringLit(s, span),
};
placeholder_exprs.push(expr);
format_str.push_str("{}");
}
}
}
let mut args: Vec<Arg> = Vec::with_capacity(placeholder_exprs.len() + 1);
args.push(Arg::Positional(Expr::StringLit(format_str, span)));
for e in placeholder_exprs {
args.push(Arg::Positional(e));
}
Expr::Call(CallExpr {
func: "printf".into(),
args,
span,
})
}
enum Segment {
Literal(String),
Placeholder(String),
}
fn scan_interpolation_segments(s: &str) -> Option<Vec<Segment>> {
let chars: Vec<char> = s.chars().collect();
let mut segments: Vec<Segment> = Vec::new();
let mut literal = String::new();
let mut i = 0;
while i < chars.len() {
let c = chars[i];
if c == '{' && i + 1 < chars.len() && chars[i + 1] == '{' {
literal.push_str("{{");
i += 2;
continue;
}
if c == '}' && i + 1 < chars.len() && chars[i + 1] == '}' {
literal.push_str("}}");
i += 2;
continue;
}
if c == '{' {
if !literal.is_empty() {
segments.push(Segment::Literal(std::mem::take(&mut literal)));
}
let body_start = i + 1;
let body_end = find_placeholder_end(&chars, body_start)?;
let body: String = chars[body_start..body_end].iter().collect();
segments.push(Segment::Placeholder(body));
i = body_end + 1; continue;
}
literal.push(c);
i += 1;
}
if !literal.is_empty() {
segments.push(Segment::Literal(literal));
}
Some(segments)
}
fn find_placeholder_end(chars: &[char], start: usize) -> Option<usize> {
let mut depth: i32 = 0;
let mut in_string = false;
let mut i = start;
while i < chars.len() {
let c = chars[i];
if in_string {
if c == '\\' && i + 1 < chars.len() {
i += 2;
continue;
}
if c == '"' {
in_string = false;
}
i += 1;
continue;
}
match c {
'"' => in_string = true,
'(' | '[' | '{' => depth += 1,
')' | ']' => depth -= 1,
'}' => {
if depth == 0 {
return Some(i);
}
depth -= 1;
}
_ => {}
}
i += 1;
}
None
}
fn parse_placeholder_body(body: &str, _span: Span) -> Result<Expr, String> {
let body = body.trim();
if body.is_empty() {
return Err("empty placeholder".into());
}
let tokens = crate::lexer::lex(body)?;
parse_expression(tokens)
}
fn parse_field_chain(p: &mut Parser, base: String, span: Span) -> Result<Expr, String> {
p.advance(); let mut source = base;
let mut field = p.expect_ident()?;
while matches!(p.peek(), TokenKind::Dot) {
p.advance();
source = format!("{source}__{field}");
field = p.expect_ident()?;
}
Ok(Expr::FieldAccess {
source,
field,
span,
})
}
fn parse_call(p: &mut Parser, func: String, span: Span) -> Result<Expr, String> {
p.advance(); let mut args = Vec::new();
if !matches!(p.peek(), TokenKind::RParen) {
loop {
args.push(parse_arg(p)?);
if matches!(p.peek(), TokenKind::Comma) {
p.advance();
} else {
break;
}
}
}
p.expect(&TokenKind::RParen)?;
Ok(Expr::Call(CallExpr { func, args, span }))
}
fn parse_arg(p: &mut Parser) -> Result<Arg, String> {
let arg_name: Option<String> = match p.peek() {
TokenKind::Ident(name) => Some(name.clone()),
TokenKind::Input => Some("input".to_string()),
_ => None,
};
if let Some(name) = arg_name
&& p.pos + 1 < p.tokens.len()
&& matches!(p.tokens[p.pos + 1].kind, TokenKind::Colon)
{
p.advance(); p.advance(); let value = parse_expr(p)?;
return Ok(Arg::Named(name, value));
}
let expr = parse_expr(p)?;
Ok(Arg::Positional(expr))
}
fn parse_array_lit(p: &mut Parser) -> Result<Expr, String> {
let span = p.span();
p.advance(); let mut elements = Vec::new();
if !matches!(p.peek(), TokenKind::RBracket) {
loop {
elements.push(parse_expr(p)?);
if matches!(p.peek(), TokenKind::Comma) {
p.advance();
} else {
break;
}
}
}
p.expect(&TokenKind::RBracket)?;
Ok(Expr::ArrayLit(elements, span))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lexer::lex;
fn parse_str(s: &str) -> PolydatFile {
let tokens = lex(s).unwrap();
parse(tokens).unwrap()
}
fn parse_str_err(s: &str) -> String {
let tokens = lex(s).unwrap();
match parse(tokens) {
Ok(_) => panic!("expected parse error from: {s:?}"),
Err(e) => e,
}
}
fn cycle_modifier_of(f: &PolydatFile) -> BindingModifier {
match &f.statements[0] {
Statement::Binding(b) => b.modifier,
other => panic!("expected cycle binding, got {other:?}"),
}
}
#[test]
fn parse_volatile_modifier() {
let f = parse_str("volatile x := 42");
let m = cycle_modifier_of(&f);
assert!(m.is_volatile() && !m.is_const() && !m.is_shared());
}
#[test]
fn parse_modifiers_in_any_order_yields_same_set() {
let m1 = cycle_modifier_of(&parse_str("const shared x := 42"));
let m2 = cycle_modifier_of(&parse_str("shared const x := 42"));
assert_eq!(
m1, m2,
"ordering shouldn't matter: `const shared` and `shared const` collapse to the same set"
);
assert!(m1.is_const() && m1.is_shared());
}
#[test]
fn parse_shared_volatile_combination() {
let m = cycle_modifier_of(&parse_str("shared volatile x := 42"));
assert!(m.is_shared() && m.is_volatile() && !m.is_const());
}
#[test]
fn parse_rejects_const_volatile_combo() {
let err = parse_str_err("const volatile x := 42");
assert!(
err.contains("const") && err.contains("volatile"),
"error should name the conflicting keywords: {err}"
);
}
#[test]
fn parse_rejects_volatile_const_combo_same_as_const_volatile() {
let err = parse_str_err("volatile const x := 42");
assert!(err.contains("const") && err.contains("volatile"));
}
#[test]
fn parse_rejects_duplicate_modifier() {
let err = parse_str_err("const const x := 42");
assert!(
err.contains("duplicate"),
"error should call out duplicate: {err}"
);
}
#[test]
fn parse_volatile_const_binding() {
let f = parse_str("volatile x := 42");
match &f.statements[0] {
Statement::Binding(b) => {
assert!(b.modifier.is_volatile());
assert!(!b.modifier.is_const());
}
other => panic!("expected binding, got {other:?}"),
}
}
#[test]
fn parse_input_bare() {
let f = parse_str("input cycle: u64");
assert_eq!(f.statements.len(), 1);
match &f.statements[0] {
Statement::InputDecl(d) => {
assert_eq!(d.name, "cycle");
assert_eq!(d.ty.as_deref(), Some("u64"));
}
other => panic!("expected InputDecl, got {other:?}"),
}
}
#[test]
fn parse_input_bare_untyped() {
let f = parse_str("input cycle");
match &f.statements[0] {
Statement::InputDecl(d) => {
assert_eq!(d.name, "cycle");
assert!(d.ty.is_none(), "no type annotation");
}
other => panic!("expected InputDecl, got {other:?}"),
}
}
#[test]
fn parse_input_tuple_form() {
let f = parse_str("input (cycle: u64, q: f64)");
assert_eq!(f.statements.len(), 2);
match &f.statements[0] {
Statement::InputDecl(d) => {
assert_eq!(d.name, "cycle");
assert_eq!(d.ty.as_deref(), Some("u64"));
}
other => panic!("expected InputDecl, got {other:?}"),
}
match &f.statements[1] {
Statement::InputDecl(d) => {
assert_eq!(d.name, "q");
assert_eq!(d.ty.as_deref(), Some("f64"));
}
other => panic!("expected InputDecl, got {other:?}"),
}
}
#[test]
fn parse_input_tuple_empty_rejected() {
let tokens = crate::lexer::lex("input ()").unwrap();
let err = parse(tokens).unwrap_err();
assert!(
err.contains("empty"),
"error should mention empty tuple: {err}"
);
}
#[test]
fn parse_const_binding() {
let f = parse_str("const lut := dist_normal(72.0, 5.0)");
assert_eq!(f.statements.len(), 1);
match &f.statements[0] {
Statement::Binding(b) => {
assert_eq!(b.targets, vec!["lut"]);
assert!(b.modifier.is_const());
match &b.value {
Expr::Call(c) => {
assert_eq!(c.func, "dist_normal");
assert_eq!(c.args.len(), 2);
}
_ => panic!("expected call"),
}
}
_ => panic!("expected const binding"),
}
}
#[test]
fn parse_cycle_binding() {
let f = parse_str("seed := hash(cycle)");
match &f.statements[0] {
Statement::Binding(b) => {
assert_eq!(b.targets, vec!["seed"]);
match &b.value {
Expr::Call(c) => {
assert_eq!(c.func, "hash");
assert_eq!(c.args.len(), 1);
}
_ => panic!("expected call"),
}
}
_ => panic!("expected cycle binding"),
}
}
#[test]
fn parse_destructuring() {
let f = parse_str("(tenant, device, reading) := mixed_radix(cycle, 100, 1000, 0)");
match &f.statements[0] {
Statement::Binding(b) => {
assert_eq!(b.targets, vec!["tenant", "device", "reading"]);
match &b.value {
Expr::Call(c) => {
assert_eq!(c.func, "mixed_radix");
assert_eq!(c.args.len(), 4);
}
_ => panic!("expected call"),
}
}
_ => panic!("expected cycle binding"),
}
}
#[test]
fn parse_named_args() {
let f = parse_str("const lut := dist_normal(mean: 72.0, stddev: 5.0)");
match &f.statements[0] {
Statement::Binding(b) => match &b.value {
Expr::Call(c) => {
assert!(matches!(&c.args[0], Arg::Named(n, _) if n == "mean"));
assert!(matches!(&c.args[1], Arg::Named(n, _) if n == "stddev"));
}
_ => panic!("expected call"),
},
_ => panic!("expected const binding"),
}
}
#[test]
fn parse_string_lit_plain() {
let f = parse_str(r#"id := "static text""#);
match &f.statements[0] {
Statement::Binding(b) => match &b.value {
Expr::StringLit(s, _) => assert_eq!(s, "static text"),
_ => panic!("expected string lit"),
},
_ => panic!("expected binding"),
}
}
#[test]
fn parse_string_lit_interpolated() {
let f = parse_str(r#"id := "{code}-{seq}""#);
match &f.statements[0] {
Statement::Binding(b) => match &b.value {
Expr::Call(c) => {
assert_eq!(c.func, "printf");
assert_eq!(c.args.len(), 3);
match &c.args[0] {
Arg::Positional(Expr::StringLit(s, _)) => assert_eq!(s, "{}-{}"),
_ => panic!("expected format string as first arg"),
}
match &c.args[1] {
Arg::Positional(Expr::Ident(n, _)) => assert_eq!(n, "code"),
_ => panic!("expected ident `code`"),
}
match &c.args[2] {
Arg::Positional(Expr::Ident(n, _)) => assert_eq!(n, "seq"),
_ => panic!("expected ident `seq`"),
}
}
other => panic!("expected printf call, got {other:?}"),
},
_ => panic!("expected binding"),
}
}
#[test]
fn parse_string_lit_format_spec_left_alone() {
let f = parse_str(r#"id := "x={:05}""#);
match &f.statements[0] {
Statement::Binding(b) => match &b.value {
Expr::StringLit(s, _) => assert_eq!(s, "x={:05}"),
_ => panic!("expected literal"),
},
_ => panic!("expected binding"),
}
}
#[test]
fn parse_string_lit_nested_call() {
let f = parse_str(r#"email := "{format_u64(hash(cycle), 10)}@example.com""#);
let call = match &f.statements[0] {
Statement::Binding(b) => match &b.value {
Expr::Call(c) => c,
other => panic!("expected printf call, got {other:?}"),
},
_ => panic!("expected binding"),
};
assert_eq!(call.func, "printf");
assert_eq!(call.args.len(), 2);
match &call.args[0] {
Arg::Positional(Expr::StringLit(s, _)) => assert_eq!(s, "{}@example.com"),
other => panic!("expected format string, got {other:?}"),
}
match &call.args[1] {
Arg::Positional(Expr::Call(inner)) => {
assert_eq!(inner.func, "format_u64");
assert_eq!(inner.args.len(), 2);
match &inner.args[0] {
Arg::Positional(Expr::Call(h)) => assert_eq!(h.func, "hash"),
other => panic!("expected hash(...) call, got {other:?}"),
}
match &inner.args[1] {
Arg::Positional(Expr::IntLit(10, _)) => {}
other => panic!("expected literal 10, got {other:?}"),
}
}
other => panic!("expected format_u64 call, got {other:?}"),
}
}
#[test]
fn parse_string_lit_arithmetic_in_placeholder() {
let f = parse_str(r#"id := "x={a + b * 2}""#);
let call = match &f.statements[0] {
Statement::Binding(b) => match &b.value {
Expr::Call(c) => c,
other => panic!("expected call, got {other:?}"),
},
_ => panic!("expected binding"),
};
assert_eq!(call.func, "printf");
match &call.args[1] {
Arg::Positional(Expr::BinOp(_, BinOpKind::Add, _)) => {}
other => panic!("expected addition, got {other:?}"),
}
}
#[test]
fn parse_string_lit_field_access() {
let f = parse_str(r#"k := "row {row.id}""#);
let call = match &f.statements[0] {
Statement::Binding(b) => match &b.value {
Expr::Call(c) => c,
other => panic!("expected call, got {other:?}"),
},
_ => panic!("expected binding"),
};
assert_eq!(call.func, "printf");
match &call.args[1] {
Arg::Positional(Expr::FieldAccess { source, field, .. }) => {
assert_eq!(source, "row");
assert_eq!(field, "id");
}
other => panic!("expected field access, got {other:?}"),
}
}
#[test]
fn parse_string_lit_escaped_braces() {
let f = parse_str(r#"k := "{{not a placeholder}} but {real}""#);
let call = match &f.statements[0] {
Statement::Binding(b) => match &b.value {
Expr::Call(c) => c,
other => panic!("expected call, got {other:?}"),
},
_ => panic!("expected binding"),
};
match &call.args[0] {
Arg::Positional(Expr::StringLit(s, _)) => {
assert_eq!(s, "{{not a placeholder}} but {}");
}
other => panic!("expected fmt string, got {other:?}"),
}
match &call.args[1] {
Arg::Positional(Expr::Ident(n, _)) => assert_eq!(n, "real"),
other => panic!("expected ident `real`, got {other:?}"),
}
}
#[test]
fn parse_string_lit_unterminated_falls_back() {
let f = parse_str(r#"k := "missing close {abc""#);
match &f.statements[0] {
Statement::Binding(b) => match &b.value {
Expr::StringLit(s, _) => assert_eq!(s, "missing close {abc"),
other => panic!("expected literal, got {other:?}"),
},
_ => panic!("expected binding"),
}
}
#[test]
fn parse_string_lit_parens_in_placeholder() {
let f = parse_str(r#"k := "{abs(x - y)}""#);
let call = match &f.statements[0] {
Statement::Binding(b) => match &b.value {
Expr::Call(c) => c,
other => panic!("expected call, got {other:?}"),
},
_ => panic!("expected binding"),
};
assert_eq!(call.func, "printf");
match &call.args[1] {
Arg::Positional(Expr::Call(inner)) => assert_eq!(inner.func, "abs"),
other => panic!("expected abs call, got {other:?}"),
}
}
#[test]
fn parse_array_lit() {
let f = parse_str("const weights := [60.0, 20.0, 15.0, 5.0]");
match &f.statements[0] {
Statement::Binding(b) => match &b.value {
Expr::ArrayLit(elems, _) => assert_eq!(elems.len(), 4),
_ => panic!("expected array lit"),
},
_ => panic!("expected const binding"),
}
}
#[test]
fn parse_nested_call() {
let f = parse_str("x := hash(interleave(a, b))");
match &f.statements[0] {
Statement::Binding(b) => match &b.value {
Expr::Call(c) => {
assert_eq!(c.func, "hash");
assert_eq!(c.args.len(), 1);
match &c.args[0] {
Arg::Positional(Expr::Call(inner)) => {
assert_eq!(inner.func, "interleave");
assert_eq!(inner.args.len(), 2);
}
_ => panic!("expected nested call"),
}
}
_ => panic!("expected call"),
},
_ => panic!("expected binding"),
}
}
#[test]
fn parse_full_program() {
let src = r#"
// Const bindings (compile-time fold or scope-init pull)
const temp_lut := dist_normal(mean: 72.0, stddev: 5.0)
const weights := [60.0, 20.0, 15.0]
// Cycle bindings (per-cycle eval)
input cycle: u64
(tenant, device) := mixed_radix(cycle, 100, 0)
tenant_h := hash(tenant)
code := mod(tenant_h, 10000)
device_id := "{code}-{seq}"
"#;
let f = parse_str(src);
assert_eq!(f.statements.len(), 7);
}
#[test]
fn parse_mixed_positional_named() {
let f = parse_str("const lut := dist_normal(72.0, 5.0, resolution: 2000)");
match &f.statements[0] {
Statement::Binding(b) => match &b.value {
Expr::Call(c) => {
assert!(matches!(&c.args[0], Arg::Positional(_)));
assert!(matches!(&c.args[1], Arg::Positional(_)));
assert!(matches!(&c.args[2], Arg::Named(n, _) if n == "resolution"));
}
_ => panic!("expected call"),
},
_ => panic!("expected const binding"),
}
}
#[test]
fn parse_simple_addition() {
let f = parse_str("y := a + b");
match &f.statements[0] {
Statement::Binding(b) => match &b.value {
Expr::BinOp(lhs, BinOpKind::Add, rhs) => {
assert!(matches!(**lhs, Expr::Ident(ref s, _) if s == "a"));
assert!(matches!(**rhs, Expr::Ident(ref s, _) if s == "b"));
}
_ => panic!("expected BinOp Add, got {:?}", b.value),
},
_ => panic!("expected cycle binding"),
}
}
#[test]
fn parse_precedence_mul_over_add() {
let f = parse_str("y := a + b * c");
match &f.statements[0] {
Statement::Binding(b) => match &b.value {
Expr::BinOp(lhs, BinOpKind::Add, rhs) => {
assert!(matches!(**lhs, Expr::Ident(ref s, _) if s == "a"));
match &**rhs {
Expr::BinOp(rl, BinOpKind::Mul, rr) => {
assert!(matches!(**rl, Expr::Ident(ref s, _) if s == "b"));
assert!(matches!(**rr, Expr::Ident(ref s, _) if s == "c"));
}
_ => panic!("expected inner Mul"),
}
}
_ => panic!("expected outer Add"),
},
_ => panic!("expected cycle binding"),
}
}
#[test]
fn parse_parenthesized_grouping() {
let f = parse_str("y := (a + b) * c");
match &f.statements[0] {
Statement::Binding(b) => {
match &b.value {
Expr::BinOp(lhs, BinOpKind::Mul, rhs) => {
match &**lhs {
Expr::BinOp(_, BinOpKind::Add, _) => {} _ => panic!("expected inner Add in lhs"),
}
assert!(matches!(**rhs, Expr::Ident(ref s, _) if s == "c"));
}
_ => panic!("expected outer Mul"),
}
}
_ => panic!("expected cycle binding"),
}
}
fn if_call(src: &str) -> CallExpr {
let f = parse_str(src);
match &f.statements[0] {
Statement::Binding(b) => match &b.value {
Expr::Call(c) => {
assert_eq!(
c.func, "if",
"block form must desugar to the `if` intrinsic"
);
assert_eq!(c.args.len(), 3, "if intrinsic takes (cond, then, else)");
c.clone()
}
other => panic!("expected Call, got {:?}", other),
},
_ => panic!("expected binding"),
}
}
#[test]
fn if_block_desugars_to_the_call_intrinsic() {
let block = if_call("y := if c { a } else { b }");
let call = if_call("y := if(c, a, b)");
for (i, (bl, ca)) in block.args.iter().zip(call.args.iter()).enumerate() {
match (bl, ca) {
(Arg::Positional(Expr::Ident(x, _)), Arg::Positional(Expr::Ident(y, _))) => {
assert_eq!(x, y, "arg {} differs between block and call form", i);
}
_ => panic!("expected plain idents in both forms"),
}
}
}
#[test]
fn if_block_accepts_expressions_in_condition_and_branches() {
let c = if_call("y := if segments > 0 { total / segments } else { 0 }");
assert!(
matches!(
&c.args[0],
Arg::Positional(Expr::BinOp(_, BinOpKind::Gt, _))
),
"condition should parse as a full expression"
);
assert!(
matches!(
&c.args[1],
Arg::Positional(Expr::BinOp(_, BinOpKind::Div, _))
),
"then-branch should parse as a full expression"
);
}
#[test]
fn if_block_chains_else_if() {
let c = if_call("y := if a { 1 } else if b { 2 } else { 3 }");
match &c.args[2] {
Arg::Positional(Expr::Call(inner)) => {
assert_eq!(inner.func, "if");
assert!(matches!(
&inner.args[1],
Arg::Positional(Expr::IntLit(2, _))
));
assert!(matches!(
&inner.args[2],
Arg::Positional(Expr::IntLit(3, _))
));
}
other => panic!("expected nested if in else position, got {:?}", other),
}
}
#[test]
fn if_block_nests_inside_other_expressions() {
let f = parse_str("y := 1 + if c { 2 } else { 3 }");
match &f.statements[0] {
Statement::Binding(b) => match &b.value {
Expr::BinOp(_, BinOpKind::Add, rhs) => {
assert!(matches!(**rhs, Expr::Call(ref c) if c.func == "if"));
}
other => panic!("expected Add with an if on the rhs, got {:?}", other),
},
_ => panic!("expected binding"),
}
}
#[test]
fn if_call_form_still_parses_as_a_call() {
let c = if_call("y := if(c, a, b)");
assert_eq!(c.args.len(), 3);
}
#[test]
fn if_block_requires_else() {
let err = parse_str_err("y := if c { a }");
assert!(
err.contains("else"),
"error should name the missing else: {}",
err
);
}
#[test]
fn if_block_reports_a_missing_brace_helpfully() {
let err = parse_str_err("y := if c a else b");
assert!(
err.contains("if <cond>"),
"error should show the block form: {}",
err
);
}
#[test]
fn parse_unary_negation() {
let f = parse_str("y := -x");
match &f.statements[0] {
Statement::Binding(b) => match &b.value {
Expr::UnaryNeg(inner, _) => {
assert!(matches!(**inner, Expr::Ident(ref s, _) if s == "x"));
}
_ => panic!("expected UnaryNeg"),
},
_ => panic!("expected cycle binding"),
}
}
#[test]
fn parse_func_call_with_infix_arg() {
let f = parse_str("y := sin(cycle * 0.25)");
match &f.statements[0] {
Statement::Binding(b) => match &b.value {
Expr::Call(c) => {
assert_eq!(c.func, "sin");
assert_eq!(c.args.len(), 1);
match &c.args[0] {
Arg::Positional(Expr::BinOp(_, BinOpKind::Mul, _)) => {}
_ => panic!("expected Mul inside sin() arg"),
}
}
_ => panic!("expected call"),
},
_ => panic!("expected cycle binding"),
}
}
#[test]
fn parse_power_right_associative() {
let f = parse_str("y := a ** b ** c");
match &f.statements[0] {
Statement::Binding(b) => match &b.value {
Expr::BinOp(lhs, BinOpKind::Pow, rhs) => {
assert!(matches!(**lhs, Expr::Ident(ref s, _) if s == "a"));
match &**rhs {
Expr::BinOp(rl, BinOpKind::Pow, rr) => {
assert!(matches!(**rl, Expr::Ident(ref s, _) if s == "b"));
assert!(matches!(**rr, Expr::Ident(ref s, _) if s == "c"));
}
_ => panic!("expected inner Pow"),
}
}
_ => panic!("expected outer Pow"),
},
_ => panic!("expected cycle binding"),
}
}
#[test]
fn parse_negate_function_call() {
let f = parse_str("y := -sin(x)");
match &f.statements[0] {
Statement::Binding(b) => match &b.value {
Expr::UnaryNeg(inner, _) => match &**inner {
Expr::Call(c) => assert_eq!(c.func, "sin"),
_ => panic!("expected Call inside UnaryNeg"),
},
_ => panic!("expected UnaryNeg"),
},
_ => panic!("expected cycle binding"),
}
}
#[test]
fn parse_all_operators() {
let f = parse_str("y := a + b - c * d / e % f ** g");
match &f.statements[0] {
Statement::Binding(_) => {} _ => panic!("expected cycle binding"),
}
}
#[test]
fn parse_star_star_power() {
let f = parse_str("y := x ** 2.0");
match &f.statements[0] {
Statement::Binding(b) => match &b.value {
Expr::BinOp(lhs, BinOpKind::Pow, rhs) => {
assert!(matches!(**lhs, Expr::Ident(ref s, _) if s == "x"));
assert!(matches!(**rhs, Expr::FloatLit(v, _) if v == 2.0));
}
_ => panic!("expected BinOp Pow, got {:?}", b.value),
},
_ => panic!("expected cycle binding"),
}
}
#[test]
fn parse_caret_is_xor() {
let f = parse_str("y := a ^ b");
match &f.statements[0] {
Statement::Binding(b) => match &b.value {
Expr::BinOp(lhs, BinOpKind::BitXor, rhs) => {
assert!(matches!(**lhs, Expr::Ident(ref s, _) if s == "a"));
assert!(matches!(**rhs, Expr::Ident(ref s, _) if s == "b"));
}
_ => panic!("expected BinOp BitXor, got {:?}", b.value),
},
_ => panic!("expected cycle binding"),
}
}
#[test]
fn parse_bitand_binds_tighter_than_bitor() {
let f = parse_str("y := a & b | c");
match &f.statements[0] {
Statement::Binding(b) => {
match &b.value {
Expr::BinOp(lhs, BinOpKind::BitOr, rhs) => {
match &**lhs {
Expr::BinOp(_, BinOpKind::BitAnd, _) => {} _ => panic!("expected inner BitAnd in lhs"),
}
assert!(matches!(**rhs, Expr::Ident(ref s, _) if s == "c"));
}
_ => panic!("expected outer BitOr"),
}
}
_ => panic!("expected cycle binding"),
}
}
#[test]
fn parse_shift_left() {
let f = parse_str("y := a << 4");
match &f.statements[0] {
Statement::Binding(b) => match &b.value {
Expr::BinOp(lhs, BinOpKind::Shl, rhs) => {
assert!(matches!(**lhs, Expr::Ident(ref s, _) if s == "a"));
assert!(matches!(**rhs, Expr::IntLit(4, _)));
}
_ => panic!("expected BinOp Shl, got {:?}", b.value),
},
_ => panic!("expected cycle binding"),
}
}
#[test]
fn parse_cursor_without_over_clause() {
let f = parse_str("cursor q = range(0, 100)");
match &f.statements[0] {
Statement::Cursor(c) => {
assert_eq!(c.name, "q");
assert!(c.over.is_none(), "no `over` → over is None");
}
other => panic!("expected Cursor, got {other:?}"),
}
}
#[test]
fn parse_cursor_with_over_iter_var() {
let f = parse_str("cursor q = range(0, 100) over p");
match &f.statements[0] {
Statement::Cursor(c) => {
assert_eq!(c.name, "q");
match &c.over {
Some(Expr::Ident(name, _)) => assert_eq!(name, "p"),
other => panic!("expected Some(Ident('p')), got {other:?}"),
}
}
other => panic!("expected Cursor, got {other:?}"),
}
}
#[test]
fn parse_cursor_with_over_dotted_param_projection() {
let f = parse_str("cursor q = range(0, 100) over cursor.partitions");
match &f.statements[0] {
Statement::Cursor(c) => {
assert!(c.over.is_some(), "should have over clause");
match &c.over {
Some(Expr::FieldAccess { .. }) => {} Some(other) => panic!("expected FieldAccess, got {other:?}"),
None => panic!("expected Some"),
}
}
other => panic!("expected Cursor, got {other:?}"),
}
}
#[test]
fn parse_cursor_over_does_not_swallow_following_statement() {
let f = parse_str("cursor q = range(0, 100) over p\nother := 42");
assert_eq!(f.statements.len(), 2);
}
#[test]
fn parse_chained_field_access_flattens_intermediate_levels() {
let f = parse_str("i := q.cursor.idx");
match &f.statements[0] {
Statement::Binding(b) => match &b.value {
Expr::FieldAccess { source, field, .. } => {
assert_eq!(source, "q__cursor");
assert_eq!(field, "idx");
}
other => panic!("expected FieldAccess, got {other:?}"),
},
other => panic!("expected binding, got {other:?}"),
}
let f = parse_str("x := a.b.c.d");
match &f.statements[0] {
Statement::Binding(b) => match &b.value {
Expr::FieldAccess { source, field, .. } => {
assert_eq!(source, "a__b__c");
assert_eq!(field, "d");
}
other => panic!("expected FieldAccess, got {other:?}"),
},
other => panic!("expected binding, got {other:?}"),
}
}
#[test]
fn parse_unary_bitnot() {
let f = parse_str("y := !x");
match &f.statements[0] {
Statement::Binding(b) => match &b.value {
Expr::UnaryBitNot(inner, _) => {
assert!(matches!(**inner, Expr::Ident(ref s, _) if s == "x"));
}
_ => panic!("expected UnaryBitNot, got {:?}", b.value),
},
_ => panic!("expected cycle binding"),
}
}
}