968 lines
32 KiB
Rust
968 lines
32 KiB
Rust
|
|
/// DreamStack Parser — recursive descent parser producing AST from tokens.
|
||
|
|
use crate::ast::*;
|
||
|
|
use crate::lexer::{Token, TokenKind};
|
||
|
|
|
||
|
|
pub struct Parser {
|
||
|
|
tokens: Vec<Token>,
|
||
|
|
pos: usize,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Parser {
|
||
|
|
pub fn new(tokens: Vec<Token>) -> Self {
|
||
|
|
Self { tokens, pos: 0 }
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn parse_program(&mut self) -> Result<Program, ParseError> {
|
||
|
|
let mut declarations = Vec::new();
|
||
|
|
self.skip_newlines();
|
||
|
|
|
||
|
|
while !self.is_at_end() {
|
||
|
|
let decl = self.parse_declaration()?;
|
||
|
|
declarations.push(decl);
|
||
|
|
self.skip_newlines();
|
||
|
|
}
|
||
|
|
|
||
|
|
Ok(Program { declarations })
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Helpers ──────────────────────────────────────────
|
||
|
|
|
||
|
|
fn peek(&self) -> &TokenKind {
|
||
|
|
self.tokens
|
||
|
|
.get(self.pos)
|
||
|
|
.map(|t| &t.kind)
|
||
|
|
.unwrap_or(&TokenKind::Eof)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn current_token(&self) -> &Token {
|
||
|
|
&self.tokens[self.pos.min(self.tokens.len() - 1)]
|
||
|
|
}
|
||
|
|
|
||
|
|
fn advance(&mut self) -> &Token {
|
||
|
|
let tok = &self.tokens[self.pos.min(self.tokens.len() - 1)];
|
||
|
|
self.pos += 1;
|
||
|
|
tok
|
||
|
|
}
|
||
|
|
|
||
|
|
fn expect(&mut self, expected: &TokenKind) -> Result<&Token, ParseError> {
|
||
|
|
if std::mem::discriminant(self.peek()) == std::mem::discriminant(expected) {
|
||
|
|
Ok(self.advance())
|
||
|
|
} else {
|
||
|
|
Err(self.error(format!("expected {expected:?}, got {:?}", self.peek())))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn expect_ident(&mut self) -> Result<String, ParseError> {
|
||
|
|
match self.peek().clone() {
|
||
|
|
TokenKind::Ident(name) => {
|
||
|
|
self.advance();
|
||
|
|
Ok(name)
|
||
|
|
}
|
||
|
|
// Also accept keywords that can be used as identifiers in some contexts
|
||
|
|
_ => Err(self.error(format!("expected identifier, got {:?}", self.peek()))),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn check(&self, kind: &TokenKind) -> bool {
|
||
|
|
std::mem::discriminant(self.peek()) == std::mem::discriminant(kind)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_at_end(&self) -> bool {
|
||
|
|
matches!(self.peek(), TokenKind::Eof)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn skip_newlines(&mut self) {
|
||
|
|
while matches!(self.peek(), TokenKind::Newline) {
|
||
|
|
self.advance();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn error(&self, msg: String) -> ParseError {
|
||
|
|
let tok = self.current_token();
|
||
|
|
ParseError {
|
||
|
|
message: msg,
|
||
|
|
line: tok.line,
|
||
|
|
col: tok.col,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Declarations ────────────────────────────────────
|
||
|
|
|
||
|
|
fn parse_declaration(&mut self) -> Result<Declaration, ParseError> {
|
||
|
|
match self.peek() {
|
||
|
|
TokenKind::Let => self.parse_let_decl(),
|
||
|
|
TokenKind::View => self.parse_view_decl(),
|
||
|
|
TokenKind::Effect => self.parse_effect_decl(),
|
||
|
|
TokenKind::On => self.parse_on_handler(),
|
||
|
|
_ => Err(self.error(format!(
|
||
|
|
"expected declaration (let, view, effect, on), got {:?}",
|
||
|
|
self.peek()
|
||
|
|
))),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_let_decl(&mut self) -> Result<Declaration, ParseError> {
|
||
|
|
let line = self.current_token().line;
|
||
|
|
self.advance(); // consume 'let'
|
||
|
|
let name = self.expect_ident()?;
|
||
|
|
self.expect(&TokenKind::Eq)?;
|
||
|
|
let value = self.parse_expr()?;
|
||
|
|
|
||
|
|
Ok(Declaration::Let(LetDecl {
|
||
|
|
name,
|
||
|
|
value,
|
||
|
|
span: Span { start: 0, end: 0, line },
|
||
|
|
}))
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_view_decl(&mut self) -> Result<Declaration, ParseError> {
|
||
|
|
let line = self.current_token().line;
|
||
|
|
self.advance(); // consume 'view'
|
||
|
|
let name = self.expect_ident()?;
|
||
|
|
|
||
|
|
// Optional parameters
|
||
|
|
let params = if self.check(&TokenKind::LParen) {
|
||
|
|
self.parse_params()?
|
||
|
|
} else {
|
||
|
|
Vec::new()
|
||
|
|
};
|
||
|
|
|
||
|
|
self.expect(&TokenKind::Eq)?;
|
||
|
|
self.skip_newlines();
|
||
|
|
let body = self.parse_expr()?;
|
||
|
|
|
||
|
|
Ok(Declaration::View(ViewDecl {
|
||
|
|
name,
|
||
|
|
params,
|
||
|
|
body,
|
||
|
|
span: Span { start: 0, end: 0, line },
|
||
|
|
}))
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_effect_decl(&mut self) -> Result<Declaration, ParseError> {
|
||
|
|
let line = self.current_token().line;
|
||
|
|
self.advance(); // consume 'effect'
|
||
|
|
let name = self.expect_ident()?;
|
||
|
|
let params = self.parse_params()?;
|
||
|
|
self.expect(&TokenKind::Colon)?;
|
||
|
|
let return_type = self.parse_type_expr()?;
|
||
|
|
|
||
|
|
Ok(Declaration::Effect(EffectDecl {
|
||
|
|
name,
|
||
|
|
params,
|
||
|
|
return_type,
|
||
|
|
span: Span { start: 0, end: 0, line },
|
||
|
|
}))
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_on_handler(&mut self) -> Result<Declaration, ParseError> {
|
||
|
|
let line = self.current_token().line;
|
||
|
|
self.advance(); // consume 'on'
|
||
|
|
let event = self.expect_ident()?;
|
||
|
|
|
||
|
|
// Optional parameter: `on drag(event) ->`
|
||
|
|
let param = if self.check(&TokenKind::LParen) {
|
||
|
|
self.advance();
|
||
|
|
let p = self.expect_ident()?;
|
||
|
|
self.expect(&TokenKind::RParen)?;
|
||
|
|
Some(p)
|
||
|
|
} else {
|
||
|
|
None
|
||
|
|
};
|
||
|
|
|
||
|
|
self.expect(&TokenKind::Arrow)?;
|
||
|
|
self.skip_newlines();
|
||
|
|
let body = self.parse_expr()?;
|
||
|
|
|
||
|
|
Ok(Declaration::OnHandler(OnHandler {
|
||
|
|
event,
|
||
|
|
param,
|
||
|
|
body,
|
||
|
|
span: Span { start: 0, end: 0, line },
|
||
|
|
}))
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_params(&mut self) -> Result<Vec<Param>, ParseError> {
|
||
|
|
self.expect(&TokenKind::LParen)?;
|
||
|
|
let mut params = Vec::new();
|
||
|
|
|
||
|
|
while !self.check(&TokenKind::RParen) && !self.is_at_end() {
|
||
|
|
let name = self.expect_ident()?;
|
||
|
|
let type_annotation = if self.check(&TokenKind::Colon) {
|
||
|
|
self.advance();
|
||
|
|
Some(self.parse_type_expr()?)
|
||
|
|
} else {
|
||
|
|
None
|
||
|
|
};
|
||
|
|
params.push(Param { name, type_annotation });
|
||
|
|
if self.check(&TokenKind::Comma) {
|
||
|
|
self.advance();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
self.expect(&TokenKind::RParen)?;
|
||
|
|
Ok(params)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_type_expr(&mut self) -> Result<TypeExpr, ParseError> {
|
||
|
|
let name = self.expect_ident()?;
|
||
|
|
if self.check(&TokenKind::Lt) {
|
||
|
|
self.advance();
|
||
|
|
let mut type_args = Vec::new();
|
||
|
|
while !self.check(&TokenKind::Gt) && !self.is_at_end() {
|
||
|
|
type_args.push(self.parse_type_expr()?);
|
||
|
|
if self.check(&TokenKind::Comma) {
|
||
|
|
self.advance();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
self.expect(&TokenKind::Gt)?;
|
||
|
|
Ok(TypeExpr::Generic(name, type_args))
|
||
|
|
} else {
|
||
|
|
Ok(TypeExpr::Named(name))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Expressions ─────────────────────────────────────
|
||
|
|
|
||
|
|
fn parse_expr(&mut self) -> Result<Expr, ParseError> {
|
||
|
|
self.parse_pipe_expr()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Pipe: `expr | operator`
|
||
|
|
fn parse_pipe_expr(&mut self) -> Result<Expr, ParseError> {
|
||
|
|
let mut left = self.parse_assignment()?;
|
||
|
|
|
||
|
|
while self.check(&TokenKind::Pipe) {
|
||
|
|
self.advance();
|
||
|
|
self.skip_newlines();
|
||
|
|
let right = self.parse_assignment()?;
|
||
|
|
left = Expr::Pipe(Box::new(left), Box::new(right));
|
||
|
|
}
|
||
|
|
|
||
|
|
Ok(left)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Assignment: `x = value`, `x += 1`
|
||
|
|
fn parse_assignment(&mut self) -> Result<Expr, ParseError> {
|
||
|
|
let expr = self.parse_or()?;
|
||
|
|
|
||
|
|
match self.peek() {
|
||
|
|
TokenKind::Eq => {
|
||
|
|
self.advance();
|
||
|
|
self.skip_newlines();
|
||
|
|
let value = self.parse_expr()?;
|
||
|
|
Ok(Expr::Assign(Box::new(expr), AssignOp::Set, Box::new(value)))
|
||
|
|
}
|
||
|
|
TokenKind::PlusEq => {
|
||
|
|
self.advance();
|
||
|
|
let value = self.parse_expr()?;
|
||
|
|
Ok(Expr::Assign(Box::new(expr), AssignOp::AddAssign, Box::new(value)))
|
||
|
|
}
|
||
|
|
TokenKind::MinusEq => {
|
||
|
|
self.advance();
|
||
|
|
let value = self.parse_expr()?;
|
||
|
|
Ok(Expr::Assign(Box::new(expr), AssignOp::SubAssign, Box::new(value)))
|
||
|
|
}
|
||
|
|
_ => Ok(expr),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// `||`
|
||
|
|
fn parse_or(&mut self) -> Result<Expr, ParseError> {
|
||
|
|
let mut left = self.parse_and()?;
|
||
|
|
while self.check(&TokenKind::Or) {
|
||
|
|
self.advance();
|
||
|
|
let right = self.parse_and()?;
|
||
|
|
left = Expr::BinOp(Box::new(left), BinOp::Or, Box::new(right));
|
||
|
|
}
|
||
|
|
Ok(left)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// `&&`
|
||
|
|
fn parse_and(&mut self) -> Result<Expr, ParseError> {
|
||
|
|
let mut left = self.parse_comparison()?;
|
||
|
|
while self.check(&TokenKind::And) {
|
||
|
|
self.advance();
|
||
|
|
let right = self.parse_comparison()?;
|
||
|
|
left = Expr::BinOp(Box::new(left), BinOp::And, Box::new(right));
|
||
|
|
}
|
||
|
|
Ok(left)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// `==`, `!=`, `<`, `>`, `<=`, `>=`
|
||
|
|
fn parse_comparison(&mut self) -> Result<Expr, ParseError> {
|
||
|
|
let mut left = self.parse_additive()?;
|
||
|
|
loop {
|
||
|
|
let op = match self.peek() {
|
||
|
|
TokenKind::EqEq => BinOp::Eq,
|
||
|
|
TokenKind::Neq => BinOp::Neq,
|
||
|
|
TokenKind::Lt => BinOp::Lt,
|
||
|
|
TokenKind::Gt => BinOp::Gt,
|
||
|
|
TokenKind::Lte => BinOp::Lte,
|
||
|
|
TokenKind::Gte => BinOp::Gte,
|
||
|
|
_ => break,
|
||
|
|
};
|
||
|
|
self.advance();
|
||
|
|
let right = self.parse_additive()?;
|
||
|
|
left = Expr::BinOp(Box::new(left), op, Box::new(right));
|
||
|
|
}
|
||
|
|
Ok(left)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// `+`, `-`
|
||
|
|
fn parse_additive(&mut self) -> Result<Expr, ParseError> {
|
||
|
|
let mut left = self.parse_multiplicative()?;
|
||
|
|
loop {
|
||
|
|
let op = match self.peek() {
|
||
|
|
TokenKind::Plus => BinOp::Add,
|
||
|
|
TokenKind::Minus => BinOp::Sub,
|
||
|
|
_ => break,
|
||
|
|
};
|
||
|
|
self.advance();
|
||
|
|
let right = self.parse_multiplicative()?;
|
||
|
|
left = Expr::BinOp(Box::new(left), op, Box::new(right));
|
||
|
|
}
|
||
|
|
Ok(left)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// `*`, `/`, `%`
|
||
|
|
fn parse_multiplicative(&mut self) -> Result<Expr, ParseError> {
|
||
|
|
let mut left = self.parse_unary()?;
|
||
|
|
loop {
|
||
|
|
let op = match self.peek() {
|
||
|
|
TokenKind::Star => BinOp::Mul,
|
||
|
|
TokenKind::Slash => BinOp::Div,
|
||
|
|
TokenKind::Percent => BinOp::Mod,
|
||
|
|
_ => break,
|
||
|
|
};
|
||
|
|
self.advance();
|
||
|
|
let right = self.parse_unary()?;
|
||
|
|
left = Expr::BinOp(Box::new(left), op, Box::new(right));
|
||
|
|
}
|
||
|
|
Ok(left)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// `-x`, `!flag`
|
||
|
|
fn parse_unary(&mut self) -> Result<Expr, ParseError> {
|
||
|
|
match self.peek() {
|
||
|
|
TokenKind::Minus => {
|
||
|
|
self.advance();
|
||
|
|
let expr = self.parse_unary()?;
|
||
|
|
Ok(Expr::UnaryOp(UnaryOp::Neg, Box::new(expr)))
|
||
|
|
}
|
||
|
|
TokenKind::Not => {
|
||
|
|
self.advance();
|
||
|
|
let expr = self.parse_unary()?;
|
||
|
|
Ok(Expr::UnaryOp(UnaryOp::Not, Box::new(expr)))
|
||
|
|
}
|
||
|
|
_ => self.parse_postfix(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Dot access: `user.name`, function calls: `clamp(a, b)`
|
||
|
|
fn parse_postfix(&mut self) -> Result<Expr, ParseError> {
|
||
|
|
let mut expr = self.parse_primary()?;
|
||
|
|
|
||
|
|
loop {
|
||
|
|
match self.peek() {
|
||
|
|
TokenKind::Dot => {
|
||
|
|
self.advance();
|
||
|
|
let field = self.expect_ident()?;
|
||
|
|
expr = Expr::DotAccess(Box::new(expr), field);
|
||
|
|
}
|
||
|
|
_ => break,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Ok(expr)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Primary expressions: literals, identifiers, containers, etc.
|
||
|
|
fn parse_primary(&mut self) -> Result<Expr, ParseError> {
|
||
|
|
match self.peek().clone() {
|
||
|
|
TokenKind::Int(n) => {
|
||
|
|
self.advance();
|
||
|
|
Ok(Expr::IntLit(n))
|
||
|
|
}
|
||
|
|
TokenKind::Float(n) => {
|
||
|
|
self.advance();
|
||
|
|
Ok(Expr::FloatLit(n))
|
||
|
|
}
|
||
|
|
TokenKind::True => {
|
||
|
|
self.advance();
|
||
|
|
Ok(Expr::BoolLit(true))
|
||
|
|
}
|
||
|
|
TokenKind::False => {
|
||
|
|
self.advance();
|
||
|
|
Ok(Expr::BoolLit(false))
|
||
|
|
}
|
||
|
|
TokenKind::StringFragment(s) => {
|
||
|
|
self.advance();
|
||
|
|
Ok(Expr::StringLit(StringLit {
|
||
|
|
segments: vec![StringSegment::Literal(s)],
|
||
|
|
}))
|
||
|
|
}
|
||
|
|
TokenKind::StringEnd => {
|
||
|
|
self.advance();
|
||
|
|
Ok(Expr::StringLit(StringLit {
|
||
|
|
segments: vec![StringSegment::Literal(String::new())],
|
||
|
|
}))
|
||
|
|
}
|
||
|
|
|
||
|
|
// Containers
|
||
|
|
TokenKind::Column => self.parse_container(ContainerKind::Column),
|
||
|
|
TokenKind::Row => self.parse_container(ContainerKind::Row),
|
||
|
|
TokenKind::Stack => self.parse_container(ContainerKind::Stack),
|
||
|
|
TokenKind::Panel => self.parse_container_with_props(ContainerKind::Panel),
|
||
|
|
|
||
|
|
// When conditional
|
||
|
|
TokenKind::When => {
|
||
|
|
self.advance();
|
||
|
|
let cond = self.parse_comparison()?;
|
||
|
|
self.expect(&TokenKind::Arrow)?;
|
||
|
|
self.skip_newlines();
|
||
|
|
let body = self.parse_expr()?;
|
||
|
|
Ok(Expr::When(Box::new(cond), Box::new(body)))
|
||
|
|
}
|
||
|
|
|
||
|
|
// Match
|
||
|
|
TokenKind::Match => {
|
||
|
|
self.advance();
|
||
|
|
let scrutinee = self.parse_primary()?;
|
||
|
|
self.skip_newlines();
|
||
|
|
let mut arms = Vec::new();
|
||
|
|
while !self.is_at_end()
|
||
|
|
&& !matches!(self.peek(), TokenKind::Let | TokenKind::View | TokenKind::On | TokenKind::Effect)
|
||
|
|
{
|
||
|
|
self.skip_newlines();
|
||
|
|
if self.is_at_end() || matches!(self.peek(), TokenKind::Let | TokenKind::View | TokenKind::On | TokenKind::Effect) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
let pattern = self.parse_pattern()?;
|
||
|
|
self.expect(&TokenKind::Arrow)?;
|
||
|
|
self.skip_newlines();
|
||
|
|
let body = self.parse_expr()?;
|
||
|
|
arms.push(MatchArm { pattern, body });
|
||
|
|
self.skip_newlines();
|
||
|
|
}
|
||
|
|
Ok(Expr::Match(Box::new(scrutinee), arms))
|
||
|
|
}
|
||
|
|
|
||
|
|
// If-then-else
|
||
|
|
TokenKind::If => {
|
||
|
|
self.advance();
|
||
|
|
let cond = self.parse_expr()?;
|
||
|
|
self.expect(&TokenKind::Then)?;
|
||
|
|
let then_branch = self.parse_expr()?;
|
||
|
|
self.expect(&TokenKind::Else)?;
|
||
|
|
let else_branch = self.parse_expr()?;
|
||
|
|
Ok(Expr::If(Box::new(cond), Box::new(then_branch), Box::new(else_branch)))
|
||
|
|
}
|
||
|
|
|
||
|
|
// Perform effect
|
||
|
|
TokenKind::Perform => {
|
||
|
|
self.advance();
|
||
|
|
let name = self.expect_ident()?;
|
||
|
|
let args = if self.check(&TokenKind::LParen) {
|
||
|
|
self.parse_call_args()?
|
||
|
|
} else {
|
||
|
|
Vec::new()
|
||
|
|
};
|
||
|
|
Ok(Expr::Perform(name, args))
|
||
|
|
}
|
||
|
|
|
||
|
|
// Stream from
|
||
|
|
TokenKind::Stream => {
|
||
|
|
self.advance();
|
||
|
|
self.expect(&TokenKind::From)?;
|
||
|
|
let source = self.expect_ident()?;
|
||
|
|
// Allow dotted source: `button.click`
|
||
|
|
let mut full_source = source;
|
||
|
|
while self.check(&TokenKind::Dot) {
|
||
|
|
self.advance();
|
||
|
|
let next = self.expect_ident()?;
|
||
|
|
full_source = format!("{full_source}.{next}");
|
||
|
|
}
|
||
|
|
Ok(Expr::StreamFrom(full_source))
|
||
|
|
}
|
||
|
|
|
||
|
|
// Spring
|
||
|
|
TokenKind::Spring => {
|
||
|
|
self.advance();
|
||
|
|
self.expect(&TokenKind::LParen)?;
|
||
|
|
let mut props = Vec::new();
|
||
|
|
while !self.check(&TokenKind::RParen) && !self.is_at_end() {
|
||
|
|
let key = self.expect_ident()?;
|
||
|
|
self.expect(&TokenKind::Colon)?;
|
||
|
|
let val = self.parse_expr()?;
|
||
|
|
props.push((key, val));
|
||
|
|
if self.check(&TokenKind::Comma) {
|
||
|
|
self.advance();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
self.expect(&TokenKind::RParen)?;
|
||
|
|
Ok(Expr::Spring(props))
|
||
|
|
}
|
||
|
|
|
||
|
|
// Record: `{ key: value }`
|
||
|
|
TokenKind::LBrace => {
|
||
|
|
self.advance();
|
||
|
|
self.skip_newlines();
|
||
|
|
let mut fields = Vec::new();
|
||
|
|
while !self.check(&TokenKind::RBrace) && !self.is_at_end() {
|
||
|
|
self.skip_newlines();
|
||
|
|
let key = self.expect_ident()?;
|
||
|
|
self.expect(&TokenKind::Colon)?;
|
||
|
|
self.skip_newlines();
|
||
|
|
let val = self.parse_expr()?;
|
||
|
|
fields.push((key, val));
|
||
|
|
self.skip_newlines();
|
||
|
|
if self.check(&TokenKind::Comma) {
|
||
|
|
self.advance();
|
||
|
|
}
|
||
|
|
self.skip_newlines();
|
||
|
|
}
|
||
|
|
self.expect(&TokenKind::RBrace)?;
|
||
|
|
Ok(Expr::Record(fields))
|
||
|
|
}
|
||
|
|
|
||
|
|
// List: `[a, b, c]`
|
||
|
|
TokenKind::LBracket => {
|
||
|
|
self.advance();
|
||
|
|
self.skip_newlines();
|
||
|
|
let mut items = Vec::new();
|
||
|
|
while !self.check(&TokenKind::RBracket) && !self.is_at_end() {
|
||
|
|
self.skip_newlines();
|
||
|
|
items.push(self.parse_expr()?);
|
||
|
|
self.skip_newlines();
|
||
|
|
if self.check(&TokenKind::Comma) {
|
||
|
|
self.advance();
|
||
|
|
}
|
||
|
|
self.skip_newlines();
|
||
|
|
}
|
||
|
|
self.expect(&TokenKind::RBracket)?;
|
||
|
|
Ok(Expr::List(items))
|
||
|
|
}
|
||
|
|
|
||
|
|
// Parenthesized expression or lambda
|
||
|
|
TokenKind::LParen => {
|
||
|
|
self.advance();
|
||
|
|
// Check for lambda: `(x -> x * 2)` or `(x, y -> x + y)`
|
||
|
|
let expr = self.parse_expr()?;
|
||
|
|
if self.check(&TokenKind::Arrow) {
|
||
|
|
// This is a lambda
|
||
|
|
let params = vec![self.expr_to_ident(&expr)?];
|
||
|
|
self.advance(); // consume ->
|
||
|
|
let body = self.parse_expr()?;
|
||
|
|
self.expect(&TokenKind::RParen)?;
|
||
|
|
Ok(Expr::Lambda(params, Box::new(body)))
|
||
|
|
} else if self.check(&TokenKind::Comma) {
|
||
|
|
// Could be multi-param lambda or tuple — check ahead
|
||
|
|
let mut items = vec![expr];
|
||
|
|
while self.check(&TokenKind::Comma) {
|
||
|
|
self.advance();
|
||
|
|
items.push(self.parse_expr()?);
|
||
|
|
}
|
||
|
|
if self.check(&TokenKind::Arrow) {
|
||
|
|
// Multi-param lambda
|
||
|
|
let mut params = Vec::new();
|
||
|
|
for item in &items {
|
||
|
|
params.push(self.expr_to_ident(item)?);
|
||
|
|
}
|
||
|
|
self.advance(); // ->
|
||
|
|
let body = self.parse_expr()?;
|
||
|
|
self.expect(&TokenKind::RParen)?;
|
||
|
|
Ok(Expr::Lambda(params, Box::new(body)))
|
||
|
|
} else {
|
||
|
|
// Just a parenthesized expression (take first)
|
||
|
|
self.expect(&TokenKind::RParen)?;
|
||
|
|
Ok(items.into_iter().next().unwrap())
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
self.expect(&TokenKind::RParen)?;
|
||
|
|
Ok(expr)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Animate modifier (used in pipe context)
|
||
|
|
TokenKind::Animate => {
|
||
|
|
self.advance();
|
||
|
|
let name = self.expect_ident()?;
|
||
|
|
// Parse optional duration: `200ms`
|
||
|
|
let mut args = Vec::new();
|
||
|
|
if let TokenKind::Int(n) = self.peek().clone() {
|
||
|
|
self.advance();
|
||
|
|
// Check for unit suffix (we treat `ms` as part of value for now)
|
||
|
|
args.push(Expr::IntLit(n));
|
||
|
|
}
|
||
|
|
Ok(Expr::Call(format!("animate_{name}"), args))
|
||
|
|
}
|
||
|
|
|
||
|
|
// Identifier — variable reference, element, or function call
|
||
|
|
TokenKind::Ident(name) => {
|
||
|
|
let name = name.clone();
|
||
|
|
self.advance();
|
||
|
|
|
||
|
|
// Function call: `name(args)`
|
||
|
|
if self.check(&TokenKind::LParen) {
|
||
|
|
let args = self.parse_call_args()?;
|
||
|
|
Ok(Expr::Call(name, args))
|
||
|
|
}
|
||
|
|
// Element with string arg: `text "hello"`, `button "+"`
|
||
|
|
else if matches!(self.peek(), TokenKind::StringFragment(_)) {
|
||
|
|
let fallback = name.clone();
|
||
|
|
match self.parse_element(name)? {
|
||
|
|
Some(el) => Ok(el),
|
||
|
|
None => Ok(Expr::Ident(fallback)),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Element with ident arg: `text label`
|
||
|
|
else if is_ui_element(&name) && matches!(self.peek(), TokenKind::Ident(_)) {
|
||
|
|
let fallback = name.clone();
|
||
|
|
match self.parse_element(name)? {
|
||
|
|
Some(el) => Ok(el),
|
||
|
|
None => Ok(Expr::Ident(fallback)),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
Ok(Expr::Ident(name))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
_ => Err(self.error(format!("unexpected token: {:?}", self.peek()))),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_element(&mut self, tag: String) -> Result<Option<Expr>, ParseError> {
|
||
|
|
let mut args = Vec::new();
|
||
|
|
let mut props = Vec::new();
|
||
|
|
let mut modifiers = Vec::new();
|
||
|
|
|
||
|
|
// Parse string or ident args
|
||
|
|
loop {
|
||
|
|
match self.peek().clone() {
|
||
|
|
TokenKind::StringFragment(s) => {
|
||
|
|
self.advance();
|
||
|
|
args.push(Expr::StringLit(StringLit {
|
||
|
|
segments: vec![StringSegment::Literal(s)],
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
TokenKind::Ident(name) if !is_declaration_keyword(&name) => {
|
||
|
|
// Only consume if it looks like an element argument
|
||
|
|
self.advance();
|
||
|
|
args.push(Expr::Ident(name));
|
||
|
|
}
|
||
|
|
_ => break,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Parse props: `{ click: handler, class: "foo" }`
|
||
|
|
if self.check(&TokenKind::LBrace) {
|
||
|
|
self.advance();
|
||
|
|
self.skip_newlines();
|
||
|
|
while !self.check(&TokenKind::RBrace) && !self.is_at_end() {
|
||
|
|
self.skip_newlines();
|
||
|
|
let key = self.expect_ident()?;
|
||
|
|
self.expect(&TokenKind::Colon)?;
|
||
|
|
self.skip_newlines();
|
||
|
|
let val = self.parse_expr()?;
|
||
|
|
props.push((key, val));
|
||
|
|
self.skip_newlines();
|
||
|
|
if self.check(&TokenKind::Comma) {
|
||
|
|
self.advance();
|
||
|
|
}
|
||
|
|
self.skip_newlines();
|
||
|
|
}
|
||
|
|
self.expect(&TokenKind::RBrace)?;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Parse modifiers: `| animate fade-in 200ms`
|
||
|
|
while self.check(&TokenKind::Pipe) {
|
||
|
|
self.advance();
|
||
|
|
let name = self.expect_ident()?;
|
||
|
|
let mut mod_args = Vec::new();
|
||
|
|
while matches!(self.peek(), TokenKind::Ident(_) | TokenKind::Int(_) | TokenKind::Float(_)) {
|
||
|
|
mod_args.push(self.parse_primary()?);
|
||
|
|
}
|
||
|
|
modifiers.push(Modifier { name, args: mod_args });
|
||
|
|
}
|
||
|
|
|
||
|
|
if args.is_empty() && props.is_empty() && modifiers.is_empty() {
|
||
|
|
return Ok(None);
|
||
|
|
}
|
||
|
|
|
||
|
|
Ok(Some(Expr::Element(Element {
|
||
|
|
tag,
|
||
|
|
args,
|
||
|
|
props,
|
||
|
|
modifiers,
|
||
|
|
})))
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_container(&mut self, kind: ContainerKind) -> Result<Expr, ParseError> {
|
||
|
|
self.advance(); // consume container keyword
|
||
|
|
self.expect(&TokenKind::LBracket)?;
|
||
|
|
self.skip_newlines();
|
||
|
|
|
||
|
|
let mut children = Vec::new();
|
||
|
|
while !self.check(&TokenKind::RBracket) && !self.is_at_end() {
|
||
|
|
self.skip_newlines();
|
||
|
|
if self.check(&TokenKind::RBracket) { break; }
|
||
|
|
children.push(self.parse_expr()?);
|
||
|
|
self.skip_newlines();
|
||
|
|
if self.check(&TokenKind::Comma) {
|
||
|
|
self.advance();
|
||
|
|
}
|
||
|
|
self.skip_newlines();
|
||
|
|
}
|
||
|
|
|
||
|
|
self.expect(&TokenKind::RBracket)?;
|
||
|
|
Ok(Expr::Container(Container {
|
||
|
|
kind,
|
||
|
|
children,
|
||
|
|
props: Vec::new(),
|
||
|
|
}))
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_container_with_props(&mut self, kind: ContainerKind) -> Result<Expr, ParseError> {
|
||
|
|
self.advance(); // consume container keyword
|
||
|
|
|
||
|
|
// Optional props: `panel { x: panel_x }`
|
||
|
|
let mut props = Vec::new();
|
||
|
|
if self.check(&TokenKind::LBrace) {
|
||
|
|
self.advance();
|
||
|
|
self.skip_newlines();
|
||
|
|
while !self.check(&TokenKind::RBrace) && !self.is_at_end() {
|
||
|
|
self.skip_newlines();
|
||
|
|
let key = self.expect_ident()?;
|
||
|
|
self.expect(&TokenKind::Colon)?;
|
||
|
|
let val = self.parse_expr()?;
|
||
|
|
props.push((key, val));
|
||
|
|
self.skip_newlines();
|
||
|
|
if self.check(&TokenKind::Comma) {
|
||
|
|
self.advance();
|
||
|
|
}
|
||
|
|
self.skip_newlines();
|
||
|
|
}
|
||
|
|
self.expect(&TokenKind::RBrace)?;
|
||
|
|
}
|
||
|
|
|
||
|
|
self.expect(&TokenKind::LBracket)?;
|
||
|
|
self.skip_newlines();
|
||
|
|
|
||
|
|
let mut children = Vec::new();
|
||
|
|
while !self.check(&TokenKind::RBracket) && !self.is_at_end() {
|
||
|
|
self.skip_newlines();
|
||
|
|
if self.check(&TokenKind::RBracket) { break; }
|
||
|
|
children.push(self.parse_expr()?);
|
||
|
|
self.skip_newlines();
|
||
|
|
if self.check(&TokenKind::Comma) {
|
||
|
|
self.advance();
|
||
|
|
}
|
||
|
|
self.skip_newlines();
|
||
|
|
}
|
||
|
|
|
||
|
|
self.expect(&TokenKind::RBracket)?;
|
||
|
|
Ok(Expr::Container(Container { kind, children, props }))
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_call_args(&mut self) -> Result<Vec<Expr>, ParseError> {
|
||
|
|
self.expect(&TokenKind::LParen)?;
|
||
|
|
let mut args = Vec::new();
|
||
|
|
while !self.check(&TokenKind::RParen) && !self.is_at_end() {
|
||
|
|
args.push(self.parse_expr()?);
|
||
|
|
if self.check(&TokenKind::Comma) {
|
||
|
|
self.advance();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
self.expect(&TokenKind::RParen)?;
|
||
|
|
Ok(args)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_pattern(&mut self) -> Result<Pattern, ParseError> {
|
||
|
|
match self.peek().clone() {
|
||
|
|
TokenKind::Ident(name) => {
|
||
|
|
self.advance();
|
||
|
|
if self.check(&TokenKind::LParen) {
|
||
|
|
// Constructor pattern: `Ok(value)`
|
||
|
|
self.advance();
|
||
|
|
let mut fields = Vec::new();
|
||
|
|
while !self.check(&TokenKind::RParen) && !self.is_at_end() {
|
||
|
|
fields.push(self.parse_pattern()?);
|
||
|
|
if self.check(&TokenKind::Comma) {
|
||
|
|
self.advance();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
self.expect(&TokenKind::RParen)?;
|
||
|
|
Ok(Pattern::Constructor(name, fields))
|
||
|
|
} else {
|
||
|
|
Ok(Pattern::Ident(name))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
TokenKind::Int(n) => {
|
||
|
|
self.advance();
|
||
|
|
Ok(Pattern::Literal(Expr::IntLit(n)))
|
||
|
|
}
|
||
|
|
TokenKind::StringFragment(s) => {
|
||
|
|
self.advance();
|
||
|
|
Ok(Pattern::Literal(Expr::StringLit(StringLit {
|
||
|
|
segments: vec![StringSegment::Literal(s)],
|
||
|
|
})))
|
||
|
|
}
|
||
|
|
_ => Err(self.error(format!("expected pattern, got {:?}", self.peek()))),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn expr_to_ident(&self, expr: &Expr) -> Result<String, ParseError> {
|
||
|
|
match expr {
|
||
|
|
Expr::Ident(name) => Ok(name.clone()),
|
||
|
|
_ => Err(self.error("expected identifier in lambda parameter".into())),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_ui_element(name: &str) -> bool {
|
||
|
|
matches!(
|
||
|
|
name,
|
||
|
|
"text" | "button" | "input" | "image" | "avatar" | "icon"
|
||
|
|
| "link" | "label" | "badge" | "chip" | "card"
|
||
|
|
| "header" | "footer" | "nav" | "section" | "div"
|
||
|
|
| "spinner" | "skeleton"
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_declaration_keyword(name: &str) -> bool {
|
||
|
|
matches!(name, "let" | "view" | "effect" | "on" | "handle")
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug)]
|
||
|
|
pub struct ParseError {
|
||
|
|
pub message: String,
|
||
|
|
pub line: usize,
|
||
|
|
pub col: usize,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl std::fmt::Display for ParseError {
|
||
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
|
|
write!(f, "Parse error at line {}:{}: {}", self.line, self.col, self.message)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl std::error::Error for ParseError {}
|
||
|
|
|
||
|
|
#[cfg(test)]
|
||
|
|
mod tests {
|
||
|
|
use super::*;
|
||
|
|
use crate::lexer::Lexer;
|
||
|
|
|
||
|
|
fn parse(src: &str) -> Program {
|
||
|
|
let mut lexer = Lexer::new(src);
|
||
|
|
let tokens = lexer.tokenize();
|
||
|
|
let mut parser = Parser::new(tokens);
|
||
|
|
parser.parse_program().expect("parse failed")
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_let_int() {
|
||
|
|
let prog = parse("let count = 0");
|
||
|
|
assert_eq!(prog.declarations.len(), 1);
|
||
|
|
match &prog.declarations[0] {
|
||
|
|
Declaration::Let(decl) => {
|
||
|
|
assert_eq!(decl.name, "count");
|
||
|
|
assert!(matches!(decl.value, Expr::IntLit(0)));
|
||
|
|
}
|
||
|
|
_ => panic!("expected let"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_let_binop() {
|
||
|
|
let prog = parse("let doubled = count * 2");
|
||
|
|
match &prog.declarations[0] {
|
||
|
|
Declaration::Let(decl) => {
|
||
|
|
assert_eq!(decl.name, "doubled");
|
||
|
|
match &decl.value {
|
||
|
|
Expr::BinOp(_, BinOp::Mul, _) => {}
|
||
|
|
other => panic!("expected BinOp(Mul), got {other:?}"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
_ => panic!("expected let"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_view_simple() {
|
||
|
|
let prog = parse(
|
||
|
|
r#"view counter =
|
||
|
|
column [
|
||
|
|
text "hello"
|
||
|
|
]"#
|
||
|
|
);
|
||
|
|
match &prog.declarations[0] {
|
||
|
|
Declaration::View(v) => {
|
||
|
|
assert_eq!(v.name, "counter");
|
||
|
|
match &v.body {
|
||
|
|
Expr::Container(c) => {
|
||
|
|
assert!(matches!(c.kind, ContainerKind::Column));
|
||
|
|
assert_eq!(c.children.len(), 1);
|
||
|
|
}
|
||
|
|
other => panic!("expected Container, got {other:?}"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
_ => panic!("expected view"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_when_expr() {
|
||
|
|
let prog = parse(
|
||
|
|
r#"view test =
|
||
|
|
column [
|
||
|
|
when count > 10 ->
|
||
|
|
text "big"
|
||
|
|
]"#
|
||
|
|
);
|
||
|
|
match &prog.declarations[0] {
|
||
|
|
Declaration::View(v) => {
|
||
|
|
match &v.body {
|
||
|
|
Expr::Container(c) => {
|
||
|
|
assert!(matches!(&c.children[0], Expr::When(_, _)));
|
||
|
|
}
|
||
|
|
other => panic!("expected Container, got {other:?}"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
_ => panic!("expected view"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_on_handler() {
|
||
|
|
let prog = parse("on toggle_sidebar ->\n count += 1");
|
||
|
|
match &prog.declarations[0] {
|
||
|
|
Declaration::OnHandler(h) => {
|
||
|
|
assert_eq!(h.event, "toggle_sidebar");
|
||
|
|
assert!(matches!(&h.body, Expr::Assign(_, AssignOp::AddAssign, _)));
|
||
|
|
}
|
||
|
|
_ => panic!("expected on handler"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_full_counter() {
|
||
|
|
let prog = parse(
|
||
|
|
r#"let count = 0
|
||
|
|
let doubled = count * 2
|
||
|
|
let label = "hello"
|
||
|
|
|
||
|
|
view counter =
|
||
|
|
column [
|
||
|
|
text label
|
||
|
|
button "+" { click: count += 1 }
|
||
|
|
when count > 10 ->
|
||
|
|
text "big"
|
||
|
|
]"#
|
||
|
|
);
|
||
|
|
assert_eq!(prog.declarations.len(), 4); // 3 lets + 1 view
|
||
|
|
}
|
||
|
|
}
|