Skip to main content

hypen_engine/reconcile/
diff.rs

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