pub mod opcodes; pub mod reactive; pub mod reader; pub mod rhei; pub mod style; pub mod types; use std::borrow::Cow; pub use rhei::RheiContext; use style::StyleSheet as SS; pub use types::{ComponentDef, Document, Element, Interner, InterpError}; pub use reactive::{ElementId, ReactiveTracker}; use compact_str::CompactString; use opcodes::*; use reader::Reader; use rhei::RHEI_PREFIX; use style::{AncestorInfo, ComputedStyle, StructuralContext}; pub use types::Value; use std::collections::{HashMap, HashSet}; pub struct Interpreter; impl Interpreter { pub fn run<'a>(bytecode: &'a [u8]) -> Result, InterpError> { if bytecode.len() < 4 { return Err(InterpError::UnexpectedEof); } if &bytecode[..4] != MAGIC { return Err(InterpError::BadMagic); } let mut r = Reader::new(bytecode); r.pos = 4; let mut variables = HashMap::new(); let mut components = HashMap::new(); let mut rhei_scripts: Vec = Vec::new(); let mut stylesheet = SS::new(); let mut tracker = ReactiveTracker::new(); let roots = Self::parse_block_elements( &mut r, &mut variables, &mut components, &mut rhei_scripts, &mut stylesheet, &mut tracker, true, )?; //let rhei_ctx = RheiContext::new(&rhei_scripts); //rhei_ctx.initialize(&mut variables); Ok(Document { roots, components, variables, rhei_scripts, stylesheet, interner: Interner::new(), tracker }) } fn parse_block_elements<'a>( r: &mut Reader<'a>, variables: &mut HashMap, components: &mut HashMap>, rhei_scripts: &mut Vec, stylesheet: &mut SS, tracker: &mut ReactiveTracker, is_root: bool, ) -> Result>, InterpError> { let mut roots: Vec = Vec::new(); let mut stack: Vec = Vec::new(); while r.remaining() > 0 { let op = r.read_byte()?; if !is_root && op == OP_END_BLOCK { break; } match op { OP_ELEM_PUSH => { let elem_id = tracker.alloc_id(); let mut el = Element::new(r.read_str_ref()?); el.element_id = elem_id; stack.push(el); } OP_ELEM_POP => { let finished = stack.pop().ok_or(InterpError::UnexpectedPop)?; Self::attach(&mut stack, &mut roots, finished); } OP_GLOBAL | OP_LET => { let name = r.read_string()?; let vop = r.read_byte()?; if let Some(value) = r.read_value(vop)? { variables.insert(name, value); } } OP_SINGLETON => { let _name = r.read_string()?; let count = r.read_u32()?; for _ in 0..count { r.read_string()?; let vop = r.read_byte()?; r.skip_value(vop)?; } } OP_CONTENT => { let vop = r.read_byte()?; if vop == OP_PROP_RHEI { let expr_ref = r.read_str_ref()?; let mut val = String::with_capacity(RHEI_PREFIX.len() + expr_ref.len()); val.push_str(RHEI_PREFIX); val.push_str(expr_ref); if let Some(el) = stack.last_mut() { tracker.scan_value(el.element_id, &val); el.push_prop("text".to_string(), val); } } else if let Some(value) = r.read_value_as_string(vop)? { if let Some(el) = stack.last_mut() { tracker.scan_value(el.element_id, &value); el.push_prop("text".to_string(), value); } } } OP_PROP_STR => { let key = r.read_str_ref()?; let val = r.read_str_ref()?; if let Some(el) = stack.last_mut() { tracker.scan_value(el.element_id, val); el.push_prop(key, val); } } OP_PROP_VAR => { let key = r.read_str_ref()?; let val_ref = r.read_str_ref()?; let mut val = String::with_capacity(val_ref.len() + 1); val.push('$'); val.push_str(val_ref); if let Some(el) = stack.last_mut() { tracker.scan_value(el.element_id, &val); el.push_prop(key, val); } } OP_PROP_INT => { let key = r.read_str_ref()?; let val = r.read_i64()?.to_string(); if let Some(el) = stack.last_mut() { el.push_prop(key, val); } } OP_PROP_FLOAT => { let key = r.read_str_ref()?; let val = r.read_f64()?.to_string(); if let Some(el) = stack.last_mut() { el.push_prop(key, val); } } OP_PROP_BOOL => { let key = r.read_str_ref()?; let val = (r.read_byte()? != 0).to_string(); if let Some(el) = stack.last_mut() { el.push_prop(key, val); } } OP_PROP_RHEI => { let key = r.read_str_ref()?; let expr_ref = r.read_str_ref()?; let mut val = String::with_capacity(RHEI_PREFIX.len() + expr_ref.len()); val.push_str(RHEI_PREFIX); val.push_str(expr_ref); if let Some(el) = stack.last_mut() { tracker.scan_value(el.element_id, &val); el.push_prop(key, val); } } OP_PROP_UNIT | OP_PROP_CALL | OP_PROP_IDENT | OP_PROP_FSPATH | OP_PROP_COLOR => { let key = r.read_str_ref()?; if let Some(val) = r.read_value_as_string(op)? { if let Some(el) = stack.last_mut() { el.push_prop(key, val); } } } OP_RHEI_BLK => { let script = r.read_string()?; if is_root && stack.is_empty() { rhei_scripts.push(script); } else { let mut text_el = Element::new("#text"); text_el.element_id = tracker.alloc_id(); let full_val = format!("{RHEI_PREFIX}{script}"); tracker.scan_value(text_el.element_id, &full_val); text_el.push_prop("text".to_string(), full_val); Self::attach(&mut stack, &mut roots, text_el); } } OP_COMPONENT => { let name = r.read_string()?; let param_count = r.read_u32()?; let params = (0..param_count) .map(|_| Ok((r.read_string()?, r.read_string()?))) .collect::, InterpError>>()?; let children = Self::parse_block_elements( r, variables, components, rhei_scripts, stylesheet, tracker, false, )?; components.insert(name.clone(), ComponentDef { name, params, children }); } OP_IF => { let vop = r.read_byte()?; let raw = r.read_value_as_string(vop)?.unwrap_or_default(); let cond_val = if vop == OP_PROP_RHEI { format!("{RHEI_PREFIX}{raw}") } else { raw }; let true_children = Self::parse_block_elements( r, variables, components, rhei_scripts, stylesheet, tracker, false, )?; let has_else = r.read_byte()? == 1; let false_children = if has_else { Self::parse_block_elements( r, variables, components, rhei_scripts, stylesheet, tracker, false, )? } else { Vec::new() }; let mut if_el = Element::new("@if"); if_el.element_id = tracker.alloc_id(); tracker.scan_value(if_el.element_id, &cond_val); if_el.push_prop("condition", cond_val); if_el.children = true_children; if !false_children.is_empty() { let mut else_el = Element::new("@else"); else_el.element_id = tracker.alloc_id(); else_el.children = false_children; if_el.children.push(else_el); } Self::attach(&mut stack, &mut roots, if_el); } OP_EACH => { let var_name = r.read_string()?; let vop = r.read_byte()?; let source_val = match vop { OP_PROP_VAR => format!("${}", r.read_string()?), OP_PROP_ARRAY => r.read_array_as_strings()?.join(","), OP_PROP_RHEI => format!("{RHEI_PREFIX}{}", r.read_string()?), _ => { r.skip_value(vop)?; String::new() } }; let block_children = Self::parse_block_elements( r, variables, components, rhei_scripts, stylesheet, tracker, false, )?; let mut each_el = Element::new("@each"); each_el.element_id = tracker.alloc_id(); tracker.scan_value(each_el.element_id, &source_val); each_el.push_prop("var_name".to_string(), var_name); each_el.push_prop("source".to_string(), source_val); each_el.children = block_children; Self::attach(&mut stack, &mut roots, each_el); } OP_ON => { let event_name = r.read_string()?; let arg_count = r.read_u32()?; let mut _args: Vec<(String, String)> = Vec::with_capacity(arg_count as usize); for _ in 0..arg_count { let k = r.read_string()?; let vop = r.read_byte()?; if let Some(v) = r.read_value_as_string(vop)? { _args.push((k, v)); } } let mut handler_script = String::new(); loop { let inner_op = r.read_byte()?; if inner_op == OP_END_BLOCK { break; } if inner_op == OP_RHEI_BLK { handler_script = r.read_string()?; } else { r.skip_opcode(inner_op)?; } } if let Some(el) = stack.last_mut() { el.push_prop( format!("__on:{event_name}"), handler_script, ); } } OP_STYLE_RULE => { let selector = r.read_string()?; let prop_count = r.read_u32()?; let mut props = HashMap::with_capacity(prop_count as usize); for _ in 0..prop_count { let key = r.read_string()?; let type_op = r.read_byte()?; if let Some(val) = r.read_value_as_string(type_op)? { props.insert(key, val); } } stylesheet.add_rule(selector, props); } OP_STYLE_ANIM => { r.skip_opcode(OP_STYLE_ANIM)?; } other => r.skip_opcode(other)?, } } Ok(roots) } pub fn evaluate_vdom<'a>( templates: &[Element<'a>], variables: &mut HashMap, components: &HashMap>, rhei: &RheiContext, stylesheet: &SS, ancestors: &[AncestorInfo], ) -> Vec> { Self::evaluate_vdom_incr(templates, variables, components, rhei, stylesheet, ancestors, &HashSet::new()) } pub fn evaluate_vdom_incr<'a>( templates: &[Element<'a>], variables: &mut HashMap, components: &HashMap>, rhei: &RheiContext, stylesheet: &SS, ancestors: &[AncestorInfo], dirty_set: &HashSet, ) -> Vec> { let mut output = Vec::with_capacity(templates.len()); // Precompute sibling info for structural pseudo-classes and sibling combinators let sibling_infos: Vec = templates.iter().map(|el| { AncestorInfo::new_with_id( el.type_name, el.id().map(String::from), el.get_prop("class") .map(|s| s.split_whitespace().map(String::from).collect()) .unwrap_or_default(), ) }).collect(); // Precompute type totals for structural pseudo-class context let mut type_counts: HashMap<&str, usize> = HashMap::new(); for el in templates { *type_counts.entry(el.type_name).or_insert(0) += 1; } let mut type_seen: HashMap<&str, usize> = HashMap::new(); for (i, el) in templates.iter().enumerate() { let type_idx = type_seen.entry(el.type_name).or_insert(0); let type_total = *type_counts.get(el.type_name).unwrap_or(&0); let structural = StructuralContext { sibling_index: i, sibling_total: templates.len(), type_index: *type_idx, type_total, has_children: !el.children.is_empty() || el.properties.iter().any(|(k, v)| k == "text" && !v.is_empty()), is_root: ancestors.is_empty(), }; *type_idx += 1; match el.type_name { "@if" => { let cond = el.get_prop("condition").unwrap_or_default(); let is_true = Self::evaluate_condition(cond, variables, rhei); let mut active_branch = Vec::new(); for child in &el.children { if child.type_name == "@else" { if !is_true { active_branch.extend(child.children.clone()); } } else if is_true { active_branch.push(child.clone()); } } output.extend(Self::evaluate_vdom_incr( &active_branch, variables, components, rhei, stylesheet, ancestors, dirty_set, )); } "@each" => { let var_name = el.get_prop("var_name").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) { Self::normalize_rhai_array(&rhei.eval_expr(expr, variables).to_owned_string()) } else { Self::resolve_string(source_expr, variables).into_owned() }; let items: Vec = if resolved_source.is_empty() { vec![] } else { resolved_source.split(',').map(str::trim).map(str::to_string).collect() }; for item in items { let old_val = variables.insert(var_name.to_string(), Value::Str(CompactString::new(item))); output.extend(Self::evaluate_vdom_incr( &el.children, variables, components, rhei, stylesheet, ancestors, dirty_set, )); if let Some(old) = old_val { variables.insert(var_name.to_string(), old); } else { variables.remove(var_name); } } } _ => { if let Some(comp) = components.get(el.type_name) { let mut new_args = Vec::with_capacity(comp.params.len()); for (param, _) in &comp.params { if let Some(arg) = el.get_prop(param) { new_args.push(( param.clone(), Self::resolve_prop(arg, variables, rhei), )); } } let mut old_vals = Vec::with_capacity(new_args.len()); for (k, v) in new_args { old_vals.push((k.clone(), variables.insert(k, Value::from(v)))); } let mut vcomp = Element::new(el.type_name); for (k, v) in &el.properties { if k.starts_with("__on:") { vcomp.set_prop(k.clone(), Self::resolve_string(v, variables).into_owned()); } else { vcomp.set_prop(k.clone(), Self::resolve_prop(v, variables, rhei)); } } let matched_sheets = Self::collect_matching_styles(&vcomp, stylesheet, &[], &structural, ancestors, &sibling_infos[0..i]); vcomp.computed_style = ComputedStyle::compute(&vcomp.properties, &matched_sheets); let child_ancestors = Self::build_ancestor_chain(ancestors, &vcomp); vcomp.children = Self::evaluate_vdom_incr( &comp.children, variables, components, rhei, stylesheet, &child_ancestors, dirty_set, ); output.push(vcomp); for (k, old) in old_vals.into_iter().rev() { if let Some(o) = old { variables.insert(k, o); } else { variables.remove(&k); } } } else { let mut vnode = Element::new(el.type_name); for (k, v) in &el.properties { if k.starts_with("__on:") { vnode.set_prop(k.clone(), Self::resolve_string(v, variables).into_owned()); continue; } if v.starts_with('$') && !v[1..].contains(|c: char| !c.is_ascii_alphanumeric() && c != '_') { vnode.set_prop(format!("__bind:{k}"), v[1..].to_string()); } vnode.set_prop(k.clone(), Self::resolve_prop(v, variables, rhei)); } let matched_sheets = Self::collect_matching_styles(&vnode, stylesheet, &[], &structural, ancestors, &sibling_infos[0..i]); vnode.computed_style = ComputedStyle::compute(&vnode.properties, &matched_sheets); let child_ancestors = Self::build_ancestor_chain(ancestors, &vnode); vnode.children = Self::evaluate_vdom_incr( &el.children, variables, components, rhei, stylesheet, &child_ancestors, dirty_set, ); output.push(vnode); } } } } output } fn build_ancestor_chain<'a>(ancestors: &[AncestorInfo], el: &Element) -> Vec { let mut chain = ancestors.to_vec(); let id = el.id().map(String::from); let classes: Vec = el .get_prop("class") .map(|s| s.split_whitespace().map(String::from).collect()) .unwrap_or_default(); chain.push(AncestorInfo::new_with_id(el.type_name, id, classes)); chain } fn collect_matching_styles<'a>( el: &Element, stylesheet: &'a SS, active_pseudo: &[&str], structural: &StructuralContext, ancestors: &[AncestorInfo], preceding_siblings: &[AncestorInfo], ) -> Vec<&'a HashMap> { let el_id = el.id(); let classes: Vec<&str> = el .get_prop("class") .map(|s| s.split_whitespace().collect()) .unwrap_or_default(); let el_attributes: HashMap = el.properties.iter() .map(|(k, v)| (k.to_string(), v.to_string())) .collect(); stylesheet.matching_rules(el.type_name, el_id, &classes, active_pseudo, structural, ancestors, preceding_siblings, &el_attributes) } fn resolve_prop(v: &str, variables: &HashMap, rhei: &RheiContext) -> String { if let Some(expr) = v.strip_prefix(RHEI_PREFIX) { rhei.eval_expr(expr, variables).to_owned_string().into() } else { Self::resolve_string(v, variables).into_owned() } } fn evaluate_condition( cond: &str, variables: &HashMap, rhei: &RheiContext, ) -> bool { if let Some(expr) = cond.strip_prefix(RHEI_PREFIX) { return rhei.eval_condition(expr, variables); } let mut clean = cond.to_string(); if let Some(start) = clean.find('{') { if let Some(end) = clean.rfind('}') { clean = clean[start + 1..end].trim().to_string(); } } let resolved = Self::resolve_string(&clean, variables).trim().to_string(); if Self::is_truthy_str(&resolved) { return true; } if resolved == "false" || resolved == "0" || resolved.is_empty() { return false; } let operators: [(&str, fn(f64, f64) -> bool); 6] = [ (">=", |a, b| a >= b), ("<=", |a, b| a <= b), (">", |a, b| a > b), ("<", |a, b| a < b), ("==", |a, b| a == b), ("!=", |a, b| a != b), ]; for (op, compare) in operators { if let Some((left, right)) = resolved.split_once(op) { let l = left.trim(); let r = right.trim(); if let (Ok(ln), Ok(rn)) = (l.parse::(), r.parse::()) { return compare(ln, rn); } if op == "==" { return l == r; } if op == "!=" { return l != r; } } } false } #[inline] 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() { "" | "false" | "0" | "null" => false, "true" | "1" => true, other => other.parse::().map(|n| n != 0.0).unwrap_or(true), } } fn normalize_rhai_array(s: &str) -> String { let trimmed = s.trim(); if trimmed.starts_with('[') && trimmed.ends_with(']') { trimmed[1..trimmed.len() - 1] .split(',') .map(|item| item.trim().to_string()) .collect::>() .join(",") } else { trimmed.to_string() } } pub fn resolve_string<'a>(val: &'a str, scope: &HashMap) -> Cow<'a, str> { if !val.contains('$') { return Cow::Borrowed(val); } let mut result = String::with_capacity(val.len() + 16); let mut chars = val.char_indices().peekable(); while let Some((_, c)) = chars.next() { if c == '$' { let mut var_name = String::new(); while let Some(&(_, next_c)) = chars.peek() { if next_c.is_ascii_alphanumeric() || next_c == '_' { var_name.push(chars.next().unwrap().1); } else { break; } } if let Some(resolved) = scope.get(&var_name) { let formatted = resolved.to_owned_string(); result.push_str(&formatted); } else { result.push('$'); result.push_str(&var_name); } } else { result.push(c); } } Cow::Owned(result) } fn attach<'a>(stack: &mut Vec>, roots: &mut Vec>, el: Element<'a>) { match stack.last_mut() { Some(parent) => parent.children.push(el), None => roots.push(el), } } }