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                            // Mirror the initial-create path in
947                            // `create_foreach_ir_tree` (diff.rs ~line 708-714):
948                            // items must be LOGICAL children of the ForEach
949                            // container while their Insert patch targets the
950                            // render parent. Collapsing the two (what
951                            // `create_ir_node_tree_impl` does) orphans items
952                            // under the grandparent and leaves
953                            // `ForEach.children` empty — every subsequent
954                            // reconcile then sees a length mismatch, hits this
955                            // rebuild branch again, and emits 0 Removes + N
956                            // Creates indefinitely.
957                            create_ir_node_tree_full(
958                                ctx,
959                                &child_with_item,
960                                Some(node_id),
961                                Some(render_parent),
962                                false,
963                            );
964                        }
965                    }
966                } else {
967                    let mut child_index = 0;
968                    for (item_index, item) in items.iter().enumerate() {
969                        let item_key =
970                            generate_item_key(item, key_path.as_deref(), item_name, item_index);
971
972                        for child_template in template {
973                            if let Some(&old_child_id) = old_children.get(child_index) {
974                                let child_with_item = replace_ir_node_item_bindings(
975                                    child_template,
976                                    item,
977                                    item_index,
978                                    item_name,
979                                    &item_key,
980                                );
981                                reconcile_ir_node_impl(ctx, old_child_id, &child_with_item);
982                            }
983                            child_index += 1;
984                        }
985                    }
986                }
987            }
988        }
989        IRNode::Conditional {
990            value,
991            branches,
992            fallback,
993            module_scope,
994        } => {
995            if !existing.is_conditional() {
996                let parent_id = existing.parent;
997                remove_subtree(ctx.tree, node_id, ctx.patches, ctx.dependencies);
998                create_ir_node_tree_impl(ctx, node, parent_id, parent_id.is_none());
999                return;
1000            }
1001
1002            let module_scope_ref = ctx.effective_scope(module_scope.as_deref());
1003            let effective_state = ctx.effective_state(module_scope.as_deref());
1004
1005            if let Value::Binding(binding) = value {
1006                ctx.dependencies
1007                    .add_dependency(node_id, binding, module_scope_ref);
1008            } else if let Value::TemplateString { bindings, .. } = value {
1009                for binding in bindings {
1010                    ctx.dependencies
1011                        .add_dependency(node_id, binding, module_scope_ref);
1012                }
1013            }
1014
1015            let evaluated_value = evaluate_value(value, effective_state, ctx.data_sources);
1016            let matched_children = find_matching_branch(
1017                &evaluated_value,
1018                branches,
1019                fallback.as_deref(),
1020                effective_state,
1021                ctx.data_sources,
1022            );
1023
1024            let old_children = existing.children.clone();
1025            let old_len = old_children.len();
1026            let render_parent = existing.parent;
1027
1028            if let Some(children) = matched_children {
1029                let new_len = children.len();
1030                let common = old_len.min(new_len);
1031
1032                for (i, child) in children.iter().enumerate().take(common) {
1033                    if let Some(&old_child_id) = old_children.get(i) {
1034                        reconcile_ir_node_impl(ctx, old_child_id, child);
1035                    }
1036                }
1037
1038                for i in common..old_len {
1039                    if let Some(&old_child_id) = old_children.get(i) {
1040                        remove_subtree(ctx.tree, old_child_id, ctx.patches, ctx.dependencies);
1041                        if let Some(cond_node) = ctx.tree.get_mut(node_id) {
1042                            cond_node.children = cond_node
1043                                .children
1044                                .iter()
1045                                .filter(|&&id| id != old_child_id)
1046                                .copied()
1047                                .collect();
1048                        }
1049                    }
1050                }
1051
1052                let children_is_root = render_parent.is_none();
1053                for child in &children[common..] {
1054                    create_ir_node_tree_full(
1055                        ctx,
1056                        child,
1057                        Some(node_id),
1058                        render_parent,
1059                        children_is_root,
1060                    );
1061                }
1062            } else {
1063                for &old_child_id in &old_children {
1064                    remove_subtree(ctx.tree, old_child_id, ctx.patches, ctx.dependencies);
1065                }
1066
1067                if let Some(cond_node) = ctx.tree.get_mut(node_id) {
1068                    cond_node.children.clear();
1069                }
1070            }
1071        }
1072        IRNode::Router {
1073            location,
1074            routes,
1075            fallback,
1076            module_scope,
1077        } => {
1078            // If the existing node isn't a Router, replace it wholesale.
1079            if !existing.is_router() {
1080                let parent_id = existing.parent;
1081                remove_subtree(ctx.tree, node_id, ctx.patches, ctx.dependencies);
1082                create_ir_node_tree_impl(ctx, node, parent_id, parent_id.is_none());
1083                return;
1084            }
1085
1086            let module_scope_ref = ctx.effective_scope(module_scope.as_deref());
1087            let effective_state = ctx.effective_state(module_scope.as_deref());
1088
1089            // Re-register the location dependency in case it was cleared.
1090            if let Value::Binding(binding) = location {
1091                ctx.dependencies
1092                    .add_dependency(node_id, binding, module_scope_ref);
1093            } else if let Value::TemplateString { bindings, .. } = location {
1094                for binding in bindings {
1095                    ctx.dependencies
1096                        .add_dependency(node_id, binding, module_scope_ref);
1097                }
1098            }
1099
1100            let evaluated = evaluate_value(location, effective_state, ctx.data_sources);
1101            let location_str = match &evaluated {
1102                serde_json::Value::String(s) => s.clone(),
1103                serde_json::Value::Null => String::new(),
1104                other => other.to_string(),
1105            };
1106
1107            // Read the current cache state out of the existing node. Each
1108            // Router instance carries its own detached-subtree cache keyed
1109            // by route pattern (see ControlFlowKind::Router).
1110            let (mut cache, prev_route_key, max_cache_size) =
1111                match existing.control_flow.as_ref() {
1112                    Some(ControlFlowKind::Router {
1113                        cache,
1114                        current_route_key,
1115                        max_cache_size,
1116                    }) => (cache.clone(), current_route_key.clone(), *max_cache_size),
1117                    _ => (IndexMap::new(), None, DEFAULT_ROUTER_CACHE_SIZE),
1118                };
1119
1120            let matched = find_matching_route_with_key(
1121                &location_str,
1122                routes,
1123                fallback.as_deref(),
1124            );
1125            let new_route_key = matched.as_ref().map(|(k, _)| k.clone());
1126
1127            // Same route as before — nothing structural to do. Descendant
1128            // nodes get their own dirty marks when state changes; the
1129            // Router only reconciles when the *route* changes.
1130            if prev_route_key.is_some() && prev_route_key == new_route_key {
1131                return;
1132            }
1133
1134            let render_parent = existing.parent;
1135            let old_children: Vec<NodeId> = existing.children.iter().copied().collect();
1136
1137            // Step 1: take the currently-rendered children off the Router.
1138            //   - If we have a prev_route_key → detach them and stash under
1139            //     that key (keep-alive: nodes, deps, props all live on).
1140            //   - If we don't (first reconcile after a cache miss or
1141            //     mismatched prior state) → fall back to hard teardown so
1142            //     we don't leak orphans.
1143            if let Some(prev_key) = prev_route_key.as_ref() {
1144                for &child_id in &old_children {
1145                    // Emit a Detach patch so the renderer unlinks its
1146                    // native node but keeps it alive. The engine-side
1147                    // node stays in the tree; its descendants stay
1148                    // under it; dependencies stay registered so state
1149                    // updates flow through while off-screen.
1150                    ctx.patches.push(Patch::detach(child_id));
1151                    if let Some(child) = ctx.tree.get_mut(child_id) {
1152                        child.parent = None;
1153                    }
1154                }
1155                if !old_children.is_empty() {
1156                    // Replace any prior entry for this key (e.g. if we
1157                    // navigated away, back, and away again).
1158                    cache.shift_remove(prev_key);
1159                    cache.insert(prev_key.clone(), old_children);
1160                }
1161            } else {
1162                for &old_child_id in &old_children {
1163                    remove_subtree(ctx.tree, old_child_id, ctx.patches, ctx.dependencies);
1164                }
1165            }
1166
1167            if let Some(router_node) = ctx.tree.get_mut(node_id) {
1168                router_node.children.clear();
1169            }
1170
1171            // Step 2: LRU-evict old cache entries if we exceeded the cap.
1172            // Dropping an entry means its subtree is gone for good, so
1173            // we tear it down properly (frees nodes + deps).
1174            while cache.len() > max_cache_size {
1175                let evicted_key = cache.keys().next().cloned();
1176                if let Some(evicted_key) = evicted_key {
1177                    if let Some(evicted_ids) = cache.shift_remove(&evicted_key) {
1178                        for evicted_id in evicted_ids {
1179                            remove_subtree(
1180                                ctx.tree,
1181                                evicted_id,
1182                                ctx.patches,
1183                                ctx.dependencies,
1184                            );
1185                        }
1186                    }
1187                } else {
1188                    break;
1189                }
1190            }
1191
1192            // Step 3: attach the new route's subtree — reuse cached
1193            // nodes if we've seen this route before, otherwise build
1194            // from scratch.
1195            if let Some(new_key) = new_route_key.as_ref() {
1196                if let Some(cached_ids) = cache.shift_remove(new_key) {
1197                    // Cache hit: reattach the detached subtrees in order.
1198                    // When the Router is itself the IR root, its own
1199                    // NodeId is a control-flow pseudo-node the renderer
1200                    // never created — attach to "root" in that case.
1201                    for cached_id in &cached_ids {
1202                        if let Some(child) = ctx.tree.get_mut(*cached_id) {
1203                            child.parent = Some(node_id);
1204                        }
1205                        if let Some(router_node) = ctx.tree.get_mut(node_id) {
1206                            router_node.children.push_back(*cached_id);
1207                        }
1208                        let attach_patch = match render_parent {
1209                            Some(rp) => Patch::attach(rp, *cached_id, None),
1210                            None => Patch::attach_root(*cached_id, None),
1211                        };
1212                        ctx.patches.push(attach_patch);
1213                    }
1214                } else if let Some((_, children)) = matched.as_ref() {
1215                    // Cache miss: build fresh IR tree for this route.
1216                    // When the Router is at the IR root (render_parent
1217                    // is None) the children must be inserted at "root";
1218                    // that's what `is_root` signals to create_element_node.
1219                    let children_is_root = render_parent.is_none();
1220                    for child in *children {
1221                        create_ir_node_tree_full(
1222                            ctx,
1223                            child,
1224                            Some(node_id),
1225                            render_parent,
1226                            children_is_root,
1227                        );
1228                    }
1229                }
1230            }
1231
1232            // Step 4: write the updated cache + current route back to
1233            // the Router node. If nothing matched (no route, no
1234            // fallback) we keep the cache but clear current_route_key
1235            // so the next reconcile treats this as a fresh state.
1236            if let Some(router_node) = ctx.tree.get_mut(node_id) {
1237                router_node.control_flow = Some(ControlFlowKind::Router {
1238                    cache,
1239                    current_route_key: new_route_key,
1240                    max_cache_size,
1241                });
1242            }
1243        }
1244    }
1245}
1246
1247/// Remove a subtree and generate Remove patches
1248fn remove_subtree(
1249    tree: &mut InstanceTree,
1250    node_id: NodeId,
1251    patches: &mut Vec<Patch>,
1252    dependencies: &mut DependencyGraph,
1253) {
1254    let ids = collect_subtree_ids(tree, node_id);
1255    for &id in &ids {
1256        patches.push(Patch::remove(id));
1257        dependencies.remove_node(id);
1258    }
1259    tree.remove(node_id);
1260}
1261
1262#[cfg(test)]
1263mod tests {
1264    use super::*;
1265    use crate::ir::Value;
1266    use serde_json::json;
1267
1268    #[test]
1269    fn test_create_simple_tree() {
1270        use crate::reactive::DependencyGraph;
1271
1272        let mut tree = InstanceTree::new();
1273        let mut patches = Vec::new();
1274        let mut dependencies = DependencyGraph::new();
1275
1276        let element = Element::new("Column")
1277            .with_child(Element::new("Text").with_prop("text", Value::Static(json!("Hello"))));
1278
1279        let state = json!({});
1280        let mut ctx = ReconcileCtx {
1281            tree: &mut tree,
1282            state: &state,
1283            patches: &mut patches,
1284            dependencies: &mut dependencies,
1285            data_sources: None,
1286            modules: None,
1287        };
1288        create_element_node(&mut ctx, &element, None, None, true);
1289
1290        // Should create 2 nodes (Column + Text) + 2 Inserts (root + child)
1291        // Create Column, Insert Column into root, Create Text, Insert Text into Column
1292        assert_eq!(patches.len(), 4);
1293
1294        // Verify root insert patch exists
1295        let root_insert = patches
1296            .iter()
1297            .find(|p| matches!(p, Patch::Insert { parent_id, .. } if parent_id == "root"));
1298        assert!(root_insert.is_some(), "Root insert patch should exist");
1299    }
1300
1301    #[test]
1302    fn test_diff_props() {
1303        let node_id = NodeId::default();
1304        let old = indexmap::indexmap! {
1305            "color".to_string() => json!("red"),
1306            "size".to_string() => json!(16),
1307        };
1308        let new = indexmap::indexmap! {
1309            "color".to_string() => json!("blue"),
1310            "size".to_string() => json!(16),
1311        };
1312
1313        let patches = diff_props(node_id, &old, &new);
1314
1315        // Only color changed
1316        assert_eq!(patches.len(), 1);
1317    }
1318}