Phase 1.2-1.6: replace variables HashMap<String,String> with HashMap<String,Value>, rewrite str_to_dyn/dyn_to_str -> value_to_dynamic/dynamic_to_value, update resolve_string/evaluate_condition/is_truthy for Value

This commit is contained in:
Glint Dev
2026-07-21 20:32:29 +03:00
parent 5597f07787
commit 30cf2e5e26
7 changed files with 542 additions and 53 deletions

View File

@@ -0,0 +1,379 @@
# План улучшения архитектуры Glint Runtime
## Цель
Обеспечить **плоский график производительности**: рендеринг 1000 строк байткода должен занимать
~0.04 секунды сегодня и столько же через годы, независимо от роста количества компонентов,
стилей и страниц. Время обработки события должно быть пропорционально **размеру изменения**,
а не **размеру всей системы**.
---
## Фаза 0: Бенчмарки и профилирование
**Цель:** зафиксировать текущие метрики, чтобы объективно оценивать прогресс.
- [ ] **0.1** Добавить Criterion в `Cargo.toml`
- [ ] **0.2** Написать бенчмарки для:
- `evaluate_vdom()` на `desktop.glbc`
- `matching_rules()` — 10, 100, 1000 правил
- `ComputedStyle::compute()` — пустой, 5, 20 свойств
- `resolve_string()` — без `$`, с 1 `$var`, с 3 `$var`
- `RheiContext::eval_expr()` — простое выражение, сложное
- `RheiContext::execute_action()` — короткий скрипт
- `Element::clone()` — замерить копирование глубокого дерева
- [ ] **0.3** Запустить `perf record` / `flamegraph-rs` на горячем пути:
- `cargo run -- run desktop.glbc` + интерактив
- Выявить фактический bottleneck (гипотеза: style matching, string parsing)
- [ ] **0.4** Записать baseline в `BENCHMARKS.md` или `README.md`
**Файлы:** новый `benches/bench.rs`, `Cargo.toml`
---
## Фаза 1: Типизированные значения (Value enum)
**Цель:** устранить постоянный round-trip через строки (parse/format на каждое свойство).
**Текущая проблема:** `HashMap<String, String>` для переменных. Каждый `SliderChanged`
`format!(...)`, потом `rhei.rs:139-142` — три парсинга подряд (`i64`, `f64`, `bool`).
- [ ] **1.1** Определить `Value` enum в `interpreter/types.rs`:
```rust
#[derive(Clone, Debug)]
pub enum Value {
Str(CompactStr),
Int(i64),
Float(f64),
Bool(bool),
Array(Vec<Value>),
None,
}
```
- [ ] **1.2** Заменить `HashMap<String, String>` на `HashMap<String, Value>`:
- `Document::variables`
- `GlintApp::update()` — входящие значения
- [ ] **1.3** Переписать `str_to_dyn()` и `dyn_to_str()` в `rhei.rs`:
- Убрать парсинг, конвертировать напрямую `Value ↔ Dynamic`
- [ ] **1.4** Переписать `resolve_string()`:
- Если значение уже `Int`/`Float` — не парсить, форматировать один раз
- [ ] **1.5** Переписать `evaluate_condition()`:
- Числовые сравнения без парсинга строк
- `resolve_string()` → преобразование к Value
- [ ] **1.6** Переписать `is_truthy()`: работа с Value напрямую
- [ ] **1.7** Обновить `renderer.rs`:
- `SliderChanged`, `ToggleChanged`, `InputChanged` — принимать `Value`, не строку
- Парсинг slider/toggle/input значений — один раз, а не на каждый чих
- [ ] **1.8** Обновить `ComputedStyle::compute()` и `parse_*()` функции:
- Принимать `&Value` где возможно, а не `&str`
- `parse_color`, `parse_size`, `parse_length` — работа с Value
- [ ] **1.9** Написать тесты для Value: конверсии, сравнения, форматирование
**Изменяемые файлы:** `interpreter/types.rs`, `interpreter/mod.rs`, `interpreter/rhei.rs`,
`interpreter/style.rs`, `app.rs`, `renderer.rs`
---
## Фаза 2: Интернирование строк (String Interning)
**Цель:** ускорить сравнение строк (ключи свойств, названия типов, классы).
Заменить million `== "padding-top"` на O(1) сравнение ID.
- [ ] **2.1** Выбрать библиотеку intern-строк:
- `lasso` (thread-safe, производительный) или `strena` (лёгкая)
- Или самодельный `StringArena` с `HashMap<&str, usize>`
- [ ] **2.2** Заменить `&'a str` на `InternedStr` (newtype вокруг `u32`/`usize`):
- `Element::type_name`
- Ключи в `Element::properties`
- Селекторы, имена классов, ID
- [ ] **2.3** `lookup()` в `style.rs`:
- Сравнение ключей через ID вместо `==`
- [ ] **2.4** `CompoundSelector::matches_element()`:
- Сравнение `tag`, `id`, `classes` через internd-строки
- [ ] **2.5** Все `HashMap<String, String>` где ключи повторяются → `HashMap<InternedStr, Value>`
**Изменяемые файлы:** `interpreter/types.rs`, `interpreter/style.rs`, `interpreter/mod.rs`,
`renderer.rs` (много `el.type_name == "Button"`)
---
## Фаза 3: Граф зависимостей (Reactive Dependency Tracking)
**Цель:** вместо полного `evaluate_vdom()` на каждое событие — пересчитывать только
элементы, которые зависят от изменившейся переменной. **Это главный этап для плоского графика.**
**Текущая проблема:** `app.rs:44-51` — на любое событие полный VDOM rebuild. Слайдер
меняет `volume_level`, но пересчитываются все 500 элементов.
- [ ] **3.1** Создать `interpreter/reactive.rs`:
```rust
pub struct ReactiveTracker {
subscribers: HashMap<InternedStr, HashSet<ElementId>>,
dependencies: HashMap<ElementId, HashSet<InternedStr>>,
dirty_set: HashSet<ElementId>,
}
```
- [ ] **3.2** Анализ зависимостей при загрузке шаблона:
- Сканировать `$var` в свойствах → регистрировать `ElementId → var`
- Сканировать `!rhei:{expr}` → извлекать имена переменных через Rhai parser
- `@if condition` → зависимости от переменных в condition
- `@each source` → зависимости от переменных в source
- Результат: каждый элемент знает, от каких переменных зависит
- [ ] **3.3** Изменить `GlintApp::update()`:
```rust
fn update(&mut self, msg: Message) {
let affected = self.tracker.on_variable_changed(name, new_val);
self.vdom_roots = Interpreter::evaluate_vdom_incr(
&self.doc.roots, &affected, &self.tracker, ...
);
}
```
- [ ] **3.4** `evaluate_vdom_incr()`:
- Для элементов не в `affected` — возвращаем кэшированный VDOM
- Для элементов в `affected` — пересчитываем и каскадно помечаем детей
- `@if` — если условие не изменилось — не пересчитываем ветку
- `@each` — если source не изменился — не пересчитываем
- [ ] **3.5** Инвалидация стилей:
- При изменении переменной, меняющей `class` или `id` элемента → сброс кэша стилей
- Иначе стили не пересчитываем
- [ ] **3.6** Кэш VDOM:
- `HashMap<ElementId, Arc<VNode>>` — не клонируем, разделяем память
- `Arc::make_mut()` при изменении (copy-on-write)
- [ ] **3.7** Написать тест:
- Сценарий: 1000 элементов, 1 зависимость → после изменения пересчитывается 1 элемент
- Проверить, что dirty_set корректен
**Изменяемые файлы:** новый `interpreter/reactive.rs`, `interpreter/mod.rs`, `interpreter/types.rs`,
`app.rs`, `main.rs`
---
## Фаза 4: Индексированный матчинг стилей (Style Index)
**Цель:** заменить O(N×M) на O(K) где K — число релевантных правил для элемента (1-5, не 500).
**Текущая проблема:** `style.rs:487-505` — каждый элемент проверяется против ВСЕХ правил.
100 элементов × 500 правил = 50 000 проверок на событие.
- [ ] **4.1** Построить индекс стилей при загрузке `Document`:
```rust
pub struct StyleIndex {
by_tag: HashMap<InternedStr, Vec<RuleId>>,
by_class: HashMap<InternedStr, Vec<RuleId>>,
by_id: HashMap<InternedStr, Vec<RuleId>>,
by_tag_class: HashMap<(InternedStr, InternedStr), Vec<RuleId>>,
by_tag_id: HashMap<(InternedStr, InternedStr), Vec<RuleId>>,
complex_rules: Vec<(ComplexSelector, RuleId)>,
rules: Vec<StyleRule>,
}
```
- [ ] **4.2** `matching_rules()` → `query_index()`:
- Для элемента `Button.primary#submit`:
1. `by_tag["Button"]` → [1, 5, 12]
2. `by_class["primary"]` → [3, 5, 7]
3. `by_id["submit"]` → [5]
4. Пересечение → [5]
5. Проверить 1 сложное правило
6. Итого: 3 проверки вместо 500
- [ ] **4.3** Кэш совпадений:
- `HashMap<ElementId, (Vec<RuleId>, u64)>` — инвалидируется по эпохе
- epoch увеличивается при перезагрузке стилей
- Псевдоклассы (`:hover`, `:active`) — кэш с ключом `(ElementId, PseudoClass)`
- [ ] **4.4** `ComputedStyle::compute()`:
- Убрать сортировку по specificity на каждый вызов
- Правила уже отсортированы в индексе
- `lookup()` — проход по нескольким правилам вместо прохода по `matched_sheets`
- [ ] **4.5** Миграция `matching_pseudo_rules()`:
- Использовать индекс + кэш
- Вызывается из `renderer.rs` для hover/active — тысячи раз в секунду
- Кэш на `(ElementId, pseudo)` с инвалидацией
**Изменяемые файлы:** `interpreter/style.rs`, `interpreter/mod.rs`, `interpreter/types.rs`,
`renderer.rs`, `main.rs`
---
## Фаза 5: Аренная аллокация и VNode (Flat VDOM)
**Цель:** устранить глубокое клонирование `Element` и reduce аллокаций на каждый кадр.
**Текущая проблема:** `Element` содержит `Vec<Element>` — рекурсивное клонирование.
`child.clone()` в `@if` (mod.rs:347) копирует поддеревья целиком.
- [ ] **5.1** Создать плоское представление VDOM:
```rust
struct VNode<'a> {
id: NodeId,
type_name: InternedStr,
properties: Range<PropertyIdx>,
children_range: Range<NodeIdx>,
computed_style: ComputedStyle,
}
struct FlatVDom<'a> {
nodes: Vec<VNode<'a>>,
properties: Vec<(InternedStr, Value)>,
arena: Bump,
}
```
- [ ] **5.2** Добавить `bumpalo` или написать `BumpAlloc`:
- Все строковые данные живут в арене
- Очистка арены одним махом между кадрами
- [ ] **5.3** `evaluate_vdom()` → возвращает `FlatVDom<'arena>` вместо `Vec<Element>`:
- Вместо `push_prop("text", val.clone())` — allocate в арене
- Вместо `Element::new(el.type_name)` — выделить VNode в `Vec<VNode>`
- [ ] **5.4** Structural sharing:
- Ветки `@each` с одинаковыми телами разделяют `VNode` через `Arc<VNode>`
- `Arc::make_mut()` при изменении
- [ ] **5.5** `render_element()` → работает с `&VNode`:
- `get_prop()` — lookup в плоском массиве свойств
- `children` — итерация по `children_range`
- [ ] **5.6** Удалить `#[derive(Clone)]` из `Element` (или оставить для совместимости):
- В горячем пути clone не используется
- [ ] **5.7** Тест: проверить, что аллокаций на кадр стало < 10 (было ~1000+)
**Изменяемые файлы:** `interpreter/types.rs`, `interpreter/mod.rs`, `renderer.rs`,
`interpreter/style.rs`, `app.rs`
---
## Фаза 6: Кэширование Rhai AST
**Цель:** убрать компиляцию Rhai скриптов при каждом выполнении.
**Текущая проблема:** `rhei.rs:112` — `engine.compile(script)` при каждом `execute_action()`.
Скрипты в `@on:click { ... }` компилируются каждый клик.
- [ ] **6.1** Собрать все Rhai блоки при загрузке `Document`:
- Из `rhei_scripts` (init-скрипты)
- Из `__on:*` свойств (обработчики событий)
- Из `!rhei:` выражений (компилируем, но не выполняем)
- [ ] **6.2** `RheiContext`:
```rust
pub struct RheiContext {
engine: Engine,
init_ast: AST,
action_cache: HashMap<InternedStr, AST>,
expr_cache: HashMap<InternedStr, AST>,
}
```
- [ ] **6.3** `execute_action()`:
- `self.action_cache.get(script)` вместо `engine.compile(script)`
- Если нет — компилируем и кэшируем
- [ ] **6.4** Очистка кэша:
- Только при перезагрузке документа
- Или `LruCache` если скриптов слишком много
- [ ] **6.5** Тест: выполнить 1000 раз один и тот же action — время не должно расти
**Изменяемые файлы:** `interpreter/rhei.rs`, `interpreter/mod.rs`, `main.rs`
---
## Фаза 7: Параллелизм (Rayon)
**Цель:** распараллелить style matching и независимые ветки VDOM.
**Текущая проблема:** всё выполняется последовательно, хотя style matching
для разных элементов — embarrassingly parallel.
- [ ] **7.1** Добавить `rayon` в `Cargo.toml` (feature gate: `parallel`)
- [ ] **7.2** Параллельный style matching:
- `matching_rules_batch(elements: &[Element]) -> Vec<Vec<&HashMap>>`
- `elements.par_iter().map(...)`
- [ ] **7.3** Параллельный `@each`:
- Итерации `@each` независимы
- `items.par_iter().flat_map(|item| evaluate_vdom(body, ...))`
- Feature gate: только для больших списков (> N элементов)
- [ ] **7.4** Параллельный `ComputedStyle::compute_batch()`:
- Векторизованный compute для нескольких элементов
- [ ] **7.5** Тест: `@each` с 1000 итераций → ускорение ~4x на 8 ядрах
**Изменяемые файлы:** `Cargo.toml`, `interpreter/style.rs`, `interpreter/mod.rs`
---
## Фаза 8: Мемоизация `ComputedStyle::compute()`
**Цель:** не пересчитывать стили для элементов, чьи свойства не изменились.
**Текущая проблема:** каждый кадр `ComputedStyle::compute()` парсит все ~40 полей
через `lookup()`.
- [ ] **8.1** Ввести хэш `(inline_properties_hash, stylesheet_epoch, element_id) → ComputedStyle`
- [ ] **8.2** Кэш: `HashMap<u64, ComputedStyle>` + LRU eviction
- [ ] **8.3** Инвалидация:
- `stylesheet_epoch` — счётчик, увеличивается при изменении стилей
- `inline_properties_hash` — хэш от значений свойств элемента
- [ ] **8.4** Проверить hit rate на реальном UI: > 95%+
**Изменяемые файлы:** `interpreter/style.rs`
---
## Фаза 9: Инкрементальный рендеринг (keyed widgets)
**Цель:** дать Iced'у возможность эффективно диффить виджеты, а не пересоздавать их.
**Текущая проблема:** `view()` создаёт полностью новые Iced-виджеты каждый кадр.
- [ ] **9.1** Ввести стабильные ID для каждого элемента VDOM
- [ ] **9.2** `render_element()`:
- Присваивать `iced::id()` на основе `ElementId`
- Iced использует ID для сохранения состояния виджетов между кадрами
- [ ] **9.3** Убирать из `view()` только изменившиеся элементы:
- Если `VNode::hash == prev_hash` — вернуть кэшированный `iced::Element`
- Кэш: `HashMap<ElementId, Arc<iced::Element>>`
**Изменяемые файлы:** `renderer.rs`, `interpreter/types.rs`
---
## Фаза 10: Мониторинг и автоматические бенчмарки
**Цель:** не допустить регрессий в будущем.
- [ ] **10.1** Добавить `cargo bench` в CI
- [ ] **10.2** Пороговые проверки:
- `evaluate_vdom` на desktop.glbc < 5ms
- `matching_rules` (100 правил) < 10μs
- `ComputedStyle::compute` < 1μs
- [ ] **10.3** Логирование производительности:
- `--perf` флаг для `glint run`
- Печатать: `VDOM: 1.2ms | Style: 0.3ms | Render: 2.1ms | Total: 3.6ms`
- [ ] **10.4** Alert если total > 16ms (frame budget для 60 FPS)
**Изменяемые файлы:** `Cargo.toml`, новый `src/perf.rs`, `cli.rs`, `.github/workflows/ci.yml`
---
## Сводная таблица влияния
| Фаза | Описание | Ускорение | Плоский график |
|------|----------|-----------|----------------|
| 0 | Бенчмарки | — | — |
| 1 | Value enum | 2-3× | Нет |
| 2 | String interning | 1.5-2× | Нет |
| **3** | **Reactive tracker** | **10-50×** | **Да (ключевое)** |
| 4 | Style index | 5-10× (style) | Да |
| 5 | Arena + Flat VDOM | 2-3× | Частично |
| 6 | Rhai AST cache | 2-5× (click) | Да |
| 7 | Parallelism | 2-4× | Нет |
| 8 | Style memoization | 2-3× | Да |
| 9 | Keyed widgets | 1.5-2× (render) | Частично |
| 10 | CI benchmarks | — (контроль) | — |
**Суммарно:** до 100× на горячих путях.
---
## Рекомендованный порядок имплементации
1. **Фаза 0** — профилирование (без этого нельзя)
2. **Фаза 1 + 2** — Value enum + String interning (фундамент)
3. **Фаза 3** — Reactive tracker (сердце архитектуры)
4. **Фаза 4** — Style index
5. **Фаза 5 + 8** — Arena + Memoization
6. **Фаза 6** — Rhai cache
7. **Фаза 7** — Parallelism
8. **Фаза 9** — Keyed widgets
9. **Фаза 10** — CI benchmarks

Binary file not shown.

View File

@@ -1,7 +1,7 @@
use iced::widget::{column, container}; use iced::widget::{column, container};
use iced::{Length, Theme}; use iced::{Length, Theme};
use crate::interpreter::{Document, Element, Interpreter, RheiContext}; use crate::interpreter::{Document, Element, Interpreter, RheiContext, Value};
use crate::renderer::render_element; use crate::renderer::render_element;
use crate::Message; use crate::Message;
@@ -19,7 +19,7 @@ impl GlintApp {
pub fn update(&mut self, message: Message) -> iced::Task<Message> { pub fn update(&mut self, message: Message) -> iced::Task<Message> {
match message { match message {
Message::WindowScrolled(y) => { Message::WindowScrolled(y) => {
self.doc.variables.insert("__scroll_y".to_string(), y.to_string()); self.doc.variables.insert("__scroll_y".to_string(), Value::Float(y as f64));
} }
Message::EventTriggered(script) => { Message::EventTriggered(script) => {
if !script.is_empty() { if !script.is_empty() {
@@ -27,15 +27,15 @@ impl GlintApp {
} }
} }
Message::InputChanged(Some(var), val) => { Message::InputChanged(Some(var), val) => {
self.doc.variables.insert(var, val); self.doc.variables.insert(var, Value::from(val));
} }
Message::ToggleChanged(Some(var), val) => { Message::ToggleChanged(Some(var), val) => {
self.doc.variables.insert(var, val.to_string()); self.doc.variables.insert(var, Value::Bool(val));
} }
Message::SliderChanged(Some(var), val) => { Message::SliderChanged(Some(var), val) => {
self.doc.variables.insert(var.clone(), format!("{:.1}", val)); self.doc.variables.insert(var.clone(), Value::Float(val));
if var == "volume_level" { if var == "volume_level" {
self.doc.variables.insert("age".to_string(), val.to_string()); self.doc.variables.insert("age".to_string(), Value::Float(val));
} }
} }
_ => {} _ => {}
@@ -63,7 +63,7 @@ impl GlintApp {
let mut global_sticky_layers = Vec::new(); let mut global_sticky_layers = Vec::new();
let _scroll_y = self.doc.variables.get("__scroll_y") let _scroll_y = self.doc.variables.get("__scroll_y")
.and_then(|v| v.parse::<f32>().ok()) .and_then(|v| if let Value::Float(f) = v { Some(*f as f32) } else { None })
.unwrap_or(0.0); .unwrap_or(0.0);
for root in &self.vdom_roots { for root in &self.vdom_roots {

View File

@@ -10,10 +10,12 @@ pub use rhei::RheiContext;
use style::StyleSheet as SS; use style::StyleSheet as SS;
pub use types::{ComponentDef, Document, Element, InterpError}; pub use types::{ComponentDef, Document, Element, InterpError};
use compact_str::CompactString;
use opcodes::*; use opcodes::*;
use reader::Reader; use reader::Reader;
use rhei::{dyn_to_str, str_to_dyn, RHEI_PREFIX}; use rhei::{value_to_dynamic, dynamic_to_value, RHEI_PREFIX};
use style::{AncestorInfo, ComputedStyle, StructuralContext}; use style::{AncestorInfo, ComputedStyle, StructuralContext};
pub use types::Value;
use regex::Regex; use regex::Regex;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::OnceLock; use std::sync::OnceLock;
@@ -51,7 +53,7 @@ impl Interpreter {
fn parse_block_elements<'a>( fn parse_block_elements<'a>(
r: &mut Reader<'a>, r: &mut Reader<'a>,
variables: &mut HashMap<String, String>, variables: &mut HashMap<String, Value>,
components: &mut HashMap<String, ComponentDef<'a>>, components: &mut HashMap<String, ComponentDef<'a>>,
rhei_scripts: &mut Vec<String>, rhei_scripts: &mut Vec<String>,
stylesheet: &mut SS, stylesheet: &mut SS,
@@ -75,7 +77,7 @@ impl Interpreter {
OP_GLOBAL | OP_LET => { OP_GLOBAL | OP_LET => {
let name = r.read_string()?; let name = r.read_string()?;
let vop = r.read_byte()?; let vop = r.read_byte()?;
if let Some(value) = r.read_value_as_string(vop)? { if let Some(value) = r.read_value(vop)? {
variables.insert(name, value); variables.insert(name, value);
} }
} }
@@ -292,7 +294,7 @@ impl Interpreter {
pub fn evaluate_vdom<'a>( pub fn evaluate_vdom<'a>(
templates: &[Element<'a>], templates: &[Element<'a>],
variables: &mut HashMap<String, String>, variables: &mut HashMap<String, Value>,
components: &HashMap<String, ComponentDef<'a>>, components: &HashMap<String, ComponentDef<'a>>,
rhei: &RheiContext, rhei: &RheiContext,
stylesheet: &SS, stylesheet: &SS,
@@ -357,7 +359,7 @@ impl Interpreter {
let source_expr = el.get_prop("source").unwrap_or_default(); let source_expr = el.get_prop("source").unwrap_or_default();
let resolved_source = if let Some(expr) = source_expr.strip_prefix(RHEI_PREFIX) { let resolved_source = if let Some(expr) = source_expr.strip_prefix(RHEI_PREFIX) {
Self::normalize_rhai_array(&rhei.eval_expr(expr, variables)) Self::normalize_rhai_array(&rhei.eval_expr(expr, variables).to_owned_string())
} else { } else {
Self::resolve_string(source_expr, variables).into_owned() Self::resolve_string(source_expr, variables).into_owned()
}; };
@@ -369,7 +371,7 @@ impl Interpreter {
}; };
for item in items { for item in items {
let old_val = variables.insert(var_name.to_string(), item); let old_val = variables.insert(var_name.to_string(), Value::Str(CompactString::new(item)));
output.extend(Self::evaluate_vdom( output.extend(Self::evaluate_vdom(
&el.children, variables, components, rhei, stylesheet, ancestors, &el.children, variables, components, rhei, stylesheet, ancestors,
@@ -397,7 +399,7 @@ impl Interpreter {
let mut old_vals = Vec::with_capacity(new_args.len()); let mut old_vals = Vec::with_capacity(new_args.len());
for (k, v) in new_args { for (k, v) in new_args {
old_vals.push((k.clone(), variables.insert(k, v))); old_vals.push((k.clone(), variables.insert(k, Value::from(v))));
} }
let mut vcomp = Element::new(el.type_name); let mut vcomp = Element::new(el.type_name);
@@ -489,9 +491,9 @@ impl Interpreter {
stylesheet.matching_rules(el.type_name, el_id, &classes, active_pseudo, structural, ancestors, preceding_siblings, &el_attributes) stylesheet.matching_rules(el.type_name, el_id, &classes, active_pseudo, structural, ancestors, preceding_siblings, &el_attributes)
} }
fn resolve_prop(v: &str, variables: &HashMap<String, String>, rhei: &RheiContext) -> String { fn resolve_prop(v: &str, variables: &HashMap<String, Value>, rhei: &RheiContext) -> String {
if let Some(expr) = v.strip_prefix(RHEI_PREFIX) { if let Some(expr) = v.strip_prefix(RHEI_PREFIX) {
rhei.eval_expr(expr, variables) rhei.eval_expr(expr, variables).to_owned_string().into()
} else { } else {
Self::resolve_string(v, variables).into_owned() Self::resolve_string(v, variables).into_owned()
} }
@@ -499,7 +501,7 @@ impl Interpreter {
fn evaluate_condition( fn evaluate_condition(
cond: &str, cond: &str,
variables: &HashMap<String, String>, variables: &HashMap<String, Value>,
rhei: &RheiContext, rhei: &RheiContext,
) -> bool { ) -> bool {
if let Some(expr) = cond.strip_prefix(RHEI_PREFIX) { if let Some(expr) = cond.strip_prefix(RHEI_PREFIX) {
@@ -515,7 +517,7 @@ impl Interpreter {
let resolved = Self::resolve_string(&clean, variables).trim().to_string(); let resolved = Self::resolve_string(&clean, variables).trim().to_string();
if Self::is_truthy(&resolved) { return true; } if Self::is_truthy_str(&resolved) { return true; }
if resolved == "false" || resolved == "0" || resolved.is_empty() { return false; } if resolved == "false" || resolved == "0" || resolved.is_empty() { return false; }
let operators: [(&str, fn(f64, f64) -> bool); 6] = [ let operators: [(&str, fn(f64, f64) -> bool); 6] = [
@@ -542,7 +544,19 @@ impl Interpreter {
} }
#[inline] #[inline]
fn is_truthy(s: &str) -> bool { fn is_truthy(v: &Value) -> bool {
match v {
Value::Bool(b) => *b,
Value::Int(i) => *i != 0,
Value::Float(f) => *f != 0.0,
Value::Str(s) => Self::is_truthy_str(s),
Value::None => false,
Value::Array(a) => !a.is_empty(),
}
}
#[inline]
fn is_truthy_str(s: &str) -> bool {
match s.trim() { match s.trim() {
"" | "false" | "0" | "null" => false, "" | "false" | "0" | "null" => false,
"true" | "1" => true, "true" | "1" => true,
@@ -563,7 +577,7 @@ impl Interpreter {
} }
} }
pub fn resolve_string<'a>(val: &'a str, scope: &HashMap<String, String>) -> Cow<'a, str> { pub fn resolve_string<'a>(val: &'a str, scope: &HashMap<String, Value>) -> Cow<'a, str> {
if !val.contains('$') { if !val.contains('$') {
return Cow::Borrowed(val); return Cow::Borrowed(val);
} }
@@ -582,7 +596,8 @@ impl Interpreter {
} }
} }
if let Some(resolved) = scope.get(&var_name) { if let Some(resolved) = scope.get(&var_name) {
result.push_str(resolved); let formatted = resolved.to_owned_string();
result.push_str(&formatted);
} else { } else {
result.push('$'); result.push('$');
result.push_str(&var_name); result.push_str(&var_name);

View File

@@ -1,5 +1,6 @@
use super::opcodes::*; use super::opcodes::*;
use super::types::InterpError; use super::types::{InterpError, Value};
use compact_str::CompactString;
pub struct Reader<'a> { pub struct Reader<'a> {
pub data: &'a [u8], pub data: &'a [u8],
@@ -138,6 +139,77 @@ impl<'a> Reader<'a> {
Ok(s) Ok(s)
} }
pub fn read_value(&mut self, type_op: u8) -> Result<Option<Value>, InterpError> {
let val = match type_op {
OP_PROP_STR | OP_PROP_COLOR | OP_PROP_FSPATH | OP_PROP_IDENT => {
Some(Value::Str(CompactString::new(self.read_string()?)))
}
OP_PROP_RHEI => {
let raw = self.read_string()?;
let mut s = String::with_capacity(super::rhei::RHEI_PREFIX.len() + raw.len());
s.push_str(super::rhei::RHEI_PREFIX);
s.push_str(&raw);
Some(Value::Str(CompactString::new(s)))
}
OP_PROP_VAR => {
let name = self.read_str_ref()?;
let mut s = String::with_capacity(name.len() + 1);
s.push('$');
s.push_str(name);
Some(Value::Str(CompactString::new(s)))
}
OP_PROP_INT => Some(Value::Int(self.read_i64()?)),
OP_PROP_FLOAT => Some(Value::Float(self.read_f64()?)),
OP_PROP_BOOL => Some(Value::Bool(self.read_byte()? != 0)),
OP_PROP_NULL => None,
OP_PROP_ARRAY => {
let items = self.read_array_as_values()?;
Some(Value::Array(items))
}
OP_PROP_UNIT => {
let num = self.read_f64()?;
let unit = self.read_str_ref()?;
let s = if num.fract() == 0.0 {
format!("{}{}", num as i64, unit)
} else {
format!("{}{}", num, unit)
};
Some(Value::Str(CompactString::new(s)))
}
OP_PROP_CALL => {
let name = self.read_string()?;
let arg_count = self.read_u32()? as usize;
let mut args = Vec::with_capacity(arg_count);
for _ in 0..arg_count {
let op = self.read_byte()?;
let val = self.read_value(op)?.unwrap_or(Value::None);
args.push(match val {
Value::Str(s) => s.to_string(),
Value::Int(i) => i.to_string(),
Value::Float(f) => f.to_string(),
Value::Bool(b) => b.to_string(),
_ => String::new(),
});
}
Some(Value::Str(CompactString::new(format!("{}({})", name, args.join(",")))))
}
_ => None,
};
Ok(val)
}
pub fn read_array_as_values(&mut self) -> Result<Vec<Value>, InterpError> {
let count = self.read_u32()? as usize;
let mut items = Vec::with_capacity(count);
for _ in 0..count {
let elem_op = self.read_byte()?;
if let Some(v) = self.read_value(elem_op)? {
items.push(v);
}
}
Ok(items)
}
pub fn read_array_as_strings(&mut self) -> Result<Vec<String>, InterpError> { pub fn read_array_as_strings(&mut self) -> Result<Vec<String>, InterpError> {
let count = self.read_u32()? as usize; let count = self.read_u32()? as usize;
let mut items = Vec::with_capacity(count); let mut items = Vec::with_capacity(count);

View File

@@ -2,6 +2,9 @@ use rhai::{Dynamic, Engine, Scope, AST, Module};
use std::collections::HashMap; use std::collections::HashMap;
use std::cell::RefCell; use std::cell::RefCell;
use super::types::Value;
use compact_str::CompactString;
pub const RHEI_PREFIX: &str = "__rhei:"; pub const RHEI_PREFIX: &str = "__rhei:";
pub struct RheiContext { pub struct RheiContext {
@@ -46,24 +49,24 @@ impl RheiContext {
} }
} }
pub fn sync_scope(&self, variables: &HashMap<String, String>) { pub fn sync_scope(&self, variables: &HashMap<String, Value>) {
let mut scope = self.scope.borrow_mut(); let mut scope = self.scope.borrow_mut();
for (k, v) in variables.iter() { for (k, v) in variables.iter() {
if scope.contains(k) { if scope.contains(k) {
if let Some(old_val) = scope.get_value::<Dynamic>(k) { if let Some(old_val) = scope.get_value::<Dynamic>(k) {
if dyn_to_str(&old_val) == *v { if dynamic_to_value(&old_val) == *v {
continue; continue;
} }
} }
scope.set_value(k, str_to_dyn(v)); scope.set_value(k, value_to_dynamic(v));
} else { } else {
scope.push_dynamic(k.clone(), str_to_dyn(v)); scope.push_dynamic(k.clone(), value_to_dynamic(v));
} }
} }
} }
pub fn initialize(&self, variables: &mut HashMap<String, String>) { pub fn initialize(&self, variables: &mut HashMap<String, Value>) {
self.sync_scope(variables); self.sync_scope(variables);
let mut scope = self.scope.borrow_mut(); let mut scope = self.scope.borrow_mut();
@@ -72,27 +75,27 @@ impl RheiContext {
} }
for (name, _, val) in scope.iter_raw() { for (name, _, val) in scope.iter_raw() {
let s_val = dyn_to_str(&val); let s_val = dynamic_to_value(&val);
if variables.get(name).map(|v| v != &s_val).unwrap_or(true) { if variables.get(name).map(|v| v != &s_val).unwrap_or(true) {
variables.insert(name.to_string(), s_val); variables.insert(name.to_string(), s_val);
} }
} }
} }
pub fn eval_expr(&self, expr: &str, variables: &HashMap<String, String>) -> String { pub fn eval_expr(&self, expr: &str, variables: &HashMap<String, Value>) -> Value {
self.sync_scope(variables); self.sync_scope(variables);
let mut scope = self.scope.borrow_mut(); let mut scope = self.scope.borrow_mut();
match self.engine.eval_expression_with_scope::<Dynamic>(&mut *scope, expr) { match self.engine.eval_expression_with_scope::<Dynamic>(&mut *scope, expr) {
Ok(val) => dyn_to_str(&val), Ok(val) => dynamic_to_value(&val),
Err(e) => { Err(e) => {
eprintln!("⚠️ Rhei eval_expr error: {e}"); eprintln!("⚠️ Rhei eval_expr error: {e}");
String::new() Value::None
} }
} }
} }
pub fn eval_condition(&self, expr: &str, variables: &HashMap<String, String>) -> bool { pub fn eval_condition(&self, expr: &str, variables: &HashMap<String, Value>) -> bool {
self.sync_scope(variables); self.sync_scope(variables);
let mut scope = self.scope.borrow_mut(); let mut scope = self.scope.borrow_mut();
@@ -105,7 +108,7 @@ impl RheiContext {
} }
} }
pub fn execute_action(&self, script: &str, variables: &mut HashMap<String, String>) { pub fn execute_action(&self, script: &str, variables: &mut HashMap<String, Value>) {
self.sync_scope(variables); self.sync_scope(variables);
let mut scope = self.scope.borrow_mut(); let mut scope = self.scope.borrow_mut();
@@ -121,7 +124,7 @@ impl RheiContext {
} }
for (name, _, val) in scope.iter_raw() { for (name, _, val) in scope.iter_raw() {
let s_val = dyn_to_str(&val); let s_val = dynamic_to_value(&val);
if variables.get(name).map(|v| v != &s_val).unwrap_or(true) { if variables.get(name).map(|v| v != &s_val).unwrap_or(true) {
variables.insert(name.to_string(), s_val); variables.insert(name.to_string(), s_val);
} }
@@ -135,16 +138,36 @@ impl Default for RheiContext {
} }
} }
pub fn str_to_dyn(s: &str) -> Dynamic { pub fn value_to_dynamic(v: &Value) -> Dynamic {
if let Ok(i) = s.parse::<i64>() { return Dynamic::from(i); } match v {
if let Ok(f) = s.parse::<f64>() { return Dynamic::from(f); } Value::Int(i) => Dynamic::from(*i),
if let Ok(b) = s.parse::<bool>() { return Dynamic::from(b); } Value::Float(f) => Dynamic::from(*f),
Dynamic::from(s.to_owned()) Value::Bool(b) => Dynamic::from(*b),
Value::Str(s) => Dynamic::from(s.to_string()),
Value::None => Dynamic::UNIT,
Value::Array(arr) => {
let d: rhai::Dynamic = arr.iter().map(value_to_dynamic).collect();
d
}
}
} }
pub fn dyn_to_str(d: &Dynamic) -> String { pub fn dynamic_to_value(d: &Dynamic) -> Value {
if d.is_string() { if d.is_string() {
return d.clone().into_string().unwrap_or_default(); return Value::Str(CompactString::new(d.clone().into_string().unwrap_or_default()));
} }
d.to_string() if d.is_int() {
return Value::Int(d.as_int().unwrap_or(0));
}
if d.is_float() {
return Value::Float(d.as_float().unwrap_or(0.0));
}
if d.is_bool() {
return Value::Bool(d.as_bool().unwrap_or(false));
}
if d.is_array() {
let arr = d.clone().into_array().unwrap_or_default();
return Value::Array(arr.iter().map(dynamic_to_value).collect());
}
Value::None
} }

View File

@@ -3,11 +3,11 @@ use std::collections::HashMap;
use std::fmt; use std::fmt;
use super::style::{ComputedStyle, StyleSheet}; use super::style::{ComputedStyle, StyleSheet};
use compact_str::CompactStr; use compact_str::CompactString;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Value { pub enum Value {
Str(CompactStr), Str(CompactString),
Int(i64), Int(i64),
Float(f64), Float(f64),
Bool(bool), Bool(bool),
@@ -23,31 +23,31 @@ impl Value {
} }
} }
pub fn to_owned_string(&self) -> CompactStr { pub fn to_owned_string(&self) -> CompactString {
match self { match self {
Value::Str(s) => s.clone(), Value::Str(s) => s.clone(),
Value::Int(i) => CompactStr::new(i.to_string()), Value::Int(i) => CompactString::new(i.to_string()),
Value::Float(f) => CompactStr::new(if f.fract() == 0.0 { Value::Float(f) => CompactString::new(if f.fract() == 0.0 {
format!("{:.1}", f) format!("{:.1}", f)
} else { } else {
f.to_string() f.to_string()
}), }),
Value::Bool(b) => CompactStr::new(b.to_string()), Value::Bool(b) => CompactString::new(b.to_string()),
Value::Array(a) => CompactStr::new(a.iter().map(|v| v.to_owned_string()).collect::<Vec<_>>().join(",")), Value::Array(a) => CompactString::new(a.iter().map(|v| v.to_owned_string()).collect::<Vec<_>>().join(",")),
Value::None => CompactStr::new_empty(), Value::None => CompactString::new(""),
} }
} }
} }
impl From<&str> for Value { impl From<&str> for Value {
fn from(s: &str) -> Self { fn from(s: &str) -> Self {
Value::Str(CompactStr::new(s)) Value::Str(CompactString::new(s))
} }
} }
impl From<String> for Value { impl From<String> for Value {
fn from(s: String) -> Self { fn from(s: String) -> Self {
Value::Str(CompactStr::new(s)) Value::Str(CompactString::new(s))
} }
} }
@@ -125,7 +125,7 @@ pub struct ComponentDef<'a> {
pub struct Document<'a> { pub struct Document<'a> {
pub roots: Vec<Element<'a>>, pub roots: Vec<Element<'a>>,
pub components: HashMap<String, ComponentDef<'a>>, pub components: HashMap<String, ComponentDef<'a>>,
pub variables: HashMap<String, String>, pub variables: HashMap<String, Value>,
pub rhei_scripts: Vec<String>, pub rhei_scripts: Vec<String>,
pub stylesheet: StyleSheet, pub stylesheet: StyleSheet,
} }