dreamstack/compiler/ds-parser/src/parser.rs
enzotar 7805b94704 feat: component registry with styled variants, dreamstack add/convert CLI, and showcase
- Phase 1: Component parser + codegen (emit_component_decl, emit_component_use, emit_match)
- Phase 2: 6 registry components (button, input, card, badge, dialog, toast)
- Phase 3: dreamstack add CLI with dependency resolution and --list/--all
- Phase 4: dreamstack convert TSX→DS transpiler with --shadcn GitHub fetch
- Phase 5: 120+ lines variant CSS (buttons, badges, cards, dialog, toast, input)
- New example: showcase.ds demonstrating all component styles
2026-02-26 13:27:49 -08:00

1642 lines
58 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(),
TokenKind::Component => self.parse_component_decl(),
TokenKind::Route => self.parse_route_decl(),
TokenKind::Constrain => self.parse_constrain_decl(),
TokenKind::Stream => self.parse_stream_decl(),
TokenKind::Every => self.parse_every_decl(),
TokenKind::Import => self.parse_import_decl(),
TokenKind::Export => self.parse_export_decl(),
TokenKind::Type => self.parse_type_alias_decl(),
TokenKind::Layout => self.parse_layout_decl(),
// Expression statement: `log("hello")`, `push(items, x)`
TokenKind::Ident(_) => {
let expr = self.parse_expr()?;
Ok(Declaration::ExprStatement(expr))
}
_ => Err(self.error(format!(
"expected declaration (let, view, effect, on, component, route, constrain, stream, every, type, layout), got {:?}",
self.peek()
))),
}
}
fn parse_every_decl(&mut self) -> Result<Declaration, ParseError> {
let line = self.current_token().line;
self.advance(); // consume 'every'
let interval = self.parse_expr()?;
self.expect(&TokenKind::Arrow)?;
self.skip_newlines();
let body = self.parse_expr()?;
Ok(Declaration::Every(EveryDecl {
interval_ms: interval,
body,
span: Span { start: 0, end: 0, line },
}))
}
// import { Card, Button } from "./components"
fn parse_import_decl(&mut self) -> Result<Declaration, ParseError> {
let line = self.current_token().line;
self.advance(); // consume 'import'
// Parse the import names: { name1, name2 }
self.expect(&TokenKind::LBrace)?;
let mut names = Vec::new();
loop {
self.skip_newlines();
if self.check(&TokenKind::RBrace) {
break;
}
match self.peek().clone() {
TokenKind::Ident(name) => {
names.push(name);
self.advance();
}
// Allow keywords to be imported as names (e.g., component names)
_ => {
let tok = self.peek().clone();
return Err(self.error(format!("expected identifier in import, got {:?}", tok)));
}
}
self.skip_newlines();
if self.check(&TokenKind::Comma) {
self.advance(); // consume ','
}
}
self.expect(&TokenKind::RBrace)?;
// Parse "from"
self.expect(&TokenKind::From)?;
// Parse the source path
let source = match self.peek().clone() {
TokenKind::StringFragment(s) => {
self.advance();
// Consume the StringEnd if present
if self.check(&TokenKind::StringEnd) {
self.advance();
}
s
}
TokenKind::StringEnd => {
self.advance();
String::new()
}
_ => return Err(self.error("expected string after 'from'".to_string())),
};
Ok(Declaration::Import(ImportDecl {
names,
source,
span: Span { start: 0, end: 0, line },
}))
}
// export let count = 0 / export component Card(...) = ...
fn parse_export_decl(&mut self) -> Result<Declaration, ParseError> {
self.advance(); // consume 'export'
// Parse the inner declaration
let inner = self.parse_declaration()?;
// Extract the name being exported
let name = match &inner {
Declaration::Let(d) => d.name.clone(),
Declaration::View(d) => d.name.clone(),
Declaration::Component(d) => d.name.clone(),
Declaration::Effect(d) => d.name.clone(),
_ => return Err(self.error("can only export let, view, component, or effect".to_string())),
};
Ok(Declaration::Export(name, Box::new(inner)))
}
fn parse_let_decl(&mut self) -> Result<Declaration, ParseError> {
let line = self.current_token().line;
self.advance(); // consume 'let'
let name = self.expect_ident()?;
// Optional type annotation: `let name: Type = value`
let type_annotation = if self.check(&TokenKind::Colon) {
self.advance(); // consume ':'
Some(self.parse_type_expr()?)
} else {
None
};
self.expect(&TokenKind::Eq)?;
let value = self.parse_expr()?;
Ok(Declaration::Let(LetDecl {
name,
type_annotation,
value,
span: Span { start: 0, end: 0, line },
}))
}
/// Parse a type alias: `type PositiveInt = Int where value > 0`
fn parse_type_alias_decl(&mut self) -> Result<Declaration, ParseError> {
let line = self.current_token().line;
self.advance(); // consume 'type'
let name = self.expect_ident()?;
self.expect(&TokenKind::Eq)?;
let definition = self.parse_type_expr()?;
Ok(Declaration::TypeAlias(TypeAliasDecl {
name,
definition,
span: Span { start: 0, end: 0, line },
}))
}
/// Parse a type expression: `Int`, `Array<String>`, `Int where value > 0`
fn parse_type_expr(&mut self) -> Result<TypeExpr, ParseError> {
// Parse base type name
let name = match self.peek().clone() {
TokenKind::Ident(n) => { self.advance(); n }
_ => return Err(self.error(format!("expected type name, got {:?}", self.peek()))),
};
// Optional generic params: `<Type1, Type2>`
let base = if self.check(&TokenKind::Lt) {
self.advance(); // <
let mut params = Vec::new();
loop {
params.push(self.parse_type_expr()?);
if self.check(&TokenKind::Comma) {
self.advance();
} else {
break;
}
}
self.expect(&TokenKind::Gt)?;
TypeExpr::Generic(name, params)
} else {
TypeExpr::Named(name)
};
// Optional `where` predicate
if self.check(&TokenKind::Where) {
self.advance(); // consume 'where'
let predicate = self.parse_expr()?;
Ok(TypeExpr::Refined {
base: Box::new(base),
predicate: Box::new(predicate),
})
} else {
Ok(base)
}
}
/// Parse a layout declaration: `layout name { constraints }`
fn parse_layout_decl(&mut self) -> Result<Declaration, ParseError> {
let line = self.current_token().line;
self.advance(); // consume 'layout'
let name = self.expect_ident()?;
self.expect(&TokenKind::LBrace)?;
self.skip_newlines();
let mut constraints = Vec::new();
while !self.check(&TokenKind::RBrace) && !matches!(self.peek(), TokenKind::Eof) {
let constraint = self.parse_layout_constraint()?;
constraints.push(constraint);
self.skip_newlines();
}
self.expect(&TokenKind::RBrace)?;
Ok(Declaration::Layout(LayoutDecl {
name,
constraints,
span: Span { start: 0, end: 0, line },
}))
}
/// Parse a single constraint: `left_expr op right_expr [strength]`
fn parse_layout_constraint(&mut self) -> Result<LayoutConstraint, ParseError> {
let left = self.parse_layout_additive()?;
let op = match self.peek() {
TokenKind::EqEq => ConstraintOp::Eq,
TokenKind::Gte => ConstraintOp::Gte,
TokenKind::Lte => ConstraintOp::Lte,
other => return Err(self.error(format!("expected ==, >=, or <= in layout constraint, got {:?}", other))),
};
self.advance();
let right = self.parse_layout_additive()?;
// Optional strength: [required] [strong] [weak]
let strength = if self.check(&TokenKind::LBracket) {
self.advance();
let s = match self.peek() {
TokenKind::Ident(n) if n == "required" => ConstraintStrength::Required,
TokenKind::Ident(n) if n == "strong" => ConstraintStrength::Strong,
TokenKind::Ident(n) if n == "medium" => ConstraintStrength::Medium,
TokenKind::Ident(n) if n == "weak" => ConstraintStrength::Weak,
_ => ConstraintStrength::Required,
};
self.advance();
self.expect(&TokenKind::RBracket)?;
s
} else {
ConstraintStrength::Required
};
Ok(LayoutConstraint { left, op, right, strength })
}
/// Parse layout additive: `term (+ | - term)*`
fn parse_layout_additive(&mut self) -> Result<LayoutExpr, ParseError> {
let mut left = self.parse_layout_multiplicative()?;
loop {
if self.check(&TokenKind::Plus) {
self.advance();
let right = self.parse_layout_multiplicative()?;
left = LayoutExpr::Add(Box::new(left), Box::new(right));
} else if self.check(&TokenKind::Minus) {
self.advance();
let right = self.parse_layout_multiplicative()?;
left = LayoutExpr::Sub(Box::new(left), Box::new(right));
} else {
break;
}
}
Ok(left)
}
/// Parse layout multiplicative: `atom (* atom)*`
fn parse_layout_multiplicative(&mut self) -> Result<LayoutExpr, ParseError> {
let mut left = self.parse_layout_atom()?;
while self.check(&TokenKind::Star) {
self.advance();
let right = self.parse_layout_atom()?;
left = LayoutExpr::Mul(Box::new(left), Box::new(right));
}
Ok(left)
}
/// Parse layout atom: `element.prop` | `number` | `(expr)`
fn parse_layout_atom(&mut self) -> Result<LayoutExpr, ParseError> {
match self.peek().clone() {
TokenKind::Int(n) => {
self.advance();
Ok(LayoutExpr::Const(n as f64))
}
TokenKind::Float(f) => {
self.advance();
Ok(LayoutExpr::Const(f))
}
TokenKind::Ident(name) => {
self.advance();
if self.check(&TokenKind::Dot) {
self.advance();
let prop = self.expect_ident()?;
Ok(LayoutExpr::Prop(name, prop))
} else {
// Bare identifier treated as element.value
Ok(LayoutExpr::Prop(name, "value".to_string()))
}
}
TokenKind::LParen => {
self.advance();
let expr = self.parse_layout_additive()?;
self.expect(&TokenKind::RParen)?;
Ok(expr)
}
other => Err(self.error(format!("expected layout expression, got {:?}", other))),
}
}
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 },
}))
}
/// `component Card(title: String) = column [ ... ]`
fn parse_component_decl(&mut self) -> Result<Declaration, ParseError> {
let line = self.current_token().line;
self.advance(); // consume 'component'
let name = self.expect_ident()?;
// Props
let props = 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::Component(ComponentDecl {
name,
props,
body,
span: Span { start: 0, end: 0, line },
}))
}
/// `route "/users/:id" -> column [ ... ]`
fn parse_route_decl(&mut self) -> Result<Declaration, ParseError> {
let line = self.current_token().line;
self.advance(); // consume 'route'
// Path string: consume "path" as StringStart + StringFragment + StringEnd
let path = if matches!(self.peek(), TokenKind::StringFragment(_)) {
// Simple string token (no interpolation)
if let TokenKind::StringFragment(s) = self.peek() {
let p = s.clone();
self.advance(); // consume fragment
// Consume trailing StringEnd if present
if matches!(self.peek(), TokenKind::StringEnd) {
self.advance();
}
p
} else {
return Err(self.error("expected path string after 'route'".to_string()));
}
} else {
return Err(self.error("expected path string after 'route'".to_string()));
};
self.expect(&TokenKind::Arrow)?;
self.skip_newlines();
let body = self.parse_expr()?;
Ok(Declaration::Route(RouteDecl {
path,
body,
span: Span { start: 0, end: 0, line },
}))
}
/// `constrain sidebar.width = clamp(200, viewport.width * 0.2, 350)`
fn parse_constrain_decl(&mut self) -> Result<Declaration, ParseError> {
let line = self.current_token().line;
self.advance(); // consume 'constrain'
let element = self.expect_ident()?;
self.expect(&TokenKind::Dot)?;
let prop = self.expect_ident()?;
self.expect(&TokenKind::Eq)?;
self.skip_newlines();
let expr = self.parse_expr()?;
Ok(Declaration::Constrain(ConstrainDecl {
element,
prop,
expr,
span: Span { start: 0, end: 0, line },
}))
}
/// Parse: `stream <view_name> on <url_expr> { mode: signal, output: a, b }`
fn parse_stream_decl(&mut self) -> Result<Declaration, ParseError> {
let line = self.current_token().line;
self.advance(); // consume `stream`
let view_name = self.expect_ident()?;
// Expect `on`
match self.peek() {
TokenKind::On => { self.advance(); }
_ => return Err(self.error("Expected 'on' after stream view name".into())),
}
let relay_url = self.parse_expr()?;
// Optional config block: `{ mode: signal, transport: webrtc, output: a, b }`
let mut mode = StreamMode::Signal;
let mut transport = StreamTransport::WebSocket;
let mut output: Vec<String> = Vec::new();
if self.check(&TokenKind::LBrace) {
self.advance(); // {
while !self.check(&TokenKind::RBrace) && !self.is_at_end() {
self.skip_newlines();
let key = self.expect_ident()?;
self.expect(&TokenKind::Colon)?;
if key == "mode" {
match self.peek() {
TokenKind::Pixel => { mode = StreamMode::Pixel; self.advance(); }
TokenKind::Delta => { mode = StreamMode::Delta; self.advance(); }
TokenKind::Signals => { mode = StreamMode::Signal; self.advance(); }
TokenKind::Ident(s) if s == "signal" => { mode = StreamMode::Signal; self.advance(); }
_ => return Err(self.error("Expected pixel, delta, or signal".into())),
}
} else if key == "transport" {
match self.peek() {
TokenKind::Ident(s) if s == "webrtc" || s == "WebRTC" => {
transport = StreamTransport::WebRTC;
self.advance();
}
TokenKind::Ident(s) if s == "websocket" || s == "ws" => {
transport = StreamTransport::WebSocket;
self.advance();
}
_ => return Err(self.error("Expected webrtc or websocket".into())),
}
} else if key == "output" {
// Parse comma-separated identifiers: output: count, doubled
loop {
let name = self.expect_ident()?;
output.push(name);
if self.check(&TokenKind::Comma) {
self.advance();
// Peek ahead: if next is a known key or RBrace, stop
// (the comma was a field separator, not an output separator)
match self.peek() {
TokenKind::RBrace => break,
TokenKind::Ident(s) if s == "mode" || s == "transport" || s == "output" => break,
_ => {} // continue parsing output names
}
} else {
break;
}
}
}
if self.check(&TokenKind::Comma) { self.advance(); }
self.skip_newlines();
}
self.expect(&TokenKind::RBrace)?;
}
Ok(Declaration::Stream(StreamDecl {
view_name,
relay_url,
mode,
transport,
output,
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)
}
// ── 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);
}
TokenKind::LBracket => {
self.advance(); // consume [
let index = self.parse_expr()?;
self.expect(&TokenKind::RBracket)?;
expr = Expr::Index(Box::new(expr), Box::new(index));
}
_ => break,
}
}
Ok(expr)
}
/// Primary expressions: literals, identifiers, containers, etc.
/// Parse a string literal, handling interpolation: `"hello {name}, you are {age}"`
/// Lexer emits: StringFragment("hello ") → StringInterp → [name tokens] → RBrace → StringFragment(", you are ") → StringInterp → [age tokens] → RBrace → StringFragment/StringEnd
fn parse_string_lit(&mut self) -> Result<Expr, ParseError> {
let mut segments = Vec::new();
loop {
match self.peek().clone() {
TokenKind::StringFragment(text) => {
self.advance();
segments.push(StringSegment::Literal(text));
}
TokenKind::StringEnd => {
self.advance();
// Empty string or end after interpolation
break;
}
TokenKind::StringInterp => {
self.advance(); // consume the { marker
let expr = self.parse_expr()?;
segments.push(StringSegment::Interpolation(Box::new(expr)));
// Consume the closing } — lexer emits RBrace and decrements interp_depth,
// then next_token transitions back to string mode automatically.
if self.check(&TokenKind::RBrace) {
self.advance();
}
}
_ => break,
}
}
if segments.is_empty() {
segments.push(StringSegment::Literal(String::new()));
}
Ok(Expr::StringLit(StringLit { segments }))
}
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(_) | TokenKind::StringEnd | TokenKind::StringInterp => {
self.parse_string_lit()
}
// 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),
TokenKind::Scene => self.parse_container_with_props(ContainerKind::Scene),
// 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))
}
// For-in: `for item in items -> body` or `for item, idx in items -> body`
TokenKind::For => {
self.advance();
let item = self.expect_ident()?;
// Optional index: `for item, idx in ...`
let index = if self.check(&TokenKind::Comma) {
self.advance();
Some(self.expect_ident()?)
} else {
None
};
self.expect(&TokenKind::In)?;
let iter_expr = self.parse_comparison()?;
self.expect(&TokenKind::Arrow)?;
self.skip_newlines();
let body = self.parse_expr()?;
Ok(Expr::ForIn {
item,
index,
iter: Box::new(iter_expr),
body: Box::new(body),
})
}
// Navigate expression: `navigate "/path"`
TokenKind::Navigate => {
self.advance();
let path_expr = self.parse_primary()?;
Ok(Expr::Call("navigate".to_string(), vec![path_expr]))
}
// Spring expression: `spring(value, stiffness, damping)`
TokenKind::Spring => {
self.advance();
if self.check(&TokenKind::LParen) {
let args = self.parse_call_args()?;
Ok(Expr::Call("spring".to_string(), args))
} else {
// Just `spring` as an identifier
Ok(Expr::Ident("spring".to_string()))
}
}
// Stream from — accept string URL or dotted identifier
TokenKind::Stream => {
self.advance();
self.expect(&TokenKind::From)?;
// Accept string URL: `stream from "ws://localhost:9100"`
// or dotted ident: `stream from button.click`
let full_source = if matches!(self.peek(), TokenKind::StringFragment(_) | TokenKind::StringEnd | TokenKind::StringInterp) {
// Parse string literal and extract the raw text
let expr = self.parse_string_lit()?;
match &expr {
Expr::StringLit(s) if s.segments.len() == 1 => {
if let StringSegment::Literal(text) = &s.segments[0] {
text.clone()
} else {
return Err(self.error("stream from requires a plain string URL".into()));
}
}
_ => return Err(self.error("stream from requires a plain string URL".into())),
}
} else {
let source = self.expect_ident()?;
let mut full = source;
while self.check(&TokenKind::Dot) {
self.advance();
let next = self.expect_ident()?;
full = format!("{full}.{next}");
}
full
};
// Parse optional { select: field1, field2 } block
let mut select: Vec<String> = Vec::new();
if self.check(&TokenKind::LBrace) {
self.advance();
self.skip_newlines();
while !self.check(&TokenKind::RBrace) && !self.is_at_end() {
let key = self.expect_ident()?;
self.expect(&TokenKind::Colon)?;
if key == "select" {
// Parse comma-separated identifiers
loop {
let name = self.expect_ident()?;
select.push(name);
if self.check(&TokenKind::Comma) {
self.advance();
// Peek: if next is RBrace or a known key, stop
match self.peek() {
TokenKind::RBrace => break,
TokenKind::Ident(s) if s == "select" || s == "mode" => break,
_ => {}
}
} else {
break;
}
}
}
// Skip trailing comma between block entries
if self.check(&TokenKind::Comma) { self.advance(); }
self.skip_newlines();
}
self.expect(&TokenKind::RBrace)?;
}
Ok(Expr::StreamFrom { source: full_source, mode: None, select })
}
// 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, component use, or function call
TokenKind::Ident(name) => {
let name = name.clone();
self.advance();
// Component use: `Button { label: "hello" }` — capitalized name + `{`
if name.chars().next().map_or(false, |c| c.is_uppercase())
&& self.check(&TokenKind::LBrace)
{
self.advance(); // consume `{`
self.skip_newlines();
let mut props = Vec::new();
let mut children = Vec::new();
while !self.check(&TokenKind::RBrace) && !self.is_at_end() {
self.skip_newlines();
// Check if this looks like a key: value prop or a child expression
let key = self.expect_ident()?;
if self.check(&TokenKind::Colon) {
self.advance(); // consume ':'
self.skip_newlines();
let val = self.parse_expr()?;
props.push((key, val));
} else {
// Bare ident — treat as child expression
children.push(Expr::Ident(key));
}
self.skip_newlines();
if self.check(&TokenKind::Comma) {
self.advance();
}
self.skip_newlines();
}
self.expect(&TokenKind::RBrace)?;
return Ok(Expr::ComponentUse { name, props, children });
}
// UI element with any arg (string, ident, parenthesized expr, or props-only)
if is_ui_element(&name) {
let next = self.peek().clone();
let looks_like_element = matches!(
next,
TokenKind::StringFragment(_) | TokenKind::StringEnd | TokenKind::StringInterp
| TokenKind::LBrace | TokenKind::LParen
) || matches!(next, TokenKind::Ident(ref n) if !is_declaration_keyword(n));
if looks_like_element {
let fallback = name.clone();
match self.parse_element(name)? {
Some(el) => Ok(el),
None => Ok(Expr::Ident(fallback)),
}
} else {
Ok(Expr::Ident(name))
}
}
// Function call: `name(args)` — only for non-element idents
else if self.check(&TokenKind::LParen) {
let args = self.parse_call_args()?;
Ok(Expr::Call(name, args))
}
// Element with string arg: `text "hello"` — fallback for non-is_ui_element tags
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)),
}
}
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, ident, or parenthesized expression args
loop {
match self.peek().clone() {
TokenKind::StringFragment(_) | TokenKind::StringEnd | TokenKind::StringInterp => {
args.push(self.parse_string_lit()?);
}
TokenKind::Ident(name) if !is_declaration_keyword(&name) => {
// Only consume if it looks like an element argument
self.advance();
args.push(Expr::Ident(name));
}
TokenKind::LParen => {
// Parenthesized expression arg: button (if cond then "A" else "B")
self.advance(); // consume (
let expr = self.parse_expr()?;
self.expect(&TokenKind::RParen)?;
args.push(expr);
}
_ => 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"
| "circle" | "rect" | "line" // Physics scene elements
)
}
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
}
#[test]
fn test_stream_decl() {
let prog = parse(r#"stream main on "ws://localhost:9100" { mode: signal }"#);
match &prog.declarations[0] {
Declaration::Stream(s) => {
assert_eq!(s.view_name, "main");
assert_eq!(s.mode, StreamMode::Signal);
}
other => panic!("expected Stream, got {other:?}"),
}
}
#[test]
fn test_stream_decl_pixel_mode() {
let prog = parse(r#"stream main on "ws://localhost:9100" { mode: pixel }"#);
match &prog.declarations[0] {
Declaration::Stream(s) => {
assert_eq!(s.mode, StreamMode::Pixel);
}
other => panic!("expected Stream, got {other:?}"),
}
}
#[test]
fn test_stream_decl_default_mode() {
let prog = parse(r#"stream main on "ws://localhost:9100""#);
match &prog.declarations[0] {
Declaration::Stream(s) => {
assert_eq!(s.mode, StreamMode::Signal);
}
other => panic!("expected Stream, got {other:?}"),
}
}
#[test]
fn test_stream_from_string_url() {
let prog = parse(r#"let remote = stream from "ws://localhost:9100""#);
match &prog.declarations[0] {
Declaration::Let(decl) => {
assert_eq!(decl.name, "remote");
match &decl.value {
Expr::StreamFrom { source, .. } => {
assert_eq!(source, "ws://localhost:9100");
}
other => panic!("expected StreamFrom, got {other:?}"),
}
}
other => panic!("expected Let, got {other:?}"),
}
}
#[test]
fn test_stream_from_dotted_ident() {
let prog = parse("let remote = stream from button.click");
match &prog.declarations[0] {
Declaration::Let(decl) => {
assert_eq!(decl.name, "remote");
match &decl.value {
Expr::StreamFrom { source, .. } => {
assert_eq!(source, "button.click");
}
other => panic!("expected StreamFrom, got {other:?}"),
}
}
other => panic!("expected Let, got {other:?}"),
}
}
#[test]
fn test_multiple_stream_from() {
let prog = parse(r#"let a = stream from "ws://localhost:9100"
let b = stream from "ws://localhost:9101""#);
assert_eq!(prog.declarations.len(), 2);
for decl in &prog.declarations {
match decl {
Declaration::Let(d) => {
assert!(matches!(d.value, Expr::StreamFrom { .. }));
}
other => panic!("expected Let, got {other:?}"),
}
}
}
}