Skip to main content

hypen_engine/ir/
node.rs

1use crate::reactive::Binding;
2use indexmap::IndexMap;
3use serde::{Deserialize, Serialize};
4use slotmap::new_key_type;
5use std::sync::Arc;
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    /// Resource reference: @resources.heart
28    Resource(String),
29}
30
31/// First-class IR node - distinguishes between regular elements and control flow constructs
32/// This provides type-safe pattern matching in the reconciler and clear separation of concerns
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub enum IRNode {
35    /// Regular UI element (Text, Column, Button, etc.)
36    Element(Element),
37
38    /// Iteration construct: ForEach(items: @{state.todos}, as: "todo", key: "id") { children }
39    /// Renders template children for each item in an array
40    ForEach {
41        /// The binding to the array in state (e.g., @{state.todos})
42        source: Binding,
43        /// The variable name for each item (default: "item", can be "todo", "user", etc.)
44        item_name: String,
45        /// Optional path to use as key for stable reconciliation (e.g., "id")
46        key_path: Option<String>,
47        /// Template children to repeat for each item
48        template: Vec<IRNode>,
49        /// Additional props for the container element
50        props: Props,
51        /// Enclosing module scope (propagated from `module <Name> { ... }` in the
52        /// DSL). When the named module is registered, the source array binding
53        /// resolves and registers under that module's state slot.
54        module_scope: Option<String>,
55    },
56
57    /// Conditional construct: When(value: @{state.status}) { Case(match: "loading") {...} Else {...} }
58    /// Renders matching branch based on evaluated value
59    Conditional {
60        /// The value to match against (binding or static)
61        value: Value,
62        /// Branches with patterns to match
63        branches: Vec<ConditionalBranch>,
64        /// Fallback if no branch matches (Else)
65        fallback: Option<Vec<IRNode>>,
66        /// Enclosing module scope (propagated from `module <Name> { ... }` in the
67        /// DSL). When the named module is registered, the condition value
68        /// resolves and registers under that module's state slot.
69        module_scope: Option<String>,
70    },
71
72    /// Router construct: Router { Route(path: "/foo") { ... } Route(path: "/bar") { ... } }
73    /// Renders the route whose path matches the current location.
74    ///
75    /// Routing is engine-side: the engine reads the location binding (default
76    /// `state.location`), matches it against each `Route(path: ...)`, and
77    /// renders only that route's children. When `location` changes, the
78    /// dependency graph dirties this node and the matching route is swapped
79    /// in via normal patches — no renderer-side routing is required.
80    Router {
81        /// Binding to the current location string. Defaults to `state.location`
82        /// when the DSL doesn't specify `Router(value: @{state.x})`.
83        location: Value,
84        /// Ordered route table. First match wins.
85        routes: Vec<RouterRoute>,
86        /// Optional fallback children rendered when no route matches.
87        fallback: Option<Vec<IRNode>>,
88        /// Enclosing module scope (propagated from `module <Name> { ... }` in
89        /// the DSL). When set, the location binding resolves under that
90        /// module's state slot.
91        module_scope: Option<String>,
92    },
93}
94
95/// A conditional branch with a pattern and children
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct ConditionalBranch {
98    /// The pattern to match against the conditional value
99    pub pattern: Value,
100    /// Children to render if this branch matches
101    pub children: Vec<IRNode>,
102}
103
104/// A router route with a path pattern and children to render when matched.
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct RouterRoute {
107    /// URL path pattern. Currently exact match; `:param` segments are
108    /// reserved for future support.
109    pub path: String,
110    /// Children to render when the current location matches `path`.
111    pub children: Vec<IRNode>,
112}
113
114impl RouterRoute {
115    pub fn new(path: impl Into<String>, children: Vec<IRNode>) -> Self {
116        Self {
117            path: path.into(),
118            children,
119        }
120    }
121}
122
123impl IRNode {
124    /// Create an Element IRNode
125    pub fn element(element: Element) -> Self {
126        IRNode::Element(element)
127    }
128
129    /// Create a ForEach IRNode
130    pub fn for_each(
131        source: Binding,
132        item_name: impl Into<String>,
133        key_path: Option<String>,
134        template: Vec<IRNode>,
135        props: Props,
136    ) -> Self {
137        IRNode::ForEach {
138            source,
139            item_name: item_name.into(),
140            key_path,
141            template,
142            props,
143            module_scope: None,
144        }
145    }
146
147    /// Create a Conditional IRNode (When/If)
148    pub fn conditional(
149        value: Value,
150        branches: Vec<ConditionalBranch>,
151        fallback: Option<Vec<IRNode>>,
152    ) -> Self {
153        IRNode::Conditional {
154            value,
155            branches,
156            fallback,
157            module_scope: None,
158        }
159    }
160
161    /// Create a Router IRNode
162    pub fn router(
163        location: Value,
164        routes: Vec<RouterRoute>,
165        fallback: Option<Vec<IRNode>>,
166    ) -> Self {
167        IRNode::Router {
168            location,
169            routes,
170            fallback,
171            module_scope: None,
172        }
173    }
174
175    /// Get the element if this is an Element variant
176    pub fn as_element(&self) -> Option<&Element> {
177        match self {
178            IRNode::Element(e) => Some(e),
179            _ => None,
180        }
181    }
182
183    /// Check if this is a ForEach node
184    pub fn is_for_each(&self) -> bool {
185        matches!(self, IRNode::ForEach { .. })
186    }
187
188    /// Check if this is a Conditional node
189    pub fn is_conditional(&self) -> bool {
190        matches!(self, IRNode::Conditional { .. })
191    }
192
193    /// Check if this is a Router node
194    pub fn is_router(&self) -> bool {
195        matches!(self, IRNode::Router { .. })
196    }
197}
198
199impl ConditionalBranch {
200    /// Create a new conditional branch
201    pub fn new(pattern: Value, children: Vec<IRNode>) -> Self {
202        Self { pattern, children }
203    }
204}
205
206/// Properties map type (underlying storage)
207pub type PropsMap = IndexMap<String, Value>;
208
209/// Arc-wrapped properties for O(1) clone with copy-on-write semantics.
210///
211/// This enables efficient Element cloning during reconciliation while
212/// preserving the ability to modify props when needed via `make_mut()`.
213#[derive(Debug, Clone)]
214pub struct Props(Arc<PropsMap>);
215
216impl Props {
217    /// Create empty props
218    pub fn new() -> Self {
219        Props(Arc::new(IndexMap::new()))
220    }
221
222    /// Create props from an IndexMap
223    pub fn from_map(map: PropsMap) -> Self {
224        Props(Arc::new(map))
225    }
226
227    /// Get mutable access to props (copy-on-write)
228    /// If this is the only reference, mutates in place.
229    /// Otherwise, clones the inner map first.
230    pub fn make_mut(&mut self) -> &mut PropsMap {
231        Arc::make_mut(&mut self.0)
232    }
233
234    /// Insert a key-value pair (uses COW internally)
235    pub fn insert(&mut self, key: String, value: Value) -> Option<Value> {
236        self.make_mut().insert(key, value)
237    }
238
239    /// Remove a key (uses COW internally)
240    pub fn remove(&mut self, key: &str) -> Option<Value> {
241        self.make_mut().shift_remove(key)
242    }
243
244    /// Get a reference to the inner map
245    pub fn inner(&self) -> &PropsMap {
246        &self.0
247    }
248}
249
250impl Default for Props {
251    fn default() -> Self {
252        Props::new()
253    }
254}
255
256impl std::ops::Deref for Props {
257    type Target = PropsMap;
258
259    fn deref(&self) -> &Self::Target {
260        &self.0
261    }
262}
263
264impl<'a> IntoIterator for &'a Props {
265    type Item = (&'a String, &'a Value);
266    type IntoIter = indexmap::map::Iter<'a, String, Value>;
267
268    fn into_iter(self) -> Self::IntoIter {
269        self.0.iter()
270    }
271}
272
273impl FromIterator<(String, Value)> for Props {
274    fn from_iter<I: IntoIterator<Item = (String, Value)>>(iter: I) -> Self {
275        Props(Arc::new(iter.into_iter().collect()))
276    }
277}
278
279// Custom serde - serialize/deserialize as plain IndexMap
280impl serde::Serialize for Props {
281    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
282    where
283        S: serde::Serializer,
284    {
285        self.0.serialize(serializer)
286    }
287}
288
289impl<'de> serde::Deserialize<'de> for Props {
290    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
291    where
292        D: serde::Deserializer<'de>,
293    {
294        let map = PropsMap::deserialize(deserializer)?;
295        Ok(Props(Arc::new(map)))
296    }
297}
298
299/// Core IR element representing a component instance or primitive
300///
301/// Children are stored as `ir_children: Vec<IRNode>`, the single source of
302/// truth for all child nodes (plain elements AND control-flow constructs).
303#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct Element {
305    /// Component/element type (e.g., "Column", "Text", "Button")
306    pub element_type: String,
307
308    /// Properties passed to this element
309    pub props: Props,
310
311    /// Children that may include control-flow nodes (ForEach, When, If) as well
312    /// as regular Element nodes wrapped in `IRNode::Element`.
313    #[serde(default, skip_serializing_if = "Vec::is_empty")]
314    pub ir_children: Vec<IRNode>,
315
316    /// Optional key for reconciliation (from user or auto-generated)
317    pub key: Option<String>,
318
319    /// When set, `@{state.xxx}` bindings in this element (and its children)
320    /// resolve against the named module's state instead of the primary module.
321    /// Set during component expansion when the source is `module X { ... }`.
322    #[serde(default, skip_serializing_if = "Option::is_none")]
323    pub module_scope: Option<String>,
324}
325
326impl Element {
327    pub fn new(element_type: impl Into<String>) -> Self {
328        Self {
329            element_type: element_type.into(),
330            props: Props::new(),
331            ir_children: Vec::new(),
332            key: None,
333            module_scope: None,
334        }
335    }
336
337    pub fn with_prop(mut self, key: impl Into<String>, value: Value) -> Self {
338        self.props.insert(key.into(), value);
339        self
340    }
341
342    pub fn with_child(mut self, child: Element) -> Self {
343        self.ir_children.push(IRNode::Element(child));
344        self
345    }
346
347    pub fn with_key(mut self, key: impl Into<String>) -> Self {
348        self.key = Some(key.into());
349        self
350    }
351}