- 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
1642 lines
58 KiB
Rust
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:?}"),
|
|
}
|
|
}
|
|
}
|
|
}
|