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}