This repository has been archived on 2026-05-05. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
el-ui-retired/vessels/el-ui-compiler/src/parser.rs
T
Will Anderson f4abfe6fdc feat: rename crates/ → vessels/ + add El ports per sub-vessel
Belated rename commit for foundation/el-ui — was missed in the
workspace-wide crates→vessels pass earlier today. Same structural
intent as the rename in the other repos: 'crates' is the Rust word,
'vessel' is El's, and the directory rename is the marker that this
slot holds an El buildable unit even if its current contents are
still Rust pending port.

Plus the El ports themselves — manifest.el + src/main.el per sub-
vessel (el-aop, el-auth, el-config, el-i18n, el-identity, el-layout,
el-platform, el-publish, el-secrets, el-services, el-style, el-ui-
compiler). The ui-compiler is a stub: elc only emits C right now;
generating browser-target JS/Wasm is the biggest open language gap
and gets its own project. Until then, el-ui-compiler emits a JS
module that throws elc.backend_missing so callers fail loudly.
Cross-repo path dependencies in Cargo.toml updated to vessels/.
2026-04-30 18:18:39 -05:00

641 lines
22 KiB
Rust

//! Parser for el-ui component files.
//!
//! Hand-written recursive descent. Produces a `Vec<Component>` from a token stream.
use crate::ast::*;
use crate::error::{CompileError, CompileResult};
use crate::lexer::Token;
pub fn parse(tokens: &[Token]) -> CompileResult<Vec<Component>> {
let mut p = Parser::new(tokens);
p.parse_program()
}
struct Parser<'a> {
tokens: &'a [Token],
pos: usize,
}
impl<'a> Parser<'a> {
fn new(tokens: &'a [Token]) -> Self {
Self { tokens, pos: 0 }
}
fn peek(&self) -> &Token {
self.tokens.get(self.pos).unwrap_or(&Token::Eof)
}
fn advance(&mut self) -> &Token {
let tok = self.tokens.get(self.pos).unwrap_or(&Token::Eof);
if self.pos < self.tokens.len() {
self.pos += 1;
}
tok
}
fn expect(&mut self, expected: &Token) -> CompileResult<()> {
let tok = self.advance();
if tok == expected {
Ok(())
} else {
Err(CompileError::Parse {
msg: format!("expected {:?}, got {:?}", expected, tok),
})
}
}
fn expect_ident(&mut self) -> CompileResult<String> {
match self.advance().clone() {
Token::Ident(s) => Ok(s),
other => Err(CompileError::Parse {
msg: format!("expected identifier, got {:?}", other),
}),
}
}
fn parse_program(&mut self) -> CompileResult<Vec<Component>> {
let mut components = Vec::new();
while *self.peek() != Token::Eof {
match self.peek() {
Token::Component => {
components.push(self.parse_component()?);
}
_ => {
// Skip unknown top-level tokens
self.advance();
}
}
}
Ok(components)
}
fn parse_component(&mut self) -> CompileResult<Component> {
self.expect(&Token::Component)?;
let name = self.expect_ident()?;
self.expect(&Token::LBrace)?;
let mut props = Vec::new();
let mut state = Vec::new();
let mut methods = Vec::new();
let mut template = Template { nodes: Vec::new() };
loop {
match self.peek().clone() {
Token::RBrace => {
self.advance();
break;
}
Token::Eof => break,
Token::Props => {
self.advance();
props = self.parse_prop_block()?;
}
Token::State => {
self.advance();
state = self.parse_state_block()?;
}
Token::Fn => {
methods.push(self.parse_method()?);
}
Token::Template => {
self.advance(); // consume `template`
template = self.parse_template_block()?;
}
_ => {
self.advance(); // skip unknown
}
}
}
Ok(Component { name, props, state, methods, template })
}
fn parse_prop_block(&mut self) -> CompileResult<Vec<PropDef>> {
self.expect(&Token::LBrace)?;
let mut props = Vec::new();
loop {
match self.peek().clone() {
Token::RBrace | Token::Eof => {
self.advance();
break;
}
Token::Ident(name) => {
self.advance();
self.expect(&Token::Colon)?;
let type_name = self.parse_type_name()?;
let default = if *self.peek() == Token::Eq {
self.advance();
Some(self.parse_default_value()?)
} else {
None
};
// Optional trailing comma/semicolon
if matches!(self.peek(), Token::Comma | Token::Semicolon) {
self.advance();
}
props.push(PropDef { name, type_name, default });
}
_ => {
self.advance();
}
}
}
Ok(props)
}
fn parse_state_block(&mut self) -> CompileResult<Vec<StateDef>> {
self.expect(&Token::LBrace)?;
let mut defs = Vec::new();
loop {
match self.peek().clone() {
Token::RBrace | Token::Eof => {
self.advance();
break;
}
Token::Ident(name) => {
self.advance();
self.expect(&Token::Colon)?;
let type_name = self.parse_type_name()?;
self.expect(&Token::Eq)?;
let initial = self.parse_default_value()?;
if matches!(self.peek(), Token::Comma | Token::Semicolon) {
self.advance();
}
defs.push(StateDef { name, type_name, initial });
}
_ => {
self.advance();
}
}
}
Ok(defs)
}
fn parse_type_name(&mut self) -> CompileResult<String> {
let name = match self.peek().clone() {
Token::Ident(s) => { self.advance(); s }
other => return Err(CompileError::Parse {
msg: format!("expected type name, got {:?}", other),
}),
};
// Check for array type [T] → already consumed base name, but array types
// start with [ so this handles bare type names only
Ok(name)
}
fn parse_default_value(&mut self) -> CompileResult<String> {
// Collect tokens until comma, semicolon, or next top-level item
// We need to handle nested structures like Fn types, etc.
let mut result = String::new();
let mut depth = 0i32;
loop {
match self.peek() {
Token::Eof => break,
Token::LParen | Token::LBrace | Token::LBracket => {
depth += 1;
let tok = self.advance();
result.push_str(&token_to_str(tok));
}
Token::RParen | Token::RBrace | Token::RBracket => {
if depth == 0 { break; }
depth -= 1;
let tok = self.advance();
result.push_str(&token_to_str(tok));
}
Token::Comma | Token::Semicolon if depth == 0 => break,
// These signal end of the value if at depth 0
Token::Ident(_) | Token::Props | Token::State | Token::Fn
| Token::Template | Token::Component if depth == 0 => break,
_ => {
let tok = self.advance();
result.push_str(&token_to_str(tok));
result.push(' ');
}
}
}
Ok(result.trim().to_owned())
}
fn parse_method(&mut self) -> CompileResult<Method> {
self.expect(&Token::Fn)?;
let name = self.expect_ident()?;
self.expect(&Token::LParen)?;
let mut params: Vec<(String, String)> = Vec::new();
loop {
match self.peek().clone() {
Token::RParen | Token::Eof => { self.advance(); break; }
Token::Ident(pname) => {
self.advance();
self.expect(&Token::Colon)?;
let ptype = self.parse_type_name()?;
params.push((pname, ptype));
if *self.peek() == Token::Comma { self.advance(); }
}
_ => { self.advance(); }
}
}
self.expect(&Token::Arrow)?;
let return_type = self.parse_type_name()?;
// Read the method body between { }
let body = self.read_block_raw()?;
Ok(Method { name, params, return_type, body })
}
/// Read everything between { and matching } as raw text.
fn read_block_raw(&mut self) -> CompileResult<String> {
self.expect(&Token::LBrace)?;
let mut result = String::new();
let mut depth = 1i32;
loop {
match self.peek() {
Token::Eof => break,
Token::LBrace => { depth += 1; self.advance(); result.push_str("{ "); }
Token::RBrace => {
depth -= 1;
self.advance();
if depth == 0 { break; }
result.push_str("} ");
}
tok => {
result.push_str(&token_to_str(tok));
result.push(' ');
self.advance();
}
}
}
Ok(result.trim().to_owned())
}
/// Parse the template block. At this point the lexer has already expanded
/// the template into special tokens (LBrace/RBrace wrapping interpolations,
/// LAngle/Ident for elements, etc.).
fn parse_template_block(&mut self) -> CompileResult<Template> {
self.expect(&Token::LBrace)?;
let nodes = self.parse_template_nodes()?;
// The matching RBrace is consumed inside parse_template_nodes
Ok(Template { nodes })
}
fn parse_template_nodes(&mut self) -> CompileResult<Vec<TemplateNode>> {
let mut nodes: Vec<TemplateNode> = Vec::new();
loop {
match self.peek().clone() {
Token::Eof | Token::RBrace => {
self.advance();
break;
}
Token::CloseTag(_) => {
// Consumed by parent element parser
break;
}
Token::LAngle => {
nodes.push(self.parse_element()?);
}
Token::LBrace => {
// Interpolation: { RawText }
self.advance(); // consume {
if let Token::RawText(expr) = self.peek().clone() {
self.advance();
nodes.push(TemplateNode::Interpolation(expr));
if *self.peek() == Token::RBrace { self.advance(); }
} else {
nodes.push(TemplateNode::Text("{".into()));
}
}
Token::HashIdent(kw) => {
let kw = kw.clone();
self.advance();
nodes.push(self.parse_block_tag(&kw)?);
}
Token::SlashIdent(_) => {
// End of a block — caller handles
break;
}
Token::ColonIdent(_) => {
// {:else} — caller handles
break;
}
Token::RawText(t) => {
let t = t.clone();
self.advance();
if !t.is_empty() {
nodes.push(TemplateNode::Text(t));
}
}
_ => {
self.advance(); // skip
}
}
}
Ok(nodes)
}
fn parse_element(&mut self) -> CompileResult<TemplateNode> {
self.expect(&Token::LAngle)?;
let tag = self.expect_ident()?;
let is_component = tag.chars().next().map(|c| c.is_uppercase()).unwrap_or(false);
let mut attrs: Vec<Attr> = Vec::new();
// Parse attributes until > or />
let mut self_closing = false;
loop {
match self.peek().clone() {
Token::SelfClose => {
self.advance();
self_closing = true;
break;
}
Token::RAngle => {
self.advance();
break;
}
Token::Eof => break,
Token::OnColon(event) => {
let event = event.clone();
self.advance();
let handler = if let Token::RawText(h) = self.peek().clone() {
self.advance();
h
} else {
String::new()
};
attrs.push(Attr::EventHandler { event, handler });
}
Token::Ident(name) => {
let name = name.clone();
self.advance();
if *self.peek() == Token::Eq {
self.advance(); // consume =
match self.peek().clone() {
Token::StringLit(val) => {
self.advance();
attrs.push(Attr::Static { name, value: val });
}
Token::RawText(expr) => {
self.advance();
// Determine if it's a bool attr
// Simple heuristic: if name is "disabled", "checked", "readonly"
let bool_attrs = ["disabled", "checked", "readonly", "required", "multiple", "selected"];
if bool_attrs.contains(&name.as_str()) {
attrs.push(Attr::BoolAttr { name, expr });
} else {
attrs.push(Attr::Dynamic { name, expr });
}
}
_ => {
attrs.push(Attr::Static { name, value: String::new() });
}
}
} else {
// Standalone attribute (boolean)
attrs.push(Attr::BoolAttr { name, expr: "true".into() });
}
}
_ => {
self.advance();
}
}
}
if self_closing || is_component {
if is_component {
return Ok(TemplateNode::Component { name: tag, props: attrs });
}
return Ok(TemplateNode::Element { tag, attrs, children: Vec::new() });
}
// Read children until </tag>
let children = self.parse_template_children(&tag)?;
Ok(TemplateNode::Element { tag, attrs, children })
}
fn parse_template_children(&mut self, close_tag: &str) -> CompileResult<Vec<TemplateNode>> {
let mut children: Vec<TemplateNode> = Vec::new();
loop {
match self.peek().clone() {
Token::Eof => break,
Token::RBrace => break,
Token::CloseTag(tag) => {
self.advance();
if tag == close_tag || tag.is_empty() {
break;
}
// Mismatched close tag — ignore
}
Token::LAngle => {
children.push(self.parse_element()?);
}
Token::LBrace => {
self.advance();
if let Token::RawText(expr) = self.peek().clone() {
self.advance();
children.push(TemplateNode::Interpolation(expr));
if *self.peek() == Token::RBrace { self.advance(); }
}
}
Token::HashIdent(kw) => {
let kw = kw.clone();
self.advance();
children.push(self.parse_block_tag(&kw)?);
}
Token::SlashIdent(_) | Token::ColonIdent(_) => break,
Token::RawText(t) => {
let t = t.clone();
self.advance();
if !t.trim().is_empty() {
children.push(TemplateNode::Text(t));
}
}
_ => { self.advance(); }
}
}
Ok(children)
}
fn parse_block_tag(&mut self, kw: &str) -> CompileResult<TemplateNode> {
match kw {
"if" => self.parse_if_block(),
"each" => self.parse_each_block(),
"activate" => self.parse_activate_block(),
_ => Err(CompileError::Parse { msg: format!("unknown block tag: #{}", kw) }),
}
}
fn parse_if_block(&mut self) -> CompileResult<TemplateNode> {
// Next token should be RawText with the condition
let condition = if let Token::RawText(cond) = self.peek().clone() {
self.advance();
cond
} else {
return Err(CompileError::Parse { msg: "expected condition after {#if}".into() });
};
let then = self.parse_template_nodes_until_close_or_else()?;
let else_ = if let Token::ColonIdent(kw) = self.peek().clone() {
if kw == "else" {
self.advance();
Some(self.parse_template_nodes_until_close_or_else()?)
} else {
None
}
} else {
None
};
// Consume {/if}
if let Token::SlashIdent(kw) = self.peek().clone() {
if kw == "if" { self.advance(); }
}
Ok(TemplateNode::If { condition, then, else_ })
}
fn parse_each_block(&mut self) -> CompileResult<TemplateNode> {
// RawText: "items as item"
let raw = if let Token::RawText(r) = self.peek().clone() {
self.advance();
r
} else {
return Err(CompileError::Parse { msg: "expected 'items as item' after {#each}".into() });
};
let (items, item_name) = parse_each_header(&raw)?;
let children = self.parse_template_nodes_until_close_or_else()?;
if let Token::SlashIdent(kw) = self.peek().clone() {
if kw == "each" { self.advance(); }
}
Ok(TemplateNode::Each { items, item_name, children })
}
fn parse_activate_block(&mut self) -> CompileResult<TemplateNode> {
// RawText: `"query" as results`
let raw = if let Token::RawText(r) = self.peek().clone() {
self.advance();
r
} else {
return Err(CompileError::Parse { msg: "expected query after {#activate}".into() });
};
let (query, result_name) = parse_activate_header(&raw)?;
let children = self.parse_template_nodes_until_close_or_else()?;
if let Token::SlashIdent(kw) = self.peek().clone() {
if kw == "activate" { self.advance(); }
}
Ok(TemplateNode::Activate { query, result_name, children })
}
fn parse_template_nodes_until_close_or_else(&mut self) -> CompileResult<Vec<TemplateNode>> {
let mut nodes: Vec<TemplateNode> = Vec::new();
loop {
match self.peek().clone() {
Token::Eof | Token::RBrace => break,
Token::SlashIdent(_) | Token::ColonIdent(_) => break,
Token::CloseTag(_) => break,
Token::LAngle => nodes.push(self.parse_element()?),
Token::LBrace => {
self.advance();
if let Token::RawText(expr) = self.peek().clone() {
self.advance();
nodes.push(TemplateNode::Interpolation(expr));
if *self.peek() == Token::RBrace { self.advance(); }
}
}
Token::HashIdent(kw) => {
let kw = kw.clone();
self.advance();
nodes.push(self.parse_block_tag(&kw)?);
}
Token::RawText(t) => {
let t = t.clone();
self.advance();
if !t.trim().is_empty() {
nodes.push(TemplateNode::Text(t));
}
}
_ => { self.advance(); }
}
}
Ok(nodes)
}
}
fn token_to_str(tok: &Token) -> String {
match tok {
Token::Ident(s) => s.clone(),
Token::StringLit(s) => format!("\"{}\"", s),
Token::IntLit(n) => n.to_string(),
Token::FloatLit(f) => f.to_string(),
Token::BoolLit(b) => b.to_string(),
Token::LBrace => "{".into(),
Token::RBrace => "}".into(),
Token::LParen => "(".into(),
Token::RParen => ")".into(),
Token::LBracket => "[".into(),
Token::RBracket => "]".into(),
Token::Colon => ":".into(),
Token::Semicolon => ";".into(),
Token::Comma => ",".into(),
Token::Dot => ".".into(),
Token::Eq => "=".into(),
Token::EqEq => "==".into(),
Token::Bang => "!".into(),
Token::BangEq => "!=".into(),
Token::Plus => "+".into(),
Token::Minus => "-".into(),
Token::Star => "*".into(),
Token::Slash => "/".into(),
Token::Arrow => "->".into(),
Token::FatArrow => "=>".into(),
Token::AmpAmp => "&&".into(),
Token::PipePipe => "||".into(),
Token::Question => "?".into(),
Token::RawText(s) => s.clone(),
Token::Return => "return".into(),
Token::If => "if".into(),
Token::Else => "else".into(),
Token::Fn => "fn".into(),
Token::Component => "component".into(),
Token::Props => "props".into(),
Token::State => "state".into(),
Token::Template => "template".into(),
_ => String::new(),
}
}
fn parse_each_header(raw: &str) -> CompileResult<(String, String)> {
// e.g., "items as item"
if let Some(idx) = raw.find(" as ") {
let items = raw[..idx].trim().to_owned();
let item_name = raw[idx + 4..].trim().to_owned();
Ok((items, item_name))
} else {
Err(CompileError::Parse { msg: format!("invalid #each header: '{}'", raw) })
}
}
fn parse_activate_header(raw: &str) -> CompileResult<(String, String)> {
// e.g., `"query string" as results`
// Strip outer quotes from query
let raw = raw.trim();
if let Some(idx) = raw.rfind(" as ") {
let query_part = raw[..idx].trim();
let result_name = raw[idx + 4..].trim().to_owned();
let query = query_part.trim_matches('"').to_owned();
Ok((query, result_name))
} else {
Err(CompileError::Parse { msg: format!("invalid #activate header: '{}'", raw) })
}
}