diff --git a/src/ast.rs b/src/ast.rs index 0cb4a50..aa23afe 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -18,6 +18,10 @@ pub enum Value { Rhei(String), Null, Array(Vec), + // glts + Call(String, Vec), // rgba(30, 30, 46, 0.88) + Unit(f64, String), // 8px, 120ms + Ident(String), } #[derive(Debug, Clone)] @@ -57,6 +61,14 @@ pub enum Directive { child_span: (u32, u32), }, RheiBlock(String), + StyleRule { + selector: String, + prop_span: (u32, u32), + }, + StyleAnim { + name: String, + frames: Vec<(String, (u32, u32))>, + }, } #[derive(Debug, Default)] diff --git a/src/compiler.rs b/src/compiler.rs index 1cba336..7740d29 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -10,23 +10,22 @@ impl<'a> Compiler<'a> { pub fn new(module: &'a ModuleSoA) -> Self { Self { module, - buf: Vec::new(), + buf: Vec::with_capacity(module.hierarchy.len() * 32 + std::mem::size_of_val(&MAGIC_HEADER)), } } pub fn compile(mut self, root_nodes: &[NodeId]) -> Vec { self.buf.extend_from_slice(&MAGIC_HEADER); - self.compile_span(root_nodes); - self.buf } + #[inline] fn compile_span(&mut self, nodes: &[NodeId]) { - for node in nodes { + for &node in nodes { match node { - NodeId::Element(id) => self.compile_element(*id), - NodeId::Directive(id) => self.compile_directive(*id), + NodeId::Element(id) => self.compile_element(id), + NodeId::Directive(id) => self.compile_directive(id), } } } @@ -39,18 +38,17 @@ impl<'a> Compiler<'a> { self.buf.push(OP_END_BLOCK); } - fn compile_element(&mut self, id: u32) { +fn compile_element(&mut self, id: u32) { let idx = id as usize; - let typ = &self.module.elem_types[idx]; - self.buf.push(OP_ELEM_PUSH); - self.write_string(typ); + self.write_string(&self.module.elem_types[idx]); let (p_start, p_len) = self.module.elem_prop_spans[idx]; - for i in p_start..(p_start + p_len) { - let key = &self.module.prop_keys[i as usize]; - let val = &self.module.prop_values[i as usize]; - self.compile_property(key, val); + let p_start = p_start as usize; + let p_end = p_start + p_len as usize; + + for i in p_start..p_end { + self.compile_property(&self.module.prop_keys[i], &self.module.prop_values[i]); } if let Some(content) = &self.module.elem_content[idx] { @@ -60,15 +58,17 @@ impl<'a> Compiler<'a> { let child_span = self.module.elem_child_spans[idx]; if child_span.1 > 0 { - self.compile_block(child_span); + let start = child_span.0 as usize; + let len = child_span.1 as usize; + let nodes = &self.module.hierarchy[start..start + len]; + self.compile_span(nodes); } self.buf.push(OP_ELEM_POP); } fn compile_directive(&mut self, id: u32) { - let dir = &self.module.directives[id as usize]; - match dir { + match &self.module.directives[id as usize] { Directive::Version(v) => { self.buf.push(OP_VERSION); self.write_i64(*v); @@ -87,19 +87,14 @@ impl<'a> Compiler<'a> { self.write_string(name); self.write_u32(prop_span.1); - let (start, len) = *prop_span; - for i in start..(start + len) { - let key = &self.module.prop_keys[i as usize]; - let val = &self.module.prop_values[i as usize]; - self.write_string(key); - self.compile_value(val); + let start = prop_span.0 as usize; + let end = start + prop_span.1 as usize; + for i in start..end { + self.write_string(&self.module.prop_keys[i]); + self.compile_value(&self.module.prop_values[i]); } } - Directive::Component { - name, - params, - child_span, - } => { + Directive::Component { name, params, child_span } => { self.buf.push(OP_COMPONENT); self.write_string(name); self.write_u32(params.len() as u32); @@ -114,36 +109,24 @@ impl<'a> Compiler<'a> { self.write_string(name); self.compile_value(value); } - Directive::If { - condition, - child_span, - else_span, - } => { + Directive::If { condition, child_span, else_span } => { self.buf.push(OP_IF); self.compile_value(condition); self.compile_block(*child_span); if let Some(es) = else_span { - self.buf.push(1); + self.buf.push(1); self.compile_block(*es); } else { - self.buf.push(0); + self.buf.push(0); } } - Directive::Each { - item, - collection, - child_span, - } => { + Directive::Each { item, collection, child_span } => { self.buf.push(OP_EACH); self.write_string(item); self.compile_value(collection); self.compile_block(*child_span); } - Directive::On { - event, - args, - child_span, - } => { + Directive::On { event, args, child_span } => { self.buf.push(OP_ON); self.write_string(event); self.write_u32(args.len() as u32); @@ -157,127 +140,134 @@ impl<'a> Compiler<'a> { self.buf.push(OP_RHEI_BLK); self.write_string(code); } + Directive::StyleRule { selector, prop_span } => { + self.buf.push(OP_STYLE_RULE); + self.write_string(selector); + self.write_u32(prop_span.1); + + let start = prop_span.0 as usize; + let end = start + prop_span.1 as usize; + for i in start..end { + self.write_string(&self.module.prop_keys[i]); + self.compile_value(&self.module.prop_values[i]); + } + } + Directive::StyleAnim { name, frames } => { + self.buf.push(OP_STYLE_ANIM); + self.write_string(name); + self.write_u32(frames.len() as u32); + for (step, span) in frames { + self.write_string(step); + self.write_u32(span.1); + let start = span.0 as usize; + let end = start + span.1 as usize; + for i in start..end { + self.write_string(&self.module.prop_keys[i]); + self.compile_value(&self.module.prop_values[i]); + } + } + } } } + #[inline] fn compile_property(&mut self, key: &str, val: &Value) { + let opcode = match val { + Value::String(_) => OP_PROP_STR, + Value::Int(_) => OP_PROP_INT, + Value::Float(_) => OP_PROP_FLOAT, + Value::Bool(_) => OP_PROP_BOOL, + Value::Color(_) => OP_PROP_COLOR, + Value::FsPath(_) => OP_PROP_FSPATH, + Value::Variable(_) => OP_PROP_VAR, + Value::Rhei(_) => OP_PROP_RHEI, + Value::Null => OP_PROP_NULL, + Value::Array(_) => OP_PROP_ARRAY, + // glts + Value::Call(_, _) => OP_PROP_CALL, + Value::Unit(_, _) => OP_PROP_UNIT, + Value::Ident(_) => OP_PROP_IDENT, + }; + self.buf.push(opcode); + self.write_string(key); + match val { - Value::String(s) => { - self.buf.push(OP_PROP_STR); - self.write_string(key); - self.write_string(s); - } - Value::Int(i) => { - self.buf.push(OP_PROP_INT); - self.write_string(key); - self.write_i64(*i); - } - Value::Float(f) => { - self.buf.push(OP_PROP_FLOAT); - self.write_string(key); - self.write_f64(*f); - } - Value::Bool(b) => { - self.buf.push(OP_PROP_BOOL); - self.write_string(key); - self.buf.push(if *b { 1 } else { 0 }); - } - Value::Color(c) => { - self.buf.push(OP_PROP_COLOR); - self.write_string(key); - self.write_string(c); - } - Value::FsPath(p) => { - self.buf.push(OP_PROP_FSPATH); - self.write_string(key); - self.write_string(p); - } - Value::Variable(v) => { - self.buf.push(OP_PROP_VAR); - self.write_string(key); - self.write_string(v); - } - Value::Rhei(r) => { - self.buf.push(OP_PROP_RHEI); - self.write_string(key); - self.write_string(r); - } - Value::Null => { - self.buf.push(OP_PROP_NULL); - self.write_string(key); - } - Value::Array(arr) => { - self.buf.push(OP_PROP_ARRAY); - self.write_string(key); - self.write_u32(arr.len() as u32); - for v in arr { - self.compile_value(v); - } - } + Value::Null => {} + _ => self.compile_value_data(val), } } + #[inline] fn compile_value(&mut self, val: &Value) { + let opcode = match val { + Value::String(_) => OP_PROP_STR, + Value::Int(_) => OP_PROP_INT, + Value::Float(_) => OP_PROP_FLOAT, + Value::Bool(_) => OP_PROP_BOOL, + Value::Color(_) => OP_PROP_COLOR, + Value::FsPath(_) => OP_PROP_FSPATH, + Value::Variable(_) => OP_PROP_VAR, + Value::Rhei(_) => OP_PROP_RHEI, + Value::Null => OP_PROP_NULL, + Value::Array(_) => OP_PROP_ARRAY, + // glts + Value::Call(_, _) => OP_PROP_CALL, + Value::Unit(_, _) => OP_PROP_UNIT, + Value::Ident(_) => OP_PROP_IDENT, + }; + self.buf.push(opcode); + self.compile_value_data(val); + } + + #[inline] + fn compile_value_data(&mut self, val: &Value) { match val { - Value::String(s) => { - self.buf.push(OP_PROP_STR); + Value::String(s) | Value::Color(s) | Value::FsPath(s) | Value::Variable(s) | Value::Rhei(s) => { self.write_string(s); } - Value::Int(i) => { - self.buf.push(OP_PROP_INT); - self.write_i64(*i); - } - Value::Float(f) => { - self.buf.push(OP_PROP_FLOAT); - self.write_f64(*f); - } - Value::Bool(b) => { - self.buf.push(OP_PROP_BOOL); - self.buf.push(if *b { 1 } else { 0 }); - } - Value::Color(c) => { - self.buf.push(OP_PROP_COLOR); - self.write_string(c); - } - Value::FsPath(p) => { - self.buf.push(OP_PROP_FSPATH); - self.write_string(p); - } - Value::Variable(v) => { - self.buf.push(OP_PROP_VAR); - self.write_string(v); - } - Value::Rhei(r) => { - self.buf.push(OP_PROP_RHEI); - self.write_string(r); - } - Value::Null => { - self.buf.push(OP_PROP_NULL); - } + Value::Int(i) => self.write_i64(*i), + Value::Float(f) => self.write_f64(*f), + Value::Bool(b) => self.buf.push(*b as u8), + Value::Null => {} Value::Array(arr) => { - self.buf.push(OP_PROP_ARRAY); self.write_u32(arr.len() as u32); for v in arr { self.compile_value(v); } } + Value::Ident(s) => self.write_string(s), + Value::Unit(num, unit) => { + self.write_f64(*num); + self.write_string(unit); + } + Value::Call(name, args) => { + self.write_string(name); + self.write_u32(args.len() as u32); + for arg in args { + self.compile_value(arg); + } + } } } + #[inline(always)] fn write_string(&mut self, s: &str) { let bytes = s.as_bytes(); self.write_u32(bytes.len() as u32); self.buf.extend_from_slice(bytes); } + #[inline(always)] fn write_u32(&mut self, v: u32) { self.buf.extend_from_slice(&v.to_le_bytes()); } + #[inline(always)] fn write_i64(&mut self, v: i64) { self.buf.extend_from_slice(&v.to_le_bytes()); } + #[inline(always)] fn write_f64(&mut self, v: f64) { self.buf.extend_from_slice(&v.to_le_bytes()); } diff --git a/src/lib.rs b/src/lib.rs index f1dfc4e..c0d2522 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,24 @@ pub mod ast; pub mod compiler; pub mod opcodes; pub mod parser; +pub mod style_parser; pub use ast::{Directive, ModuleSoA, NodeId, Value}; pub use compiler::Compiler; pub use parser::Parser; +pub use style_parser::StyleParser; + +pub fn compile_project(gltm_src: &str, glts_src: &str) -> Result, String> { + let mut module = ModuleSoA::new(); + + let mut style_parser = StyleParser::new(glts_src, &mut module); + style_parser.parse_all()?; + + let mut gltm_parser = Parser::with_module(gltm_src, module); + gltm_parser.parse_all()?; + + let module = gltm_parser.module; + let compiler = Compiler::new(&module); + + Ok(compiler.compile(&module.hierarchy)) +} diff --git a/src/opcodes.rs b/src/opcodes.rs index 9f33c8b..f80fbc6 100644 --- a/src/opcodes.rs +++ b/src/opcodes.rs @@ -29,4 +29,14 @@ pub const OP_PROP_FSPATH: u8 = 0x25; pub const OP_PROP_VAR: u8 = 0x26; pub const OP_PROP_RHEI: u8 = 0x27; +pub const OP_STYLE_RULE: u8 = 0x0B; +pub const OP_STYLE_ANIM: u8 = 0x0C; + +pub const OP_PROP_CALL: u8 = 0x2A; +pub const OP_PROP_UNIT: u8 = 0x2B; +pub const OP_PROP_IDENT: u8 = 0x2C; + + pub const OP_END_BLOCK: u8 = 0xFF; + + diff --git a/src/parser.rs b/src/parser.rs index 34cc6db..6c1389e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -7,20 +7,25 @@ pub struct Parser<'a> { } impl<'a> Parser<'a> { + pub fn new(src: &'a str) -> Self { + Self::with_module(src, ModuleSoA::new()) + } + + pub fn with_module(src: &'a str, module: ModuleSoA) -> Self { Self { src: src.as_bytes(), pos: 0, - module: ModuleSoA::new(), + module, } } - #[inline] + #[inline(always)] fn peek(&self) -> Option { self.src.get(self.pos).copied() } - #[inline] + #[inline(always)] fn advance(&mut self) { self.pos += 1; } @@ -36,15 +41,15 @@ impl<'a> Parser<'a> { } fn skip_whitespace(&mut self) { - while let Some(c) = self.peek() { + let len = self.src.len(); + while self.pos < len { + let c = self.src[self.pos]; if c.is_ascii_whitespace() { - self.advance(); - } else if c == b'/' && self.src.get(self.pos + 1) == Some(&b'/') { - while let Some(cc) = self.peek() { - if cc == b'\n' { - break; - } - self.advance(); + self.pos += 1; + } else if c == b'/' && self.pos + 1 < len && self.src[self.pos + 1] == b'/' { + self.pos += 2; + while self.pos < len && self.src[self.pos] != b'\n' { + self.pos += 1; } } else { break; @@ -54,86 +59,89 @@ impl<'a> Parser<'a> { fn parse_ident(&mut self) -> String { let start = self.pos; - while let Some(c) = self.peek() { - if c.is_ascii_alphanumeric() || c == b'-' || c == b'_' || c == b'.' { - self.advance(); + let len = self.src.len(); + while self.pos < len { + let c = self.src[self.pos]; + if c.is_ascii_alphanumeric() || matches!(c, b'-' | b'_' | b'.') { + self.pos += 1; } else { break; } } - std::str::from_utf8(&self.src[start..self.pos]) - .unwrap() - .to_string() + unsafe { std::str::from_utf8_unchecked(&self.src[start..self.pos]).to_string() } } -fn parse_rhei_expr(&mut self) -> Result { + fn parse_rhei_expr(&mut self) -> Result { self.skip_whitespace(); if self.consume_if(b'{') { let start = self.pos; let mut depth = 1; - while depth > 0 { - let c = self.peek().ok_or("Unexpected EOF in Rhei block")?; - self.advance(); - if c == b'{' { depth += 1; } - if c == b'}' { depth -= 1; } + let len = self.src.len(); + + while depth > 0 && self.pos < len { + match self.src[self.pos] { + b'{' => depth += 1, + b'}' => depth -= 1, + _ => {} + } + self.pos += 1; } - let expr = std::str::from_utf8(&self.src[start..self.pos - 1]) - .unwrap().trim().to_string(); - Ok(expr) + if depth > 0 { + return Err("Unexpected EOF in Rhei block".into()); + } + + let expr = unsafe { std::str::from_utf8_unchecked(&self.src[start..self.pos - 1]) }; + Ok(expr.trim().to_string()) } else { let start = self.pos; let mut parens = 0; let mut brackets = 0; let mut in_str = false; let mut escape = false; - while let Some(c) = self.peek() { + let len = self.src.len(); + + while self.pos < len { + let c = self.src[self.pos]; if escape { escape = false; - self.advance(); + self.pos += 1; continue; } - if c == b'\\' { - escape = true; - self.advance(); - continue; - } - if c == b'"' { in_str = !in_str; } - if !in_str { - if c == b'(' { parens += 1; } - if c == b')' { + + match c { + b'\\' => escape = true, + b'"' => in_str = !in_str, + b'(' if !in_str => parens += 1, + b')' if !in_str => { if parens == 0 { break; } parens -= 1; } - if c == b'[' { brackets += 1; } - if c == b']' { - if brackets > 0 { brackets -= 1; } - } - if parens == 0 && brackets == 0 { - if c == b',' || c == b'\n' || c == b'}' || c == b'{' { - break; - } - } + b'[' if !in_str => brackets += 1, + b']' if !in_str && brackets > 0 => brackets -= 1, + b',' | b'\n' | b'}' | b'{' if !in_str && parens == 0 && brackets == 0 => break, + _ => {} } - self.advance(); + self.pos += 1; } - let expr = std::str::from_utf8(&self.src[start..self.pos]) - .unwrap().trim().to_string(); - Ok(expr) + let expr = unsafe { std::str::from_utf8_unchecked(&self.src[start..self.pos]) }; + Ok(expr.trim().to_string()) } } fn parse_value(&mut self) -> Result { self.skip_whitespace(); let c = self.peek().ok_or("Expected value, found EOF")?; + match c { b'"' => { self.advance(); let start = self.pos; - while self.peek() != Some(b'"') { - if self.peek().is_none() { - return Err("Unexpected EOF inside string literal".into()); - } - self.advance(); + let len = self.src.len(); + while self.pos < len && self.src[self.pos] != b'"' { + self.pos += 1; + } + if self.pos >= len { + return Err("Unexpected EOF inside string literal".into()); } let val = std::str::from_utf8(&self.src[start..self.pos]).unwrap().to_string(); self.advance(); @@ -145,8 +153,7 @@ fn parse_rhei_expr(&mut self) -> Result { loop { self.skip_whitespace(); if self.consume_if(b']') { break; } - let val = self.parse_value()?; - items.push(val); + items.push(self.parse_value()?); self.skip_whitespace(); self.consume_if(b','); } @@ -154,9 +161,7 @@ fn parse_rhei_expr(&mut self) -> Result { } b'#' => { self.advance(); - let mut color = String::from("#"); - color.push_str(&self.parse_ident()); - Ok(Value::Color(color)) + Ok(Value::Color(format!("#{}", self.parse_ident()))) } b'$' => { self.advance(); @@ -164,17 +169,16 @@ fn parse_rhei_expr(&mut self) -> Result { } b'!' => { self.advance(); - let ident = self.parse_ident(); - if ident != "rhei" { return Err("Expected !rhei".into()); } + if self.parse_ident() != "rhei" { return Err("Expected !rhei".into()); } self.consume_if(b':'); Ok(Value::Rhei(self.parse_rhei_expr()?)) } b'0'..=b'9' | b'-' => { let s = self.parse_ident(); if s.contains('.') { - Ok(Value::Float(s.parse().unwrap())) + s.parse().map(Value::Float).map_err(|_| format!("Invalid float: {}", s)) } else { - Ok(Value::Int(s.parse().unwrap())) + s.parse().map(Value::Int).map_err(|_| format!("Invalid int: {}", s)) } } _ => { @@ -186,13 +190,15 @@ fn parse_rhei_expr(&mut self) -> Result { "fs" => { self.consume_if(b':'); let start = self.pos; - while let Some(cc) = self.peek() { - if cc.is_ascii_whitespace() || cc == b',' || cc == b')' || cc == b'}' { + let len = self.src.len(); + while self.pos < len { + let cc = self.src[self.pos]; + if cc.is_ascii_whitespace() || matches!(cc, b',' | b')' | b'}') { break; } - self.advance(); + self.pos += 1; } - let path = std::str::from_utf8(&self.src[start..self.pos]).unwrap().to_string(); + let path = unsafe { std::str::from_utf8_unchecked(&self.src[start..self.pos]) }.to_string(); Ok(Value::FsPath(path)) } _ => Err(format!("Unknown value token: {}", s)), @@ -206,22 +212,22 @@ fn parse_rhei_expr(&mut self) -> Result { self.advance(); loop { self.skip_whitespace(); - if self.consume_if(b')') { - break; - } + if self.consume_if(b')') { break; } + let key = self.parse_ident(); if key.is_empty() { - return Err(format!( - "Expected property identifier, found {:?}", - self.peek().map(|b| b as char) - )); + return Err(format!("Expected property identifier, found {:?}", self.peek().map(|b| b as char))); } + self.skip_whitespace(); - if self.consume_if(b'=') { - let val = self.parse_value()?; - self.module.prop_keys.push(key); - self.module.prop_values.push(val); + if !self.consume_if(b'=') { + return Err(format!("Expected '=' after property key '{}', found {:?}", key, self.peek().map(|b| b as char))); } + + let val = self.parse_value()?; + self.module.prop_keys.push(key); + self.module.prop_values.push(val); + self.skip_whitespace(); self.consume_if(b','); } @@ -238,11 +244,8 @@ fn parse_rhei_expr(&mut self) -> Result { let mut direct_children = Vec::new(); loop { self.skip_whitespace(); - if self.consume_if(b'}') { - break; - } - let node = self.parse_node()?; - direct_children.push(node); + if self.consume_if(b'}') { break; } + direct_children.push(self.parse_node()?); } let start_idx = self.module.hierarchy.len() as u32; @@ -257,148 +260,102 @@ fn parse_rhei_expr(&mut self) -> Result { let dir = match name.as_str() { "version" => { self.skip_whitespace(); - let v = self.parse_ident().parse().unwrap(); - Directive::Version(v) + Directive::Version(self.parse_ident().parse().map_err(|_| "Invalid version")?) } - "style" => Directive::Style(match self.parse_value()? { - Value::String(s) => s, + "style" => match self.parse_value()? { + Value::String(s) => Directive::Style(s), _ => return Err("Style must be a string".into()), - }), + }, "global" => { self.skip_whitespace(); - if !self.consume_if(b'$') { - return Err("Expected $ var".into()); - } - let var_name = self.parse_ident(); + if !self.consume_if(b'$') { return Err("Expected '$' for variable name in @global".into()); } + let name = self.parse_ident(); self.skip_whitespace(); - self.consume_if(b'='); - let val = self.parse_value()?; - Directive::Global { - name: var_name, - value: val, - } + if !self.consume_if(b'=') { return Err(format!("Expected '=' after global variable name '${}'", name)); } + Directive::Global { name, value: self.parse_value()? } } "singleton" => { self.skip_whitespace(); - let sname = self.parse_ident(); + let name = self.parse_ident(); self.skip_whitespace(); let start_idx = self.module.prop_keys.len() as u32; - if self.consume_if(b'{') { - loop { - self.skip_whitespace(); - if self.consume_if(b'}') { - break; - } + if !self.consume_if(b'{') { return Err(format!("Expected '{{' for singleton '{}'", name)); } + loop { + self.skip_whitespace(); + if self.consume_if(b'}') { break; } - let key = self.parse_ident(); - if key.is_empty() { - return Err(format!( - "Expected singleton property identifier, found {:?}", - self.peek().map(|b| b as char) - )); - } - self.skip_whitespace(); - self.consume_if(b'='); - let val = self.parse_value()?; - self.module.prop_keys.push(key); - self.module.prop_values.push(val); - } + let key = self.parse_ident(); + if key.is_empty() { return Err("Expected singleton property identifier".into()); } + self.skip_whitespace(); + if !self.consume_if(b'=') { return Err(format!("Expected '=' after singleton property key '{}'", key)); } + let val = self.parse_value()?; + self.module.prop_keys.push(key); + self.module.prop_values.push(val); + + self.skip_whitespace(); + self.consume_if(b','); } let len = (self.module.prop_keys.len() as u32) - start_idx; - Directive::Singleton { - name: sname, - prop_span: (start_idx, len), - } + Directive::Singleton { name, prop_span: (start_idx, len) } } "component" => { self.skip_whitespace(); - let cname = self.parse_ident(); + let name = self.parse_ident(); let mut params = Vec::new(); self.skip_whitespace(); if self.consume_if(b'(') { loop { self.skip_whitespace(); - if self.consume_if(b')') { - break; - } + if self.consume_if(b')') { break; } let pname = self.parse_ident(); + if pname.is_empty() { return Err("Expected parameter name in @component".into()); } - if pname.is_empty() { - return Err(format!( - "Expected parameter name in @component, found {:?}", - self.peek().map(|b| b as char) - )); - } - self.skip_whitespace(); - self.consume_if(b':'); + if !self.consume_if(b':') { return Err(format!("Expected ':' after parameter '{}'", pname)); } self.skip_whitespace(); let ptype = self.parse_ident(); + if ptype.is_empty() { return Err(format!("Expected type after ':' for parameter '{}'", pname)); } params.push((pname, ptype)); + self.skip_whitespace(); self.consume_if(b','); } } - let child_span = self.parse_block()?; - Directive::Component { - name: cname, - params, - child_span, - } + Directive::Component { name, params, child_span: self.parse_block()? } } "let" => { self.skip_whitespace(); - self.consume_if(b'$'); - let var_name = self.parse_ident(); + if !self.consume_if(b'$') { return Err("Expected '$' for variable name in @let".into()); } + let name = self.parse_ident(); self.skip_whitespace(); - self.consume_if(b'='); - let val = self.parse_value()?; - Directive::Let { - name: var_name, - value: val, - } + if !self.consume_if(b'=') { return Err(format!("Expected '=' after variable name '${}'", name)); } + Directive::Let { name, value: self.parse_value()? } } -"if" => { + "if" => { self.skip_whitespace(); - - let expr_str = self.parse_rhei_expr()?; - let condition = Value::Rhei(expr_str); - + let condition = self.parse_value()?; let child_span = self.parse_block()?; let mut else_span = None; + self.skip_whitespace(); - let backup = self.pos; if self.consume_if(b'@') { if self.parse_ident() == "else" { else_span = Some(self.parse_block()?); } else { - self.pos = backup; + self.pos = backup; // rollback } } - Directive::If { - condition, - child_span, - else_span, - } + Directive::If { condition, child_span, else_span } } "each" => { self.skip_whitespace(); - self.consume_if(b'$'); + if !self.consume_if(b'$') { return Err("Expected '$' before item identifier in @each".into()); } let item = self.parse_ident(); self.skip_whitespace(); - let in_kw = self.parse_ident(); - if in_kw != "in" { - return Err("Expected 'in' in @each".into()); - } - let collection = self.parse_value()?; - let child_span = self.parse_block()?; - Directive::Each { - item, - collection, - child_span, - } + if self.parse_ident() != "in" { return Err("Expected 'in' keyword in @each".into()); } + Directive::Each { item, collection: self.parse_value()?, child_span: self.parse_block()? } } "on" => { self.skip_whitespace(); @@ -408,55 +365,55 @@ fn parse_rhei_expr(&mut self) -> Result { if self.consume_if(b'(') { loop { self.skip_whitespace(); - if self.consume_if(b')') { - break; - } + if self.consume_if(b')') { break; } let k = self.parse_ident(); + if k.is_empty() { return Err("Expected argument name in @on".into()); } self.skip_whitespace(); - self.consume_if(b'='); - let v = self.parse_value()?; - args.push((k, v)); + if !self.consume_if(b'=') { return Err(format!("Expected '=' after event argument key '{}'", k)); } + args.push((k, self.parse_value()?)); self.skip_whitespace(); self.consume_if(b','); } } - let child_span = self.parse_block()?; - Directive::On { - event, - args, - child_span, - } + Directive::On { event, args, child_span: self.parse_block()? } } _ => return Err(format!("Unknown directive: @{}", name)), }; Ok(NodeId::Directive(self.module.push_directive(dir))) } -fn parse_element(&mut self) -> Result { - let name = self.parse_ident(); - let el_id = self.module.push_element(name); + fn parse_element(&mut self) -> Result { + let name = self.parse_ident(); + let el_id = self.module.push_element(name); - self.skip_whitespace(); - if self.peek() == Some(b'(') { - let span = self.parse_properties()?; - self.module.elem_prop_spans[el_id as usize] = span; + self.skip_whitespace(); + if self.peek() == Some(b'(') { + self.module.elem_prop_spans[el_id as usize] = self.parse_properties()?; + } + + self.skip_whitespace(); + match self.peek() { + Some(b'{') => { + self.module.elem_child_spans[el_id as usize] = self.parse_block()?; } + Some(ch) if matches!(ch, b'"' | b'#' | b'$' | b'!' | b'-' | b'[' | b'0'..=b'9') => { + self.module.elem_content[el_id as usize] = Some(self.parse_value()?); + } + Some(b'a'..=b'z') => { + let backup = self.pos; + let ident = self.parse_ident(); + self.pos = backup; - self.skip_whitespace(); - let p = self.peek(); - if p == Some(b'{') { - let span = self.parse_block()?; - self.module.elem_child_spans[el_id as usize] = span; - } else if let Some(ch) = p { - if ch == b'"' || ch == b'#' || ch == b'$' || ch == b'!' || ch == b'-' || ch.is_ascii_digit() || ch == b'[' || ch.is_ascii_lowercase() { - let val = self.parse_value()?; - self.module.elem_content[el_id as usize] = Some(val); + if matches!(ident.as_str(), "true" | "false" | "null" | "fs") { + self.module.elem_content[el_id as usize] = Some(self.parse_value()?); } } - - Ok(NodeId::Element(el_id)) + _ => {} } + Ok(NodeId::Element(el_id)) +} + pub fn parse_node(&mut self) -> Result { self.skip_whitespace(); let c = self.peek().ok_or("EOF reached")?; @@ -465,37 +422,23 @@ fn parse_element(&mut self) -> Result { b'@' => self.parse_directive(), b'!' => { self.advance(); - let i = self.parse_ident(); - if i != "rhei" { return Err("Expected rhei".into()); } + if self.parse_ident() != "rhei" { return Err("Expected rhei".into()); } self.consume_if(b':'); let expr = self.parse_rhei_expr()?; - Ok(NodeId::Directive( - self.module.push_directive(Directive::RheiBlock(expr)), - )) + Ok(NodeId::Directive(self.module.push_directive(Directive::RheiBlock(expr)))) } b'A'..=b'Z' => self.parse_element(), - b'"' | b'#' | b'$' | b'-' | b'0'..=b'9' | b'[' => { + ch if matches!(ch, b'"' | b'#' | b'$' | b'-' | b'[' | b'0'..=b'9') => { let val = self.parse_value()?; let el_id = self.module.push_element("#text".to_string()); self.module.elem_content[el_id as usize] = Some(val); Ok(NodeId::Element(el_id)) } - ch if ch.is_ascii_lowercase() => { - let val = self.parse_value()?; - let el_id = self.module.push_element("#text".to_string()); - self.module.elem_content[el_id as usize] = Some(val); - Ok(NodeId::Element(el_id)) - } - _ => { + _ => { let extract_start = self.pos.saturating_sub(10); let extract_end = std::cmp::min(self.src.len(), self.pos + 20); - let context = std::str::from_utf8(&self.src[extract_start..extract_end]) - .unwrap_or(""); - - Err(format!( - "Unexpected token: '{}' at position {}. Context around: \"...{}...\"", - c as char, self.pos, context.trim() - )) + let context = std::str::from_utf8(&self.src[extract_start..extract_end]).unwrap_or(""); + Err(format!("Unexpected token: '{}' at position {}. Context: \"...{}...\"", c as char, self.pos, context.trim())) } } } diff --git a/src/style_parser.rs b/src/style_parser.rs new file mode 100644 index 0000000..2d4c3fb --- /dev/null +++ b/src/style_parser.rs @@ -0,0 +1,312 @@ +use crate::ast::*; +use std::collections::HashMap; + +pub struct StyleParser<'a> { + src: &'a [u8], + pos: usize, + pub module: &'a mut ModuleSoA, + variables: HashMap, + mixins: HashMap>, +} + +impl<'a> StyleParser<'a> { + pub fn new(src: &'a str, module: &'a mut ModuleSoA) -> Self { + Self { + src: src.as_bytes(), + pos: 0, + module, + variables: HashMap::new(), + mixins: HashMap::new(), + } + } + + fn peek(&self) -> Option { + self.src.get(self.pos).copied() + } + + fn advance(&mut self) { + self.pos += 1; + } + + fn consume_if(&mut self, expected: u8) -> bool { + if self.peek() == Some(expected) { + self.pos += 1; + true + } else { + false + } + } + + fn skip_whitespace(&mut self) { + let len = self.src.len(); + while self.pos < len { + let c = self.src[self.pos]; + if c.is_ascii_whitespace() { + self.pos += 1; + } else if c == b'/' && self.pos + 1 < len && self.src[self.pos + 1] == b'/' { + self.pos += 2; + while self.pos < len && self.src[self.pos] != b'\n' { + self.pos += 1; + } + } else { + break; + } + } + } + + fn parse_ident(&mut self) -> String { + let start = self.pos; + while self.pos < self.src.len() { + let c = self.src[self.pos]; + if c.is_ascii_alphanumeric() || matches!(c, b'-' | b'_') { + self.pos += 1; + } else { + break; + } + } + unsafe { std::str::from_utf8_unchecked(&self.src[start..self.pos]).to_string() } + } + + fn parse_selector(&mut self) -> String { + let start = self.pos; + while self.pos < self.src.len() && self.src[self.pos] != b'{' { + self.pos += 1; + } + unsafe { std::str::from_utf8_unchecked(&self.src[start..self.pos]).trim().to_string() } + } + + fn parse_single_value(&self, s: &str) -> Value { + let s = s.trim_end_matches(','); + + if let Some(stripped) = s.strip_prefix('#') { + return Value::Color(format!("#{}", stripped)); + } + + if let Some(var_name) = s.strip_prefix('$') { + if let Some(val) = self.variables.get(var_name) { + return val.clone(); + } + return Value::Variable(var_name.to_string()); + } + + if s.contains('(') && s.ends_with(')') { + let parts: Vec<&str> = s.splitn(2, '(').collect(); + let name = parts[0].trim().to_string(); + let args_str = parts[1].trim_end_matches(')'); + let args = args_str.split(',') + .filter(|a| !a.trim().is_empty()) + .map(|a| self.parse_single_value(a.trim())) + .collect(); + return Value::Call(name, args); + } + + let mut num_end = 0; + for (i, c) in s.char_indices() { + if c.is_ascii_digit() || c == '.' || c == '-' { + num_end = i + c.len_utf8(); + } else { break; } + } + + if num_end > 0 && num_end < s.len() { + if let Ok(num) = s[..num_end].parse::() { + return Value::Unit(num, s[num_end..].to_string()); + } + } + + if let Ok(i) = s.parse::() { return Value::Int(i); } + if let Ok(f) = s.parse::() { return Value::Float(f); } + if s == "true" { return Value::Bool(true); } + if s == "false" { return Value::Bool(false); } + if s == "null" { return Value::Null; } + + Value::Ident(s.to_string()) + } + + fn parse_value_line(&mut self) -> Value { + let start = self.pos; + let mut parens = 0; + + while self.pos < self.src.len() { + let c = self.src[self.pos]; + if c == b'(' { parens += 1; } + else if c == b')' { parens -= 1; } + else if parens == 0 && matches!(c, b'\n' | b'\r' | b';' | b'}') { + break; + } + self.pos += 1; + } + + let line = unsafe { std::str::from_utf8_unchecked(&self.src[start..self.pos]) }.trim(); + if self.peek() == Some(b';') { self.advance(); } + + let mut tokens = Vec::new(); + let mut current = String::new(); + let mut p = 0; + for c in line.chars() { + match c { + '(' => { p += 1; current.push(c); } + ')' => { p -= 1; current.push(c); } + ' ' | '\t' if p == 0 => { + if !current.is_empty() { + tokens.push(current.clone()); + current.clear(); + } + } + _ => current.push(c), + } + } + if !current.is_empty() { tokens.push(current); } + + if tokens.len() == 1 { + self.parse_single_value(&tokens[0]) + } else { + Value::Array(tokens.into_iter().map(|t| self.parse_single_value(&t)).collect()) + } + } + + fn parse_properties(&mut self, target: &mut Vec<(String, Value)>) -> Result<(), String> { + self.consume_if(b'{'); + loop { + self.skip_whitespace(); + if self.consume_if(b'}') { break; } + + if self.peek() == Some(b'@') { + self.advance(); + let ident = self.parse_ident(); + if ident == "use" { + self.skip_whitespace(); + let mixin_name = self.parse_ident(); + if let Some(props) = self.mixins.get(&mixin_name) { + target.extend(props.clone()); + } else { + return Err(format!("Unknown mixin: {}", mixin_name)); + } + } + continue; + } + + let key = self.parse_ident(); + if key.is_empty() { return Err("Expected property key".into()); } + + self.skip_whitespace(); + if !self.consume_if(b':') { return Err("Expected ':' after property".into()); } + self.skip_whitespace(); + + let val = self.parse_value_line(); + target.push((key, val)); + } + Ok(()) + } + + fn parse_top_level(&mut self) -> Result, String> { + match self.peek() { + Some(b'$') => { + self.advance(); + let name = self.parse_ident(); + self.skip_whitespace(); + self.consume_if(b'='); + self.skip_whitespace(); + let val = self.parse_value_line(); + self.variables.insert(name, val); + Ok(None) + } + Some(b'@') => { + self.advance(); + let name = self.parse_ident(); + match name.as_str() { + "mixin" => { + self.skip_whitespace(); + let mixin_name = self.parse_ident(); + self.skip_whitespace(); + let mut props = Vec::new(); + self.parse_properties(&mut props)?; + self.mixins.insert(mixin_name, props); + Ok(None) + } + "anim" => { + self.skip_whitespace(); + let anim_name = self.parse_ident(); + self.skip_whitespace(); + self.consume_if(b'{'); + + let mut frames = Vec::new(); + loop { + self.skip_whitespace(); + if self.consume_if(b'}') { break; } + + let step = self.parse_selector(); + let mut props = Vec::new(); + self.parse_properties(&mut props)?; + + let start_idx = self.module.prop_keys.len() as u32; + for (k, v) in props { + self.module.prop_keys.push(k); + self.module.prop_values.push(v); + } + let len = self.module.prop_keys.len() as u32 - start_idx; + frames.push((step, (start_idx, len))); + } + + let id = self.module.push_directive(Directive::StyleAnim { name: anim_name, frames }); + Ok(Some(NodeId::Directive(id))) + } + "if" => { + self.skip_whitespace(); + let start = self.pos; + while self.pos < self.src.len() && self.src[self.pos] != b'{' { self.pos += 1; } + let cond_str = unsafe { std::str::from_utf8_unchecked(&self.src[start..self.pos]) }.trim(); + + self.consume_if(b'{'); + let start_idx = self.module.hierarchy.len() as u32; + loop { + self.skip_whitespace(); + if self.consume_if(b'}') { break; } + if let Some(node) = self.parse_top_level()? { + self.module.hierarchy.push(node); + } + } + let len = self.module.hierarchy.len() as u32 - start_idx; + + // Condition evaluates at runtime via Rhei + let condition = Value::Rhei(cond_str.to_string()); + let id = self.module.push_directive(Directive::If { + condition, + child_span: (start_idx, len), + else_span: None + }); + Ok(Some(NodeId::Directive(id))) + } + _ => Err(format!("Unknown directive: @{}", name)), + } + } + _ => { + let selector = self.parse_selector(); + if selector.is_empty() { return Err("Expected selector".into()); } + + let mut props = Vec::new(); + self.parse_properties(&mut props)?; + + let start_idx = self.module.prop_keys.len() as u32; + for (k, v) in props { + self.module.prop_keys.push(k); + self.module.prop_values.push(v); + } + let len = self.module.prop_keys.len() as u32 - start_idx; + + let id = self.module.push_directive(Directive::StyleRule { selector, prop_span: (start_idx, len) }); + Ok(Some(NodeId::Directive(id))) + } + } + } + + pub fn parse_all(&mut self) -> Result<(), String> { + self.skip_whitespace(); + while self.pos < self.src.len() { + if let Some(node) = self.parse_top_level()? { + self.module.hierarchy.push(node); + } + self.skip_whitespace(); + } + Ok(()) + } +} diff --git a/src/tests.rs b/src/tests.rs index b041b57..d08443e 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -257,3 +257,272 @@ fn test_full_desktop_compilation() { ); assert!(bytecode.len() > 50, "Байткод подозрительно мал"); } + +#[cfg(test)] +mod style_parser_tests { + use crate::ast::{Directive, ModuleSoA, NodeId, Value}; + use crate::style_parser::StyleParser; + + #[test] + fn test_style_parser_basic_rule() { + let input = "Screen { background: transparent; color: #fff }"; + let mut m = ModuleSoA::new(); + let mut parser = StyleParser::new(input, &mut m); + parser.parse_all().expect("Парсинг базового правила должен пройти успешно"); + + assert_eq!(m.hierarchy.len(), 1, "Должна быть одна директива"); + assert_eq!(m.directives.len(), 1); + + if let Directive::StyleRule { selector, prop_span } = &m.directives[0] { + assert_eq!(selector, "Screen"); + assert_eq!(prop_span.1, 2, "Должно быть 2 свойства"); + + let start = prop_span.0 as usize; + assert_eq!(m.prop_keys[start], "background"); + assert_eq!(m.prop_values[start], Value::Ident("transparent".to_string())); + + assert_eq!(m.prop_keys[start + 1], "color"); + assert_eq!(m.prop_values[start + 1], Value::Color("#fff".to_string())); + } else { + panic!("Ожидался узел StyleRule"); + } + } + + #[test] + fn test_style_parser_compile_time_variables() { + let input = r#" + $bg = #1e1e2e + $pad = 16px + Panel { + background: $bg + padding: $pad + color: $Config.textColor + } + "#; + let mut m = ModuleSoA::new(); + let mut parser = StyleParser::new(input, &mut m); + parser.parse_all().expect("Парсинг с переменными"); + + if let Directive::StyleRule { prop_span, .. } = &m.directives[0] { + let start = prop_span.0 as usize; + assert_eq!(m.prop_values[start], Value::Color("#1e1e2e".to_string())); + assert_eq!(m.prop_values[start + 1], Value::Unit(16.0, "px".to_string())); + assert_eq!(m.prop_values[start + 2], Value::Variable("Config.textColor".to_string())); + } else { + panic!("Ожидался StyleRule"); + } + } + + #[test] + fn test_style_parser_mixins() { + let input = r#" + @mixin flex-center { + display: flex + align-items: center + } + Box { + @use flex-center + width: 100% + } + "#; + let mut m = ModuleSoA::new(); + let mut parser = StyleParser::new(input, &mut m); + parser.parse_all().unwrap(); + + if let Directive::StyleRule { prop_span, .. } = &m.directives[0] { + assert_eq!(prop_span.1, 3, "Свойства из миксина должны добавиться к правилу"); + + let start = prop_span.0 as usize; + assert_eq!(m.prop_keys[start], "display"); + assert_eq!(m.prop_values[start], Value::Ident("flex".to_string())); + + assert_eq!(m.prop_keys[start + 1], "align-items"); + assert_eq!(m.prop_values[start + 1], Value::Ident("center".to_string())); + + assert_eq!(m.prop_keys[start + 2], "width"); + assert_eq!(m.prop_values[start + 2], Value::Unit(100.0, "%".to_string())); + } else { + panic!("Ожидался StyleRule"); + } + } + + #[test] + fn test_style_parser_values_and_calls() { + let input = r#" + Overlay { + background: rgba(30, 30, 46, 0.88) + inset: 0 0 36px 0 + box-shadow: 0 2px 8px rgba(0,0,0,0.45) + } + "#; + let mut m = ModuleSoA::new(); + let mut parser = StyleParser::new(input, &mut m); + parser.parse_all().unwrap(); + + let start = match &m.directives[0] { + Directive::StyleRule { prop_span, .. } => prop_span.0 as usize, + _ => panic!("Ожидался StyleRule"), + }; + + // background: rgba(...) + assert_eq!( + m.prop_values[start], + Value::Call( + "rgba".to_string(), + vec![Value::Int(30), Value::Int(30), Value::Int(46), Value::Float(0.88)] + ) + ); + + // inset: 0 0 36px 0 (парсится как Array) + assert_eq!( + m.prop_values[start + 1], + Value::Array(vec![ + Value::Int(0), + Value::Int(0), + Value::Unit(36.0, "px".to_string()), + Value::Int(0) + ]) + ); + + if let Value::Array(arr) = &m.prop_values[start + 2] { + assert_eq!(arr[0], Value::Int(0)); + assert_eq!(arr[1], Value::Unit(2.0, "px".to_string())); + assert_eq!(arr[2], Value::Unit(8.0, "px".to_string())); + assert!(matches!(arr[3], Value::Call(..))); + } else { + panic!("box-shadow должен быть спарсен как массив (набор значений)"); + } + } + + #[test] + fn test_style_parser_animation() { + let input = r#" + @anim fade-in { + from { opacity: 0 } + to { opacity: 1 } + } + "#; + let mut m = ModuleSoA::new(); + let mut parser = StyleParser::new(input, &mut m); + parser.parse_all().unwrap(); + + if let Directive::StyleAnim { name, frames } = &m.directives[0] { + assert_eq!(name, "fade-in"); + assert_eq!(frames.len(), 2); + assert_eq!(frames[0].0, "from"); + assert_eq!(frames[1].0, "to"); + } else { + panic!("Ожидалась директива StyleAnim"); + } + } +} + +#[cfg(test)] +mod style_compiler_tests { + use crate::ast::ModuleSoA; + use crate::compiler::Compiler; + use crate::opcodes::*; + use crate::style_parser::StyleParser; + + struct BytecodeBuilder { + buf: Vec, + } + impl BytecodeBuilder { + fn new() -> Self { + let mut b = Self { buf: Vec::new() }; + b.buf.extend_from_slice(&MAGIC_HEADER); + b + } + fn push_u8(mut self, val: u8) -> Self { + self.buf.push(val); + self + } + fn push_u32(mut self, val: u32) -> Self { + self.buf.extend_from_slice(&val.to_le_bytes()); + self + } + fn push_str(mut self, s: &str) -> Self { + let bytes = s.as_bytes(); + self.buf.extend_from_slice(&(bytes.len() as u32).to_le_bytes()); + self.buf.extend_from_slice(bytes); + self + } + fn push_f64(mut self, val: f64) -> Self { + self.buf.extend_from_slice(&val.to_le_bytes()); + self + } + fn build(self) -> Vec { + self.buf + } + } + + #[test] + fn test_compiler_style_exact_bytes() { + let input = "Box { padding: 8px; color: transparent }"; + let mut m = ModuleSoA::new(); + let mut parser = StyleParser::new(input, &mut m); + parser.parse_all().unwrap(); + + let compiler = Compiler::new(&m); + let bytecode = compiler.compile(&m.hierarchy); + + let expected = BytecodeBuilder::new() + .push_u8(OP_STYLE_RULE) + .push_str("Box") + .push_u32(2) + // 1: padding: 8px + .push_str("padding") + .push_u8(OP_PROP_UNIT) + .push_f64(8.0) + .push_str("px") + // 2: color: transparent + .push_str("color") + .push_u8(OP_PROP_IDENT) + .push_str("transparent") + .build(); + + assert_eq!(bytecode, expected); + } +} + +#[cfg(test)] +mod integration_tests { + use crate::compile_project; + use crate::opcodes::MAGIC_HEADER; + + const TEST_GLTS: &str = r#" + $accent = #cba6f7 + @mixin flex-col { + display: flex + flex-direction: column + } + Panel.desktop { + @use flex-col + background: rgba(30, 30, 46, 0.88) + border: 1px solid $accent + } + "#; + + const TEST_GLTM: &str = r#" + @version 1 + Panel(id="main", class="desktop") { + Button(label="Start") + } + "#; + + #[test] + fn test_full_compile_project() { + let result = compile_project(TEST_GLTM, TEST_GLTS); + assert!(result.is_ok(), "Проект должен успешно скомпилироваться: {:?}", result.err()); + + let bytecode = result.unwrap(); + + assert_eq!( + &bytecode[0..4], + &MAGIC_HEADER, + "Байткод должен начинаться с магического заголовка GLBC" + ); + + assert!(bytecode.len() > 100, "Байткод слишком мал, возможно скомпилировалось не все"); + } +}