This commit is contained in:
Faynot
2026-05-17 15:18:28 +03:00
commit 762af99f1d
12 changed files with 2137 additions and 0 deletions

94
src/ast.rs Normal file
View File

@@ -0,0 +1,94 @@
#![allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum NodeId {
Element(u32),
Directive(u32),
}
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
String(String),
Int(i64),
Float(f64),
Bool(bool),
Color(String),
FsPath(String),
Variable(String),
Rhei(String),
}
#[derive(Debug, Clone)]
pub enum Directive {
Version(i64),
Style(String),
Global {
name: String,
value: Value,
},
Singleton {
name: String,
prop_span: (u32, u32),
},
Component {
name: String,
params: Vec<(String, String)>,
child_span: (u32, u32),
},
Let {
name: String,
value: Value,
},
If {
condition: Value,
child_span: (u32, u32),
else_span: Option<(u32, u32)>,
},
Each {
item: String,
collection: Value,
child_span: (u32, u32),
},
On {
event: String,
args: Vec<(String, Value)>,
child_span: (u32, u32),
},
RheiBlock(String),
}
#[derive(Debug, Default)]
pub struct ModuleSoA {
pub elem_types: Vec<String>,
pub elem_prop_spans: Vec<(u32, u32)>,
pub elem_child_spans: Vec<(u32, u32)>,
pub elem_content: Vec<Option<Value>>,
pub prop_keys: Vec<String>,
pub prop_values: Vec<Value>,
pub directives: Vec<Directive>,
pub hierarchy: Vec<NodeId>,
}
impl ModuleSoA {
pub fn new() -> Self {
Self::default()
}
pub fn push_element(&mut self, typ: String) -> u32 {
let id = self.elem_types.len() as u32;
self.elem_types.push(typ);
self.elem_prop_spans.push((0, 0));
self.elem_child_spans.push((0, 0));
self.elem_content.push(None);
id
}
pub fn push_directive(&mut self, dir: Directive) -> u32 {
let id = self.directives.len() as u32;
self.directives.push(dir);
id
}
}

262
src/compiler.rs Normal file
View File

@@ -0,0 +1,262 @@
use crate::ast::*;
use crate::opcodes::*;
pub struct Compiler<'a> {
module: &'a ModuleSoA,
buf: Vec<u8>,
}
impl<'a> Compiler<'a> {
pub fn new(module: &'a ModuleSoA) -> Self {
Self {
module,
buf: Vec::new(),
}
}
pub fn compile(mut self, root_nodes: &[NodeId]) -> Vec<u8> {
self.buf.extend_from_slice(&MAGIC_HEADER);
self.compile_span(root_nodes);
self.buf
}
fn compile_span(&mut self, nodes: &[NodeId]) {
for node in nodes {
match node {
NodeId::Element(id) => self.compile_element(*id),
NodeId::Directive(id) => self.compile_directive(*id),
}
}
}
fn compile_block(&mut self, span: (u32, u32)) {
let start = span.0 as usize;
let len = span.1 as usize;
let nodes = &self.module.hierarchy[start..start + len];
self.compile_span(nodes);
self.buf.push(OP_END_BLOCK);
}
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);
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);
}
if let Some(content) = &self.module.elem_content[idx] {
self.buf.push(OP_CONTENT);
self.compile_value(content);
}
let child_span = self.module.elem_child_spans[idx];
if child_span.1 > 0 {
self.compile_block(child_span);
}
self.buf.push(OP_ELEM_POP);
}
fn compile_directive(&mut self, id: u32) {
let dir = &self.module.directives[id as usize];
match dir {
Directive::Version(v) => {
self.buf.push(OP_VERSION);
self.write_i64(*v);
}
Directive::Style(s) => {
self.buf.push(OP_STYLE);
self.write_string(s);
}
Directive::Global { name, value } => {
self.buf.push(OP_GLOBAL);
self.write_string(name);
self.compile_value(value);
}
Directive::Singleton { name, prop_span } => {
self.buf.push(OP_SINGLETON);
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);
}
}
Directive::Component {
name,
params,
child_span,
} => {
self.buf.push(OP_COMPONENT);
self.write_string(name);
self.write_u32(params.len() as u32);
for (pname, ptype) in params {
self.write_string(pname);
self.write_string(ptype);
}
self.compile_block(*child_span);
}
Directive::Let { name, value } => {
self.buf.push(OP_LET);
self.write_string(name);
self.compile_value(value);
}
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); // Has else
self.compile_block(*es);
} else {
self.buf.push(0); // No else
}
}
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,
} => {
self.buf.push(OP_ON);
self.write_string(event);
self.write_u32(args.len() as u32);
for (k, v) in args {
self.write_string(k);
self.compile_value(v);
}
self.compile_block(*child_span);
}
Directive::RheiBlock(code) => {
self.buf.push(OP_RHEI_BLK);
self.write_string(code);
}
}
}
fn compile_property(&mut self, key: &str, val: &Value) {
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);
}
}
}
fn compile_value(&mut self, val: &Value) {
match val {
Value::String(s) => {
self.buf.push(OP_PROP_STR);
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);
}
}
}
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);
}
fn write_u32(&mut self, v: u32) {
self.buf.extend_from_slice(&v.to_le_bytes());
}
fn write_i64(&mut self, v: i64) {
self.buf.extend_from_slice(&v.to_le_bytes());
}
fn write_f64(&mut self, v: f64) {
self.buf.extend_from_slice(&v.to_le_bytes());
}
}

302
src/main.rs Normal file
View File

@@ -0,0 +1,302 @@
mod ast;
mod compiler;
mod opcodes;
mod parser;
use compiler::Compiler;
use parser::Parser;
use std::collections::HashSet;
use std::fs::File;
use std::io::Write;
const INPUT: &str = r#"
@version 1
@style "desktop.glts"
@global $username = !rhei: os.env("USER")
@global $hostname = !rhei: os.hostname()
@global $locale = "ru_RU"
@global $scaleFactor = 1.0
@singleton Config {
wallpaper = fs:/home/$username/.config/de/wallpaper.jpg
wallpaperFit = "cover"
iconTheme = "papirus-dark"
fontMain = "Inter"
fontMono = "JetBrains Mono"
fontSize = 13
workspaces = 4
animEnabled = true
accentColor = #cba6f7
taskbarPos = "bottom"
}
@singleton SystemState {
battery = !rhei: sys.battery()
volume = !rhei: sys.volume.get()
brightness = !rhei: sys.brightness.get()
network = !rhei: sys.network.status()
timezone = !rhei: os.timezone()
}
@component DesktopIcon(label: str, icon: fspath, exec: str) {
Button(class="desktop-icon") {
@on click {
!rhei: process.spawn($exec)
}
@on dblclick {
!rhei {
process.spawn($exec)
wm.raise(process.lastPid())
}
}
Image(src=$icon, class="icon-img") {}
Label(class="icon-label") $label
}
}
@component BatteryWidget {
@if $SystemState.battery.present {
@let $lvl = $SystemState.battery.level
@let $ico = !rhei: icons.battery($lvl)
Panel(class="tray-item") {
Icon(src=$ico, class="tray-icon") {}
@if $SystemState.battery.charging {
Icon(src=fs:/usr/share/glint-de/icons/charging.svg,
class="tray-icon tray-icon--accent") {}
}
}
}
}
@component ClockWidget {
@let $time = !rhei: sys.time.format("%H:%M", $SystemState.timezone)
@let $date = !rhei: sys.time.format("%d %b", $SystemState.timezone)
Panel(class="clock-widget") {
@on click {
!rhei: ui.toggle("calendar-popup")
}
Label(class="clock-time") $time
Label(class="clock-date") $date
}
}
@component SystemTray {
Panel(id="system-tray", class="tray") {
BatteryWidget {}
@if $SystemState.network.connected {
Icon(class="tray-icon",
src=!rhei: icons.network($SystemState.network.type)) {}
} @else {
Icon(class="tray-icon tray-icon--warn",
src=fs:/usr/share/glint-de/icons/net-offline.svg) {}
}
Slider(id="vol-slider", class="tray-slider",
value=$SystemState.volume, min=0, max=100) {
@on change {
!rhei: sys.volume.set($self.value)
}
}
ClockWidget {}
}
}
Screen(id="root", width=1920, height=1080, scale=$scaleFactor) {
Wallpaper(id="bg",
src=$Config.wallpaper,
fallback=fs:/usr/share/glint-de/wallpapers/default.jpg,
fit=$Config.wallpaperFit,
watch=true) {}
Panel(id="desktop", class="desktop") {
@let $desktopFiles = !rhei: fs.glob("/home/$username/Desktop/*.desktop")
@each $f in $desktopFiles {
@let $entry = !rhei: xdg.parseDesktop($f)
DesktopIcon(
label=$entry.name,
icon=!rhei: icons.resolve($entry.icon, $Config.iconTheme),
exec=$entry.exec
) {}
}
@on contextmenu {
!rhei: ui.show("desktop-ctx")
}
}
ContextMenu(id="desktop-ctx", class="ctx-menu", visible=false) {
MenuItem(class="ctx-item") {
@on click { !rhei: ui.show("settings") }
"Настройки рабочего стола"
}
MenuItem(class="ctx-item") {
@on click { !rhei: wallpaper.showPicker() }
"Сменить обои"
}
MenuDivider {}
MenuItem(class="ctx-item ctx-item--danger") {
@on click { !rhei: session.logout() }
"Выйти из сеанса"
}
}
Panel(id="taskbar", class="taskbar") {
Button(id="launcher-btn", class="launcher-button") {
@on click { !rhei: ui.toggle("app-launcher") }
Image(src=fs:/usr/share/glint-de/logo.svg) {}
}
Panel(id="window-list", class="window-list") {
@let $wins = !rhei: wm.getWindows()
@each $w in $wins {
Button(class="taskbar-win", data-wid=$w.id,
active=$w.focused) {
@on click { !rhei: wm.focus($w.id) }
@on middleclick { !rhei: wm.close($w.id) }
Icon(src=$w.icon, class="win-icon") {}
Label(class="win-title") $w.title
}
}
}
SystemTray {}
}
Overlay(id="app-launcher", class="launcher", visible=false) {
@on keydown(key="Escape") {
!rhei: ui.hide("app-launcher")
}
Input(id="launcher-search", class="launcher-search",
placeholder="Поиск приложений...",
autofocus=true) {
@on input {
!rhei: launcher.filter($self.value)
}
}
Grid(id="app-grid", class="launcher-grid", columns=6) {
@let $apps = !rhei: apps.listAll()
@each $app in $apps {
Button(class="launcher-app") {
@on click {
!rhei {
process.spawn($app.exec)
ui.hide("app-launcher")
}
}
Image(src=$app.icon, class="app-icon") {}
Label(class="app-name") $app.name
}
}
}
}
Overlay(id="notif-layer", class="notif-layer") {
@let $notifs = !rhei: notifications.active()
@each $n in $notifs {
Panel(class="notif-card", data-id=$n.id) {
Panel(class="notif-header") {
Icon(src=$n.appIcon, class="notif-icon") {}
Label(class="notif-app") $n.appName
Button(class="notif-close") {
@on click { !rhei: notifications.dismiss($n.id) }
"×"
}
}
Label(class="notif-title") $n.summary
@if $n.body {
Label(class="notif-body") $n.body
}
}
}
}
@if !rhei: os.env("GLINT_DEV") {
!rhei {
debug.overlay.show()
debug.log("DE инициализирован. Пользователь: " + $username)
debug.log("Синглтоны: Config, SystemState")
}
}
}
"#;
fn main() {
let mut parser = Parser::new(INPUT);
match parser.parse_all() {
Ok(_) => {
println!("Parse is successful");
let m = &parser.module;
let mut child_indices = HashSet::new();
for (start, len) in &m.elem_child_spans {
for i in *start..(*start + *len) {
child_indices.insert(i as usize);
}
}
for dir in &m.directives {
let span = match dir {
ast::Directive::Component { child_span, .. } => Some(child_span),
ast::Directive::If {
child_span,
else_span,
..
} => {
if let Some(es) = else_span {
for i in es.0..(es.0 + es.1) {
child_indices.insert(i as usize);
}
}
Some(child_span)
}
ast::Directive::Each { child_span, .. } => Some(child_span),
ast::Directive::On { child_span, .. } => Some(child_span),
_ => None,
};
if let Some((start, len)) = span {
for i in *start..(*start + *len) {
child_indices.insert(i as usize);
}
}
}
let mut root_nodes = Vec::new();
for (i, node) in m.hierarchy.iter().enumerate() {
if !child_indices.contains(&i) {
root_nodes.push(node.clone());
}
}
println!("Compile...");
let compiler = Compiler::new(m);
let bytecode = compiler.compile(&root_nodes);
let output_path = "desktop.glbc";
let mut file = File::create(output_path).expect("Не удалось создать файл");
file.write_all(&bytecode)
.expect("Не удалось записать байткод");
println!(
"Compile is successful. Compiled in {} ({} bytes)",
output_path,
bytecode.len()
);
}
Err(e) => {
eprintln!("❌ Error of parse: {}", e);
}
}
}
#[cfg(test)]
mod tests;

30
src/opcodes.rs Normal file
View File

@@ -0,0 +1,30 @@
pub const MAGIC_HEADER: [u8; 4] = *b"GLBC";
// direvctives
pub const OP_VERSION: u8 = 0x01;
pub const OP_STYLE: u8 = 0x02;
pub const OP_GLOBAL: u8 = 0x03;
pub const OP_SINGLETON: u8 = 0x04;
pub const OP_COMPONENT: u8 = 0x05;
pub const OP_LET: u8 = 0x06;
pub const OP_IF: u8 = 0x07;
pub const OP_EACH: u8 = 0x08;
pub const OP_ON: u8 = 0x09;
pub const OP_RHEI_BLK: u8 = 0x0A;
// elements managment
pub const OP_ELEM_PUSH: u8 = 0x10;
pub const OP_ELEM_POP: u8 = 0x11;
pub const OP_CONTENT: u8 = 0x12;
// types
pub const OP_PROP_STR: u8 = 0x20;
pub const OP_PROP_INT: u8 = 0x21;
pub const OP_PROP_FLOAT: u8 = 0x22;
pub const OP_PROP_BOOL: u8 = 0x23;
pub const OP_PROP_COLOR: u8 = 0x24;
pub const OP_PROP_FSPATH: u8 = 0x25;
pub const OP_PROP_VAR: u8 = 0x26;
pub const OP_PROP_RHEI: u8 = 0x27;
pub const OP_END_BLOCK: u8 = 0xFF;

482
src/parser.rs Normal file
View File

@@ -0,0 +1,482 @@
use crate::ast::*;
pub struct Parser<'a> {
src: &'a [u8],
pos: usize,
pub module: ModuleSoA,
}
impl<'a> Parser<'a> {
pub fn new(src: &'a str) -> Self {
Self {
src: src.as_bytes(),
pos: 0,
module: ModuleSoA::new(),
}
}
#[inline]
fn peek(&self) -> Option<u8> {
self.src.get(self.pos).copied()
}
#[inline]
fn advance(&mut self) {
self.pos += 1;
}
#[inline]
fn consume_if(&mut self, expected: u8) -> bool {
if self.peek() == Some(expected) {
self.pos += 1;
true
} else {
false
}
}
fn skip_whitespace(&mut self) {
while let Some(c) = self.peek() {
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();
}
} else {
break;
}
}
}
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();
} else {
break;
}
}
std::str::from_utf8(&self.src[start..self.pos])
.unwrap()
.to_string()
}
fn parse_rhei_expr(&mut self) -> Result<String, String> {
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 expr = std::str::from_utf8(&self.src[start..self.pos - 1])
.unwrap()
.trim()
.to_string();
Ok(expr)
} else {
let start = self.pos;
let mut parens = 0;
let mut in_str = false;
let mut escape = false;
while let Some(c) = self.peek() {
if escape {
escape = false;
self.advance();
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')' {
if parens == 0 {
break;
}
parens -= 1;
}
if parens == 0 && (c == b',' || c == b'\n' || c == b'}' || c == b'{') {
break;
}
}
self.advance();
}
let expr = std::str::from_utf8(&self.src[start..self.pos])
.unwrap()
.trim()
.to_string();
Ok(expr)
}
}
fn parse_value(&mut self) -> Result<Value, String> {
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 val = std::str::from_utf8(&self.src[start..self.pos])
.unwrap()
.to_string();
self.advance();
Ok(Value::String(val))
}
b'#' => {
self.advance();
let mut color = String::from("#");
color.push_str(&self.parse_ident());
Ok(Value::Color(color))
}
b'$' => {
self.advance();
Ok(Value::Variable(self.parse_ident()))
}
b'!' => {
self.advance();
let ident = self.parse_ident();
if 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()))
} else {
Ok(Value::Int(s.parse().unwrap()))
}
}
_ => {
let s = self.parse_ident();
match s.as_str() {
"true" => Ok(Value::Bool(true)),
"false" => Ok(Value::Bool(false)),
"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'}' {
break;
}
self.advance();
}
let path = std::str::from_utf8(&self.src[start..self.pos])
.unwrap()
.to_string();
Ok(Value::FsPath(path))
}
_ => Err(format!("Unknown value token: {}", s)),
}
}
}
}
fn parse_properties(&mut self) -> Result<(u32, u32), String> {
let start_idx = self.module.prop_keys.len() as u32;
self.advance();
loop {
self.skip_whitespace();
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)
));
}
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);
}
self.skip_whitespace();
self.consume_if(b',');
}
let len = (self.module.prop_keys.len() as u32) - start_idx;
Ok((start_idx, len))
}
fn parse_block(&mut self) -> Result<(u32, u32), String> {
self.skip_whitespace();
if !self.consume_if(b'{') {
return Ok((self.module.hierarchy.len() as u32, 0));
}
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);
}
let start_idx = self.module.hierarchy.len() as u32;
let len = direct_children.len() as u32;
self.module.hierarchy.extend(direct_children);
Ok((start_idx, len))
}
fn parse_directive(&mut self) -> Result<NodeId, String> {
self.advance();
let name = self.parse_ident();
let dir = match name.as_str() {
"version" => {
self.skip_whitespace();
let v = self.parse_ident().parse().unwrap();
Directive::Version(v)
}
"style" => Directive::Style(match self.parse_value()? {
Value::String(s) => 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();
self.skip_whitespace();
self.consume_if(b'=');
let val = self.parse_value()?;
Directive::Global {
name: var_name,
value: val,
}
}
"singleton" => {
self.skip_whitespace();
let sname = 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;
}
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 len = (self.module.prop_keys.len() as u32) - start_idx;
Directive::Singleton {
name: sname,
prop_span: (start_idx, len),
}
}
"component" => {
self.skip_whitespace();
let cname = 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;
}
let pname = self.parse_ident();
self.skip_whitespace();
self.consume_if(b':');
self.skip_whitespace();
let ptype = self.parse_ident();
params.push((pname, ptype));
self.skip_whitespace();
self.consume_if(b',');
}
}
let child_span = self.parse_block()?;
Directive::Component {
name: cname,
params,
child_span,
}
}
"let" => {
self.skip_whitespace();
self.consume_if(b'$');
let var_name = self.parse_ident();
self.skip_whitespace();
self.consume_if(b'=');
let val = self.parse_value()?;
Directive::Let {
name: var_name,
value: val,
}
}
"if" => {
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;
}
}
Directive::If {
condition,
child_span,
else_span,
}
}
"each" => {
self.skip_whitespace();
self.consume_if(b'$');
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,
}
}
"on" => {
self.skip_whitespace();
let event = self.parse_ident();
let mut args = Vec::new();
self.skip_whitespace();
if self.consume_if(b'(') {
loop {
self.skip_whitespace();
if self.consume_if(b')') {
break;
}
let k = self.parse_ident();
self.skip_whitespace();
self.consume_if(b'=');
let v = self.parse_value()?;
args.push((k, v));
self.skip_whitespace();
self.consume_if(b',');
}
}
let child_span = self.parse_block()?;
Directive::On {
event,
args,
child_span,
}
}
_ => return Err(format!("Unknown directive: @{}", name)),
};
Ok(NodeId::Directive(self.module.push_directive(dir)))
}
fn parse_element(&mut self) -> Result<NodeId, String> {
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();
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 p == Some(b'"') || p == Some(b'$') {
let val = self.parse_value()?;
self.module.elem_content[el_id as usize] = Some(val);
}
Ok(NodeId::Element(el_id))
}
pub fn parse_node(&mut self) -> Result<NodeId, String> {
self.skip_whitespace();
let c = self.peek().ok_or("EOF reached")?;
match c {
b'@' => self.parse_directive(),
b'!' => {
self.advance();
let i = self.parse_ident();
if i != "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)),
))
}
b'A'..=b'Z' => self.parse_element(),
b'"' | b'$' => {
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))
}
_ => Err(format!("Unexpected token: {}", c as char)),
}
}
pub fn parse_all(&mut self) -> Result<(), String> {
self.skip_whitespace();
while self.peek().is_some() {
let node = self.parse_node()?;
self.module.hierarchy.push(node);
self.skip_whitespace();
}
Ok(())
}
}

259
src/tests.rs Normal file
View File

@@ -0,0 +1,259 @@
#[cfg(test)]
mod parser_tests {
use crate::ast::Value;
use crate::parser::Parser;
#[test]
fn test_parser_exact_element_state() {
let input = r#"Button(id="btn", active=true, color=#ff0000, pad=10.5) "Click Me""#;
let mut parser = Parser::new(input);
parser.parse_all().expect("Парсинг не должен падать");
let m = &parser.module;
assert_eq!(
m.elem_types.len(),
1,
"Должен быть 1 элемент с inline-контентом"
);
assert_eq!(m.elem_prop_spans.len(), 1);
assert_eq!(m.elem_child_spans.len(), 1);
assert_eq!(m.elem_content.len(), 1);
assert_eq!(m.elem_types[0], "Button");
let (p_start, p_len) = m.elem_prop_spans[0];
assert_eq!(p_start, 0);
assert_eq!(p_len, 4);
let expected_keys = vec!["id", "active", "color", "pad"];
let expected_vals = vec![
Value::String("btn".to_string()),
Value::Bool(true),
Value::Color("#ff0000".to_string()),
Value::Float(10.5),
];
for i in 0..4 {
let idx = (p_start + i) as usize;
assert_eq!(
m.prop_keys[idx], expected_keys[i as usize],
"Ключ {} не совпал",
i
);
assert_eq!(
m.prop_values[idx], expected_vals[i as usize],
"Значение {} не совпало",
i
);
}
assert_eq!(
m.elem_content[0],
Some(Value::String("Click Me".to_string())),
"Контент текста потерян или искажен"
);
}
#[test]
fn test_parser_hierarchy_spans() {
let input = r#"
Root {
ChildA {}
ChildB { SubChild {} }
}
"#;
let mut parser = Parser::new(input);
parser.parse_all().expect("Парсинг должен пройти");
let m = &parser.module;
let root_idx = m.elem_types.iter().position(|t| t == "Root").unwrap();
let a_idx = m.elem_types.iter().position(|t| t == "ChildA").unwrap();
let b_idx = m.elem_types.iter().position(|t| t == "ChildB").unwrap();
let root_span = m.elem_child_spans[root_idx];
assert_eq!(root_span.1, 2, "У Root должно быть ровно 2 ребенка");
let child1 = &m.hierarchy[(root_span.0) as usize];
let child2 = &m.hierarchy[(root_span.0 + 1) as usize];
match (child1, child2) {
(crate::ast::NodeId::Element(id1), crate::ast::NodeId::Element(id2)) => {
assert_eq!(*id1 as usize, a_idx);
assert_eq!(*id2 as usize, b_idx);
}
_ => panic!("Дети Root должны быть элементами"),
}
}
}
#[cfg(test)]
mod compiler_tests {
use crate::compiler::Compiler;
use crate::opcodes::*;
use crate::parser::Parser;
struct BytecodeBuilder {
buf: Vec<u8>,
}
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_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_i64(mut self, val: i64) -> Self {
self.buf.extend_from_slice(&val.to_le_bytes());
self
}
fn build(self) -> Vec<u8> {
self.buf
}
}
#[test]
fn test_compiler_exact_bytes_directive() {
let input = "@version 42";
let mut parser = Parser::new(input);
parser.parse_all().unwrap();
let root_nodes = vec![parser.module.hierarchy[0]];
let compiler = Compiler::new(&parser.module);
let bytecode = compiler.compile(&root_nodes);
let expected = BytecodeBuilder::new()
.push_u8(OP_VERSION)
.push_i64(42)
.build();
assert_eq!(bytecode, expected);
}
#[test]
fn test_compiler_exact_bytes_element() {
let input = r#"Box(id="main")"#;
let mut parser = Parser::new(input);
parser.parse_all().unwrap();
let root_nodes = vec![parser.module.hierarchy[0]];
let compiler = Compiler::new(&parser.module);
let bytecode = compiler.compile(&root_nodes);
let expected = BytecodeBuilder::new()
.push_u8(OP_ELEM_PUSH)
.push_str("Box")
.push_u8(OP_PROP_STR)
.push_str("id")
.push_str("main")
.push_u8(OP_ELEM_POP)
.build();
assert_eq!(bytecode, expected);
}
#[test]
fn test_compiler_rhei_zero_parsing() {
let input = r#"@global $net = !rhei: sys.network().status"#;
let mut parser = Parser::new(input);
parser.parse_all().unwrap();
let root_nodes = vec![parser.module.hierarchy[0]];
let compiler = Compiler::new(&parser.module);
let bytecode = compiler.compile(&root_nodes);
let expected = BytecodeBuilder::new()
.push_u8(OP_GLOBAL)
.push_str("net")
.push_u8(OP_PROP_RHEI)
.push_str("sys.network().status")
.build();
assert_eq!(bytecode, expected);
}
}
const TEST_GLINT_INPUT: &str = r#"
@version 1
@style "desktop.glts"
@global $username = !rhei: os.env("USER")
@global $hostname = !rhei: os.hostname()
@global $locale = "ru_RU"
@global $scaleFactor = 1.0
@singleton Config {
wallpaper = fs:/home/$username/.config/de/wallpaper.jpg
wallpaperFit = "cover"
iconTheme = "papirus-dark"
fontMain = "Inter"
fontMono = "JetBrains Mono"
fontSize = 13
workspaces = 4
animEnabled = true
accentColor = #cba6f7
taskbarPos = "bottom"
}
Screen(id="root", width=1920, height=1080, scale=$scaleFactor) {
Panel(id="taskbar", class="taskbar") {
Button(id="launcher-btn", class="launcher-button") {
Image(src=fs:/usr/share/glint-de/logo.svg) {}
}
}
}
"#;
#[test]
fn test_full_desktop_compilation() {
use crate::compiler::Compiler;
use crate::opcodes::MAGIC_HEADER;
use crate::parser::Parser;
let mut parser = Parser::new(TEST_GLINT_INPUT);
assert!(
parser.parse_all().is_ok(),
"Парсер вернул ошибку на валидном конфиге DE!"
);
let m = &parser.module;
assert!(!m.elem_types.is_empty(), "Не найдено ни одного элемента!");
assert_eq!(m.elem_types.len(), m.elem_prop_spans.len());
assert_eq!(m.elem_prop_spans.len(), m.elem_child_spans.len());
assert_eq!(m.prop_keys.len(), m.prop_values.len());
let mut child_indices = std::collections::HashSet::new();
for (start, len) in &m.elem_child_spans {
for i in *start..(*start + *len) {
child_indices.insert(i as usize);
}
}
let mut root_nodes = Vec::new();
for (i, node) in m.hierarchy.iter().enumerate() {
if !child_indices.contains(&i) {
root_nodes.push(node.clone());
}
}
let compiler = Compiler::new(m);
let bytecode = compiler.compile(&root_nodes);
assert_eq!(
&bytecode[0..4],
&MAGIC_HEADER,
"Отсутствует или поврежден GLBC заголовок"
);
assert!(bytecode.len() > 50, "Байткод подозрительно мал");
}