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#[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}