Skip to main content

hypen_engine/reconcile/
tree.rs

1use super::resolve::{resolve_props, resolve_props_full};
2use crate::ir::{Element, IRNode, NodeId, Props};
3use indexmap::IndexMap;
4use slotmap::SlotMap;
5use std::sync::Arc;
6
7/// Data sources type alias for readability
8type DataSources = indexmap::IndexMap<String, serde_json::Value>;
9
10/// Resolved prop map, shared behind `Arc` for O(1) cloning.
11///
12/// Every `InstanceNode` holds one of these and every `Patch::Create`
13/// takes one. Emitting a Create for an existing node is therefore an
14/// `Arc::clone` rather than a deep copy of the resolved `IndexMap`.
15/// Resolution itself (`resolve_props_full`) still allocates fresh on
16/// every call — the Arc is only for downstream sharing, not for any
17/// form of caching across state updates.
18pub type ResolvedProps = Arc<IndexMap<String, serde_json::Value>>;
19
20/// Default cap on the number of cached route subtrees per Router node.
21/// Oldest entry is evicted (and its subtree torn down) when exceeded.
22/// Chosen to comfortably cover typical bottom-nav and small-stack apps
23/// without unbounded memory growth.
24pub const DEFAULT_ROUTER_CACHE_SIZE: usize = 10;
25
26/// The kind of control flow node for re-reconciliation
27#[derive(Debug, Clone)]
28pub enum ControlFlowKind {
29    /// ForEach iteration container
30    ForEach {
31        item_name: String,
32        key_path: Option<String>,
33    },
34    /// Conditional (When/If) container
35    Conditional,
36    /// Router container — selects which Route's children to render based on
37    /// the current location.
38    ///
39    /// Retains detached route subtrees between navigations so that
40    /// navigating back to a previously-visited route emits `Attach`
41    /// patches instead of rebuilding the tree. The cache is an
42    /// insertion-ordered LRU; inserting past `max_cache_size` evicts
43    /// the oldest entry and tears down its subtree.
44    Router {
45        /// Detached top-level child NodeIds keyed by route pattern.
46        /// Each entry corresponds to a route that was previously
47        /// rendered and has since been unlinked from the Router's
48        /// children; descendants of these NodeIds stay in the
49        /// InstanceTree + DependencyGraph (so state updates still
50        /// flow through them — keep-alive semantics).
51        cache: IndexMap<String, Vec<NodeId>>,
52        /// Route pattern of the currently-rendered route, if any.
53        /// `None` when no route matches (fallback rendered, or on
54        /// brand-new Router before first location resolution).
55        current_route_key: Option<String>,
56        /// Maximum number of routes to cache before LRU eviction.
57        max_cache_size: usize,
58    },
59}
60
61/// Instance node - a concrete instance of an element in the tree
62///
63/// Uses im::Vector for children to enable O(1) structural sharing during clones.
64/// This is critical for reconciliation performance where nodes are frequently cloned.
65#[derive(Debug, Clone)]
66pub struct InstanceNode {
67    /// Unique node ID
68    pub id: NodeId,
69
70    /// Element type (e.g., "Column", "Text", "__ForEach", "__Conditional")
71    pub element_type: String,
72
73    /// Resolved props (bindings evaluated to actual values). Wrapped in
74    /// `Arc` so emitting a `Create` patch for this node is O(1).
75    pub props: ResolvedProps,
76
77    /// Raw props (including bindings) for change detection - Arc-wrapped for O(1) clone
78    pub raw_props: Props,
79
80    /// Original element template (for List re-rendering) - legacy
81    /// Only populated for List elements that need to re-render children
82    /// Arc-wrapped for O(1) clone during reconciliation
83    pub element_template: Option<Arc<Element>>,
84
85    /// Original IRNode template (for ForEach/Conditional re-rendering)
86    /// Used for control flow nodes that need to re-render on state change
87    /// Arc-wrapped for O(1) clone during reconciliation
88    pub ir_node_template: Option<Arc<IRNode>>,
89
90    /// Control flow metadata for ForEach/Conditional nodes
91    pub control_flow: Option<ControlFlowKind>,
92
93    // Event handling removed - now done at renderer level
94    /// Optional key for reconciliation
95    pub key: Option<String>,
96
97    /// Parent node ID
98    pub parent: Option<NodeId>,
99
100    /// Child node IDs (ordered) - uses im::Vector for O(1) clone
101    pub children: im::Vector<NodeId>,
102
103    /// Module scope this node belongs to (if any).
104    /// Used during dirty re-rendering to resolve `@{state.xxx}` against
105    /// the correct named module's state.
106    pub module_scope: Option<String>,
107}
108
109impl InstanceNode {
110    pub fn new(id: NodeId, element: &Element, state: &serde_json::Value) -> Self {
111        Self::new_full(id, element, state, None)
112    }
113
114    pub fn new_full(
115        id: NodeId,
116        element: &Element,
117        state: &serde_json::Value,
118        data_sources: Option<&DataSources>,
119    ) -> Self {
120        let props = resolve_props_full(&element.props, state, None, data_sources);
121
122        Self {
123            id,
124            element_type: element.element_type.clone(),
125            props,
126            raw_props: element.props.clone(),
127            element_template: None,
128            ir_node_template: None,
129            control_flow: None,
130            key: element.key.clone(),
131            parent: None,
132            children: im::Vector::new(),
133            module_scope: element.module_scope.clone(),
134        }
135    }
136
137    /// Create a control flow container node (ForEach or Conditional)
138    pub fn new_control_flow(
139        id: NodeId,
140        element_type: &str,
141        props: ResolvedProps,
142        raw_props: Props,
143        control_flow: ControlFlowKind,
144        ir_node_template: IRNode,
145    ) -> Self {
146        Self {
147            id,
148            element_type: element_type.to_string(),
149            props,
150            raw_props,
151            element_template: None,
152            ir_node_template: Some(Arc::new(ir_node_template)),
153            control_flow: Some(control_flow),
154            key: None,
155            parent: None,
156            children: im::Vector::new(),
157            module_scope: None,
158        }
159    }
160
161    /// Update props by re-evaluating bindings against new state
162    pub fn update_props(&mut self, state: &serde_json::Value) {
163        self.props = resolve_props(&self.raw_props, state);
164    }
165
166    /// Update props by re-evaluating bindings against state and data sources
167    pub fn update_props_with_data_sources(
168        &mut self,
169        state: &serde_json::Value,
170        data_sources: Option<&IndexMap<String, serde_json::Value>>,
171    ) {
172        self.props = resolve_props_full(&self.raw_props, state, None, data_sources);
173    }
174
175    /// Check if this is a ForEach control flow node
176    pub fn is_foreach(&self) -> bool {
177        matches!(self.control_flow, Some(ControlFlowKind::ForEach { .. }))
178    }
179
180    /// Check if this is a Conditional control flow node
181    pub fn is_conditional(&self) -> bool {
182        matches!(self.control_flow, Some(ControlFlowKind::Conditional))
183    }
184
185    /// Check if this is a Router control flow node
186    pub fn is_router(&self) -> bool {
187        matches!(self.control_flow, Some(ControlFlowKind::Router { .. }))
188    }
189}
190
191/// The instance tree - maintains the current UI tree state
192pub struct InstanceTree {
193    /// All nodes in the tree
194    nodes: SlotMap<NodeId, InstanceNode>,
195
196    /// Root node ID
197    root: Option<NodeId>,
198}
199
200impl InstanceTree {
201    pub fn new() -> Self {
202        Self {
203            nodes: SlotMap::with_key(),
204            root: None,
205        }
206    }
207
208    /// Clear all nodes and reset the tree
209    pub fn clear(&mut self) {
210        self.nodes.clear();
211        self.root = None;
212    }
213
214    /// Create a new node and return its ID
215    pub fn create_node(&mut self, element: &Element, state: &serde_json::Value) -> NodeId {
216        self.nodes
217            .insert_with_key(|id| InstanceNode::new(id, element, state))
218    }
219
220    /// Create a new node with data sources and return its ID
221    pub fn create_node_full(
222        &mut self,
223        element: &Element,
224        state: &serde_json::Value,
225        data_sources: Option<&DataSources>,
226    ) -> NodeId {
227        self.nodes
228            .insert_with_key(|id| InstanceNode::new_full(id, element, state, data_sources))
229    }
230
231    /// Create a control flow node (ForEach or Conditional) and return its ID
232    pub fn create_control_flow_node(
233        &mut self,
234        element_type: &str,
235        props: ResolvedProps,
236        raw_props: Props,
237        control_flow: ControlFlowKind,
238        ir_node_template: IRNode,
239    ) -> NodeId {
240        self.nodes.insert_with_key(|id| {
241            InstanceNode::new_control_flow(
242                id,
243                element_type,
244                props,
245                raw_props,
246                control_flow,
247                ir_node_template,
248            )
249        })
250    }
251
252    /// Get a node by ID
253    pub fn get(&self, id: NodeId) -> Option<&InstanceNode> {
254        self.nodes.get(id)
255    }
256
257    /// Get a mutable node by ID
258    pub fn get_mut(&mut self, id: NodeId) -> Option<&mut InstanceNode> {
259        self.nodes.get_mut(id)
260    }
261
262    /// Remove a node and all its descendants
263    pub fn remove(&mut self, id: NodeId) -> Option<InstanceNode> {
264        if let Some(node) = self.nodes.get(id) {
265            let children = node.children.clone();
266            // Remove all children recursively
267            for child_id in children {
268                self.remove(child_id);
269            }
270        }
271        self.nodes.remove(id)
272    }
273
274    /// Set the root node
275    pub fn set_root(&mut self, id: NodeId) {
276        self.root = Some(id);
277    }
278
279    /// Get the root node ID
280    pub fn root(&self) -> Option<NodeId> {
281        self.root
282    }
283
284    /// Add a child to a parent node
285    pub fn add_child(&mut self, parent_id: NodeId, child_id: NodeId, before: Option<NodeId>) {
286        if let Some(parent) = self.nodes.get_mut(parent_id) {
287            if let Some(before_id) = before {
288                if let Some(pos) = parent.children.iter().position(|&id| id == before_id) {
289                    parent.children.insert(pos, child_id);
290                } else {
291                    parent.children.push_back(child_id);
292                }
293            } else {
294                parent.children.push_back(child_id);
295            }
296        }
297
298        if let Some(child) = self.nodes.get_mut(child_id) {
299            child.parent = Some(parent_id);
300        }
301    }
302
303    /// Remove a child from its parent
304    pub fn remove_child(&mut self, parent_id: NodeId, child_id: NodeId) {
305        if let Some(parent) = self.nodes.get_mut(parent_id) {
306            parent.children = parent
307                .children
308                .iter()
309                .filter(|&&id| id != child_id)
310                .copied()
311                .collect();
312        }
313
314        if let Some(child) = self.nodes.get_mut(child_id) {
315            child.parent = None;
316        }
317    }
318
319    /// Update all nodes that depend on changed state
320    pub fn update_nodes(
321        &mut self,
322        node_ids: &indexmap::IndexSet<NodeId>,
323        state: &serde_json::Value,
324    ) {
325        for &node_id in node_ids {
326            if let Some(node) = self.nodes.get_mut(node_id) {
327                node.update_props(state);
328            }
329        }
330    }
331
332    /// Return the total number of nodes in the tree.
333    pub fn len(&self) -> usize {
334        self.nodes.len()
335    }
336
337    /// Return whether the tree is empty.
338    pub fn is_empty(&self) -> bool {
339        self.nodes.is_empty()
340    }
341
342    /// Iterate over all nodes
343    pub fn iter(&self) -> impl Iterator<Item = (NodeId, &InstanceNode)> {
344        self.nodes.iter()
345    }
346}
347
348impl Default for InstanceTree {
349    fn default() -> Self {
350        Self::new()
351    }
352}
353
354#[cfg(test)]
355mod tests {
356
357    use crate::reconcile::resolve::evaluate_binding;
358    use serde_json::json;
359
360    #[test]
361    fn test_evaluate_binding() {
362        use crate::reactive::Binding;
363
364        let state = json!({
365            "user": {
366                "name": "Alice",
367                "age": 30
368            }
369        });
370
371        let name_binding = Binding::state(vec!["user".to_string(), "name".to_string()]);
372        let age_binding = Binding::state(vec!["user".to_string(), "age".to_string()]);
373        let email_binding = Binding::state(vec!["user".to_string(), "email".to_string()]);
374
375        assert_eq!(
376            evaluate_binding(&name_binding, &state),
377            Some(json!("Alice"))
378        );
379        assert_eq!(evaluate_binding(&age_binding, &state), Some(json!(30)));
380        assert_eq!(evaluate_binding(&email_binding, &state), None);
381    }
382}