Skip to main content

hypen_engine/reconcile/
diff.rs

1use super::conditionals::{
2    evaluate_value, find_matching_branch, find_matching_route_with_key,
3};
4use super::tree::DEFAULT_ROUTER_CACHE_SIZE;
5use super::item_bindings::replace_ir_node_item_bindings;
6use super::keyed::{generate_item_key, reconcile_iterable_children};
7use super::resolve::{evaluate_binding, resolve_props_full};
8use super::{ControlFlowKind, InstanceTree, Patch};
9use crate::ir::{Element, IRNode, NodeId, Props, RouterRoute, Value};
10use crate::reactive::DependencyGraph;
11use indexmap::IndexMap;
12
13/// Data sources type alias for readability
14type DataSources = indexmap::IndexMap<String, serde_json::Value>;
15
16/// Module instances map type alias
17type Modules = indexmap::IndexMap<String, crate::lifecycle::ModuleInstance>;
18
19/// Shared mutable context threaded through the recursive tree-building and
20/// reconciliation helpers.  Grouping these fields removes the repetitive
21/// parameter tuple that was being copy-pasted across every internal function.
22pub(crate) struct ReconcileCtx<'a> {
23    pub tree: &'a mut InstanceTree,
24    pub state: &'a serde_json::Value,
25    pub patches: &'a mut Vec<Patch>,
26    pub dependencies: &'a mut DependencyGraph,
27    pub data_sources: Option<&'a DataSources>,
28    pub modules: Option<&'a Modules>,
29}
30
31impl<'a> ReconcileCtx<'a> {
32    /// Resolve a raw `module_scope` to its effective form.
33    ///
34    /// Returns `Some(scope)` only when the named module is actually registered
35    /// in `ctx.modules`. When the scope name has no matching named module
36    /// (e.g. the legacy "wrap your primary module's DSL in `module App { ... }`"
37    /// pattern), this returns `None` so dependency registration falls back to
38    /// raw paths and state lookup falls back to the primary module's state.
39    fn effective_scope<'s>(&self, raw: Option<&'s str>) -> Option<&'s str> {
40        raw.filter(|scope| {
41            self.modules
42                .map(|m| m.contains_key(*scope))
43                .unwrap_or(false)
44        })
45    }
46
47    /// Resolve the state slot to read bindings against.
48    ///
49    /// Returns the named module's state when `effective_scope` matches a
50    /// registered module, otherwise the primary state from the context.
51    fn effective_state(&self, raw_scope: Option<&str>) -> &'a serde_json::Value {
52        match self.effective_scope(raw_scope) {
53            Some(scope) => self
54                .modules
55                .and_then(|m| m.get(scope))
56                .map(|m| m.get_state())
57                .unwrap_or(self.state),
58            None => self.state,
59        }
60    }
61}
62
63/// Reconcile an IRNode tree against the instance tree and generate patches
64/// This is the primary entry point for the IRNode-based reconciliation system,
65/// which supports first-class ForEach, When/If, and custom item variable names.
66pub fn reconcile_ir(
67    tree: &mut InstanceTree,
68    node: &IRNode,
69    parent_id: Option<NodeId>,
70    state: &serde_json::Value,
71    dependencies: &mut DependencyGraph,
72) -> Vec<Patch> {
73    reconcile_ir_with_ds(tree, node, parent_id, state, dependencies, None, None)
74}
75
76/// Reconcile an IRNode tree with data source context
77pub fn reconcile_ir_with_ds(
78    tree: &mut InstanceTree,
79    node: &IRNode,
80    parent_id: Option<NodeId>,
81    state: &serde_json::Value,
82    dependencies: &mut DependencyGraph,
83    data_sources: Option<&DataSources>,
84    modules: Option<&indexmap::IndexMap<String, crate::lifecycle::ModuleInstance>>,
85) -> Vec<Patch> {
86    let mut patches = Vec::new();
87
88    // For initial render, create the tree
89    if tree.root().is_none() {
90        let mut ctx = ReconcileCtx {
91            tree,
92            state,
93            patches: &mut patches,
94            dependencies,
95            data_sources,
96            modules,
97        };
98        let node_id = create_ir_node_tree_impl(&mut ctx, node, parent_id, true);
99        ctx.tree.set_root(node_id);
100        return patches;
101    }
102
103    // Incremental update: reconcile root against existing tree
104    if let Some(root_id) = tree.root() {
105        let mut ctx = ReconcileCtx {
106            tree,
107            state,
108            patches: &mut patches,
109            dependencies,
110            data_sources,
111            modules,
112        };
113        reconcile_ir_node_impl(&mut ctx, root_id, node);
114    }
115
116    patches
117}
118
119/// Create a tree node for an `Element`.
120///
121/// Used by the IRNode dispatcher's `IRNode::Element` arm. The
122/// `render_parent` parameter exists so control-flow constructs (ForEach,
123/// Conditional, Router) can mount their children's tree slot under the
124/// CFN container while emitting the actual `Insert` patch into the
125/// CFN's grandparent — the renderer treats CFN containers as transparent.
126/// When `render_parent == logical_parent` the two parameters collapse
127/// to the normal "render where the tree puts it" behavior.
128fn create_element_node(
129    ctx: &mut ReconcileCtx,
130    element: &Element,
131    logical_parent: Option<NodeId>,
132    render_parent: Option<NodeId>,
133    is_root: bool,
134) -> NodeId {
135    let module_scope_ref = ctx.effective_scope(element.module_scope.as_deref());
136    let effective_state = ctx.effective_state(element.module_scope.as_deref());
137
138    // Iterable element fast-path (List, Grid, …) — only valid in the
139    // "normal" path where logical and render parents agree, since
140    // create_list_tree_impl emits its own Insert patches against the
141    // tree-side parent.
142    if logical_parent == render_parent {
143        if let Some(Value::Binding(_)) = element.props.get("0") {
144            if !element.ir_children.is_empty() {
145                return create_list_tree_impl(ctx, element, logical_parent, is_root);
146            }
147        }
148    }
149
150    // Allocate the node and register reactive dependencies for every
151    // binding in its props.
152    let node_id = ctx
153        .tree
154        .create_node_full(element, effective_state, ctx.data_sources);
155
156    for value in element.props.values() {
157        match value {
158            Value::Binding(binding) => {
159                ctx.dependencies
160                    .add_dependency(node_id, binding, module_scope_ref);
161            }
162            Value::TemplateString { bindings, .. } => {
163                for binding in bindings {
164                    ctx.dependencies
165                        .add_dependency(node_id, binding, module_scope_ref);
166                }
167            }
168            _ => {}
169        }
170    }
171
172    // Build the Create patch payload. Lazy elements stash the first
173    // child's component name in a `__lazy_child` prop so renderers know
174    // what to fetch when the user activates the slot.
175    let is_lazy = element
176        .props
177        .get("__lazy")
178        .and_then(|v| match v {
179            Value::Static(val) => val.as_bool(),
180            _ => None,
181        })
182        .unwrap_or(false);
183
184    let mut props = ctx
185        .tree
186        .get(node_id)
187        .map(|n| n.props.clone())
188        .unwrap_or_else(|| std::sync::Arc::new(indexmap::IndexMap::new()));
189    if is_lazy && !element.ir_children.is_empty() {
190        if let Some(IRNode::Element(first_child)) = element.ir_children.first() {
191            // Copy-on-write: only clone the map if it's still shared with
192            // the InstanceNode we pulled it from.
193            std::sync::Arc::make_mut(&mut props).insert(
194                "__lazy_child".to_string(),
195                serde_json::json!(first_child.element_type),
196            );
197        }
198    }
199    ctx.patches
200        .push(Patch::create(node_id, element.element_type.clone(), props));
201
202    // Tree-side: hang the node off its logical parent.
203    if let Some(parent) = logical_parent {
204        ctx.tree.add_child(parent, node_id, None);
205    }
206
207    // Patch-side: emit Insert into render_parent when set; otherwise
208    // fall back to "root" (for root-level children of a control-flow
209    // container whose own NodeId is not known to the renderer) before
210    // finally falling through to the logical parent (which only fires
211    // when render_parent was omitted by a caller that passed the same
212    // parent for both).
213    if let Some(rp) = render_parent {
214        ctx.patches.push(Patch::insert(rp, node_id, None));
215    } else if is_root {
216        ctx.patches.push(Patch::insert_root(node_id));
217    } else if let Some(lp) = logical_parent {
218        ctx.patches.push(Patch::insert(lp, node_id, None));
219    }
220
221    // Recurse into children (skipped for lazy elements). Module-scoped
222    // state propagates so `${state.x}` inside the subtree resolves
223    // against the right module slot.
224    if !is_lazy {
225        let old_state = ctx.state;
226        ctx.state = effective_state;
227        for child_ir in &element.ir_children {
228            create_ir_node_tree_impl(ctx, child_ir, Some(node_id), false);
229        }
230        ctx.state = old_state;
231    }
232
233    node_id
234}
235
236/// Create an iterable element (List, Grid, etc.) that iterates over an array in state
237fn create_list_tree_impl(
238    ctx: &mut ReconcileCtx,
239    element: &Element,
240    parent_id: Option<NodeId>,
241    is_root: bool,
242) -> NodeId {
243    let module_scope_ref = ctx.effective_scope(element.module_scope.as_deref());
244    let effective_state = ctx.effective_state(element.module_scope.as_deref());
245
246    // Get the array binding from first prop (prop "0")
247    let array = if let Some(Value::Binding(binding)) = element.props.get("0") {
248        evaluate_binding(binding, effective_state).unwrap_or(serde_json::Value::Array(vec![]))
249    } else {
250        serde_json::Value::Array(vec![])
251    };
252
253    // Create a container element - use the original element type (List, Grid, etc.)
254    // but remove the "0" prop since it's only for iteration, not rendering
255    let mut list_element = Element::new(&element.element_type);
256    for (key, value) in &element.props {
257        if key != "0" {
258            list_element.props.insert(key.clone(), value.clone());
259        }
260    }
261
262    let node_id = ctx
263        .tree
264        .create_node_full(&list_element, effective_state, ctx.data_sources);
265
266    // Register the List node as depending on the array binding
267    if let Some(Value::Binding(binding)) = element.props.get("0") {
268        ctx.dependencies
269            .add_dependency(node_id, binding, module_scope_ref);
270    }
271
272    // Store the original element template for re-reconciliation
273    if let Some(node) = ctx.tree.get_mut(node_id) {
274        node.raw_props = element.props.clone();
275        node.element_template = Some(std::sync::Arc::new(element.clone()));
276    }
277
278    // Generate Create patch for container
279    let node = ctx.tree.get(node_id).unwrap();
280    ctx.patches.push(Patch::create(
281        node_id,
282        node.element_type.clone(),
283        node.props.clone(),
284    ));
285
286    // Insert container
287    if let Some(parent) = parent_id {
288        ctx.tree.add_child(parent, node_id, None);
289        ctx.patches.push(Patch::insert(parent, node_id, None));
290    } else if is_root {
291        ctx.patches.push(Patch::insert_root(node_id));
292    }
293
294    // Create children for each item in the array. Stamp every per-template
295    // child with a key so the next reconcile can match by identity instead
296    // of position.
297    if let serde_json::Value::Array(items) = &array {
298        let key_path = element.props.get("key.0").and_then(|v| match v {
299            Value::Static(serde_json::Value::String(s)) => Some(s.as_str()),
300            _ => None,
301        });
302        let multi_template = element.ir_children.len() > 1;
303
304        for (index, item) in items.iter().enumerate() {
305            let item_key = generate_item_key(item, key_path, "item", index);
306
307            for (template_idx, child_ir) in element.ir_children.iter().enumerate() {
308                let child_key = if multi_template {
309                    format!("{}#{}", item_key, template_idx)
310                } else {
311                    item_key.clone()
312                };
313                let child_with_item = replace_ir_node_item_bindings(
314                    child_ir, item, index, "item", &item_key,
315                );
316                let child_id = create_ir_node_tree_impl(
317                    ctx,
318                    &child_with_item,
319                    Some(node_id),
320                    false,
321                );
322                if let Some(child_node) = ctx.tree.get_mut(child_id) {
323                    child_node.key = Some(child_key);
324                }
325            }
326        }
327    }
328
329    node_id
330}
331
332/// Reconcile an existing tree node against a new `Element`.
333///
334/// Inlined into [`reconcile_ir_node_impl`]'s `IRNode::Element` arm — there
335/// is no public Element-only entry point any more, so this stays private
336/// and lives next to the IR dispatcher that calls it.
337fn reconcile_element_node(ctx: &mut ReconcileCtx, node_id: NodeId, element: &Element) {
338    let node = match ctx.tree.get(node_id).cloned() {
339        Some(n) => n,
340        None => return,
341    };
342
343    let effective_state = ctx.effective_state(element.module_scope.as_deref());
344    let module_scope_ref = element.module_scope.as_deref();
345
346    // Special handling for iterable elements (List, Grid, …) — the source
347    // array binding lives in props["0"] and the per-item template is in
348    // ir_children.
349    let is_iterable = element.props.get("0").is_some() && !element.ir_children.is_empty();
350
351    if is_iterable {
352        let array = if let Some(Value::Binding(binding)) = element.props.get("0") {
353            evaluate_binding(binding, effective_state).unwrap_or(serde_json::Value::Array(vec![]))
354        } else {
355            serde_json::Value::Array(vec![])
356        };
357
358        if let serde_json::Value::Array(items) = &array {
359            // Look for an explicit `key:` prop on the iterable element so
360            // `Grid(@items, key: "uuid")` overrides the default id auto-detect.
361            let key_path = element.props.get("key.0").and_then(|v| match v {
362                Value::Static(serde_json::Value::String(s)) => Some(s.as_str()),
363                _ => None,
364            });
365
366            reconcile_iterable_children(
367                ctx,
368                node_id,
369                items,
370                "item",
371                key_path,
372                &element.ir_children,
373            );
374        }
375
376        return;
377    }
378
379    // If element type changed, replace the entire subtree.
380    if node.element_type != element.element_type {
381        replace_subtree_impl(ctx, node_id, &node, element);
382        return;
383    }
384
385    // Register dependencies for every binding in the new props.
386    for value in element.props.values() {
387        match value {
388            Value::Binding(binding) => {
389                ctx.dependencies
390                    .add_dependency(node_id, binding, module_scope_ref);
391            }
392            Value::TemplateString { bindings, .. } => {
393                for binding in bindings {
394                    ctx.dependencies
395                        .add_dependency(node_id, binding, module_scope_ref);
396                }
397            }
398            _ => {}
399        }
400    }
401
402    // Diff and apply prop changes.
403    let new_props = resolve_props_full(&element.props, effective_state, None, ctx.data_sources);
404    let prop_patches = diff_props(node_id, &node.props, &new_props);
405    ctx.patches.extend(prop_patches);
406
407    if let Some(node) = ctx.tree.get_mut(node_id) {
408        node.props = new_props; // move the Arc directly — no extra clone
409        node.raw_props = element.props.clone();
410    }
411
412    // Reconcile children (skip when this element is lazy — the renderer
413    // hasn't asked for the subtree yet).
414    let is_lazy = element
415        .props
416        .get("__lazy")
417        .and_then(|v| match v {
418            Value::Static(val) => val.as_bool(),
419            _ => None,
420        })
421        .unwrap_or(false);
422
423    if !is_lazy {
424        let old_children = node.children.clone();
425        let new_children = &element.ir_children;
426
427        for (i, new_child_ir) in new_children.iter().enumerate() {
428            if let Some(&old_child_id) = old_children.get(i) {
429                reconcile_ir_node_impl(ctx, old_child_id, new_child_ir);
430            } else {
431                create_ir_node_tree_impl(ctx, new_child_ir, Some(node_id), false);
432            }
433        }
434
435        if old_children.len() > new_children.len() {
436            for old_child_id in old_children.iter().skip(new_children.len()).copied() {
437                let subtree_ids = collect_subtree_ids(ctx.tree, old_child_id);
438                for &id in &subtree_ids {
439                    ctx.patches.push(Patch::remove(id));
440                    ctx.dependencies.remove_node(id);
441                }
442                ctx.tree.remove_child(node_id, old_child_id);
443                ctx.tree.remove(old_child_id);
444            }
445        }
446    }
447}
448
449/// Replace an entire subtree when element types don't match.
450fn replace_subtree_impl(
451    ctx: &mut ReconcileCtx,
452    old_node_id: NodeId,
453    old_node: &super::InstanceNode,
454    new_element: &Element,
455) {
456    let parent_id = old_node.parent;
457
458    let old_position = if let Some(pid) = parent_id {
459        ctx.tree
460            .get(pid)
461            .and_then(|parent| parent.children.iter().position(|&id| id == old_node_id))
462    } else {
463        None
464    };
465
466    let ids_to_remove = collect_subtree_ids(ctx.tree, old_node_id);
467
468    for &id in &ids_to_remove {
469        ctx.patches.push(Patch::remove(id));
470        ctx.dependencies.remove_node(id);
471    }
472
473    if let Some(pid) = parent_id {
474        if let Some(parent) = ctx.tree.get_mut(pid) {
475            parent.children = parent
476                .children
477                .iter()
478                .filter(|&&id| id != old_node_id)
479                .copied()
480                .collect();
481        }
482    }
483
484    ctx.tree.remove(old_node_id);
485
486    let is_root = parent_id.is_none();
487    let new_node_id = create_element_node(ctx, new_element, parent_id, parent_id, is_root);
488
489    if is_root {
490        ctx.tree.set_root(new_node_id);
491    } else if let Some(pid) = parent_id {
492        if let Some(pos) = old_position {
493            if let Some(parent) = ctx.tree.get_mut(pid) {
494                let current_len = parent.children.len();
495                if pos < current_len - 1 {
496                    let new_id = parent.children.pop_back().unwrap();
497                    parent.children.insert(pos, new_id);
498                    let next_sibling = parent.children.get(pos + 1).copied();
499                    ctx.patches
500                        .push(Patch::move_node(pid, new_node_id, next_sibling));
501                }
502            }
503        }
504    }
505}
506
507/// Collect all node IDs in a subtree (post-order: children before parents)
508fn collect_subtree_ids(tree: &InstanceTree, root_id: NodeId) -> Vec<NodeId> {
509    let mut result = Vec::new();
510    let mut stack: Vec<(NodeId, bool)> = vec![(root_id, false)];
511
512    while let Some((node_id, children_processed)) = stack.pop() {
513        if children_processed {
514            result.push(node_id);
515        } else {
516            stack.push((node_id, true));
517            if let Some(node) = tree.get(node_id) {
518                for &child_id in node.children.iter().rev() {
519                    stack.push((child_id, false));
520                }
521            }
522        }
523    }
524
525    result
526}
527
528/// Diff two sets of props and generate SetProp/RemoveProp patches
529pub fn diff_props(
530    node_id: NodeId,
531    old_props: &IndexMap<String, serde_json::Value>,
532    new_props: &IndexMap<String, serde_json::Value>,
533) -> Vec<Patch> {
534    let mut patches = Vec::new();
535
536    for (key, new_value) in new_props {
537        if old_props.get(key) != Some(new_value) {
538            patches.push(Patch::set_prop(node_id, key.clone(), new_value.clone()));
539        }
540    }
541
542    for key in old_props.keys() {
543        if !new_props.contains_key(key) {
544            patches.push(Patch::remove_prop(node_id, key.clone()));
545        }
546    }
547
548    patches
549}
550
551// ============================================================================
552// IRNode-based reconciliation (first-class control flow constructs)
553// ============================================================================
554
555/// Create a tree from an IRNode using a `ReconcileCtx`.
556///
557/// Convenience wrapper that uses the same node for both logical (tree)
558/// and render (patch) parents — the common case.
559pub(crate) fn create_ir_node_tree_impl(
560    ctx: &mut ReconcileCtx,
561    node: &IRNode,
562    parent_id: Option<NodeId>,
563    is_root: bool,
564) -> NodeId {
565    create_ir_node_tree_full(ctx, node, parent_id, parent_id, is_root)
566}
567
568/// Create a tree from an IRNode with separate logical and render parents.
569///
570/// `logical_parent` controls where the new node lives in the instance
571/// tree; `render_parent` controls which parent the `Insert` patch
572/// references. They diverge for control-flow children — a ForEach item
573/// is logically owned by the ForEach container, but its `Insert` patch
574/// targets the ForEach's grandparent because the renderer treats the
575/// container as transparent.
576fn create_ir_node_tree_full(
577    ctx: &mut ReconcileCtx,
578    node: &IRNode,
579    logical_parent: Option<NodeId>,
580    render_parent: Option<NodeId>,
581    is_root: bool,
582) -> NodeId {
583    match node {
584        IRNode::Element(element) => {
585            create_element_node(ctx, element, logical_parent, render_parent, is_root)
586        }
587        IRNode::ForEach { .. } | IRNode::Conditional { .. } | IRNode::Router { .. } => {
588            // Control-flow containers always render under the logical
589            // parent — they don't take a render-parent split themselves.
590            create_control_flow_tree(ctx, node, logical_parent, is_root)
591        }
592    }
593}
594
595/// Create a ForEach or Conditional tree from an IRNode, destructuring inside.
596fn create_control_flow_tree(
597    ctx: &mut ReconcileCtx,
598    node: &IRNode,
599    parent_id: Option<NodeId>,
600    is_root: bool,
601) -> NodeId {
602    match node {
603        IRNode::ForEach { .. } => create_foreach_ir_tree(
604            ctx,
605            node,
606            parent_id,
607            is_root,
608        ),
609        IRNode::Conditional {
610            value,
611            branches,
612            fallback,
613            ..
614        } => create_conditional_tree(
615            ctx,
616            value,
617            branches,
618            fallback.as_deref(),
619            node,
620            parent_id,
621            is_root,
622        ),
623        IRNode::Router {
624            location,
625            routes,
626            fallback,
627            ..
628        } => create_router_tree(
629            ctx,
630            location,
631            routes,
632            fallback.as_deref(),
633            node,
634            parent_id,
635            is_root,
636        ),
637        IRNode::Element(_) => unreachable!("create_control_flow_tree called with Element"),
638    }
639}
640
641/// Create a ForEach iteration tree from IRNode::ForEach
642fn create_foreach_ir_tree(
643    ctx: &mut ReconcileCtx,
644    node: &IRNode,
645    parent_id: Option<NodeId>,
646    is_root: bool,
647) -> NodeId {
648    let (source, item_name, key_path, template, props, raw_scope) = match node {
649        IRNode::ForEach {
650            source,
651            item_name,
652            key_path,
653            template,
654            props,
655            module_scope,
656        } => (
657            source,
658            item_name.as_str(),
659            key_path.as_deref(),
660            template.as_slice(),
661            props,
662            module_scope.as_deref(),
663        ),
664        _ => unreachable!("create_foreach_ir_tree called with non-ForEach node"),
665    };
666
667    // Resolve scope: only "real" if a named module is registered.
668    let module_scope_ref = ctx.effective_scope(raw_scope);
669    let effective_state = ctx.effective_state(raw_scope);
670
671    let array =
672        evaluate_binding(source, effective_state).unwrap_or(serde_json::Value::Array(vec![]));
673
674    let resolved_props = resolve_props_full(props, effective_state, None, ctx.data_sources);
675
676    let node_id = ctx.tree.create_control_flow_node(
677        "__ForEach",
678        resolved_props,
679        props.clone(),
680        ControlFlowKind::ForEach {
681            item_name: item_name.to_string(),
682            key_path: key_path.map(|s| s.to_string()),
683        },
684        node.clone(),
685    );
686
687    ctx.dependencies
688        .add_dependency(node_id, source, module_scope_ref);
689
690    if let Some(parent) = parent_id {
691        ctx.tree.add_child(parent, node_id, None);
692    }
693
694    let render_parent = parent_id;
695
696    if let serde_json::Value::Array(items) = &array {
697        for (index, item) in items.iter().enumerate() {
698            let item_key = generate_item_key(item, key_path, item_name, index);
699
700            for child_template in template {
701                let child_with_item = replace_ir_node_item_bindings(
702                    child_template,
703                    item,
704                    index,
705                    item_name,
706                    &item_key,
707                );
708                create_ir_node_tree_full(
709                    ctx,
710                    &child_with_item,
711                    Some(node_id),
712                    render_parent,
713                    is_root && render_parent.is_none(),
714                );
715            }
716        }
717    }
718
719    node_id
720}
721
722/// Create a Conditional (When/If) tree from IRNode::Conditional
723fn create_conditional_tree(
724    ctx: &mut ReconcileCtx,
725    value: &Value,
726    branches: &[crate::ir::ConditionalBranch],
727    fallback: Option<&[IRNode]>,
728    original_node: &IRNode,
729    parent_id: Option<NodeId>,
730    is_root: bool,
731) -> NodeId {
732    let raw_scope = match original_node {
733        IRNode::Conditional { module_scope, .. } => module_scope.as_deref(),
734        _ => None,
735    };
736    let module_scope_ref = ctx.effective_scope(raw_scope);
737    let effective_state = ctx.effective_state(raw_scope);
738
739    let evaluated_value = evaluate_value(value, effective_state, ctx.data_sources);
740
741    let mut raw_props = Props::new();
742    raw_props.insert("__condition".to_string(), value.clone());
743
744    let node_id = ctx.tree.create_control_flow_node(
745        "__Conditional",
746        std::sync::Arc::new(IndexMap::new()),
747        raw_props,
748        ControlFlowKind::Conditional,
749        original_node.clone(),
750    );
751
752    if let Value::Binding(binding) = value {
753        ctx.dependencies
754            .add_dependency(node_id, binding, module_scope_ref);
755    } else if let Value::TemplateString { bindings, .. } = value {
756        for binding in bindings {
757            ctx.dependencies
758                .add_dependency(node_id, binding, module_scope_ref);
759        }
760    }
761
762    if let Some(parent) = parent_id {
763        ctx.tree.add_child(parent, node_id, None);
764    }
765
766    let matched_children =
767        find_matching_branch(&evaluated_value, branches, fallback, effective_state, ctx.data_sources);
768
769    let render_parent = parent_id;
770
771    if let Some(children) = matched_children {
772        for child in children {
773            create_ir_node_tree_full(
774                ctx,
775                child,
776                Some(node_id),
777                render_parent,
778                is_root && render_parent.is_none(),
779            );
780        }
781    }
782
783    node_id
784}
785
786/// Create a Router tree from IRNode::Router.
787///
788/// Mirrors `create_conditional_tree`: builds a single `__Router` control-flow
789/// node, registers a dependency on the location binding, picks the matching
790/// route, and renders only that route's children. The renderer never sees
791/// `Router` or `Route` element types — it just sees the matched children
792/// inserted under the Router's render parent.
793fn create_router_tree(
794    ctx: &mut ReconcileCtx,
795    location: &Value,
796    routes: &[RouterRoute],
797    fallback: Option<&[IRNode]>,
798    original_node: &IRNode,
799    parent_id: Option<NodeId>,
800    is_root: bool,
801) -> NodeId {
802    let raw_scope = match original_node {
803        IRNode::Router { module_scope, .. } => module_scope.as_deref(),
804        _ => None,
805    };
806    let module_scope_ref = ctx.effective_scope(raw_scope);
807    let effective_state = ctx.effective_state(raw_scope);
808
809    // Resolve location to a string. Anything that isn't a string falls back
810    // to the empty path so the fallback (or no route) is selected.
811    let evaluated = evaluate_value(location, effective_state, ctx.data_sources);
812    let location_str = match &evaluated {
813        serde_json::Value::String(s) => s.clone(),
814        serde_json::Value::Null => String::new(),
815        other => other.to_string(),
816    };
817
818    let mut raw_props = Props::new();
819    raw_props.insert("__location".to_string(), location.clone());
820
821    // Seed the Router with an empty cache and the initial route key
822    // (filled in once we know which route matched below).
823    let node_id = ctx.tree.create_control_flow_node(
824        "__Router",
825        std::sync::Arc::new(IndexMap::new()),
826        raw_props,
827        ControlFlowKind::Router {
828            cache: IndexMap::new(),
829            current_route_key: None,
830            max_cache_size: DEFAULT_ROUTER_CACHE_SIZE,
831        },
832        original_node.clone(),
833    );
834
835    // Register dependency on the location binding so updates to state.location
836    // dirty this Router node and trigger reconciliation.
837    if let Value::Binding(binding) = location {
838        ctx.dependencies
839            .add_dependency(node_id, binding, module_scope_ref);
840    } else if let Value::TemplateString { bindings, .. } = location {
841        for binding in bindings {
842            ctx.dependencies
843                .add_dependency(node_id, binding, module_scope_ref);
844        }
845    }
846
847    if let Some(parent) = parent_id {
848        ctx.tree.add_child(parent, node_id, None);
849    }
850
851    // Find the matched route (with its pattern key) so we can remember
852    // which route produced the current children. On the next location
853    // change, the Router reconciler will cache *these* NodeIds under
854    // that key before swapping in new children.
855    let matched = find_matching_route_with_key(&location_str, routes, fallback);
856    let render_parent = parent_id;
857
858    if let Some((route_key, children)) = matched.as_ref() {
859        for child in *children {
860            create_ir_node_tree_full(
861                ctx,
862                child,
863                Some(node_id),
864                render_parent,
865                is_root && render_parent.is_none(),
866            );
867        }
868
869        // Record the route we just rendered so the reconciler knows
870        // which cache bucket to populate on navigation away.
871        if let Some(router_node) = ctx.tree.get_mut(node_id) {
872            if let Some(ControlFlowKind::Router {
873                current_route_key, ..
874            }) = router_node.control_flow.as_mut()
875            {
876                *current_route_key = Some(route_key.clone());
877            }
878        }
879    }
880
881    node_id
882}
883
884/// Reconcile an existing tree against a new IRNode using a `ReconcileCtx`.
885pub(crate) fn reconcile_ir_node_impl(ctx: &mut ReconcileCtx, node_id: NodeId, node: &IRNode) {
886    let existing_node = ctx.tree.get(node_id).cloned();
887    if existing_node.is_none() {
888        return;
889    }
890    let existing = existing_node.unwrap();
891
892    match node {
893        IRNode::Element(element) => {
894            reconcile_element_node(ctx, node_id, element);
895        }
896        IRNode::ForEach {
897            source,
898            item_name,
899            key_path,
900            template,
901            props: _,
902            module_scope,
903        } => {
904            if !existing.is_foreach() {
905                let parent_id = existing.parent;
906                remove_subtree(ctx.tree, node_id, ctx.patches, ctx.dependencies);
907                create_ir_node_tree_impl(ctx, node, parent_id, parent_id.is_none());
908                return;
909            }
910
911            let module_scope_ref = ctx.effective_scope(module_scope.as_deref());
912            let effective_state = ctx.effective_state(module_scope.as_deref());
913
914            ctx.dependencies
915                .add_dependency(node_id, source, module_scope_ref);
916
917            let array = evaluate_binding(source, effective_state)
918                .unwrap_or(serde_json::Value::Array(vec![]));
919
920            if let serde_json::Value::Array(items) = &array {
921                let old_children = existing.children.clone();
922                let expected_children_count = items.len() * template.len();
923                let render_parent = existing.parent.unwrap_or(node_id);
924
925                if old_children.len() != expected_children_count {
926                    for &old_child_id in &old_children {
927                        ctx.patches.push(Patch::remove(old_child_id));
928                    }
929
930                    if let Some(node) = ctx.tree.get_mut(node_id) {
931                        node.children.clear();
932                    }
933
934                    for (index, item) in items.iter().enumerate() {
935                        let item_key =
936                            generate_item_key(item, key_path.as_deref(), item_name, index);
937
938                        for child_template in template {
939                            let child_with_item = replace_ir_node_item_bindings(
940                                child_template,
941                                item,
942                                index,
943                                item_name,
944                                &item_key,
945                            );
946                            create_ir_node_tree_impl(
947                                ctx,
948                                &child_with_item,
949                                Some(render_parent),
950                                false,
951                            );
952                        }
953                    }
954                } else {
955                    let mut child_index = 0;
956                    for (item_index, item) in items.iter().enumerate() {
957                        let item_key =
958                            generate_item_key(item, key_path.as_deref(), item_name, item_index);
959
960                        for child_template in template {
961                            if let Some(&old_child_id) = old_children.get(child_index) {
962                                let child_with_item = replace_ir_node_item_bindings(
963                                    child_template,
964                                    item,
965                                    item_index,
966                                    item_name,
967                                    &item_key,
968                                );
969                                reconcile_ir_node_impl(ctx, old_child_id, &child_with_item);
970                            }
971                            child_index += 1;
972                        }
973                    }
974                }
975            }
976        }
977        IRNode::Conditional {
978            value,
979            branches,
980            fallback,
981            module_scope,
982        } => {
983            if !existing.is_conditional() {
984                let parent_id = existing.parent;
985                remove_subtree(ctx.tree, node_id, ctx.patches, ctx.dependencies);
986                create_ir_node_tree_impl(ctx, node, parent_id, parent_id.is_none());
987                return;
988            }
989
990            let module_scope_ref = ctx.effective_scope(module_scope.as_deref());
991            let effective_state = ctx.effective_state(module_scope.as_deref());
992
993            if let Value::Binding(binding) = value {
994                ctx.dependencies
995                    .add_dependency(node_id, binding, module_scope_ref);
996            } else if let Value::TemplateString { bindings, .. } = value {
997                for binding in bindings {
998                    ctx.dependencies
999                        .add_dependency(node_id, binding, module_scope_ref);
1000                }
1001            }
1002
1003            let evaluated_value = evaluate_value(value, effective_state, ctx.data_sources);
1004            let matched_children = find_matching_branch(
1005                &evaluated_value,
1006                branches,
1007                fallback.as_deref(),
1008                effective_state,
1009                ctx.data_sources,
1010            );
1011
1012            let old_children = existing.children.clone();
1013            let old_len = old_children.len();
1014            let render_parent = existing.parent;
1015
1016            if let Some(children) = matched_children {
1017                let new_len = children.len();
1018                let common = old_len.min(new_len);
1019
1020                for (i, child) in children.iter().enumerate().take(common) {
1021                    if let Some(&old_child_id) = old_children.get(i) {
1022                        reconcile_ir_node_impl(ctx, old_child_id, child);
1023                    }
1024                }
1025
1026                for i in common..old_len {
1027                    if let Some(&old_child_id) = old_children.get(i) {
1028                        remove_subtree(ctx.tree, old_child_id, ctx.patches, ctx.dependencies);
1029                        if let Some(cond_node) = ctx.tree.get_mut(node_id) {
1030                            cond_node.children = cond_node
1031                                .children
1032                                .iter()
1033                                .filter(|&&id| id != old_child_id)
1034                                .copied()
1035                                .collect();
1036                        }
1037                    }
1038                }
1039
1040                let children_is_root = render_parent.is_none();
1041                for child in &children[common..] {
1042                    create_ir_node_tree_full(
1043                        ctx,
1044                        child,
1045                        Some(node_id),
1046                        render_parent,
1047                        children_is_root,
1048                    );
1049                }
1050            } else {
1051                for &old_child_id in &old_children {
1052                    remove_subtree(ctx.tree, old_child_id, ctx.patches, ctx.dependencies);
1053                }
1054
1055                if let Some(cond_node) = ctx.tree.get_mut(node_id) {
1056                    cond_node.children.clear();
1057                }
1058            }
1059        }
1060        IRNode::Router {
1061            location,
1062            routes,
1063            fallback,
1064            module_scope,
1065        } => {
1066            // If the existing node isn't a Router, replace it wholesale.
1067            if !existing.is_router() {
1068                let parent_id = existing.parent;
1069                remove_subtree(ctx.tree, node_id, ctx.patches, ctx.dependencies);
1070                create_ir_node_tree_impl(ctx, node, parent_id, parent_id.is_none());
1071                return;
1072            }
1073
1074            let module_scope_ref = ctx.effective_scope(module_scope.as_deref());
1075            let effective_state = ctx.effective_state(module_scope.as_deref());
1076
1077            // Re-register the location dependency in case it was cleared.
1078            if let Value::Binding(binding) = location {
1079                ctx.dependencies
1080                    .add_dependency(node_id, binding, module_scope_ref);
1081            } else if let Value::TemplateString { bindings, .. } = location {
1082                for binding in bindings {
1083                    ctx.dependencies
1084                        .add_dependency(node_id, binding, module_scope_ref);
1085                }
1086            }
1087
1088            let evaluated = evaluate_value(location, effective_state, ctx.data_sources);
1089            let location_str = match &evaluated {
1090                serde_json::Value::String(s) => s.clone(),
1091                serde_json::Value::Null => String::new(),
1092                other => other.to_string(),
1093            };
1094
1095            // Read the current cache state out of the existing node. Each
1096            // Router instance carries its own detached-subtree cache keyed
1097            // by route pattern (see ControlFlowKind::Router).
1098            let (mut cache, prev_route_key, max_cache_size) =
1099                match existing.control_flow.as_ref() {
1100                    Some(ControlFlowKind::Router {
1101                        cache,
1102                        current_route_key,
1103                        max_cache_size,
1104                    }) => (cache.clone(), current_route_key.clone(), *max_cache_size),
1105                    _ => (IndexMap::new(), None, DEFAULT_ROUTER_CACHE_SIZE),
1106                };
1107
1108            let matched = find_matching_route_with_key(
1109                &location_str,
1110                routes,
1111                fallback.as_deref(),
1112            );
1113            let new_route_key = matched.as_ref().map(|(k, _)| k.clone());
1114
1115            // Same route as before — nothing structural to do. Descendant
1116            // nodes get their own dirty marks when state changes; the
1117            // Router only reconciles when the *route* changes.
1118            if prev_route_key.is_some() && prev_route_key == new_route_key {
1119                return;
1120            }
1121
1122            let render_parent = existing.parent;
1123            let old_children: Vec<NodeId> = existing.children.iter().copied().collect();
1124
1125            // Step 1: take the currently-rendered children off the Router.
1126            //   - If we have a prev_route_key → detach them and stash under
1127            //     that key (keep-alive: nodes, deps, props all live on).
1128            //   - If we don't (first reconcile after a cache miss or
1129            //     mismatched prior state) → fall back to hard teardown so
1130            //     we don't leak orphans.
1131            if let Some(prev_key) = prev_route_key.as_ref() {
1132                for &child_id in &old_children {
1133                    // Emit a Detach patch so the renderer unlinks its
1134                    // native node but keeps it alive. The engine-side
1135                    // node stays in the tree; its descendants stay
1136                    // under it; dependencies stay registered so state
1137                    // updates flow through while off-screen.
1138                    ctx.patches.push(Patch::detach(child_id));
1139                    if let Some(child) = ctx.tree.get_mut(child_id) {
1140                        child.parent = None;
1141                    }
1142                }
1143                if !old_children.is_empty() {
1144                    // Replace any prior entry for this key (e.g. if we
1145                    // navigated away, back, and away again).
1146                    cache.shift_remove(prev_key);
1147                    cache.insert(prev_key.clone(), old_children);
1148                }
1149            } else {
1150                for &old_child_id in &old_children {
1151                    remove_subtree(ctx.tree, old_child_id, ctx.patches, ctx.dependencies);
1152                }
1153            }
1154
1155            if let Some(router_node) = ctx.tree.get_mut(node_id) {
1156                router_node.children.clear();
1157            }
1158
1159            // Step 2: LRU-evict old cache entries if we exceeded the cap.
1160            // Dropping an entry means its subtree is gone for good, so
1161            // we tear it down properly (frees nodes + deps).
1162            while cache.len() > max_cache_size {
1163                let evicted_key = cache.keys().next().cloned();
1164                if let Some(evicted_key) = evicted_key {
1165                    if let Some(evicted_ids) = cache.shift_remove(&evicted_key) {
1166                        for evicted_id in evicted_ids {
1167                            remove_subtree(
1168                                ctx.tree,
1169                                evicted_id,
1170                                ctx.patches,
1171                                ctx.dependencies,
1172                            );
1173                        }
1174                    }
1175                } else {
1176                    break;
1177                }
1178            }
1179
1180            // Step 3: attach the new route's subtree — reuse cached
1181            // nodes if we've seen this route before, otherwise build
1182            // from scratch.
1183            if let Some(new_key) = new_route_key.as_ref() {
1184                if let Some(cached_ids) = cache.shift_remove(new_key) {
1185                    // Cache hit: reattach the detached subtrees in order.
1186                    // When the Router is itself the IR root, its own
1187                    // NodeId is a control-flow pseudo-node the renderer
1188                    // never created — attach to "root" in that case.
1189                    for cached_id in &cached_ids {
1190                        if let Some(child) = ctx.tree.get_mut(*cached_id) {
1191                            child.parent = Some(node_id);
1192                        }
1193                        if let Some(router_node) = ctx.tree.get_mut(node_id) {
1194                            router_node.children.push_back(*cached_id);
1195                        }
1196                        let attach_patch = match render_parent {
1197                            Some(rp) => Patch::attach(rp, *cached_id, None),
1198                            None => Patch::attach_root(*cached_id, None),
1199                        };
1200                        ctx.patches.push(attach_patch);
1201                    }
1202                } else if let Some((_, children)) = matched.as_ref() {
1203                    // Cache miss: build fresh IR tree for this route.
1204                    // When the Router is at the IR root (render_parent
1205                    // is None) the children must be inserted at "root";
1206                    // that's what `is_root` signals to create_element_node.
1207                    let children_is_root = render_parent.is_none();
1208                    for child in *children {
1209                        create_ir_node_tree_full(
1210                            ctx,
1211                            child,
1212                            Some(node_id),
1213                            render_parent,
1214                            children_is_root,
1215                        );
1216                    }
1217                }
1218            }
1219
1220            // Step 4: write the updated cache + current route back to
1221            // the Router node. If nothing matched (no route, no
1222            // fallback) we keep the cache but clear current_route_key
1223            // so the next reconcile treats this as a fresh state.
1224            if let Some(router_node) = ctx.tree.get_mut(node_id) {
1225                router_node.control_flow = Some(ControlFlowKind::Router {
1226                    cache,
1227                    current_route_key: new_route_key,
1228                    max_cache_size,
1229                });
1230            }
1231        }
1232    }
1233}
1234
1235/// Remove a subtree and generate Remove patches
1236fn remove_subtree(
1237    tree: &mut InstanceTree,
1238    node_id: NodeId,
1239    patches: &mut Vec<Patch>,
1240    dependencies: &mut DependencyGraph,
1241) {
1242    let ids = collect_subtree_ids(tree, node_id);
1243    for &id in &ids {
1244        patches.push(Patch::remove(id));
1245        dependencies.remove_node(id);
1246    }
1247    tree.remove(node_id);
1248}
1249
1250#[cfg(test)]
1251mod tests {
1252    use super::*;
1253    use crate::ir::Value;
1254    use serde_json::json;
1255
1256    #[test]
1257    fn test_create_simple_tree() {
1258        use crate::reactive::DependencyGraph;
1259
1260        let mut tree = InstanceTree::new();
1261        let mut patches = Vec::new();
1262        let mut dependencies = DependencyGraph::new();
1263
1264        let element = Element::new("Column")
1265            .with_child(Element::new("Text").with_prop("text", Value::Static(json!("Hello"))));
1266
1267        let state = json!({});
1268        let mut ctx = ReconcileCtx {
1269            tree: &mut tree,
1270            state: &state,
1271            patches: &mut patches,
1272            dependencies: &mut dependencies,
1273            data_sources: None,
1274            modules: None,
1275        };
1276        create_element_node(&mut ctx, &element, None, None, true);
1277
1278        // Should create 2 nodes (Column + Text) + 2 Inserts (root + child)
1279        // Create Column, Insert Column into root, Create Text, Insert Text into Column
1280        assert_eq!(patches.len(), 4);
1281
1282        // Verify root insert patch exists
1283        let root_insert = patches
1284            .iter()
1285            .find(|p| matches!(p, Patch::Insert { parent_id, .. } if parent_id == "root"));
1286        assert!(root_insert.is_some(), "Root insert patch should exist");
1287    }
1288
1289    #[test]
1290    fn test_diff_props() {
1291        let node_id = NodeId::default();
1292        let old = indexmap::indexmap! {
1293            "color".to_string() => json!("red"),
1294            "size".to_string() => json!(16),
1295        };
1296        let new = indexmap::indexmap! {
1297            "color".to_string() => json!("blue"),
1298            "size".to_string() => json!(16),
1299        };
1300
1301        let patches = diff_props(node_id, &old, &new);
1302
1303        // Only color changed
1304        assert_eq!(patches.len(), 1);
1305    }
1306}