Skip to main content

hypen_engine/reconcile/
diff.rs

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