Skip to main content

pax_manifest/
program_ir.rs

1use std::collections::{BTreeMap, HashMap, VecDeque};
2
3use pax_message::serde::{Deserialize, Serialize};
4
5use crate::{
6    ComponentDefinition, ComponentTemplate, GradientDefinition, GradientElement,
7    GradientShapeDefinition, GradientStopDefinition, LiteralBlockDefinition, PaxManifest,
8    RouteBranchDescriptor, SettingElement, SettingsBlockElement, SettingsConditionalBlock,
9    SettingsConditionalBranch, TemplateNodeDefinition, TemplateNodeId, TimelineBlockElement,
10    TimelineDefinition, TimelineSelectorBlockDefinition, TimelineSelectorElement,
11    TimelineTrackDefinition, TimelineTrackElement, Token, TransitionDefinition, TypeDefinition,
12    TypeId, ValueDefinition,
13};
14
15const MAGIC: &[u8; 8] = b"PAXP\x00IR\x00";
16const VERSION: u8 = 5;
17
18/// Runtime-facing semantic program model derived from a rich `PaxManifest`.
19///
20/// `ProgramIR` is a transitional intermediate representation: it preserves the
21/// semantic program structure while stripping obviously source-only and
22/// compiler-only manifest data. Debug/designtime flows may continue to mount
23/// rich manifests directly until later phases cut execution over to `ProgramIR`.
24#[derive(Serialize, Deserialize, Clone, Default)]
25#[serde(crate = "pax_message::serde")]
26pub struct ProgramIR {
27    pub components: BTreeMap<TypeId, ProgramComponent>,
28    pub main_component_type_id: TypeId,
29    pub type_table: BTreeMap<TypeId, TypeDefinition>,
30    pub assets_dirs: Vec<String>,
31}
32
33#[derive(Serialize, Deserialize, Clone, Default)]
34#[serde(crate = "pax_message::serde")]
35pub struct ProgramComponent {
36    pub type_id: TypeId,
37    pub is_main_component: bool,
38    pub is_primitive: bool,
39    pub is_struct_only_component: bool,
40    pub template: Option<ComponentTemplate>,
41    pub settings: Option<Vec<SettingsBlockElement>>,
42    #[serde(default)]
43    pub timelines: Vec<TimelineDefinition>,
44    #[serde(default)]
45    pub route_branch: Option<RouteBranchDescriptor>,
46}
47
48impl ProgramIR {
49    pub fn from_manifest(manifest: &PaxManifest) -> Self {
50        manifest.into()
51    }
52}
53
54impl From<&PaxManifest> for ProgramIR {
55    fn from(manifest: &PaxManifest) -> Self {
56        Self {
57            components: manifest
58                .components
59                .iter()
60                .map(|(type_id, definition)| (type_id.clone(), ProgramComponent::from(definition)))
61                .collect(),
62            main_component_type_id: manifest.main_component_type_id.clone(),
63            type_table: manifest
64                .type_table
65                .iter()
66                .map(|(type_id, definition)| (type_id.clone(), definition.clone()))
67                .collect(),
68            assets_dirs: manifest.assets_dirs.clone(),
69        }
70    }
71}
72
73impl From<&ComponentDefinition> for ProgramComponent {
74    fn from(definition: &ComponentDefinition) -> Self {
75        Self {
76            type_id: definition.type_id.clone(),
77            is_main_component: definition.is_main_component,
78            is_primitive: definition.is_primitive,
79            is_struct_only_component: definition.is_struct_only_component,
80            template: definition
81                .template
82                .as_ref()
83                .map(sanitize_component_template),
84            settings: definition
85                .settings
86                .as_ref()
87                .map(sanitize_settings_block_elements),
88            timelines: definition.timelines.iter().map(sanitize_timeline).collect(),
89            route_branch: definition.route_branch.clone(),
90        }
91    }
92}
93
94fn sanitize_component_template(template: &ComponentTemplate) -> ComponentTemplate {
95    let root: VecDeque<_> = template
96        .get_root()
97        .into_iter()
98        .filter(|id| should_keep_template_node(template, id))
99        .collect();
100
101    let mut children = HashMap::new();
102    let mut nodes = HashMap::new();
103    for id in root.iter() {
104        collect_sanitized_template_parts(template, id, &mut children, &mut nodes);
105    }
106
107    ComponentTemplate::from_parts(
108        template.get_containing_component_type_id(),
109        root,
110        children,
111        nodes,
112        template.get_next_id(),
113        None,
114    )
115}
116
117fn collect_sanitized_template_parts(
118    template: &ComponentTemplate,
119    id: &TemplateNodeId,
120    children: &mut HashMap<TemplateNodeId, VecDeque<TemplateNodeId>>,
121    nodes: &mut HashMap<TemplateNodeId, TemplateNodeDefinition>,
122) {
123    let Some(node) = template.get_node(id) else {
124        return;
125    };
126    let Some(node) = sanitize_template_node(node) else {
127        return;
128    };
129    nodes.insert(id.clone(), node);
130
131    let filtered_children: VecDeque<_> = template
132        .get_children(id)
133        .unwrap_or_default()
134        .into_iter()
135        .filter(|child_id| should_keep_template_node(template, child_id))
136        .collect();
137
138    if !filtered_children.is_empty() {
139        children.insert(id.clone(), filtered_children.clone());
140    }
141
142    for child_id in filtered_children {
143        collect_sanitized_template_parts(template, &child_id, children, nodes);
144    }
145}
146
147fn should_keep_template_node(template: &ComponentTemplate, id: &TemplateNodeId) -> bool {
148    template
149        .get_node(id)
150        .map(|node| node.raw_comment_string.is_none())
151        .unwrap_or(false)
152}
153
154fn sanitize_template_node(node: &TemplateNodeDefinition) -> Option<TemplateNodeDefinition> {
155    if node.raw_comment_string.is_some() {
156        return None;
157    }
158
159    let mut control_flow_settings = node.control_flow_settings.clone();
160    if let Some(settings) = &mut control_flow_settings {
161        for route in &mut settings.route_branches {
162            route.metadata = None;
163        }
164    }
165
166    Some(TemplateNodeDefinition {
167        type_id: node.type_id.clone(),
168        control_flow_settings,
169        settings: node.settings.as_ref().map(sanitize_setting_elements),
170        selector_info: crate::TemplateNodeSelectorInfo {
171            source_location: node.selector_info.source_location.clone(),
172            id: node.selector_info.id.as_ref().map(strip_token),
173            class_binding: node.selector_info.class_binding.clone(),
174        },
175        raw_comment_string: None,
176    })
177}
178
179fn sanitize_settings_block_elements(
180    elements: &Vec<SettingsBlockElement>,
181) -> Vec<SettingsBlockElement> {
182    elements
183        .iter()
184        .filter_map(|element| match element {
185            SettingsBlockElement::SelectorBlock(token, block) => {
186                Some(SettingsBlockElement::SelectorBlock(
187                    strip_token(token),
188                    sanitize_literal_block(block),
189                ))
190            }
191            SettingsBlockElement::Handler(token, handlers) => Some(SettingsBlockElement::Handler(
192                strip_token(token),
193                handlers.iter().map(strip_token).collect(),
194            )),
195            SettingsBlockElement::Transition(token, value) => Some(
196                SettingsBlockElement::Transition(strip_token(token), strip_token(value)),
197            ),
198            SettingsBlockElement::Conditional(block) => Some(SettingsBlockElement::Conditional(
199                SettingsConditionalBlock {
200                    branches: block
201                        .branches
202                        .iter()
203                        .map(|branch| SettingsConditionalBranch {
204                            condition_expression: branch.condition_expression.clone(),
205                            elements: sanitize_settings_block_elements(&branch.elements),
206                        })
207                        .collect(),
208                },
209            )),
210            SettingsBlockElement::Comment(_) => None,
211        })
212        .collect()
213}
214
215fn sanitize_setting_elements(elements: &Vec<SettingElement>) -> Vec<SettingElement> {
216    elements
217        .iter()
218        .filter_map(|element| match element {
219            SettingElement::Setting(token, value) => Some(SettingElement::Setting(
220                strip_token(token),
221                sanitize_value_definition(value),
222            )),
223            SettingElement::Comment(_) => None,
224        })
225        .collect()
226}
227
228fn sanitize_literal_block(block: &LiteralBlockDefinition) -> LiteralBlockDefinition {
229    LiteralBlockDefinition {
230        explicit_type_pascal_identifier: block
231            .explicit_type_pascal_identifier
232            .as_ref()
233            .map(strip_token),
234        elements: sanitize_setting_elements(&block.elements),
235    }
236}
237
238fn sanitize_timeline(definition: &TimelineDefinition) -> TimelineDefinition {
239    TimelineDefinition {
240        name: definition.name.as_ref().map(strip_token),
241        playhead: definition.playhead.as_ref().map(sanitize_value_definition),
242        duration: definition.duration.as_ref().map(sanitize_value_definition),
243        repeat: definition.repeat,
244        interruption: definition.interruption,
245        elements: definition
246            .elements
247            .iter()
248            .filter_map(|element| match element {
249                TimelineBlockElement::SelectorBlock(token, selector) => {
250                    Some(TimelineBlockElement::SelectorBlock(
251                        strip_token(token),
252                        sanitize_timeline_selector(selector),
253                    ))
254                }
255                TimelineBlockElement::Comment(_) => None,
256            })
257            .collect(),
258    }
259}
260
261fn sanitize_timeline_selector(
262    selector: &TimelineSelectorBlockDefinition,
263) -> TimelineSelectorBlockDefinition {
264    TimelineSelectorBlockDefinition {
265        elements: selector
266            .elements
267            .iter()
268            .filter_map(|element| match element {
269                TimelineSelectorElement::Track(token, track) => {
270                    Some(TimelineSelectorElement::Track(
271                        strip_token(token),
272                        sanitize_timeline_track(track),
273                    ))
274                }
275                TimelineSelectorElement::Comment(_) => None,
276            })
277            .collect(),
278    }
279}
280
281fn sanitize_timeline_track(track: &TimelineTrackDefinition) -> TimelineTrackDefinition {
282    TimelineTrackDefinition {
283        elements: track
284            .elements
285            .iter()
286            .filter_map(|element| match element {
287                TimelineTrackElement::Keyframe(keyframe) => {
288                    Some(TimelineTrackElement::Keyframe(crate::TimelineKeyframe {
289                        marker: keyframe.marker.clone(),
290                        value: sanitize_value_definition(&keyframe.value),
291                        easing: keyframe.easing.as_ref().map(strip_token),
292                    }))
293                }
294                TimelineTrackElement::Comment(_) => None,
295            })
296            .collect(),
297        playhead: track
298            .playhead
299            .as_ref()
300            .map(|value| Box::new(sanitize_value_definition(value))),
301        duration: track
302            .duration
303            .as_ref()
304            .map(|value| Box::new(sanitize_value_definition(value))),
305        repeat: track.repeat,
306        starting_value: track
307            .starting_value
308            .as_ref()
309            .map(|value| Box::new(sanitize_value_definition(value))),
310        interruption: track.interruption,
311        use_local_property_scope: track.use_local_property_scope,
312    }
313}
314
315fn sanitize_gradient(definition: &GradientDefinition) -> GradientDefinition {
316    GradientDefinition {
317        shape: sanitize_gradient_shape(&definition.shape),
318        elements: definition
319            .elements
320            .iter()
321            .filter_map(|element| match element {
322                GradientElement::Stop(stop) => {
323                    Some(GradientElement::Stop(GradientStopDefinition {
324                        position: stop.position.clone(),
325                        color: sanitize_value_definition(&stop.color),
326                    }))
327                }
328                GradientElement::Comment(_) => None,
329            })
330            .collect(),
331    }
332}
333
334fn sanitize_gradient_shape(shape: &GradientShapeDefinition) -> GradientShapeDefinition {
335    match shape {
336        GradientShapeDefinition::Mesh { rows } => GradientShapeDefinition::Mesh {
337            rows: Box::new(sanitize_value_definition(rows)),
338        },
339        GradientShapeDefinition::Linear { start, end } => GradientShapeDefinition::Linear {
340            start: start
341                .as_ref()
342                .map(|value| Box::new(sanitize_value_definition(value))),
343            end: end
344                .as_ref()
345                .map(|value| Box::new(sanitize_value_definition(value))),
346        },
347        GradientShapeDefinition::Radial { start, end, radius } => GradientShapeDefinition::Radial {
348            start: Box::new(sanitize_value_definition(start)),
349            end: Box::new(sanitize_value_definition(end)),
350            radius: Box::new(sanitize_value_definition(radius)),
351        },
352    }
353}
354
355fn sanitize_value_definition(value: &ValueDefinition) -> ValueDefinition {
356    match value {
357        ValueDefinition::Undefined => ValueDefinition::Undefined,
358        ValueDefinition::LiteralValue(value) => ValueDefinition::LiteralValue(value.clone()),
359        ValueDefinition::List(values) => {
360            ValueDefinition::List(values.iter().map(sanitize_value_definition).collect())
361        }
362        ValueDefinition::Block(block) => ValueDefinition::Block(sanitize_literal_block(block)),
363        ValueDefinition::Timeline(track) => {
364            ValueDefinition::Timeline(sanitize_timeline_track(track))
365        }
366        ValueDefinition::Gradient(gradient) => {
367            ValueDefinition::Gradient(sanitize_gradient(gradient))
368        }
369        ValueDefinition::Transition(transition) => {
370            ValueDefinition::Transition(sanitize_transition_definition(transition))
371        }
372        ValueDefinition::Expression(info) => ValueDefinition::Expression(info.clone()),
373        ValueDefinition::Identifier(identifier) => ValueDefinition::Identifier(identifier.clone()),
374        ValueDefinition::DoubleBinding(identifier) => {
375            ValueDefinition::DoubleBinding(identifier.clone())
376        }
377        ValueDefinition::EventBindingTarget(identifier) => {
378            ValueDefinition::EventBindingTarget(identifier.clone())
379        }
380    }
381}
382
383fn sanitize_transition_definition(transition: &TransitionDefinition) -> TransitionDefinition {
384    TransitionDefinition {
385        enter: transition.enter.as_ref().map(sanitize_timeline_track),
386        exit: transition.exit.as_ref().map(sanitize_timeline_track),
387        starting_value: transition
388            .starting_value
389            .as_ref()
390            .map(|value| Box::new(sanitize_value_definition(value))),
391    }
392}
393
394fn strip_token(token: &Token) -> Token {
395    Token::new_without_location(token.token_value.clone())
396}
397
398pub mod binary {
399    use super::{ProgramIR, MAGIC, VERSION};
400    use crate::binary::{decode_with_header, encode_with_header, Result};
401
402    pub fn to_vec(program: &ProgramIR) -> Result<Vec<u8>> {
403        encode_with_header(MAGIC, VERSION, program)
404    }
405
406    pub fn from_slice(bytes: &[u8]) -> Result<ProgramIR> {
407        decode_with_header(bytes, MAGIC, VERSION)
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use std::collections::{BTreeMap, HashMap};
414
415    use pax_runtime_api::{Color, Numeric, PaxValue, Size};
416
417    use super::ProgramIR;
418    use crate::{
419        binary::Result as BinaryResult, ComponentDefinition, ComponentTemplate, GradientDefinition,
420        GradientElement, GradientShapeDefinition, GradientStopDefinition, LiteralBlockDefinition,
421        LocationInfo, PaxIdentifier, PaxManifest, SettingsBlockElement, TemplateNodeDefinition,
422        TimelineBlockElement, TimelineDefinition, TimelineKeyframe, TimelineMarker,
423        TimelineSelectorBlockDefinition, TimelineSelectorElement, TimelineTrackDefinition,
424        TimelineTrackElement, Token, TypeId, ValueDefinition,
425    };
426
427    fn test_location() -> LocationInfo {
428        LocationInfo {
429            start_line_col: (1, 1),
430            end_line_col: (1, 5),
431        }
432    }
433
434    fn build_manifest() -> PaxManifest {
435        let component_type_id = TypeId::build_singleton("example::Main", Some("Main"));
436        let child_type_id = TypeId::build_singleton("example::Child", Some("Child"));
437        let mut template =
438            ComponentTemplate::new(component_type_id.clone(), Some("src/main.pax".to_string()));
439
440        template.add(TemplateNodeDefinition {
441            type_id: TypeId::build_comment(),
442            control_flow_settings: None,
443            settings: None,
444            selector_info: Default::default(),
445            raw_comment_string: Some("// comment".to_string()),
446        });
447
448        template.add(TemplateNodeDefinition {
449            type_id: child_type_id,
450            control_flow_settings: None,
451            settings: Some(vec![
452                crate::SettingElement::Comment("inline comment".to_string()),
453                crate::SettingElement::Setting(
454                    Token::new("fill".to_string(), test_location()),
455                    ValueDefinition::Block(LiteralBlockDefinition {
456                        explicit_type_pascal_identifier: Some(Token::new(
457                            "Color".to_string(),
458                            test_location(),
459                        )),
460                        elements: vec![
461                            crate::SettingElement::Comment("nested comment".to_string()),
462                            crate::SettingElement::Setting(
463                                Token::new("value".to_string(), test_location()),
464                                ValueDefinition::LiteralValue(PaxValue::Numeric(1.into())),
465                            ),
466                        ],
467                    }),
468                ),
469            ]),
470            selector_info: Default::default(),
471            raw_comment_string: None,
472        });
473
474        let mut components = BTreeMap::new();
475        components.insert(
476            component_type_id.clone(),
477            ComponentDefinition {
478                type_id: component_type_id.clone(),
479                is_main_component: true,
480                is_primitive: false,
481                is_struct_only_component: false,
482                module_path: "example::main".to_string(),
483                primitive_instance_import_path: Some("example::instance".to_string()),
484                template: Some(template),
485                settings: Some(vec![
486                    SettingsBlockElement::Comment("settings comment".to_string()),
487                    SettingsBlockElement::Handler(
488                        Token::new("click".to_string(), test_location()),
489                        vec![Token::new("handle_click".to_string(), test_location())],
490                    ),
491                    SettingsBlockElement::SelectorBlock(
492                        Token::new("#id".to_string(), test_location()),
493                        LiteralBlockDefinition {
494                            explicit_type_pascal_identifier: None,
495                            elements: vec![
496                                crate::SettingElement::Comment("selector comment".to_string()),
497                                crate::SettingElement::Setting(
498                                    Token::new("value".to_string(), test_location()),
499                                    ValueDefinition::Identifier(PaxIdentifier::new("self.value")),
500                                ),
501                                crate::SettingElement::Setting(
502                                    Token::new("fill".to_string(), test_location()),
503                                    ValueDefinition::Gradient(GradientDefinition {
504                                        shape: GradientShapeDefinition::Linear {
505                                            start: None,
506                                            end: None,
507                                        },
508                                        elements: vec![
509                                            GradientElement::Comment(
510                                                "gradient comment".to_string(),
511                                            ),
512                                            GradientElement::Stop(GradientStopDefinition {
513                                                position: Size::Percent(Numeric::F64(0.0)),
514                                                color: ValueDefinition::LiteralValue(
515                                                    PaxValue::Color(Box::new(Color::RED)),
516                                                ),
517                                            }),
518                                            GradientElement::Stop(GradientStopDefinition {
519                                                position: Size::Percent(Numeric::F64(100.0)),
520                                                color: ValueDefinition::LiteralValue(
521                                                    PaxValue::Color(Box::new(Color::BLUE)),
522                                                ),
523                                            }),
524                                        ],
525                                    }),
526                                ),
527                            ],
528                        },
529                    ),
530                ]),
531                timelines: vec![TimelineDefinition {
532                    name: Some(Token::new("spin".to_string(), test_location())),
533                    playhead: Some(ValueDefinition::Identifier(PaxIdentifier::new(
534                        "self.elapsed_frames",
535                    ))),
536                    duration: Some(ValueDefinition::LiteralValue(PaxValue::Numeric(120.into()))),
537                    repeat: true,
538                    interruption: crate::InOutInterruption::Restart,
539                    elements: vec![
540                        TimelineBlockElement::Comment("timeline comment".to_string()),
541                        TimelineBlockElement::SelectorBlock(
542                            Token::new("#id".to_string(), test_location()),
543                            TimelineSelectorBlockDefinition {
544                                elements: vec![
545                                    TimelineSelectorElement::Comment(
546                                        "selector comment".to_string(),
547                                    ),
548                                    TimelineSelectorElement::Track(
549                                        Token::new("rotate".to_string(), test_location()),
550                                        TimelineTrackDefinition {
551                                            elements: vec![
552                                                TimelineTrackElement::Comment(
553                                                    "track comment".to_string(),
554                                                ),
555                                                TimelineTrackElement::Keyframe(TimelineKeyframe {
556                                                    marker: TimelineMarker::Frame(0),
557                                                    value: ValueDefinition::LiteralValue(
558                                                        PaxValue::Numeric(0.into()),
559                                                    ),
560                                                    easing: Some(Token::new(
561                                                        "Linear".to_string(),
562                                                        test_location(),
563                                                    )),
564                                                }),
565                                            ],
566                                            playhead: None,
567                                            duration: None,
568                                            repeat: None,
569                                            starting_value: None,
570                                            interruption: crate::InOutInterruption::Restart,
571                                            use_local_property_scope: false,
572                                        },
573                                    ),
574                                ],
575                            },
576                        ),
577                    ],
578                }],
579                route_branch: None,
580            },
581        );
582
583        PaxManifest {
584            components,
585            main_component_type_id: component_type_id,
586            type_table: HashMap::new(),
587            assets_dirs: vec!["assets".to_string()],
588            engine_import_path: "pax_engine".to_string(),
589        }
590    }
591
592    #[test]
593    fn program_ir_strips_source_and_comment_metadata() {
594        let manifest = build_manifest();
595        let ir = ProgramIR::from_manifest(&manifest);
596
597        assert_eq!(ir.assets_dirs, vec!["assets".to_string()]);
598        assert_eq!(ir.components.len(), 1);
599
600        let component = ir
601            .components
602            .get(&manifest.main_component_type_id)
603            .expect("component should exist");
604
605        let template = component.template.as_ref().expect("template should exist");
606        assert_eq!(template.get_file_path(), None);
607        assert_eq!(template.get_root().len(), 1);
608
609        let only_root = template.get_root().into_iter().next().unwrap();
610        let root_node = template
611            .get_node(&only_root)
612            .expect("root node should exist");
613        let settings = root_node.settings.as_ref().expect("settings should remain");
614        assert_eq!(settings.len(), 1);
615        match &settings[0] {
616            crate::SettingElement::Setting(token, ValueDefinition::Block(block)) => {
617                assert_eq!(token.token_location, None);
618                assert_eq!(block.elements.len(), 1);
619                assert_eq!(
620                    block
621                        .explicit_type_pascal_identifier
622                        .as_ref()
623                        .and_then(|token| token.token_location.clone()),
624                    None
625                );
626            }
627            _ => panic!("expected sanitized block setting"),
628        }
629
630        let component_settings = component.settings.as_ref().expect("settings should remain");
631        assert_eq!(component_settings.len(), 2);
632        match &component_settings[0] {
633            SettingsBlockElement::Handler(token, handlers) => {
634                assert_eq!(token.token_location, None);
635                assert!(handlers
636                    .iter()
637                    .all(|handler| handler.token_location.is_none()));
638            }
639            _ => panic!("expected handler setting"),
640        }
641        match &component_settings[1] {
642            SettingsBlockElement::SelectorBlock(token, block) => {
643                assert_eq!(token.token_location, None);
644                assert_eq!(block.elements.len(), 2);
645                let Some(crate::SettingElement::Setting(_, ValueDefinition::Gradient(gradient))) =
646                    block.elements.get(1)
647                else {
648                    panic!("expected sanitized gradient setting");
649                };
650                assert!(matches!(
651                    &gradient.shape,
652                    GradientShapeDefinition::Linear {
653                        start: None,
654                        end: None
655                    }
656                ));
657                assert_eq!(gradient.elements.len(), 2);
658            }
659            _ => panic!("expected selector setting"),
660        }
661
662        let timeline = component.timelines.first().expect("timeline should remain");
663        assert_eq!(
664            timeline
665                .name
666                .as_ref()
667                .and_then(|token| token.token_location.clone()),
668            None
669        );
670        assert_eq!(timeline.elements.len(), 1);
671        match &timeline.elements[0] {
672            TimelineBlockElement::SelectorBlock(token, selector) => {
673                assert_eq!(token.token_location, None);
674                assert_eq!(selector.elements.len(), 1);
675                match &selector.elements[0] {
676                    TimelineSelectorElement::Track(track_token, track) => {
677                        assert_eq!(track_token.token_location, None);
678                        assert_eq!(track.elements.len(), 1);
679                        match &track.elements[0] {
680                            TimelineTrackElement::Keyframe(keyframe) => {
681                                assert_eq!(
682                                    keyframe
683                                        .easing
684                                        .as_ref()
685                                        .and_then(|token| token.token_location.clone()),
686                                    None
687                                );
688                            }
689                            _ => panic!("expected sanitized keyframe"),
690                        }
691                    }
692                    _ => panic!("expected sanitized track"),
693                }
694            }
695            _ => panic!("expected sanitized timeline selector"),
696        }
697    }
698
699    #[test]
700    #[cfg(feature = "parsing")]
701    fn pax995_program_binary_preserves_ast_dependencies_and_results() -> BinaryResult<()> {
702        use pax_language::{parse_pax_expression, Computable, DependencyCollector};
703        use pax_runtime_api::{Functions, ToPaxValue};
704        use std::rc::Rc;
705        let graph = pax_runtime_api::PropertyGraph::new(|| {});
706        let _graph = graph.enter();
707        Functions::register_all_functions();
708        for source in [
709            "items[indices[index]]",
710            "[items[index], items[indices[index]], items[index]]",
711            "1 + 2 > 2",
712            "true || false && false",
713            "2 * 3 %% 2",
714            "false && items[9]",
715            "true || items[9]",
716            "None ?? (false ? items[9] : items[index])",
717            "2 ^ 3 ^ 2",
718        ] {
719            let expr = crate::ExpressionInfo::new(parse_pax_expression(source).unwrap());
720            let mut manifest = build_manifest();
721            manifest
722                .components
723                .get_mut(&manifest.main_component_type_id)
724                .unwrap()
725                .settings = Some(vec![SettingsBlockElement::SelectorBlock(
726                Token::new_without_location("#probe".into()),
727                LiteralBlockDefinition {
728                    explicit_type_pascal_identifier: None,
729                    elements: vec![crate::SettingElement::Setting(
730                        Token::new_without_location("value".into()),
731                        ValueDefinition::Expression(expr.clone()),
732                    )],
733                },
734            )]);
735            let manifest = crate::binary::from_slice(&crate::binary::to_vec(&manifest)?)?;
736            let ir = ProgramIR::from_manifest(&manifest);
737            let decoded = super::binary::from_slice(&super::binary::to_vec(&ir)?)?;
738            let settings = decoded.components[&decoded.main_component_type_id]
739                .settings
740                .as_ref()
741                .unwrap();
742            let SettingsBlockElement::SelectorBlock(_, values) = &settings[0] else {
743                panic!("selector")
744            };
745            let crate::SettingElement::Setting(_, ValueDefinition::Expression(actual)) =
746                &values.elements[0]
747            else {
748                panic!("expression")
749            };
750            assert_eq!(actual.expression, expr.expression, "{source}");
751            assert_eq!(actual.dependencies, expr.dependencies, "{source}");
752            let regenerated = crate::ExpressionInfo::new(parse_pax_expression(source).unwrap());
753            assert_eq!(actual.dependencies, regenerated.dependencies, "{source}");
754            let mut expected_deps = expr.expression.collect_dependencies();
755            let mut actual_deps = actual.dependencies.clone();
756            expected_deps.sort();
757            actual_deps.sort();
758            assert_eq!(actual_deps, expected_deps);
759            let idr = Rc::new(HashMap::from([
760                ("items".into(), vec![10_i64, 20].to_pax_value()),
761                ("indices".into(), vec![0_i64, 1].to_pax_value()),
762                ("index".into(), 1_i64.to_pax_value()),
763            ]));
764            assert_eq!(
765                actual.expression.compute(idr.clone()),
766                expr.expression.compute(idr),
767                "{source}"
768            );
769        }
770        Ok(())
771    }
772
773    #[test]
774    fn proximity_bindings_survive_rich_and_release_program_paths() -> BinaryResult<()> {
775        let mut manifest = build_manifest();
776        let events = [
777            ("viewport_proximity_enter", "ViewportProximityEnter"),
778            ("viewport_proximity_change", "ViewportProximityChange"),
779            ("viewport_proximity_exit", "ViewportProximityExit"),
780        ];
781        let component = manifest
782            .components
783            .get_mut(&manifest.main_component_type_id)
784            .unwrap();
785        let template = component.template.as_mut().unwrap();
786        let id = template
787            .get_root()
788            .into_iter()
789            .find(|id| template.get_node(id).unwrap().raw_comment_string.is_none())
790            .unwrap();
791        let mut node = template.get_node(&id).unwrap().clone();
792        for (event, _) in events {
793            component
794                .settings
795                .as_mut()
796                .unwrap()
797                .push(SettingsBlockElement::Handler(
798                    Token::new(event.into(), test_location()),
799                    vec![Token::new(format!("handle_{event}"), test_location())],
800                ));
801            node.settings
802                .as_mut()
803                .unwrap()
804                .push(crate::SettingElement::Setting(
805                    Token::new(event.into(), test_location()),
806                    ValueDefinition::EventBindingTarget(PaxIdentifier::new(&format!(
807                        "self.handle_{event}"
808                    ))),
809                ));
810        }
811        template.set_node(id, node);
812        let rich = crate::binary::from_slice(&crate::binary::to_vec(&manifest)?)?;
813        let ir = ProgramIR::from_manifest(&rich);
814        let decoded = super::binary::from_slice(&super::binary::to_vec(&ir)?)?;
815        #[cfg(feature = "compiler")]
816        let generated_rust = crate::rust_manifest::to_rust_expression(&rich);
817        let component = &decoded.components[&decoded.main_component_type_id];
818        let template = component.template.as_ref().unwrap();
819        let node = template.get_node(&template.get_root()[0]).unwrap();
820        for (event, payload) in events {
821            assert_eq!(rich.event_to_args_map()[event].as_deref(), Some(payload));
822            assert!(component.settings.as_ref().unwrap().iter().any(|setting| matches!(setting,
823                SettingsBlockElement::Handler(key, handlers) if key.token_value == event && handlers[0].token_value == format!("handle_{event}"))));
824            assert!(node.settings.as_ref().unwrap().iter().any(|setting| matches!(setting,
825                crate::SettingElement::Setting(key, ValueDefinition::EventBindingTarget(_)) if key.token_value == event)));
826            #[cfg(feature = "compiler")]
827            {
828                assert!(generated_rust.contains(event));
829                assert!(generated_rust.contains(&format!("handle_{event}")));
830            }
831        }
832        Ok(())
833    }
834
835    #[test]
836    fn program_ir_binary_round_trips() -> BinaryResult<()> {
837        let manifest = build_manifest();
838        let ir = ProgramIR::from_manifest(&manifest);
839
840        let bytes = super::binary::to_vec(&ir)?;
841        let decoded = super::binary::from_slice(&bytes)?;
842
843        assert_eq!(decoded.main_component_type_id, ir.main_component_type_id);
844        assert_eq!(decoded.assets_dirs, ir.assets_dirs);
845        assert_eq!(decoded.components.len(), ir.components.len());
846        assert_eq!(decoded.type_table.len(), ir.type_table.len());
847
848        let component = decoded
849            .components
850            .get(&decoded.main_component_type_id)
851            .expect("decoded component should exist");
852        assert!(component.template.is_some());
853        assert_eq!(
854            component.timelines[0].interruption,
855            crate::InOutInterruption::Restart
856        );
857        let TimelineBlockElement::SelectorBlock(_, selector) = &component.timelines[0].elements[0]
858        else {
859            panic!("expected decoded selector block")
860        };
861        let TimelineSelectorElement::Track(_, track) = &selector.elements[0] else {
862            panic!("expected decoded timeline track")
863        };
864        assert_eq!(track.interruption, crate::InOutInterruption::Restart);
865        assert_eq!(
866            component
867                .template
868                .as_ref()
869                .expect("decoded template should exist")
870                .get_root()
871                .len(),
872            1
873        );
874
875        Ok(())
876    }
877
878    #[test]
879    fn imported_settings_policy_and_blended_paint_survive_rich_and_baked_programs(
880    ) -> BinaryResult<()> {
881        let policy = PaxValue::Enum(Box::new((
882            "SettingsTransition".into(),
883            "Ease".into(),
884            vec![
885                PaxValue::Duration(pax_runtime_api::Duration::Milliseconds(400.into())),
886                PaxValue::Enum(Box::new((
887                    "TransitionCurve".into(),
888                    "InOutQuad".into(),
889                    vec![],
890                ))),
891            ],
892        )));
893        use pax_runtime_api::{
894            CoercionRules, Color, GradientStop, Interpolatable, Paint, Size, ToPaxValue,
895        };
896        let paint = Paint::linearGradient(
897            (Size::Percent(0.into()), Size::Percent(0.into())),
898            (Size::Percent(100.into()), Size::Percent(100.into())),
899            vec![GradientStop::get(Color::BLACK, Size::Percent(0.into()))],
900        )
901        .interpolate(
902            &Paint::RadialGradient(pax_runtime_api::RadialGradient {
903                start: (Size::Percent(60.into()), Size::Percent(40.into())),
904                end: (Size::Percent(50.into()), Size::Percent(50.into())),
905                radius: 68.0,
906                stops: vec![GradientStop::get(Color::WHITE, Size::Percent(100.into()))],
907            }),
908            0.375,
909        );
910        let paint_value = paint.clone().to_pax_value();
911        let mut manifest = build_manifest();
912        manifest
913            .components
914            .get_mut(&manifest.main_component_type_id)
915            .unwrap()
916            .settings = Some(vec![SettingsBlockElement::SelectorBlock(
917            Token::new("#import".into(), test_location()),
918            LiteralBlockDefinition {
919                explicit_type_pascal_identifier: None,
920                elements: vec![
921                    crate::SettingElement::Setting(
922                        Token::new("transition".into(), test_location()),
923                        ValueDefinition::LiteralValue(policy.clone()),
924                    ),
925                    crate::SettingElement::Setting(
926                        Token::new("fill".into(), test_location()),
927                        ValueDefinition::LiteralValue(paint_value.clone()),
928                    ),
929                ],
930            },
931        )]);
932        let rich = crate::binary::from_slice(&crate::binary::to_vec(&manifest)?)?;
933        let decoded =
934            super::binary::from_slice(&super::binary::to_vec(&ProgramIR::from_manifest(&rich))?)?;
935        let settings = decoded.components[&decoded.main_component_type_id]
936            .settings
937            .as_ref()
938            .unwrap();
939        let SettingsBlockElement::SelectorBlock(_, block) = &settings[0] else {
940            panic!("missing import settings");
941        };
942        let crate::SettingElement::Setting(_, ValueDefinition::LiteralValue(value)) =
943            &block.elements[0]
944        else {
945            panic!("missing transition policy");
946        };
947        assert_eq!(value, &policy);
948        let crate::SettingElement::Setting(_, ValueDefinition::LiteralValue(value)) =
949            &block.elements[1]
950        else {
951            panic!("missing paint mixture");
952        };
953        assert_eq!(value, &paint_value);
954        assert_eq!(Paint::try_coerce(value.clone()).unwrap(), paint);
955
956        #[cfg(feature = "compiler")]
957        {
958            let rust = crate::rust_manifest::to_rust_expression(&manifest);
959            assert!(rust.contains("SettingsTransition"));
960            assert!(rust.contains("Blend"));
961            assert!(rust.contains("InOutQuad"));
962            assert!(rust.contains("Duration::Milliseconds"));
963        }
964        Ok(())
965    }
966}