Skip to main content

pax_runtime/
repeat.rs

1use std::collections::{HashMap, HashSet};
2use std::iter;
3use std::rc::Rc;
4use_RefCell!();
5
6use pax_language::Computable;
7use pax_manifest::ExpressionInfo;
8use pax_runtime_api::CoercionRules;
9use pax_runtime_api::{
10    borrow, borrow_mut, use_RefCell, ImplToFromPaxAny, PaxValue, Property, ToPaxValue, Variable,
11};
12
13use crate::api::Layer;
14use crate::{
15    BaseInstance, ExpandedNode, InstanceFlags, InstanceNode, InstantiationArgs, RuntimeContext,
16    RuntimePropertiesStackFrame,
17};
18
19mod value_eq;
20
21fn update_repeat_bindings(
22    index: &Property<usize>,
23    item: &Property<PaxValue>,
24    i: usize,
25    value: PaxValue,
26) {
27    index.set_if_neq(i);
28    // PaxValue's language equality is approximate. Invalidation requires exact
29    // representation equality, otherwise small animation/data edits disappear.
30    if !item.read(|previous| value_eq::same_value(previous, &value)) {
31        item.set(value);
32    }
33}
34
35const INTERNAL_REPEAT_INDEX_SYMBOL: &str = "$repeat_index";
36const INTERNAL_REPEAT_ELEMENT_SYMBOL: &str = "$repeat_element";
37const INTERNAL_REPEAT_KEY_SYMBOL: &str = "$repeat_key";
38
39/// A special "control-flow" primitive associated with the `for` statement.
40/// Repeat allows for nodes to be rendered dynamically per data specified in `source_expression`.
41/// That is: for a `source_expression` of length `n`, `Repeat` will render its
42/// template `n` times, each with an embedded component context (`RepeatItem`)
43/// with an index `i` and a pointer to that relevant datum `source_expression[i]`
44pub struct RepeatInstance {
45    pub base: BaseInstance,
46}
47
48impl ImplToFromPaxAny for RepeatProperties {}
49///Contains modal _vec_ and _range_ variants, describing whether the Repeat source
50///is encoded as a `Vec<T>` (where T is a `PaxValue` properties type) or as a `Range<isize>`
51#[derive(Default)]
52pub struct RepeatProperties {
53    pub source_expression: Property<PaxValue>,
54    pub iterator_i_symbol: Property<Option<String>>,
55    pub iterator_elem_symbol: Property<Option<String>>,
56    pub repeat_key_expression: Option<ExpressionInfo>,
57}
58
59impl ToPaxValue for RepeatProperties {
60    fn to_pax_value(self) -> PaxValue {
61        PaxValue::Object(
62            vec![
63                (
64                    "source_expression".to_string(),
65                    self.source_expression.to_pax_value(),
66                ),
67                (
68                    "iterator_i_symbol".to_string(),
69                    self.iterator_i_symbol.to_pax_value(),
70                ),
71                (
72                    "iterator_elem_symbol".to_string(),
73                    self.iterator_elem_symbol.to_pax_value(),
74                ),
75            ]
76            .into_iter()
77            .collect(),
78        )
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use crate::api::math::Transform2;
86    use crate::api::CommonProperties;
87    use crate::{ComponentInstance, ExpandedNode, Globals, RouteLocation, TransformAndBounds};
88    use pax_language::interpreter::property_resolution::IdentifierResolver;
89    use pax_language::parse_pax_expression;
90    use pax_manifest::cartridge_generation::{
91        ComponentTransitionConfig, TRANSITION_PHASE_ENTER, TRANSITION_PHASE_EXIT,
92        TRANSITION_PHASE_IDLE,
93    };
94    use pax_runtime_api::pax_value::{PaxAny, ToFromPaxAny};
95    use pax_runtime_api::{borrow, CoercionRules, Numeric, Platform, Size, TargetInfo, OS};
96    use std::cell::RefCell;
97
98    fn test_globals() -> Globals {
99        Globals {
100            elapsed_frames: Property::new(0),
101            elapsed_millis: Property::new(0),
102            viewport: Property::new(TransformAndBounds {
103                transform: Transform2::identity(),
104                bounds: (100.0, 100.0),
105            }),
106            gyro: Property::new(Default::default()),
107            accel: Property::new(Default::default()),
108            route_location: Property::new(RouteLocation::root()),
109            browser_allows_scroller_vector_layers: Property::new(true),
110            browser_allows_nested_scroller_vector_layers: Property::new(true),
111            platform: Platform::Unknown,
112            os: OS::Unknown,
113            target: TargetInfo::new(Platform::Unknown, OS::Unknown),
114            get_elapsed_millis: Rc::new(|| 0),
115        }
116    }
117
118    fn default_properties_factory() -> Box<
119        dyn Fn(
120            Rc<RuntimePropertiesStackFrame>,
121            Option<Rc<ExpandedNode>>,
122        ) -> Option<Rc<RefCell<PaxAny>>>,
123    > {
124        Box::new(|_, expanded_node| {
125            expanded_node
126                .is_none()
127                .then(|| Rc::new(RefCell::new(PaxAny::Builtin(PaxValue::default()))))
128        })
129    }
130
131    fn default_common_properties_factory() -> Box<
132        dyn Fn(
133            Rc<RuntimePropertiesStackFrame>,
134            Option<Rc<ExpandedNode>>,
135        ) -> Option<Rc<RefCell<CommonProperties>>>,
136    > {
137        Box::new(|_, expanded_node| {
138            expanded_node
139                .is_none()
140                .then(|| Rc::new(RefCell::new(CommonProperties::default())))
141        })
142    }
143
144    fn component_args(template: Option<Vec<Rc<dyn InstanceNode>>>) -> InstantiationArgs {
145        InstantiationArgs {
146            prototypical_common_properties: crate::CommonPropertiesInit::Factory(
147                default_common_properties_factory(),
148            ),
149            prototypical_properties: crate::PropertiesInit::Factory(default_properties_factory()),
150            handler_registry: None,
151            children: None,
152            component_template: template.map(RefCell::new),
153            component_settings: None,
154            template_node_identifier: None,
155            template_node_type_id: None,
156            template_node_selector_info: None,
157            transition_config: Default::default(),
158            properties_scope: crate::PropertiesScopeInit::None,
159        }
160    }
161
162    fn leaf_args(transition_config: ComponentTransitionConfig) -> InstantiationArgs {
163        InstantiationArgs {
164            prototypical_common_properties: crate::CommonPropertiesInit::Factory(
165                default_common_properties_factory(),
166            ),
167            prototypical_properties: crate::PropertiesInit::Factory(default_properties_factory()),
168            handler_registry: None,
169            children: None,
170            component_template: Some(RefCell::new(Vec::new())),
171            component_settings: None,
172            template_node_identifier: None,
173            template_node_type_id: None,
174            template_node_selector_info: None,
175            transition_config,
176            properties_scope: crate::PropertiesScopeInit::None,
177        }
178    }
179
180    fn repeat_args(
181        source: Property<PaxValue>,
182        children: Vec<Rc<dyn InstanceNode>>,
183    ) -> InstantiationArgs {
184        repeat_args_with_key(source, children, true)
185    }
186
187    fn unkeyed_repeat_args(
188        source: Property<PaxValue>,
189        children: Vec<Rc<dyn InstanceNode>>,
190    ) -> InstantiationArgs {
191        repeat_args_with_key(source, children, false)
192    }
193
194    fn repeat_args_with_key(
195        source: Property<PaxValue>,
196        children: Vec<Rc<dyn InstanceNode>>,
197        use_key: bool,
198    ) -> InstantiationArgs {
199        let key_expression = ExpressionInfo::new(parse_pax_expression("item.id").unwrap());
200        let source_for_factory = source.clone();
201        InstantiationArgs {
202            prototypical_common_properties: crate::CommonPropertiesInit::Factory(
203                default_common_properties_factory(),
204            ),
205            prototypical_properties: crate::PropertiesInit::Factory(Box::new(
206                move |_, expanded_node| {
207                    expanded_node.is_none().then(|| {
208                        Rc::new(RefCell::new(
209                            RepeatProperties {
210                                source_expression: source_for_factory.clone(),
211                                iterator_i_symbol: Property::new(Some("i".to_string())),
212                                iterator_elem_symbol: Property::new(Some("item".to_string())),
213                                repeat_key_expression: use_key.then(|| key_expression.clone()),
214                            }
215                            .to_pax_any(),
216                        ))
217                    })
218                },
219            )),
220            handler_registry: None,
221            children: Some(RefCell::new(children)),
222            component_template: None,
223            component_settings: None,
224            template_node_identifier: None,
225            template_node_type_id: None,
226            template_node_selector_info: None,
227            transition_config: Default::default(),
228            properties_scope: crate::PropertiesScopeInit::None,
229        }
230    }
231
232    fn positioned_leaf_args() -> InstantiationArgs {
233        InstantiationArgs {
234            prototypical_common_properties: crate::CommonPropertiesInit::Factory(Box::new(
235                |env, expanded_node| {
236                    if expanded_node.is_some() {
237                        return None;
238                    }
239                    let i_untyped = env.resolve_symbol_as_erased_property("i").unwrap();
240                    let i = Property::<usize>::new_from_untyped(i_untyped.clone());
241                    let deps = [i_untyped];
242
243                    let mut cp = CommonProperties::default();
244                    cp.x = Property::computed(
245                        move || Some(Size::Pixels((i.get() as f64 * 10.0).into())),
246                        &deps,
247                    );
248                    cp.width = Property::new(Some(Size::Pixels(10.into())));
249                    cp.height = Property::new(Some(Size::Pixels(10.into())));
250                    Some(Rc::new(RefCell::new(cp)))
251                },
252            )),
253            prototypical_properties: crate::PropertiesInit::Factory(default_properties_factory()),
254            handler_registry: None,
255            children: None,
256            component_template: Some(RefCell::new(Vec::new())),
257            component_settings: None,
258            template_node_identifier: None,
259            template_node_type_id: None,
260            template_node_selector_info: None,
261            transition_config: Default::default(),
262            properties_scope: crate::PropertiesScopeInit::None,
263        }
264    }
265
266    fn transitioned_positioned_leaf_args() -> InstantiationArgs {
267        let mut args = positioned_leaf_args();
268        args.transition_config = ComponentTransitionConfig {
269            has_enter: true,
270            enter_frame_count: 10,
271            has_exit: true,
272            exit_frame_count: 10,
273            timeout_ms: 5_000,
274            ..Default::default()
275        };
276        args
277    }
278
279    fn expression_positioned_leaf_args(expr: &str) -> InstantiationArgs {
280        let expression = ExpressionInfo::new(parse_pax_expression(expr).unwrap());
281        InstantiationArgs {
282            prototypical_common_properties: crate::CommonPropertiesInit::Factory(Box::new(
283                move |env, expanded_node| {
284                    if expanded_node.is_some() {
285                        return None;
286                    }
287                    let mut deps = Vec::new();
288                    for dependency in &expression.dependencies {
289                        let property = env
290                            .resolve_symbol_as_erased_property(dependency)
291                            .unwrap_or_else(|| panic!("missing dependency: {dependency}"));
292                        deps.push(property);
293                    }
294
295                    let expression_for_eval = expression.clone();
296                    let env_for_eval = Rc::clone(&env);
297                    let mut cp = CommonProperties::default();
298                    cp.x = Property::computed(
299                        move || {
300                            let env_for_eval: Rc<dyn IdentifierResolver> = env_for_eval.clone();
301                            expression_for_eval
302                                .expression
303                                .compute(env_for_eval)
304                                .ok()
305                                .and_then(|value| Size::try_coerce(value).ok())
306                        },
307                        &deps,
308                    );
309                    cp.width = Property::new(Some(Size::Pixels(10.into())));
310                    cp.height = Property::new(Some(Size::Pixels(10.into())));
311                    Some(Rc::new(RefCell::new(cp)))
312                },
313            )),
314            prototypical_properties: crate::PropertiesInit::Factory(default_properties_factory()),
315            handler_registry: None,
316            children: None,
317            component_template: Some(RefCell::new(Vec::new())),
318            component_settings: None,
319            template_node_identifier: None,
320            template_node_type_id: None,
321            template_node_selector_info: None,
322            transition_config: Default::default(),
323            properties_scope: crate::PropertiesScopeInit::None,
324        }
325    }
326
327    fn transitioned_expression_positioned_leaf_args(expr: &str) -> InstantiationArgs {
328        let mut args = expression_positioned_leaf_args(expr);
329        args.transition_config = ComponentTransitionConfig {
330            has_enter: true,
331            enter_frame_count: 10,
332            has_exit: true,
333            exit_frame_count: 10,
334            timeout_ms: 5_000,
335            ..Default::default()
336        };
337        args
338    }
339
340    fn item(id: &str) -> PaxValue {
341        item_with_x(id, 0.0)
342    }
343
344    fn item_with_x(id: &str, x: f64) -> PaxValue {
345        PaxValue::Object(
346            vec![
347                ("id".to_string(), PaxValue::String(id.to_string())),
348                ("x".to_string(), x.to_pax_value()),
349            ]
350            .into_iter()
351            .collect(),
352        )
353    }
354
355    fn source(ids: &[&str]) -> PaxValue {
356        PaxValue::Vec(ids.iter().map(|id| item(id)).collect())
357    }
358
359    fn source_with_x(entries: &[(&str, f64)]) -> PaxValue {
360        PaxValue::Vec(entries.iter().map(|(id, x)| item_with_x(id, *x)).collect())
361    }
362
363    fn ids(nodes: &[Rc<ExpandedNode>]) -> Vec<u32> {
364        nodes.iter().map(|node| node.id.0).collect()
365    }
366
367    fn mounted_repeat(source: Property<PaxValue>) -> (Rc<ExpandedNode>, Rc<RuntimeContext>) {
368        let leaf: Rc<dyn InstanceNode> =
369            ComponentInstance::instantiate(leaf_args(Default::default()));
370        let repeat: Rc<dyn InstanceNode> =
371            RepeatInstance::instantiate(repeat_args(source, vec![leaf]));
372        let root = ComponentInstance::instantiate(component_args(Some(vec![repeat])));
373        let context = Rc::new(RuntimeContext::new(test_globals()));
374        let root = ExpandedNode::initialize_root(root, &context);
375        root.recurse_update(&context);
376        context.drain_node_effects();
377        (root, context)
378    }
379
380    fn binding_probe(
381        child: &ExpandedNode,
382        symbol: &str,
383    ) -> (Property<()>, Rc<std::cell::Cell<usize>>) {
384        let property = child
385            .stack
386            .resolve_symbol_as_erased_property(symbol)
387            .unwrap();
388        let runs = Rc::new(std::cell::Cell::new(0));
389        let count = Rc::clone(&runs);
390        let probe = Property::computed(move || count.set(count.get() + 1), &[property]);
391        probe.get();
392        (probe, runs)
393    }
394
395    #[test]
396    fn keyed_repeat_only_notifies_changed_items_and_indices() {
397        let source_property = Property::new(source_with_x(&[("a", 1.0), ("b", 2.0)]));
398        let (root, context) = mounted_repeat(source_property.clone());
399        let repeat = root.children.get().remove(0);
400        let children = repeat.children.get();
401        let probes: Vec<_> = children
402            .iter()
403            .flat_map(|child| [binding_probe(child, "item"), binding_probe(child, "i")])
404            .collect();
405        let counts = || {
406            probes
407                .iter()
408                .map(|(probe, runs)| {
409                    probe.get();
410                    runs.get()
411                })
412                .collect::<Vec<_>>()
413        };
414
415        source_property.set(source_with_x(&[("a", 1.0), ("b", 2.0)]));
416        context.drain_node_effects();
417        assert_eq!(ids(&repeat.children.get()), ids(&children));
418        assert_eq!(
419            counts(),
420            [1, 1, 1, 1],
421            "identical source must not notify retained items"
422        );
423
424        source_property.set(source_with_x(&[("a", 1.0 + 1e-8), ("b", 2.0)]));
425        context.drain_node_effects();
426        assert_eq!(
427            counts(),
428            [2, 1, 1, 1],
429            "even sub-epsilon edits must propagate"
430        );
431
432        source_property.set(source_with_x(&[("b", 2.0), ("a", 1.0 + 1e-8)]));
433        context.drain_node_effects();
434        assert_eq!(
435            ids(&repeat.children.get()),
436            [children[1].id.0, children[0].id.0]
437        );
438        assert_eq!(counts(), [2, 2, 1, 2], "reordering only changes indices");
439    }
440
441    #[test]
442    fn keyed_repeat_reuses_reordered_children_and_exits_removed_keys() {
443        let source_property = Property::new(source(&["a", "b", "c"]));
444        let leaf_transition_config = ComponentTransitionConfig {
445            has_exit: true,
446            exit_frame_count: 30,
447            timeout_ms: 5_000,
448            ..Default::default()
449        };
450        let leaf: Rc<dyn InstanceNode> =
451            ComponentInstance::instantiate(leaf_args(leaf_transition_config));
452        let repeat: Rc<dyn InstanceNode> =
453            RepeatInstance::instantiate(repeat_args(source_property.clone(), vec![leaf]));
454        let root_component =
455            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&repeat)])));
456        let context = Rc::new(RuntimeContext::new(test_globals()));
457        let root = ExpandedNode::initialize_root(root_component, &context);
458
459        root.recurse_update(&context);
460        let repeat_node = root.children.get().remove(0);
461        let initial = repeat_node.children.get();
462        assert_eq!(initial.len(), 3);
463        let initial_ids = ids(&initial);
464
465        source_property.set(source(&["c", "a", "b"]));
466        root.recurse_update(&context);
467        let reordered_active = borrow!(repeat_node.active_children).clone();
468        assert_eq!(
469            ids(&reordered_active),
470            vec![initial_ids[2], initial_ids[0], initial_ids[1]]
471        );
472        assert!(borrow!(repeat_node.exiting_children).is_empty());
473
474        source_property.set(source(&["c", "d", "a"]));
475        root.recurse_update(&context);
476        let active = borrow!(repeat_node.active_children).clone();
477        let exiting = borrow!(repeat_node.exiting_children).clone();
478
479        assert_eq!(active.len(), 3);
480        assert_eq!(exiting.len(), 1);
481        assert_eq!(active[0].id.0, initial_ids[2]);
482        assert_ne!(active[1].id.0, initial_ids[0]);
483        assert_ne!(active[1].id.0, initial_ids[1]);
484        assert_ne!(active[1].id.0, initial_ids[2]);
485        assert_eq!(active[2].id.0, initial_ids[0]);
486        assert_eq!(exiting[0].id.0, initial_ids[1]);
487        assert_eq!(exiting[0].transition_phase.get(), TRANSITION_PHASE_EXIT);
488
489        source_property.set(source(&["c", "b", "a"]));
490        root.recurse_update(&context);
491        let active = borrow!(repeat_node.active_children).clone();
492        assert_eq!(active[1].id.0, initial_ids[1]);
493        assert_eq!(borrow!(repeat_node.mounted_children).len(), 4);
494        assert_eq!(active[1].transition_phase.get(), TRANSITION_PHASE_IDLE);
495    }
496
497    #[test]
498    fn retained_children_keep_parent_bindings_on_data_updates_and_reorder() {
499        let source_property = Property::new(source_with_x(&[("a", 1.0), ("b", 2.0)]));
500        let (root, context) = mounted_repeat(source_property.clone());
501        let repeat = root.children.get().remove(0);
502        let children = repeat.children.get();
503        let initial: Vec<_> = children
504            .iter()
505            .map(|c| c.parent_binding_rebuilds.get())
506            .collect();
507        for value in [
508            source_with_x(&[("a", 3.0), ("b", 2.0)]),
509            source_with_x(&[("b", 2.0), ("a", 3.0)]),
510        ] {
511            source_property.set(value);
512            context.drain_node_effects();
513            assert_eq!(
514                children
515                    .iter()
516                    .map(|c| c.parent_binding_rebuilds.get())
517                    .collect::<Vec<_>>(),
518                initial
519            );
520        }
521        source_property.set(source_with_x(&[("b", 2.0), ("a", 3.0), ("c", 4.0)]));
522        context.drain_node_effects();
523        assert_eq!(
524            children
525                .iter()
526                .map(|c| c.parent_binding_rebuilds.get())
527                .collect::<Vec<_>>(),
528            initial
529        );
530        assert_eq!(repeat.children.get().len(), 3);
531    }
532
533    #[test]
534    fn equal_valued_parent_sources_are_not_interchangeable() {
535        let (root, context) = mounted_repeat(Property::new(source(&["a"])));
536        let repeat = root.children.get().remove(0);
537        let children = repeat.children.get();
538        let child = &children[0];
539        let first = Property::new(None);
540        let second = Property::new(None);
541        repeat.attach_children(children.clone(), &context, &first);
542        let n = child.parent_binding_rebuilds.get();
543        repeat.attach_children(children.clone(), &context, &first);
544        assert_eq!(child.parent_binding_rebuilds.get(), n);
545        repeat.attach_children(children.clone(), &context, &second);
546        assert_eq!(child.parent_binding_rebuilds.get(), n + 1);
547        first.set(Some(root.id));
548        assert_eq!(
549            child.parent_frame.get(),
550            None,
551            "old frame source must be disconnected"
552        );
553        second.set(Some(repeat.id));
554        assert_eq!(child.parent_frame.get(), Some(repeat.id));
555
556        // Existing subscriptions must follow parent values without reattachment.
557        let cp = repeat.get_common_properties();
558        borrow!(cp)
559            .opacity
560            .set(Some(pax_runtime_api::Opacity::from(0.25)));
561        assert_eq!(child.computed_opacity.get(), 0.25);
562        assert_eq!(child.parent_binding_rebuilds.get(), n + 1);
563    }
564
565    #[test]
566    fn data_only_reconciliation_skips_structure_and_churn_releases_properties() {
567        let source_property = Property::new(source_with_x(&[("a", 1.0), ("b", 2.0)]));
568        let (root, context) = mounted_repeat(source_property.clone());
569        let repeat = root.children.get().remove(0);
570        let counts = || {
571            (
572                repeat.layout_hull_rebuilds.get(),
573                repeat.structural_children_updates.get(),
574            )
575        };
576        let initial = counts();
577        source_property.set(source_with_x(&[("a", 8.0), ("b", 2.0)]));
578        context.drain_node_effects();
579        assert_eq!(counts(), initial);
580        let settled = pax_runtime_api::properties::property_table_total_properties_count();
581        for _ in 0..40 {
582            source_property.set(source_with_x(&[("a", 8.0), ("b", 2.0), ("c", 3.0)]));
583            context.drain_node_effects();
584            source_property.set(source_with_x(&[("a", 8.0), ("b", 2.0)]));
585            context.drain_node_effects();
586            assert_eq!(
587                pax_runtime_api::properties::property_table_total_properties_count(),
588                settled
589            );
590        }
591    }
592
593    #[test]
594    fn nested_repeats_do_not_reconcile_unchanged_sibling_payloads() {
595        let data = |x| {
596            PaxValue::Vec(vec![
597                PaxValue::Object(vec![
598                    ("id".into(), PaxValue::String("a".into())),
599                    ("parts".into(), source_with_x(&[("part", x)])),
600                ]),
601                PaxValue::Object(vec![
602                    ("id".into(), PaxValue::String("b".into())),
603                    ("parts".into(), source_with_x(&[("part", 2.0)])),
604                ]),
605            ])
606        };
607        let source_property = Property::new(data(1.0));
608        let source_evaluations = Rc::new(std::cell::Cell::new(0));
609        let evaluations = source_evaluations.clone();
610        let leaf: Rc<dyn InstanceNode> =
611            ComponentInstance::instantiate(leaf_args(Default::default()));
612        let mut inner_args = repeat_args(Property::new(PaxValue::default()), vec![leaf]);
613        inner_args.prototypical_properties =
614            crate::PropertiesInit::Factory(Box::new(move |env, node| {
615                if node.is_some() {
616                    return None;
617                }
618                let outer_item = Property::<PaxValue>::new_from_untyped(
619                    env.resolve_symbol_as_erased_property("item").unwrap(),
620                );
621                let dep = outer_item.untyped();
622                let evaluations = evaluations.clone();
623                let source_expression = Property::computed(
624                    move || {
625                        evaluations.set(evaluations.get() + 1);
626                        let PaxValue::Object(fields) = outer_item.get() else {
627                            panic!("expected item object")
628                        };
629                        fields
630                            .into_iter()
631                            .find(|(name, _)| name == "parts")
632                            .unwrap()
633                            .1
634                    },
635                    &[dep],
636                );
637                Some(Rc::new(RefCell::new(
638                    RepeatProperties {
639                        source_expression,
640                        iterator_i_symbol: Property::new(Some("i".into())),
641                        iterator_elem_symbol: Property::new(Some("item".into())),
642                        repeat_key_expression: Some(ExpressionInfo::new(
643                            parse_pax_expression("item.id").unwrap(),
644                        )),
645                    }
646                    .to_pax_any(),
647                )))
648            }));
649        let inner: Rc<dyn InstanceNode> = RepeatInstance::instantiate(inner_args);
650        let outer: Rc<dyn InstanceNode> =
651            RepeatInstance::instantiate(repeat_args(source_property.clone(), vec![inner]));
652        let root = ComponentInstance::instantiate(component_args(Some(vec![outer])));
653        let context = Rc::new(RuntimeContext::new(test_globals()));
654        let root = ExpandedNode::initialize_root(root, &context);
655        root.recurse_update(&context);
656        context.drain_node_effects();
657        let outer = root.children.get().remove(0);
658        let inners = outer.children.get();
659        let first = inners[0].children.get()[0].clone();
660        let other = inners[1].children.get()[0].clone();
661        let (first_probe, first_runs) = binding_probe(&first, "item");
662        let (other_probe, other_runs) = binding_probe(&other, "item");
663        let evaluations = source_evaluations.get();
664        source_property.set(data(1.0));
665        context.drain_node_effects();
666        assert_eq!(source_evaluations.get(), evaluations);
667        source_property.set(data(1.0 + 1e-8));
668        context.drain_node_effects();
669        first_probe.get();
670        other_probe.get();
671        assert_eq!(source_evaluations.get(), evaluations + 1);
672        assert_eq!((first_runs.get(), other_runs.get()), (2, 1));
673        assert_eq!(inners[0].children.get()[0].id, first.id);
674        assert_eq!(inners[1].children.get()[0].id, other.id);
675    }
676
677    #[test]
678    fn parent_values_resize_reactively_and_replaced_sources_rebind() {
679        let (root, context) = mounted_repeat(Property::new(source(&["a"])));
680        let repeat = root.children.get().remove(0);
681        let children = repeat.children.get();
682        let child = &children[0];
683        let parent_cp = repeat.get_common_properties();
684        let child_cp = child.get_common_properties();
685        borrow!(parent_cp).width.set(Some(Size::Pixels(100.into())));
686        borrow!(child_cp).width.set(Some(Size::Percent(50.into())));
687        assert_eq!(child.transform_and_bounds.get().bounds.0, 50.0);
688        let rebuilds = child.parent_binding_rebuilds.get();
689        borrow!(parent_cp).width.set(Some(Size::Pixels(200.into())));
690        assert_eq!(child.transform_and_bounds.get().bounds.0, 100.0);
691        repeat.attach_children(children.clone(), &context, &repeat.parent_frame);
692        assert_eq!(child.parent_binding_rebuilds.get(), rebuilds);
693
694        let old_opacity = borrow!(child_cp).opacity.clone();
695        let new_opacity = Property::new(old_opacity.get());
696        borrow_mut!(child_cp).opacity = new_opacity.clone();
697        repeat.attach_children(children.clone(), &context, &repeat.parent_frame);
698        assert_eq!(child.parent_binding_rebuilds.get(), rebuilds + 1);
699        old_opacity.set(Some(0.25.into()));
700        assert_eq!(child.computed_opacity.get(), 1.0);
701        new_opacity.set(Some(0.75.into()));
702        assert_eq!(child.computed_opacity.get(), 0.75);
703
704        let template = borrow!(child.instance_node).clone();
705        child.recreate_with_new_data(template, &context);
706        context.drain_node_effects();
707        let rebuilt = child.parent_binding_rebuilds.get();
708        repeat.attach_children(children.clone(), &context, &repeat.parent_frame);
709        assert_eq!(
710            child.parent_binding_rebuilds.get(),
711            rebuilt + 1,
712            "reload must invalidate the binding fast path"
713        );
714    }
715
716    #[test]
717    fn repeat_exit_partition_and_final_removal_are_structural_changes() {
718        let source_property = Property::new(source(&["a"]));
719        let config = ComponentTransitionConfig {
720            has_exit: true,
721            exit_frame_count: 5,
722            timeout_ms: 5_000,
723            ..Default::default()
724        };
725        let leaf: Rc<dyn InstanceNode> = ComponentInstance::instantiate(leaf_args(config));
726        let repeat: Rc<dyn InstanceNode> =
727            RepeatInstance::instantiate(repeat_args(source_property.clone(), vec![leaf]));
728        let root = ComponentInstance::instantiate(component_args(Some(vec![repeat])));
729        let context = Rc::new(RuntimeContext::new(test_globals()));
730        let root = ExpandedNode::initialize_root(root, &context);
731        root.recurse_update(&context);
732        context.drain_node_effects();
733        let repeat = root.children.get().remove(0);
734        let id = repeat.children.get()[0].id;
735        let before_exit = repeat.structural_children_updates.get();
736        source_property.set(source(&[]));
737        context.drain_node_effects();
738        assert_eq!(repeat.children.get()[0].id, id);
739        assert!(
740            repeat.structural_children_updates.get() > before_exit,
741            "active-to-exiting affects projection even with identical rendered IDs"
742        );
743        let before_prune = repeat.structural_children_updates.get();
744        context.globals().elapsed_frames.set(10);
745        context.drain_node_effects();
746        assert!(repeat.children.get().is_empty());
747        assert!(repeat.structural_children_updates.get() > before_prune);
748
749        // Cleanup and a new selection in the same tick must both be observed.
750        source_property.set(source(&["a"]));
751        context.drain_node_effects();
752        source_property.set(source(&[]));
753        context.drain_node_effects();
754        context.globals().elapsed_frames.set(20);
755        source_property.set(source(&["b"]));
756        context.drain_node_effects();
757        assert_eq!(repeat.children.get().len(), 1);
758        assert!(borrow!(repeat.exiting_children).is_empty());
759    }
760
761    #[test]
762    fn external_key_dependencies_and_invalid_key_fallback_remain_reactive() {
763        let source_property = Property::new(source(&["a"]));
764        let leaf: Rc<dyn InstanceNode> =
765            ComponentInstance::instantiate(leaf_args(Default::default()));
766        let mut args = repeat_args(source_property.clone(), vec![leaf]);
767        let source_copy = source_property.clone();
768        args.prototypical_properties = crate::PropertiesInit::Factory(Box::new(move |_, node| {
769            node.is_none().then(|| {
770                Rc::new(RefCell::new(
771                    RepeatProperties {
772                        source_expression: source_copy.clone(),
773                        iterator_i_symbol: Property::new(Some("i".into())),
774                        iterator_elem_symbol: Property::new(Some("item".into())),
775                        repeat_key_expression: Some(ExpressionInfo::new(
776                            parse_pax_expression("$frames").unwrap(),
777                        )),
778                    }
779                    .to_pax_any(),
780                ))
781            })
782        }));
783        let repeat: Rc<dyn InstanceNode> = RepeatInstance::instantiate(args);
784        let root = ComponentInstance::instantiate(component_args(Some(vec![repeat])));
785        let context = Rc::new(RuntimeContext::new(test_globals()));
786        let root = ExpandedNode::initialize_root(root, &context);
787        root.recurse_update(&context);
788        context.drain_node_effects();
789        let repeat = root.children.get().remove(0);
790        let first_id = repeat.children.get()[0].id;
791        context.globals().elapsed_frames.set(1);
792        context.drain_node_effects();
793        assert_ne!(repeat.children.get()[0].id, first_id);
794
795        let invalid = PaxValue::Object(vec![("id".into(), PaxValue::Bool(true))]);
796        let data = PaxValue::Vec(vec![item("same"), item("same"), invalid]);
797        let source_property = Property::new(data.clone());
798        let (root, context) = mounted_repeat(source_property.clone());
799        let repeat = root.children.get().remove(0);
800        let first_ids = ids(&repeat.children.get());
801        assert_eq!(first_ids.iter().collect::<HashSet<_>>().len(), 3);
802        source_property.set(data);
803        context.drain_node_effects();
804        assert_eq!(ids(&repeat.children.get()), first_ids);
805    }
806
807    #[test]
808    fn keyed_groups_keep_all_template_roots_together_on_reorder() {
809        let source_property = Property::new(source(&["a", "b"]));
810        let leaf: Rc<dyn InstanceNode> =
811            ComponentInstance::instantiate(leaf_args(Default::default()));
812        let repeat: Rc<dyn InstanceNode> = RepeatInstance::instantiate(repeat_args(
813            source_property.clone(),
814            vec![leaf.clone(), leaf],
815        ));
816        let context = Rc::new(RuntimeContext::new(test_globals()));
817        let root = ExpandedNode::initialize_root(
818            ComponentInstance::instantiate(component_args(Some(vec![repeat]))),
819            &context,
820        );
821        root.recurse_update(&context);
822        context.drain_node_effects();
823        let repeat = root.children.get().remove(0);
824        let initial = ids(&repeat.children.get());
825        assert_eq!(initial.len(), 4);
826        source_property.set(source(&["b", "a"]));
827        context.drain_node_effects();
828        assert_eq!(
829            ids(&repeat.children.get()),
830            [initial[2], initial[3], initial[0], initial[1]]
831        );
832    }
833
834    #[test]
835    fn detached_mask_style_repeat_binds_inserted_children_and_releases_last_child() {
836        let source_property = Property::new(source(&[]));
837        let context = Rc::new(RuntimeContext::new(test_globals()));
838        let root = ExpandedNode::initialize_root(
839            ComponentInstance::instantiate(component_args(None)),
840            &context,
841        );
842        let mut args = leaf_args(Default::default());
843        args.prototypical_common_properties =
844            crate::CommonPropertiesInit::Factory(Box::new(|_, node| {
845                node.is_none().then(|| {
846                    let mut cp = CommonProperties::default();
847                    cp.width = Property::new(Some(Size::Percent(50.into())));
848                    cp.height = Property::new(Some(Size::Percent(50.into())));
849                    Rc::new(RefCell::new(cp))
850                })
851            }));
852        let leaf: Rc<dyn InstanceNode> = ComponentInstance::instantiate(args);
853        let repeat: Rc<dyn InstanceNode> =
854            RepeatInstance::instantiate(repeat_args(source_property.clone(), vec![leaf]));
855        let sidecar = root.create_children_detached(
856            iter::once((repeat, root.stack.clone())),
857            &context,
858            &Rc::downgrade(&root),
859        );
860        let sidecar = root.attach_sidecar_children(sidecar, &context, &root.parent_frame);
861        let repeat = &sidecar[0];
862        repeat.recurse_control_flow_expansion(&context);
863        context.drain_node_effects();
864        assert!(repeat.children.get().is_empty());
865        source_property.set(source(&["ring"]));
866        let children = repeat.children.get();
867        repeat.attach_children(children.clone(), &context, &repeat.parent_frame);
868        context.drain_node_effects();
869        assert_eq!(
870            children[0].attached.get(),
871            0,
872            "mask geometry must not acquire visible mount lifecycle"
873        );
874        assert_eq!(children[0].transform_and_bounds.get().bounds, (50.0, 50.0));
875        context.globals().viewport.set(TransformAndBounds {
876            transform: Transform2::identity(),
877            bounds: (300.0, 200.0),
878        });
879        context.drain_node_effects();
880        assert_eq!(
881            children[0].transform_and_bounds.get().bounds,
882            (150.0, 100.0)
883        );
884        let rebuilds = children[0].parent_binding_rebuilds.get();
885        source_property.set(source(&["ring"]));
886        repeat.attach_children(repeat.children.get(), &context, &repeat.parent_frame);
887        assert_eq!(children[0].parent_binding_rebuilds.get(), rebuilds);
888        source_property.set(source(&[]));
889        repeat.attach_children(repeat.children.get(), &context, &repeat.parent_frame);
890        context.drain_node_effects();
891        assert!(repeat.children.get().is_empty());
892        assert!(borrow!(repeat.mounted_children).is_empty());
893    }
894
895    #[test]
896    fn unkeyed_repeat_rescues_a_position_readded_during_exit() {
897        let source_property = Property::new(source(&["a", "b"]));
898        let leaf_transition_config = ComponentTransitionConfig {
899            has_enter: true,
900            enter_frame_count: 10,
901            has_exit: true,
902            exit_frame_count: 30,
903            timeout_ms: 5_000,
904            ..Default::default()
905        };
906        let leaf: Rc<dyn InstanceNode> =
907            ComponentInstance::instantiate(leaf_args(leaf_transition_config));
908        let repeat: Rc<dyn InstanceNode> =
909            RepeatInstance::instantiate(unkeyed_repeat_args(source_property.clone(), vec![leaf]));
910        let root_component =
911            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&repeat)])));
912        let context = Rc::new(RuntimeContext::new(test_globals()));
913        let root = ExpandedNode::initialize_root(root_component, &context);
914
915        root.recurse_update(&context);
916        let repeat_node = root.children.get().remove(0);
917        let second_id = borrow!(repeat_node.active_children)[1].id;
918
919        source_property.set(source(&["a"]));
920        root.recurse_update(&context);
921        assert_eq!(borrow!(repeat_node.exiting_children)[0].id, second_id);
922
923        source_property.set(source(&["a", "b"]));
924        root.recurse_update(&context);
925        assert_eq!(borrow!(repeat_node.active_children)[1].id, second_id);
926        assert!(borrow!(repeat_node.exiting_children).is_empty());
927        assert_eq!(
928            borrow!(repeat_node.active_children)[1]
929                .transition_phase
930                .get(),
931            TRANSITION_PHASE_ENTER
932        );
933    }
934
935    #[test]
936    fn keyed_repeat_assigns_distinct_scope_bindings_per_child() {
937        let source_property = Property::new(source(&["a", "b", "c"]));
938        let leaf: Rc<dyn InstanceNode> =
939            ComponentInstance::instantiate(leaf_args(Default::default()));
940        let repeat: Rc<dyn InstanceNode> =
941            RepeatInstance::instantiate(repeat_args(source_property, vec![leaf]));
942        let root_component =
943            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&repeat)])));
944        let context = Rc::new(RuntimeContext::new(test_globals()));
945        let root = ExpandedNode::initialize_root(root_component, &context);
946
947        root.recurse_update(&context);
948        let repeat_node = root.children.get().remove(0);
949        let children = repeat_node.children.get();
950        assert_eq!(children.len(), 3);
951
952        let indices: Vec<_> = children
953            .iter()
954            .map(|child| {
955                child
956                    .stack
957                    .resolve_symbol("i")
958                    .and_then(|variable| Numeric::try_coerce(variable.get_as_pax_value()).ok())
959                    .map(|value| value.to_int())
960            })
961            .collect();
962        assert_eq!(indices, vec![Some(0), Some(1), Some(2)]);
963    }
964
965    #[test]
966    fn keyed_repeat_assigns_distinct_element_bindings_per_child() {
967        let source_property = Property::new(source_with_x(&[("a", 0.0), ("b", 10.0), ("c", 20.0)]));
968        let leaf: Rc<dyn InstanceNode> =
969            ComponentInstance::instantiate(leaf_args(Default::default()));
970        let repeat: Rc<dyn InstanceNode> =
971            RepeatInstance::instantiate(repeat_args(source_property, vec![leaf]));
972        let root_component =
973            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&repeat)])));
974        let context = Rc::new(RuntimeContext::new(test_globals()));
975        let root = ExpandedNode::initialize_root(root_component, &context);
976
977        root.recurse_update(&context);
978        let repeat_node = root.children.get().remove(0);
979        let children = repeat_node.children.get();
980        assert_eq!(children.len(), 3);
981
982        let xs: Vec<_> = children
983            .iter()
984            .map(|child| {
985                child
986                    .stack
987                    .resolve_symbol("item")
988                    .map(|variable| variable.get_as_pax_value())
989                    .and_then(|value| match value {
990                        PaxValue::Object(fields) => fields
991                            .into_iter()
992                            .find_map(|(name, value)| (name == "x").then_some(value)),
993                        _ => None,
994                    })
995                    .and_then(|value| Numeric::try_coerce(value).ok())
996                    .map(|value| value.to_float())
997            })
998            .collect();
999
1000        assert_eq!(xs, vec![Some(0.0), Some(10.0), Some(20.0)]);
1001    }
1002
1003    #[test]
1004    fn keyed_repeat_resolves_distinct_common_properties_per_child() {
1005        let source_property = Property::new(source(&["a", "b", "c"]));
1006        let leaf: Rc<dyn InstanceNode> = ComponentInstance::instantiate(positioned_leaf_args());
1007        let repeat: Rc<dyn InstanceNode> =
1008            RepeatInstance::instantiate(repeat_args(source_property, vec![leaf]));
1009        let root_component =
1010            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&repeat)])));
1011        let context = Rc::new(RuntimeContext::new(test_globals()));
1012        let root = ExpandedNode::initialize_root(root_component, &context);
1013
1014        root.recurse_update(&context);
1015        let repeat_node = root.children.get().remove(0);
1016        let children = repeat_node.children.get();
1017        assert_eq!(children.len(), 3);
1018
1019        let xs: Vec<_> = children
1020            .iter()
1021            .map(|child| {
1022                borrow!(child.get_common_properties())
1023                    .x
1024                    .get()
1025                    .map(|size| size.expect_pixels().to_float())
1026            })
1027            .collect();
1028
1029        assert_eq!(xs, vec![Some(0.0), Some(10.0), Some(20.0)]);
1030    }
1031
1032    #[test]
1033    fn keyed_repeat_resolves_distinct_expression_common_properties_from_element_binding() {
1034        let source_property = Property::new(source_with_x(&[("a", 0.0), ("b", 10.0), ("c", 20.0)]));
1035        let leaf: Rc<dyn InstanceNode> =
1036            ComponentInstance::instantiate(expression_positioned_leaf_args("(item.x)px"));
1037        let repeat: Rc<dyn InstanceNode> =
1038            RepeatInstance::instantiate(repeat_args(source_property, vec![leaf]));
1039        let root_component =
1040            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&repeat)])));
1041        let context = Rc::new(RuntimeContext::new(test_globals()));
1042        let root = ExpandedNode::initialize_root(root_component, &context);
1043
1044        root.recurse_update(&context);
1045        let repeat_node = root.children.get().remove(0);
1046        let children = repeat_node.children.get();
1047        assert_eq!(children.len(), 3);
1048
1049        let xs: Vec<_> = children
1050            .iter()
1051            .map(|child| {
1052                borrow!(child.get_common_properties())
1053                    .x
1054                    .get()
1055                    .map(|size| size.expect_pixels().to_float())
1056            })
1057            .collect();
1058
1059        assert_eq!(xs, vec![Some(0.0), Some(10.0), Some(20.0)]);
1060    }
1061
1062    #[test]
1063    fn keyed_repeat_resolves_distinct_common_properties_for_transition_components() {
1064        let source_property = Property::new(source(&["a", "b", "c"]));
1065        let leaf: Rc<dyn InstanceNode> =
1066            ComponentInstance::instantiate(transitioned_positioned_leaf_args());
1067        let repeat: Rc<dyn InstanceNode> =
1068            RepeatInstance::instantiate(repeat_args(source_property, vec![leaf]));
1069        let root_component =
1070            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&repeat)])));
1071        let context = Rc::new(RuntimeContext::new(test_globals()));
1072        let root = ExpandedNode::initialize_root(root_component, &context);
1073
1074        root.recurse_update(&context);
1075        let repeat_node = root.children.get().remove(0);
1076        let children = repeat_node.children.get();
1077        assert_eq!(children.len(), 3);
1078
1079        let xs: Vec<_> = children
1080            .iter()
1081            .map(|child| {
1082                borrow!(child.get_common_properties())
1083                    .x
1084                    .get()
1085                    .map(|size| size.expect_pixels().to_float())
1086            })
1087            .collect();
1088
1089        assert_eq!(xs, vec![Some(0.0), Some(10.0), Some(20.0)]);
1090    }
1091
1092    #[test]
1093    fn keyed_repeat_resolves_expression_common_properties_for_transition_components() {
1094        let source_property = Property::new(source_with_x(&[("a", 0.0), ("b", 10.0), ("c", 20.0)]));
1095        let leaf: Rc<dyn InstanceNode> = ComponentInstance::instantiate(
1096            transitioned_expression_positioned_leaf_args("(item.x)px"),
1097        );
1098        let repeat: Rc<dyn InstanceNode> =
1099            RepeatInstance::instantiate(repeat_args(source_property, vec![leaf]));
1100        let root_component =
1101            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&repeat)])));
1102        let context = Rc::new(RuntimeContext::new(test_globals()));
1103        let root = ExpandedNode::initialize_root(root_component, &context);
1104
1105        root.recurse_update(&context);
1106        let repeat_node = root.children.get().remove(0);
1107        let children = repeat_node.children.get();
1108        assert_eq!(children.len(), 3);
1109
1110        let xs: Vec<_> = children
1111            .iter()
1112            .map(|child| {
1113                borrow!(child.get_common_properties())
1114                    .x
1115                    .get()
1116                    .map(|size| size.expect_pixels().to_float())
1117            })
1118            .collect();
1119
1120        assert_eq!(xs, vec![Some(0.0), Some(10.0), Some(20.0)]);
1121    }
1122
1123    #[test]
1124    fn unkeyed_repeat_stale_element_binding_defaults_after_source_shrinks() {
1125        let source_property = Property::new(source_with_x(&[("a", 0.0), ("b", 10.0)]));
1126        let leaf: Rc<dyn InstanceNode> =
1127            ComponentInstance::instantiate(leaf_args(Default::default()));
1128        let repeat: Rc<dyn InstanceNode> =
1129            RepeatInstance::instantiate(unkeyed_repeat_args(source_property.clone(), vec![leaf]));
1130        let root_component =
1131            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&repeat)])));
1132        let context = Rc::new(RuntimeContext::new(test_globals()));
1133        let root = ExpandedNode::initialize_root(root_component, &context);
1134
1135        root.recurse_update(&context);
1136        let repeat_node = root.children.get().remove(0);
1137        let children = repeat_node.children.get();
1138        assert_eq!(children.len(), 2);
1139
1140        let stale_item = children[1].stack.resolve_symbol("item").unwrap();
1141        source_property.set(source_with_x(&[("a", 0.0)]));
1142
1143        assert_eq!(stale_item.get_as_pax_value(), PaxValue::default());
1144    }
1145}
1146
1147#[derive(Clone, Debug, Eq, Hash, PartialEq)]
1148enum RepeatKey {
1149    String(String),
1150    Int(i64),
1151    Synthetic(usize),
1152}
1153
1154#[derive(Clone)]
1155struct RepeatChildGroup {
1156    key: RepeatKey,
1157    elem: Property<PaxValue>,
1158    i: Property<usize>,
1159    children: Vec<Rc<ExpandedNode>>,
1160}
1161
1162/// Per-iteration bindings exposed inside a `for` template body.
1163pub struct RepeatItem {
1164    pub elem: Property<PaxValue>,
1165    pub i: Property<usize>,
1166}
1167
1168impl ToPaxValue for RepeatItem {
1169    fn to_pax_value(self) -> PaxValue {
1170        PaxValue::Object(
1171            vec![
1172                ("elem".to_string(), self.elem.get().to_pax_value()),
1173                ("i".to_string(), self.i.get().to_pax_value()),
1174            ]
1175            .into_iter()
1176            .collect(),
1177        )
1178    }
1179}
1180
1181impl InstanceNode for RepeatInstance {
1182    fn instantiate(args: InstantiationArgs) -> Rc<Self>
1183    where
1184        Self: Sized,
1185    {
1186        Rc::new(Self {
1187            base: BaseInstance::new(
1188                args,
1189                InstanceFlags {
1190                    invisible_to_slot: true,
1191                    invisible_to_raycasting: true,
1192                    layer: Layer::DontCare,
1193                    is_component: false,
1194                    is_slot: false,
1195                },
1196            ),
1197        })
1198    }
1199
1200    fn resolve_debug(
1201        &self,
1202        f: &mut std::fmt::Formatter,
1203        _expanded_node: Option<&ExpandedNode>,
1204    ) -> std::fmt::Result {
1205        f.debug_struct("Repeat").finish()
1206    }
1207
1208    fn base(&self) -> &BaseInstance {
1209        &self.base
1210    }
1211
1212    fn handle_mount(
1213        self: Rc<Self>,
1214        expanded_node: &Rc<ExpandedNode>,
1215        context: &Rc<RuntimeContext>,
1216    ) {
1217        self.handle_setup(expanded_node, context, true);
1218    }
1219
1220    fn handle_control_flow_node_expansion(
1221        self: Rc<Self>,
1222        expanded_node: &Rc<ExpandedNode>,
1223        context: &Rc<RuntimeContext>,
1224    ) {
1225        self.handle_setup(expanded_node, context, false);
1226    }
1227}
1228
1229impl RepeatInstance {
1230    fn handle_setup(
1231        self: Rc<Self>,
1232        expanded_node: &Rc<ExpandedNode>,
1233        context: &Rc<RuntimeContext>,
1234        is_mount: bool,
1235    ) {
1236        // No-op: wait with creating child-nodes until update tick, since the
1237        // condition has then been evaluated
1238        let weak_ref_self = Rc::downgrade(expanded_node);
1239        let cloned_self = Rc::clone(&self);
1240        let cloned_context = Rc::clone(context);
1241        let source_expression =
1242            expanded_node.with_properties_unwrapped(|properties: &mut RepeatProperties| {
1243                properties.source_expression.clone()
1244            });
1245
1246        let i_symbol =
1247            expanded_node.with_properties_unwrapped(|properties: &mut RepeatProperties| {
1248                properties.iterator_i_symbol.clone()
1249            });
1250        let elem_symbol =
1251            expanded_node.with_properties_unwrapped(|properties: &mut RepeatProperties| {
1252                properties.iterator_elem_symbol.clone()
1253            });
1254        let repeat_key_expression =
1255            expanded_node.with_properties_unwrapped(|properties: &mut RepeatProperties| {
1256                properties.repeat_key_expression.clone()
1257            });
1258
1259        let mut deps = vec![
1260            source_expression.untyped(),
1261            i_symbol.untyped(),
1262            elem_symbol.untyped(),
1263        ];
1264        if let Some(repeat_key_expression) = &repeat_key_expression {
1265            for dependency in &repeat_key_expression.dependencies {
1266                if let Some(property) = expanded_node
1267                    .stack
1268                    .resolve_symbol_as_erased_property(dependency)
1269                {
1270                    deps.push(property);
1271                }
1272            }
1273        }
1274
1275        let last_length = Rc::new(RefCell::new(0));
1276        let last_elem_sym = Rc::new(RefCell::new(None));
1277        let last_i_sym = Rc::new(RefCell::new(None));
1278        let cached_children: Rc<RefCell<Vec<Rc<ExpandedNode>>>> = Default::default();
1279        let keyed_groups: Rc<RefCell<Vec<RepeatChildGroup>>> = Default::default();
1280
1281        let children = Property::computed_with_name(
1282            move || {
1283                let Some(cloned_expanded_node) = weak_ref_self.upgrade() else {
1284                    panic!("ran evaluator after expanded node dropped (repeat elem)")
1285                };
1286                let source_len = source_expression.read(Self::source_len);
1287                if let Some(repeat_key_expression) = &repeat_key_expression {
1288                    let i_symbol_value = i_symbol.get();
1289                    let elem_symbol_value = elem_symbol.get();
1290                    let symbols_changed = i_symbol_value != *borrow!(last_i_sym)
1291                        || elem_symbol_value != *borrow!(last_elem_sym);
1292                    *borrow_mut!(last_i_sym) = i_symbol_value.clone();
1293                    *borrow_mut!(last_elem_sym) = elem_symbol_value.clone();
1294                    if symbols_changed {
1295                        borrow_mut!(keyed_groups).clear();
1296                    }
1297                    return Self::compute_keyed_children(
1298                        &cloned_expanded_node,
1299                        &cloned_context,
1300                        Rc::clone(&cloned_self),
1301                        &source_expression,
1302                        i_symbol_value,
1303                        elem_symbol_value,
1304                        repeat_key_expression,
1305                        Rc::clone(&keyed_groups),
1306                        source_len,
1307                        is_mount,
1308                        !symbols_changed,
1309                    );
1310                }
1311
1312                if source_len == *borrow!(last_length)
1313                    && i_symbol.read(|i| i == &*borrow!(last_i_sym))
1314                    && elem_symbol.read(|e| e == &*borrow!(last_elem_sym))
1315                {
1316                    return if cloned_expanded_node.attached.get() > 0 {
1317                        cloned_expanded_node.current_attached_children()
1318                    } else {
1319                        borrow!(cached_children).clone()
1320                    };
1321                }
1322                let i_symbol_value = i_symbol.get();
1323                let elem_symbol_value = elem_symbol.get();
1324                let symbols_unchanged = i_symbol_value == *borrow!(last_i_sym)
1325                    && elem_symbol_value == *borrow!(last_elem_sym);
1326                *borrow_mut!(last_length) = source_len;
1327                *borrow_mut!(last_i_sym) = i_symbol_value.clone();
1328                *borrow_mut!(last_elem_sym) = elem_symbol_value.clone();
1329
1330                let template_children = borrow!(cloned_self.base().get_instance_children()).clone();
1331                let active_children = borrow!(cloned_expanded_node.active_children).clone();
1332                let mut selected_children = Vec::new();
1333                for i in 0..source_len {
1334                    let property_i = Property::new(i);
1335                    let cp_source_expression = source_expression.clone();
1336                    let property_elem = Property::computed_with_name(
1337                        move || cp_source_expression.read(|source| Self::source_elem(source, i)),
1338                        &[source_expression.untyped()],
1339                        "repeat elem",
1340                    );
1341                    let scope = Self::repeat_scope(
1342                        i_symbol_value.clone(),
1343                        elem_symbol_value.clone(),
1344                        property_i,
1345                        property_elem,
1346                    );
1347                    let new_env = cloned_expanded_node.stack.push(scope);
1348                    for (template_offset, template) in template_children.iter().enumerate() {
1349                        let flat_index = i * template_children.len() + template_offset;
1350                        let active = symbols_unchanged
1351                            .then(|| active_children.get(flat_index))
1352                            .flatten()
1353                            .filter(|child| {
1354                                let child_template = borrow!(child.instance_node).clone();
1355                                Rc::ptr_eq(&child_template, template)
1356                            })
1357                            .cloned();
1358                        let rescued = active.or_else(|| {
1359                            (symbols_unchanged
1360                                && is_mount
1361                                && cloned_expanded_node.attached.get() > 0)
1362                                .then(|| {
1363                                    cloned_expanded_node.rescue_exiting_child_matching(|child| {
1364                                        let child_template = borrow!(child.instance_node).clone();
1365                                        let template_matches =
1366                                            Rc::ptr_eq(&child_template, template);
1367                                        let index_matches = child
1368                                            .stack
1369                                            .resolve_symbol_as_erased_property(
1370                                                INTERNAL_REPEAT_INDEX_SYMBOL,
1371                                            )
1372                                            .map(Property::<usize>::new_from_untyped)
1373                                            .map(|index| index.get() == i)
1374                                            .unwrap_or(true);
1375                                        template_matches && index_matches
1376                                    })
1377                                })
1378                                .flatten()
1379                        });
1380                        let child = rescued.unwrap_or_else(|| {
1381                            cloned_expanded_node
1382                                .create_children_detached(
1383                                    iter::once((Rc::clone(template), Rc::clone(&new_env))),
1384                                    &cloned_context,
1385                                    &Rc::downgrade(&cloned_expanded_node),
1386                                )
1387                                .pop()
1388                                .expect("repeat child creation returned no child")
1389                        });
1390                        if !is_mount && child.attached.get() == 0 {
1391                            child.recurse_control_flow_expansion(&cloned_context);
1392                        }
1393                        selected_children.push(child);
1394                    }
1395                }
1396                let ret = if is_mount {
1397                    cloned_expanded_node.attach_children(
1398                        selected_children,
1399                        &cloned_context,
1400                        &cloned_expanded_node.parent_frame,
1401                    )
1402                } else {
1403                    selected_children
1404                };
1405                *borrow_mut!(cached_children) = ret.clone();
1406                ret
1407            },
1408            &deps,
1409            &format!("repeat_children (node id: {})", expanded_node.id.0),
1410        );
1411        expanded_node.children.replace_with(children);
1412    }
1413
1414    fn source_len(source: &PaxValue) -> usize {
1415        if let PaxValue::Range(start, end) = source {
1416            (isize::try_coerce(*end.clone()).unwrap() - isize::try_coerce(*start.clone()).unwrap())
1417                as usize
1418        } else if let PaxValue::Vec(v) = source {
1419            v.len()
1420        } else {
1421            log::warn!("source is not a vec");
1422            0
1423        }
1424    }
1425
1426    fn source_elem(source: &PaxValue, i: usize) -> PaxValue {
1427        if let PaxValue::Range(start, _) = source {
1428            let start = isize::try_coerce(*start.clone()).unwrap();
1429            (start + i as isize).to_pax_value()
1430        } else if let PaxValue::Vec(v) = source {
1431            v.get(i).cloned().unwrap_or_default()
1432        } else {
1433            log::warn!("source is not a vec");
1434            Default::default()
1435        }
1436    }
1437
1438    fn repeat_scope(
1439        i_symbol: Option<String>,
1440        elem_symbol: Option<String>,
1441        property_i: Property<usize>,
1442        property_elem: Property<PaxValue>,
1443    ) -> HashMap<String, Variable> {
1444        let mut scope = HashMap::new();
1445        if let Some(i_symbol) = i_symbol {
1446            scope.insert(
1447                i_symbol,
1448                Variable::new_from_typed_property(property_i.clone()),
1449            );
1450        }
1451        if let Some(elem_symbol) = elem_symbol {
1452            scope.insert(
1453                elem_symbol,
1454                Variable::new_from_typed_property(property_elem.clone()),
1455            );
1456        }
1457        scope.insert(
1458            INTERNAL_REPEAT_INDEX_SYMBOL.to_string(),
1459            Variable::new_from_typed_property(property_i),
1460        );
1461        scope.insert(
1462            INTERNAL_REPEAT_ELEMENT_SYMBOL.to_string(),
1463            Variable::new_from_typed_property(property_elem),
1464        );
1465        scope
1466    }
1467
1468    fn encoded_repeat_key(key: &RepeatKey) -> String {
1469        match key {
1470            RepeatKey::String(value) => format!("string:{value}"),
1471            RepeatKey::Int(value) => format!("int:{value}"),
1472            RepeatKey::Synthetic(value) => format!("synthetic:{value}"),
1473        }
1474    }
1475
1476    fn repeat_scope_with_key(
1477        i_symbol: Option<String>,
1478        elem_symbol: Option<String>,
1479        property_i: Property<usize>,
1480        property_elem: Property<PaxValue>,
1481        key: &RepeatKey,
1482    ) -> HashMap<String, Variable> {
1483        let mut scope = Self::repeat_scope(i_symbol, elem_symbol, property_i, property_elem);
1484        scope.insert(
1485            INTERNAL_REPEAT_KEY_SYMBOL.to_string(),
1486            Variable::new_from_typed_property(Property::new(Self::encoded_repeat_key(key))),
1487        );
1488        scope
1489    }
1490
1491    fn repeat_key_from_value(value: PaxValue, index: usize) -> Option<RepeatKey> {
1492        match value {
1493            PaxValue::String(value) => Some(RepeatKey::String(value)),
1494            PaxValue::Numeric(value) if !value.is_float() => Some(RepeatKey::Int(value.to_int())),
1495            _ => {
1496                log::warn!(
1497                    "repeat key for index {} must evaluate to a string or integer; using positional fallback",
1498                    index
1499                );
1500                None
1501            }
1502        }
1503    }
1504
1505    fn evaluate_key(
1506        key_expression: &ExpressionInfo,
1507        env: Rc<RuntimePropertiesStackFrame>,
1508        index: usize,
1509    ) -> Option<RepeatKey> {
1510        key_expression
1511            .expression
1512            .compute(env)
1513            .map(|value| Self::repeat_key_from_value(value, index))
1514            .unwrap_or_else(|err| {
1515                log::warn!(
1516                    "failed to compute repeat key for index {}: {}; using positional fallback",
1517                    index,
1518                    err
1519                );
1520                None
1521            })
1522    }
1523
1524    fn compute_keyed_children(
1525        expanded_node: &Rc<ExpandedNode>,
1526        context: &Rc<RuntimeContext>,
1527        repeat: Rc<RepeatInstance>,
1528        source_expression: &Property<PaxValue>,
1529        i_symbol_value: Option<String>,
1530        elem_symbol_value: Option<String>,
1531        key_expression: &ExpressionInfo,
1532        keyed_groups: Rc<RefCell<Vec<RepeatChildGroup>>>,
1533        source_len: usize,
1534        is_mount: bool,
1535        allow_rescue: bool,
1536    ) -> Vec<Rc<ExpandedNode>> {
1537        let mut old_groups_by_key: HashMap<RepeatKey, RepeatChildGroup> = borrow_mut!(keyed_groups)
1538            .drain(..)
1539            .map(|group| (group.key.clone(), group))
1540            .collect();
1541        let mut new_groups = Vec::new();
1542        let mut new_children = Vec::new();
1543        let mut used_keys = HashSet::new();
1544        let template_children = repeat.base().get_instance_children();
1545
1546        for i in 0..source_len {
1547            let elem = source_expression.read(|source| Self::source_elem(source, i));
1548            let key_probe_i = Property::new(i);
1549            let key_probe_elem = Property::new(elem.clone());
1550            let key_probe_scope = Self::repeat_scope(
1551                i_symbol_value.clone(),
1552                elem_symbol_value.clone(),
1553                key_probe_i,
1554                key_probe_elem,
1555            );
1556            let key_probe_env = expanded_node.stack.push(key_probe_scope);
1557            let evaluated_key = Self::evaluate_key(key_expression, key_probe_env, i);
1558            let key = match evaluated_key {
1559                Some(key) if used_keys.insert(key.clone()) => key,
1560                Some(key) => {
1561                    log::warn!(
1562                        "duplicate repeat key {:?} at index {}; using positional fallback",
1563                        key,
1564                        i
1565                    );
1566                    RepeatKey::Synthetic(i)
1567                }
1568                None => RepeatKey::Synthetic(i),
1569            };
1570
1571            let group = if let Some(group) = old_groups_by_key.remove(&key) {
1572                update_repeat_bindings(&group.i, &group.elem, i, elem);
1573                group
1574            } else if allow_rescue && is_mount {
1575                Self::rescue_keyed_group(
1576                    expanded_node,
1577                    context,
1578                    &template_children,
1579                    i_symbol_value.clone(),
1580                    elem_symbol_value.clone(),
1581                    &key,
1582                    i,
1583                    elem.clone(),
1584                )
1585                .unwrap_or_else(|| {
1586                    Self::create_keyed_group(
1587                        expanded_node,
1588                        context,
1589                        &template_children,
1590                        i_symbol_value.clone(),
1591                        elem_symbol_value.clone(),
1592                        key.clone(),
1593                        i,
1594                        elem,
1595                        is_mount,
1596                    )
1597                })
1598            } else {
1599                Self::create_keyed_group(
1600                    expanded_node,
1601                    context,
1602                    &template_children,
1603                    i_symbol_value.clone(),
1604                    elem_symbol_value.clone(),
1605                    key.clone(),
1606                    i,
1607                    elem,
1608                    is_mount,
1609                )
1610            };
1611
1612            new_children.extend(group.children.iter().cloned());
1613            new_groups.push(group);
1614        }
1615
1616        *borrow_mut!(keyed_groups) = new_groups;
1617        if is_mount {
1618            expanded_node.attach_children(new_children, context, &expanded_node.parent_frame)
1619        } else {
1620            new_children
1621        }
1622    }
1623
1624    fn create_keyed_group(
1625        expanded_node: &Rc<ExpandedNode>,
1626        context: &Rc<RuntimeContext>,
1627        template_children: &RefCell<Vec<Rc<dyn InstanceNode>>>,
1628        i_symbol: Option<String>,
1629        elem_symbol: Option<String>,
1630        key: RepeatKey,
1631        i: usize,
1632        elem: PaxValue,
1633        is_mount: bool,
1634    ) -> RepeatChildGroup {
1635        let property_i = Property::new(i);
1636        let property_elem = Property::new(elem);
1637        let scope = Self::repeat_scope_with_key(
1638            i_symbol,
1639            elem_symbol,
1640            property_i.clone(),
1641            property_elem.clone(),
1642            &key,
1643        );
1644        let new_env = expanded_node.stack.push(scope);
1645        let children = expanded_node.create_children_detached(
1646            borrow!(template_children)
1647                .clone()
1648                .into_iter()
1649                .zip(iter::repeat(new_env)),
1650            context,
1651            &Rc::downgrade(expanded_node),
1652        );
1653        if !is_mount {
1654            for child in &children {
1655                child.recurse_control_flow_expansion(context);
1656            }
1657        }
1658        RepeatChildGroup {
1659            key,
1660            elem: property_elem,
1661            i: property_i,
1662            children,
1663        }
1664    }
1665
1666    fn rescue_keyed_group(
1667        expanded_node: &Rc<ExpandedNode>,
1668        context: &Rc<RuntimeContext>,
1669        template_children: &RefCell<Vec<Rc<dyn InstanceNode>>>,
1670        i_symbol: Option<String>,
1671        elem_symbol: Option<String>,
1672        key: &RepeatKey,
1673        i: usize,
1674        elem: PaxValue,
1675    ) -> Option<RepeatChildGroup> {
1676        let encoded_key = Self::encoded_repeat_key(key);
1677        let templates = borrow!(template_children).clone();
1678        let mut children = templates
1679            .iter()
1680            .map(|template| {
1681                expanded_node.rescue_exiting_child_matching(|child| {
1682                    let child_template = borrow!(child.instance_node).clone();
1683                    Rc::ptr_eq(&child_template, template)
1684                        && child
1685                            .stack
1686                            .resolve_symbol_as_erased_property(INTERNAL_REPEAT_KEY_SYMBOL)
1687                            .map(Property::<String>::new_from_untyped)
1688                            .map(|key| key.get() == encoded_key)
1689                            .unwrap_or(false)
1690                })
1691            })
1692            .collect::<Vec<_>>();
1693        let seed = children.iter().flatten().next()?.clone();
1694        let property_i = seed
1695            .stack
1696            .resolve_symbol_as_erased_property(INTERNAL_REPEAT_INDEX_SYMBOL)
1697            .map(Property::<usize>::new_from_untyped)?;
1698        let property_elem = seed
1699            .stack
1700            .resolve_symbol_as_erased_property(INTERNAL_REPEAT_ELEMENT_SYMBOL)
1701            .map(Property::<PaxValue>::new_from_untyped)?;
1702        update_repeat_bindings(&property_i, &property_elem, i, elem);
1703        let env = expanded_node.stack.push(Self::repeat_scope_with_key(
1704            i_symbol,
1705            elem_symbol,
1706            property_i.clone(),
1707            property_elem.clone(),
1708            key,
1709        ));
1710        let resolved_children = children
1711            .drain(..)
1712            .zip(templates)
1713            .map(|(child, template)| {
1714                child.unwrap_or_else(|| {
1715                    expanded_node
1716                        .create_children_detached(
1717                            iter::once((template, Rc::clone(&env))),
1718                            context,
1719                            &Rc::downgrade(expanded_node),
1720                        )
1721                        .pop()
1722                        .expect("repeat child creation returned no child")
1723                })
1724            })
1725            .collect();
1726        Some(RepeatChildGroup {
1727            key: key.clone(),
1728            elem: property_elem,
1729            i: property_i,
1730            children: resolved_children,
1731        })
1732    }
1733}