init
This commit is contained in:
94
src/ast.rs
Normal file
94
src/ast.rs
Normal 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
262
src/compiler.rs
Normal 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
302
src/main.rs
Normal 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
30
src/opcodes.rs
Normal 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
482
src/parser.rs
Normal 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
259
src/tests.rs
Normal 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, "Байткод подозрительно мал");
|
||||
}
|
||||
Reference in New Issue
Block a user