use crate::dsl::ast::*;
use crate::dsl::lexer::{Token, TokenKind, Span};
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<GkFile, String> {
let mut parser = Parser::new(tokens);
let mut statements = Vec::new();
while !parser.at_eof() {
parse_statement_into(&mut parser, &mut statements)?;
}
Ok(GkFile { 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::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 {
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_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::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()?;
p.expect(&TokenKind::ColonEq)?;
let value = parse_expr(p)?;
Ok(Statement::Binding(Binding {
targets: vec![name],
value,
modifier,
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,
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)?;
loop {
let op = match p.peek() {
TokenKind::EqEq => Some((BinOpKind::Eq, 1, 2)),
TokenKind::BangEq => Some((BinOpKind::Ne, 1, 2)),
TokenKind::Lt => Some((BinOpKind::Lt, 3, 4)),
TokenKind::Gt => Some((BinOpKind::Gt, 3, 4)),
TokenKind::LtEq => Some((BinOpKind::Le, 3, 4)),
TokenKind::GtEq => Some((BinOpKind::Ge, 3, 4)),
TokenKind::Pipe => Some((BinOpKind::BitOr, 5, 6)),
TokenKind::Caret => Some((BinOpKind::BitXor, 7, 8)),
TokenKind::Ampersand => Some((BinOpKind::BitAnd, 9, 10)),
TokenKind::ShiftLeft => Some((BinOpKind::Shl, 11, 12)),
TokenKind::ShiftRight=> Some((BinOpKind::Shr, 11, 12)),
TokenKind::Plus => Some((BinOpKind::Add, 13, 14)),
TokenKind::Minus => Some((BinOpKind::Sub, 13, 14)),
TokenKind::Star => Some((BinOpKind::Mul, 15, 16)),
TokenKind::Slash => Some((BinOpKind::Div, 15, 16)),
TokenKind::Percent => Some((BinOpKind::Mod, 15, 16)),
TokenKind::StarStar => Some((BinOpKind::Pow, 18, 17)), _ => 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_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::Ident(name) => {
p.advance();
if matches!(p.peek(), TokenKind::LParen) {
parse_call(p, name, span)
} else if matches!(p.peek(), TokenKind::Dot) {
p.advance(); let field = p.expect_ident()?;
Ok(Expr::FieldAccess { source: name, field, span })
} else {
Ok(Expr::Ident(name, span))
}
}
TokenKind::Input => {
p.advance();
let name = "input".to_string();
if matches!(p.peek(), TokenKind::Dot) {
p.advance();
let field = p.expect_ident()?;
Ok(Expr::FieldAccess { source: name, field, span })
} else {
Ok(Expr::Ident(name, span))
}
}
TokenKind::Cursor => {
p.advance();
let name = "cursor".to_string();
if matches!(p.peek(), TokenKind::Dot) {
p.advance();
let field = p.expect_ident()?;
Ok(Expr::FieldAccess { source: name, field, span })
} else {
Ok(Expr::Ident(name, span))
}
}
TokenKind::Over => {
p.advance();
let name = "over".to_string();
if matches!(p.peek(), TokenKind::Dot) {
p.advance();
let field = p.expect_ident()?;
Ok(Expr::FieldAccess { source: name, field, 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 = match find_placeholder_end(&chars, body_start) {
Some(end) => end,
None => return None,
};
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::dsl::lexer::lex(body)?;
parse_expression(tokens)
}
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::dsl::lexer::lex;
fn parse_str(s: &str) -> GkFile {
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: &GkFile) -> 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 volatile_binding_compiles_to_named_output() {
let kernel = crate::dsl::compile_gk(
"volatile y := 42\n"
).expect("compile volatile y");
let names = kernel.program().output_names();
assert!(names.contains(&"y"), "output names: {names:?}");
let m = kernel.program().output_modifier("y");
assert!(m.is_volatile(), "modifier should record volatile");
}
#[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::dsl::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"),
}
}
#[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_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"),
}
}
}