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 = 3;
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::Linear { start, end } => GradientShapeDefinition::Linear {
337            start: start
338                .as_ref()
339                .map(|value| Box::new(sanitize_value_definition(value))),
340            end: end
341                .as_ref()
342                .map(|value| Box::new(sanitize_value_definition(value))),
343        },
344        GradientShapeDefinition::Radial { start, end, radius } => GradientShapeDefinition::Radial {
345            start: Box::new(sanitize_value_definition(start)),
346            end: Box::new(sanitize_value_definition(end)),
347            radius: Box::new(sanitize_value_definition(radius)),
348        },
349    }
350}
351
352fn sanitize_value_definition(value: &ValueDefinition) -> ValueDefinition {
353    match value {
354        ValueDefinition::Undefined => ValueDefinition::Undefined,
355        ValueDefinition::LiteralValue(value) => ValueDefinition::LiteralValue(value.clone()),
356        ValueDefinition::Block(block) => ValueDefinition::Block(sanitize_literal_block(block)),
357        ValueDefinition::Timeline(track) => {
358            ValueDefinition::Timeline(sanitize_timeline_track(track))
359        }
360        ValueDefinition::Gradient(gradient) => {
361            ValueDefinition::Gradient(sanitize_gradient(gradient))
362        }
363        ValueDefinition::Transition(transition) => {
364            ValueDefinition::Transition(sanitize_transition_definition(transition))
365        }
366        ValueDefinition::Expression(info) => ValueDefinition::Expression(info.clone()),
367        ValueDefinition::Identifier(identifier) => ValueDefinition::Identifier(identifier.clone()),
368        ValueDefinition::DoubleBinding(identifier) => {
369            ValueDefinition::DoubleBinding(identifier.clone())
370        }
371        ValueDefinition::EventBindingTarget(identifier) => {
372            ValueDefinition::EventBindingTarget(identifier.clone())
373        }
374    }
375}
376
377fn sanitize_transition_definition(transition: &TransitionDefinition) -> TransitionDefinition {
378    TransitionDefinition {
379        enter: transition.enter.as_ref().map(sanitize_timeline_track),
380        exit: transition.exit.as_ref().map(sanitize_timeline_track),
381        starting_value: transition
382            .starting_value
383            .as_ref()
384            .map(|value| Box::new(sanitize_value_definition(value))),
385    }
386}
387
388fn strip_token(token: &Token) -> Token {
389    Token::new_without_location(token.token_value.clone())
390}
391
392pub mod binary {
393    use super::{ProgramIR, MAGIC, VERSION};
394    use crate::binary::{decode_with_header, encode_with_header, Result};
395
396    pub fn to_vec(program: &ProgramIR) -> Result<Vec<u8>> {
397        encode_with_header(MAGIC, VERSION, program)
398    }
399
400    pub fn from_slice(bytes: &[u8]) -> Result<ProgramIR> {
401        decode_with_header(bytes, MAGIC, VERSION)
402    }
403}
404
405#[cfg(test)]
406mod tests {
407    use std::collections::{BTreeMap, HashMap};
408
409    use pax_runtime_api::{Color, Numeric, PaxValue, Size};
410
411    use super::ProgramIR;
412    use crate::{
413        binary::Result as BinaryResult, ComponentDefinition, ComponentTemplate, GradientDefinition,
414        GradientElement, GradientShapeDefinition, GradientStopDefinition, LiteralBlockDefinition,
415        LocationInfo, PaxIdentifier, PaxManifest, SettingsBlockElement, TemplateNodeDefinition,
416        TimelineBlockElement, TimelineDefinition, TimelineKeyframe, TimelineMarker,
417        TimelineSelectorBlockDefinition, TimelineSelectorElement, TimelineTrackDefinition,
418        TimelineTrackElement, Token, TypeId, ValueDefinition,
419    };
420
421    fn test_location() -> LocationInfo {
422        LocationInfo {
423            start_line_col: (1, 1),
424            end_line_col: (1, 5),
425        }
426    }
427
428    fn build_manifest() -> PaxManifest {
429        let component_type_id = TypeId::build_singleton("example::Main", Some("Main"));
430        let child_type_id = TypeId::build_singleton("example::Child", Some("Child"));
431        let mut template =
432            ComponentTemplate::new(component_type_id.clone(), Some("src/main.pax".to_string()));
433
434        template.add(TemplateNodeDefinition {
435            type_id: TypeId::build_comment(),
436            control_flow_settings: None,
437            settings: None,
438            selector_info: Default::default(),
439            raw_comment_string: Some("// comment".to_string()),
440        });
441
442        template.add(TemplateNodeDefinition {
443            type_id: child_type_id,
444            control_flow_settings: None,
445            settings: Some(vec![
446                crate::SettingElement::Comment("inline comment".to_string()),
447                crate::SettingElement::Setting(
448                    Token::new("fill".to_string(), test_location()),
449                    ValueDefinition::Block(LiteralBlockDefinition {
450                        explicit_type_pascal_identifier: Some(Token::new(
451                            "Color".to_string(),
452                            test_location(),
453                        )),
454                        elements: vec![
455                            crate::SettingElement::Comment("nested comment".to_string()),
456                            crate::SettingElement::Setting(
457                                Token::new("value".to_string(), test_location()),
458                                ValueDefinition::LiteralValue(PaxValue::Numeric(1.into())),
459                            ),
460                        ],
461                    }),
462                ),
463            ]),
464            selector_info: Default::default(),
465            raw_comment_string: None,
466        });
467
468        let mut components = BTreeMap::new();
469        components.insert(
470            component_type_id.clone(),
471            ComponentDefinition {
472                type_id: component_type_id.clone(),
473                is_main_component: true,
474                is_primitive: false,
475                is_struct_only_component: false,
476                module_path: "example::main".to_string(),
477                primitive_instance_import_path: Some("example::instance".to_string()),
478                template: Some(template),
479                settings: Some(vec![
480                    SettingsBlockElement::Comment("settings comment".to_string()),
481                    SettingsBlockElement::Handler(
482                        Token::new("click".to_string(), test_location()),
483                        vec![Token::new("handle_click".to_string(), test_location())],
484                    ),
485                    SettingsBlockElement::SelectorBlock(
486                        Token::new("#id".to_string(), test_location()),
487                        LiteralBlockDefinition {
488                            explicit_type_pascal_identifier: None,
489                            elements: vec![
490                                crate::SettingElement::Comment("selector comment".to_string()),
491                                crate::SettingElement::Setting(
492                                    Token::new("value".to_string(), test_location()),
493                                    ValueDefinition::Identifier(PaxIdentifier::new("self.value")),
494                                ),
495                                crate::SettingElement::Setting(
496                                    Token::new("fill".to_string(), test_location()),
497                                    ValueDefinition::Gradient(GradientDefinition {
498                                        shape: GradientShapeDefinition::Linear {
499                                            start: None,
500                                            end: None,
501                                        },
502                                        elements: vec![
503                                            GradientElement::Comment(
504                                                "gradient comment".to_string(),
505                                            ),
506                                            GradientElement::Stop(GradientStopDefinition {
507                                                position: Size::Percent(Numeric::F64(0.0)),
508                                                color: ValueDefinition::LiteralValue(
509                                                    PaxValue::Color(Box::new(Color::RED)),
510                                                ),
511                                            }),
512                                            GradientElement::Stop(GradientStopDefinition {
513                                                position: Size::Percent(Numeric::F64(100.0)),
514                                                color: ValueDefinition::LiteralValue(
515                                                    PaxValue::Color(Box::new(Color::BLUE)),
516                                                ),
517                                            }),
518                                        ],
519                                    }),
520                                ),
521                            ],
522                        },
523                    ),
524                ]),
525                timelines: vec![TimelineDefinition {
526                    name: Some(Token::new("spin".to_string(), test_location())),
527                    playhead: Some(ValueDefinition::Identifier(PaxIdentifier::new(
528                        "self.elapsed_frames",
529                    ))),
530                    duration: Some(ValueDefinition::LiteralValue(PaxValue::Numeric(120.into()))),
531                    repeat: true,
532                    interruption: crate::InOutInterruption::Restart,
533                    elements: vec![
534                        TimelineBlockElement::Comment("timeline comment".to_string()),
535                        TimelineBlockElement::SelectorBlock(
536                            Token::new("#id".to_string(), test_location()),
537                            TimelineSelectorBlockDefinition {
538                                elements: vec![
539                                    TimelineSelectorElement::Comment(
540                                        "selector comment".to_string(),
541                                    ),
542                                    TimelineSelectorElement::Track(
543                                        Token::new("rotate".to_string(), test_location()),
544                                        TimelineTrackDefinition {
545                                            elements: vec![
546                                                TimelineTrackElement::Comment(
547                                                    "track comment".to_string(),
548                                                ),
549                                                TimelineTrackElement::Keyframe(TimelineKeyframe {
550                                                    marker: TimelineMarker::Frame(0),
551                                                    value: ValueDefinition::LiteralValue(
552                                                        PaxValue::Numeric(0.into()),
553                                                    ),
554                                                    easing: Some(Token::new(
555                                                        "Linear".to_string(),
556                                                        test_location(),
557                                                    )),
558                                                }),
559                                            ],
560                                            playhead: None,
561                                            duration: None,
562                                            repeat: None,
563                                            starting_value: None,
564                                            interruption: crate::InOutInterruption::Restart,
565                                            use_local_property_scope: false,
566                                        },
567                                    ),
568                                ],
569                            },
570                        ),
571                    ],
572                }],
573                route_branch: None,
574            },
575        );
576
577        PaxManifest {
578            components,
579            main_component_type_id: component_type_id,
580            type_table: HashMap::new(),
581            assets_dirs: vec!["assets".to_string()],
582            engine_import_path: "pax_engine".to_string(),
583        }
584    }
585
586    #[test]
587    fn program_ir_strips_source_and_comment_metadata() {
588        let manifest = build_manifest();
589        let ir = ProgramIR::from_manifest(&manifest);
590
591        assert_eq!(ir.assets_dirs, vec!["assets".to_string()]);
592        assert_eq!(ir.components.len(), 1);
593
594        let component = ir
595            .components
596            .get(&manifest.main_component_type_id)
597            .expect("component should exist");
598
599        let template = component.template.as_ref().expect("template should exist");
600        assert_eq!(template.get_file_path(), None);
601        assert_eq!(template.get_root().len(), 1);
602
603        let only_root = template.get_root().into_iter().next().unwrap();
604        let root_node = template
605            .get_node(&only_root)
606            .expect("root node should exist");
607        let settings = root_node.settings.as_ref().expect("settings should remain");
608        assert_eq!(settings.len(), 1);
609        match &settings[0] {
610            crate::SettingElement::Setting(token, ValueDefinition::Block(block)) => {
611                assert_eq!(token.token_location, None);
612                assert_eq!(block.elements.len(), 1);
613                assert_eq!(
614                    block
615                        .explicit_type_pascal_identifier
616                        .as_ref()
617                        .and_then(|token| token.token_location.clone()),
618                    None
619                );
620            }
621            _ => panic!("expected sanitized block setting"),
622        }
623
624        let component_settings = component.settings.as_ref().expect("settings should remain");
625        assert_eq!(component_settings.len(), 2);
626        match &component_settings[0] {
627            SettingsBlockElement::Handler(token, handlers) => {
628                assert_eq!(token.token_location, None);
629                assert!(handlers
630                    .iter()
631                    .all(|handler| handler.token_location.is_none()));
632            }
633            _ => panic!("expected handler setting"),
634        }
635        match &component_settings[1] {
636            SettingsBlockElement::SelectorBlock(token, block) => {
637                assert_eq!(token.token_location, None);
638                assert_eq!(block.elements.len(), 2);
639                let Some(crate::SettingElement::Setting(_, ValueDefinition::Gradient(gradient))) =
640                    block.elements.get(1)
641                else {
642                    panic!("expected sanitized gradient setting");
643                };
644                assert!(matches!(
645                    &gradient.shape,
646                    GradientShapeDefinition::Linear {
647                        start: None,
648                        end: None
649                    }
650                ));
651                assert_eq!(gradient.elements.len(), 2);
652            }
653            _ => panic!("expected selector setting"),
654        }
655
656        let timeline = component.timelines.first().expect("timeline should remain");
657        assert_eq!(
658            timeline
659                .name
660                .as_ref()
661                .and_then(|token| token.token_location.clone()),
662            None
663        );
664        assert_eq!(timeline.elements.len(), 1);
665        match &timeline.elements[0] {
666            TimelineBlockElement::SelectorBlock(token, selector) => {
667                assert_eq!(token.token_location, None);
668                assert_eq!(selector.elements.len(), 1);
669                match &selector.elements[0] {
670                    TimelineSelectorElement::Track(track_token, track) => {
671                        assert_eq!(track_token.token_location, None);
672                        assert_eq!(track.elements.len(), 1);
673                        match &track.elements[0] {
674                            TimelineTrackElement::Keyframe(keyframe) => {
675                                assert_eq!(
676                                    keyframe
677                                        .easing
678                                        .as_ref()
679                                        .and_then(|token| token.token_location.clone()),
680                                    None
681                                );
682                            }
683                            _ => panic!("expected sanitized keyframe"),
684                        }
685                    }
686                    _ => panic!("expected sanitized track"),
687                }
688            }
689            _ => panic!("expected sanitized timeline selector"),
690        }
691    }
692
693    #[test]
694    #[cfg(feature = "parsing")]
695    fn pax995_program_binary_preserves_ast_dependencies_and_results() -> BinaryResult<()> {
696        use pax_language::{parse_pax_expression, Computable, DependencyCollector};
697        use pax_runtime_api::{Functions, ToPaxValue};
698        use std::rc::Rc;
699        Functions::register_all_functions();
700        for source in [
701            "items[indices[index]]",
702            "1 + 2 > 2",
703            "true || false && false",
704            "2 * 3 %% 2",
705            "false && items[9]",
706            "true || items[9]",
707            "None ?? (false ? items[9] : items[index])",
708            "2 ^ 3 ^ 2",
709        ] {
710            let expr = crate::ExpressionInfo::new(parse_pax_expression(source).unwrap());
711            let mut manifest = build_manifest();
712            manifest
713                .components
714                .get_mut(&manifest.main_component_type_id)
715                .unwrap()
716                .settings = Some(vec![SettingsBlockElement::SelectorBlock(
717                Token::new_without_location("#probe".into()),
718                LiteralBlockDefinition {
719                    explicit_type_pascal_identifier: None,
720                    elements: vec![crate::SettingElement::Setting(
721                        Token::new_without_location("value".into()),
722                        ValueDefinition::Expression(expr.clone()),
723                    )],
724                },
725            )]);
726            let manifest = crate::binary::from_slice(&crate::binary::to_vec(&manifest)?)?;
727            let ir = ProgramIR::from_manifest(&manifest);
728            let decoded = super::binary::from_slice(&super::binary::to_vec(&ir)?)?;
729            let settings = decoded.components[&decoded.main_component_type_id]
730                .settings
731                .as_ref()
732                .unwrap();
733            let SettingsBlockElement::SelectorBlock(_, values) = &settings[0] else {
734                panic!("selector")
735            };
736            let crate::SettingElement::Setting(_, ValueDefinition::Expression(actual)) =
737                &values.elements[0]
738            else {
739                panic!("expression")
740            };
741            assert_eq!(actual.expression, expr.expression, "{source}");
742            assert_eq!(actual.dependencies, expr.dependencies, "{source}");
743            let mut expected_deps = expr.expression.collect_dependencies();
744            let mut actual_deps = actual.dependencies.clone();
745            expected_deps.sort();
746            actual_deps.sort();
747            assert_eq!(actual_deps, expected_deps);
748            let idr = Rc::new(HashMap::from([
749                ("items".into(), vec![10_i64, 20].to_pax_value()),
750                ("indices".into(), vec![0_i64, 1].to_pax_value()),
751                ("index".into(), 1_i64.to_pax_value()),
752            ]));
753            assert_eq!(
754                actual.expression.compute(idr.clone()),
755                expr.expression.compute(idr),
756                "{source}"
757            );
758        }
759        Ok(())
760    }
761
762    #[test]
763    fn program_ir_binary_round_trips() -> BinaryResult<()> {
764        let manifest = build_manifest();
765        let ir = ProgramIR::from_manifest(&manifest);
766
767        let bytes = super::binary::to_vec(&ir)?;
768        let decoded = super::binary::from_slice(&bytes)?;
769
770        assert_eq!(decoded.main_component_type_id, ir.main_component_type_id);
771        assert_eq!(decoded.assets_dirs, ir.assets_dirs);
772        assert_eq!(decoded.components.len(), ir.components.len());
773        assert_eq!(decoded.type_table.len(), ir.type_table.len());
774
775        let component = decoded
776            .components
777            .get(&decoded.main_component_type_id)
778            .expect("decoded component should exist");
779        assert!(component.template.is_some());
780        assert_eq!(
781            component.timelines[0].interruption,
782            crate::InOutInterruption::Restart
783        );
784        let TimelineBlockElement::SelectorBlock(_, selector) = &component.timelines[0].elements[0]
785        else {
786            panic!("expected decoded selector block")
787        };
788        let TimelineSelectorElement::Track(_, track) = &selector.elements[0] else {
789            panic!("expected decoded timeline track")
790        };
791        assert_eq!(track.interruption, crate::InOutInterruption::Restart);
792        assert_eq!(
793            component
794                .template
795                .as_ref()
796                .expect("decoded template should exist")
797                .get_root()
798                .len(),
799            1
800        );
801
802        Ok(())
803    }
804}