Skip to main content

hypen_engine/ir/
node.rs

1use serde::{Deserialize, Serialize};
2use slotmap::new_key_type;
3use indexmap::IndexMap;
4use std::sync::Arc;
5use crate::reactive::Binding;
6
7// Stable, unique node identifier for reconciliation
8new_key_type! {
9    pub struct NodeId;
10}
11
12/// IR value - either static or a binding to reactive state
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub enum Value {
15    /// Static value (string, number, bool, etc.)
16    Static(serde_json::Value),
17    /// Binding to state: parsed from ${state.user.name}
18    Binding(Binding),
19    /// Template string with embedded bindings: "Count: ${state.count}"
20    /// Stores the template string and all bindings found within it
21    TemplateString {
22        template: String,
23        bindings: Vec<Binding>,
24    },
25    /// Action reference: @actions.signIn
26    Action(String),
27}
28
29/// First-class IR node - distinguishes between regular elements and control flow constructs
30/// This provides type-safe pattern matching in the reconciler and clear separation of concerns
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub enum IRNode {
33    /// Regular UI element (Text, Column, Button, etc.)
34    Element(Element),
35
36    /// Iteration construct: ForEach(items: ${state.todos}, as: "todo", key: "id") { children }
37    /// Renders template children for each item in an array
38    ForEach {
39        /// The binding to the array in state (e.g., ${state.todos})
40        source: Binding,
41        /// The variable name for each item (default: "item", can be "todo", "user", etc.)
42        item_name: String,
43        /// Optional path to use as key for stable reconciliation (e.g., "id")
44        key_path: Option<String>,
45        /// Template children to repeat for each item
46        template: Vec<IRNode>,
47        /// Additional props for the container element
48        props: Props,
49    },
50
51    /// Conditional construct: When(value: ${state.status}) { Case(match: "loading") {...} Else {...} }
52    /// Renders matching branch based on evaluated value
53    Conditional {
54        /// The value to match against (binding or static)
55        value: Value,
56        /// Branches with patterns to match
57        branches: Vec<ConditionalBranch>,
58        /// Fallback if no branch matches (Else)
59        fallback: Option<Vec<IRNode>>,
60    },
61}
62
63/// A conditional branch with a pattern and children
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct ConditionalBranch {
66    /// The pattern to match against the conditional value
67    pub pattern: Value,
68    /// Children to render if this branch matches
69    pub children: Vec<IRNode>,
70}
71
72impl IRNode {
73    /// Create an Element IRNode
74    pub fn element(element: Element) -> Self {
75        IRNode::Element(element)
76    }
77
78    /// Create a ForEach IRNode
79    pub fn for_each(
80        source: Binding,
81        item_name: impl Into<String>,
82        key_path: Option<String>,
83        template: Vec<IRNode>,
84        props: Props,
85    ) -> Self {
86        IRNode::ForEach {
87            source,
88            item_name: item_name.into(),
89            key_path,
90            template,
91            props,
92        }
93    }
94
95    /// Create a Conditional IRNode (When/If)
96    pub fn conditional(
97        value: Value,
98        branches: Vec<ConditionalBranch>,
99        fallback: Option<Vec<IRNode>>,
100    ) -> Self {
101        IRNode::Conditional {
102            value,
103            branches,
104            fallback,
105        }
106    }
107
108    /// Get the element if this is an Element variant
109    pub fn as_element(&self) -> Option<&Element> {
110        match self {
111            IRNode::Element(e) => Some(e),
112            _ => None,
113        }
114    }
115
116    /// Check if this is a ForEach node
117    pub fn is_for_each(&self) -> bool {
118        matches!(self, IRNode::ForEach { .. })
119    }
120
121    /// Check if this is a Conditional node
122    pub fn is_conditional(&self) -> bool {
123        matches!(self, IRNode::Conditional { .. })
124    }
125}
126
127impl ConditionalBranch {
128    /// Create a new conditional branch
129    pub fn new(pattern: Value, children: Vec<IRNode>) -> Self {
130        Self { pattern, children }
131    }
132}
133
134/// Properties map type (underlying storage)
135pub type PropsMap = IndexMap<String, Value>;
136
137/// Arc-wrapped properties for O(1) clone with copy-on-write semantics.
138///
139/// This enables efficient Element cloning during reconciliation while
140/// preserving the ability to modify props when needed via `make_mut()`.
141#[derive(Debug, Clone)]
142pub struct Props(Arc<PropsMap>);
143
144impl Props {
145    /// Create empty props
146    pub fn new() -> Self {
147        Props(Arc::new(IndexMap::new()))
148    }
149
150    /// Create props from an IndexMap
151    pub fn from_map(map: PropsMap) -> Self {
152        Props(Arc::new(map))
153    }
154
155    /// Get mutable access to props (copy-on-write)
156    /// If this is the only reference, mutates in place.
157    /// Otherwise, clones the inner map first.
158    pub fn make_mut(&mut self) -> &mut PropsMap {
159        Arc::make_mut(&mut self.0)
160    }
161
162    /// Insert a key-value pair (uses COW internally)
163    pub fn insert(&mut self, key: String, value: Value) -> Option<Value> {
164        self.make_mut().insert(key, value)
165    }
166
167    /// Remove a key (uses COW internally)
168    pub fn remove(&mut self, key: &str) -> Option<Value> {
169        self.make_mut().shift_remove(key)
170    }
171
172    /// Get a reference to the inner map
173    pub fn inner(&self) -> &PropsMap {
174        &self.0
175    }
176}
177
178impl Default for Props {
179    fn default() -> Self {
180        Props::new()
181    }
182}
183
184impl std::ops::Deref for Props {
185    type Target = PropsMap;
186
187    fn deref(&self) -> &Self::Target {
188        &self.0
189    }
190}
191
192impl<'a> IntoIterator for &'a Props {
193    type Item = (&'a String, &'a Value);
194    type IntoIter = indexmap::map::Iter<'a, String, Value>;
195
196    fn into_iter(self) -> Self::IntoIter {
197        self.0.iter()
198    }
199}
200
201impl FromIterator<(String, Value)> for Props {
202    fn from_iter<I: IntoIterator<Item = (String, Value)>>(iter: I) -> Self {
203        Props(Arc::new(iter.into_iter().collect()))
204    }
205}
206
207// Custom serde - serialize/deserialize as plain IndexMap
208impl serde::Serialize for Props {
209    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
210    where
211        S: serde::Serializer,
212    {
213        self.0.serialize(serializer)
214    }
215}
216
217impl<'de> serde::Deserialize<'de> for Props {
218    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
219    where
220        D: serde::Deserializer<'de>,
221    {
222        let map = PropsMap::deserialize(deserializer)?;
223        Ok(Props(Arc::new(map)))
224    }
225}
226
227/// Core IR element representing a component instance or primitive
228///
229/// Uses Arc-wrapped children with im::Vector for O(1) clone performance.
230/// This is critical for reconciliation where elements are frequently cloned.
231#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct Element {
233    /// Component/element type (e.g., "Column", "Text", "Button")
234    pub element_type: String,
235
236    /// Properties passed to this element
237    pub props: Props,
238
239    /// Children elements (Arc-wrapped in im::Vector for O(1) structural sharing)
240    #[serde(with = "arc_element_vec")]
241    pub children: im::Vector<Arc<Element>>,
242
243    /// Optional key for reconciliation (from user or auto-generated)
244    pub key: Option<String>,
245}
246
247/// Custom serde for im::Vector<Arc<Element>> - serializes as plain Vec
248mod arc_element_vec {
249    use super::*;
250    use serde::{Deserializer, Serializer};
251
252    pub fn serialize<S>(vec: &im::Vector<Arc<Element>>, serializer: S) -> Result<S::Ok, S::Error>
253    where
254        S: Serializer,
255    {
256        use serde::ser::SerializeSeq;
257        let mut seq = serializer.serialize_seq(Some(vec.len()))?;
258        for elem in vec.iter() {
259            seq.serialize_element(elem.as_ref())?;
260        }
261        seq.end()
262    }
263
264    pub fn deserialize<'de, D>(deserializer: D) -> Result<im::Vector<Arc<Element>>, D::Error>
265    where
266        D: Deserializer<'de>,
267    {
268        let vec: Vec<Element> = Vec::deserialize(deserializer)?;
269        Ok(vec.into_iter().map(Arc::new).collect())
270    }
271}
272
273impl Element {
274    pub fn new(element_type: impl Into<String>) -> Self {
275        Self {
276            element_type: element_type.into(),
277            props: Props::new(),
278            children: im::Vector::new(),
279            key: None,
280        }
281    }
282
283    pub fn with_prop(mut self, key: impl Into<String>, value: Value) -> Self {
284        self.props.insert(key.into(), value);
285        self
286    }
287
288    pub fn with_child(mut self, child: Element) -> Self {
289        self.children.push_back(Arc::new(child));
290        self
291    }
292
293    /// Add an Arc-wrapped child (avoids extra Arc allocation if already wrapped)
294    pub fn with_arc_child(mut self, child: Arc<Element>) -> Self {
295        self.children.push_back(child);
296        self
297    }
298
299    pub fn with_key(mut self, key: impl Into<String>) -> Self {
300        self.key = Some(key.into());
301        self
302    }
303}