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