1use crate::SourceLocation;
15use crate::hir::{HirId, HirScopeId};
16use perl_semantic_facts::AnchorId;
17use std::collections::BTreeMap;
18
19pub const PIR_RECEIPT_VERSION: u32 = 1;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
28#[non_exhaustive]
29pub struct PirId {
30 index: u32,
31}
32
33impl PirId {
34 #[inline]
36 pub const fn from_index(index: u32) -> Self {
37 Self { index }
38 }
39
40 #[inline]
42 pub const fn index(self) -> u32 {
43 self.index
44 }
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
53#[non_exhaustive]
54pub enum PirAnchorKind {
55 ExplicitSource,
57 SourceBackedGenerated,
59 GeneratedNoSource,
61 DynamicBoundary,
63 AmbientInput,
65 Unknown,
67}
68
69impl PirAnchorKind {
70 #[must_use]
72 pub const fn name(self) -> &'static str {
73 match self {
74 Self::ExplicitSource => "ExplicitSource",
75 Self::SourceBackedGenerated => "SourceBackedGenerated",
76 Self::GeneratedNoSource => "GeneratedNoSource",
77 Self::DynamicBoundary => "DynamicBoundary",
78 Self::AmbientInput => "AmbientInput",
79 Self::Unknown => "Unknown",
80 }
81 }
82
83 #[must_use]
85 pub const fn is_source_backed(self) -> bool {
86 matches!(self, Self::ExplicitSource | Self::SourceBackedGenerated | Self::DynamicBoundary)
87 }
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
96#[non_exhaustive]
97pub struct PirSourceAnchor {
98 pub kind: PirAnchorKind,
100 pub range: Option<SourceLocation>,
102 pub anchor_id: Option<AnchorId>,
104 pub hir_item: Option<HirId>,
106}
107
108impl PirSourceAnchor {
109 #[must_use]
111 pub fn explicit(range: SourceLocation, hir_item: HirId) -> Self {
112 Self {
113 kind: PirAnchorKind::ExplicitSource,
114 range: Some(range),
115 anchor_id: Some(AnchorId(range.start as u64)),
116 hir_item: Some(hir_item),
117 }
118 }
119
120 #[must_use]
122 pub fn dynamic_boundary(range: SourceLocation, hir_item: HirId) -> Self {
123 Self {
124 kind: PirAnchorKind::DynamicBoundary,
125 range: Some(range),
126 anchor_id: Some(AnchorId(range.start as u64)),
127 hir_item: Some(hir_item),
128 }
129 }
130
131 #[must_use]
133 pub fn is_anchored(&self) -> bool {
134 self.range.is_some()
135 }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
144#[non_exhaustive]
145pub enum PirContext {
146 Scalar,
148 List,
150 Void,
152 Lvalue,
154 Unknown,
156}
157
158impl PirContext {
159 #[must_use]
161 pub const fn name(self) -> &'static str {
162 match self {
163 Self::Scalar => "Scalar",
164 Self::List => "List",
165 Self::Void => "Void",
166 Self::Lvalue => "Lvalue",
167 Self::Unknown => "Unknown",
168 }
169 }
170}
171
172#[derive(Debug, Clone, PartialEq, Eq)]
174#[non_exhaustive]
175pub struct LexicalName {
176 pub sigil: String,
178 pub name: String,
180}
181
182#[derive(Debug, Clone, PartialEq, Eq)]
184#[non_exhaustive]
185pub struct SymbolName {
186 pub sigil: String,
188 pub name: String,
190 pub package: Option<String>,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq)]
196#[non_exhaustive]
197pub enum PirCallee {
198 Named {
200 name: String,
202 package: Option<String>,
204 },
205 Dynamic,
207}
208
209#[derive(Debug, Clone, PartialEq, Eq)]
211#[non_exhaustive]
212pub enum PirReceiver {
213 Class(String),
215 Expression {
217 kind: &'static str,
219 },
220 Dynamic,
222}
223
224#[derive(Debug, Clone, PartialEq, Eq)]
226#[non_exhaustive]
227pub enum PirMethod {
228 Named(String),
230 Dynamic,
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
236#[non_exhaustive]
237pub enum PirDynamicBoundaryKind {
238 DynamicCallee,
240 DynamicReceiver,
242 DynamicMethodName,
244 SymbolicReference,
246 TypeglobAccess,
248 DynamicDereference,
250 RuntimeStashMutation,
252 EvalExpression,
254 DoExpression,
256 Autoload,
258 Unknown,
260}
261
262impl PirDynamicBoundaryKind {
263 #[must_use]
265 pub const fn name(self) -> &'static str {
266 match self {
267 Self::DynamicCallee => "DynamicCallee",
268 Self::DynamicReceiver => "DynamicReceiver",
269 Self::DynamicMethodName => "DynamicMethodName",
270 Self::SymbolicReference => "SymbolicReference",
271 Self::TypeglobAccess => "TypeglobAccess",
272 Self::DynamicDereference => "DynamicDereference",
273 Self::RuntimeStashMutation => "RuntimeStashMutation",
274 Self::EvalExpression => "EvalExpression",
275 Self::DoExpression => "DoExpression",
276 Self::Autoload => "Autoload",
277 Self::Unknown => "Unknown",
278 }
279 }
280}
281
282#[derive(Debug, Clone, PartialEq, Eq)]
289#[non_exhaustive]
290pub enum PirOperation {
291 LexicalRead {
293 name: LexicalName,
295 },
296 LexicalWrite {
298 name: LexicalName,
300 },
301 StashRead {
303 symbol: SymbolName,
305 },
306 StashWrite {
308 symbol: SymbolName,
310 },
311 Modify {
316 name: LexicalName,
318 op: String,
320 },
321 StashModify {
325 symbol: SymbolName,
327 op: String,
329 },
330 Assign,
332 Call {
334 callee: PirCallee,
336 arg_count: usize,
338 },
339 MethodCall {
341 receiver: PirReceiver,
343 method: PirMethod,
345 arg_count: usize,
347 },
348 Branch {
350 condition: Option<PirId>,
352 },
353 Loop {
355 condition: Option<PirId>,
357 },
358 Return,
360 DynamicBoundary {
362 kind: PirDynamicBoundaryKind,
364 reason: String,
366 },
367}
368
369impl PirOperation {
370 #[must_use]
372 pub const fn name(&self) -> &'static str {
373 match self {
374 Self::LexicalRead { .. } => "LexicalRead",
375 Self::LexicalWrite { .. } => "LexicalWrite",
376 Self::StashRead { .. } => "StashRead",
377 Self::StashWrite { .. } => "StashWrite",
378 Self::Modify { .. } => "Modify",
379 Self::StashModify { .. } => "StashModify",
380 Self::Assign => "Assign",
381 Self::Call { .. } => "Call",
382 Self::MethodCall { .. } => "MethodCall",
383 Self::Branch { .. } => "Branch",
384 Self::Loop { .. } => "Loop",
385 Self::Return => "Return",
386 Self::DynamicBoundary { .. } => "DynamicBoundary",
387 }
388 }
389
390 pub const ALL_OPERATION_NAMES: &[&'static str] = &[
395 "Assign",
396 "Branch",
397 "Call",
398 "DynamicBoundary",
399 "LexicalRead",
400 "LexicalWrite",
401 "Loop",
402 "MethodCall",
403 "Modify",
404 "Return",
405 "StashModify",
406 "StashRead",
407 "StashWrite",
408 ];
409}
410
411#[derive(Debug, Clone, PartialEq, Eq)]
413#[non_exhaustive]
414pub struct PirNode {
415 pub id: PirId,
417 pub source_anchor: PirSourceAnchor,
419 pub operation: PirOperation,
421 pub context: PirContext,
423 pub dynamic_boundary: Option<PirId>,
425 pub scope: Option<HirScopeId>,
427 pub package_context: Option<String>,
429}
430
431#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
433#[non_exhaustive]
434pub enum PirEdgeKind {
435 Fallthrough,
437 Branch,
439 Loop,
441 Return,
443 DynamicExit,
445 Unknown,
447}
448
449impl PirEdgeKind {
450 #[must_use]
452 pub const fn name(self) -> &'static str {
453 match self {
454 Self::Fallthrough => "Fallthrough",
455 Self::Branch => "Branch",
456 Self::Loop => "Loop",
457 Self::Return => "Return",
458 Self::DynamicExit => "DynamicExit",
459 Self::Unknown => "Unknown",
460 }
461 }
462}
463
464#[derive(Debug, Clone, Copy, PartialEq, Eq)]
469#[non_exhaustive]
470pub struct PirEdge {
471 pub from: PirId,
473 pub to: Option<PirId>,
475 pub kind: PirEdgeKind,
477}
478
479#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
481#[non_exhaustive]
482pub enum PirLoweringMode {
483 HirV0,
485}
486
487impl PirLoweringMode {
488 #[must_use]
490 pub const fn name(self) -> &'static str {
491 match self {
492 Self::HirV0 => "HirV0",
493 }
494 }
495}
496
497#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
499#[non_exhaustive]
500pub struct PirAnchorCoverage {
501 pub anchored: usize,
503 pub unanchored: usize,
505}
506
507impl PirAnchorCoverage {
508 #[must_use]
510 pub const fn total(&self) -> usize {
511 self.anchored + self.unanchored
512 }
513}
514
515#[derive(Debug, Clone, PartialEq, Eq)]
521#[non_exhaustive]
522pub struct PirReceipt {
523 pub schema_version: u32,
525 pub source_identity: Option<String>,
527 pub lowering_mode: PirLoweringMode,
529 pub node_count: usize,
531 pub edge_count: usize,
533 pub operation_counts: BTreeMap<&'static str, usize>,
535 pub context_counts: BTreeMap<&'static str, usize>,
537 pub source_anchor_coverage: PirAnchorCoverage,
539 pub dynamic_boundary_counts: BTreeMap<&'static str, usize>,
541 pub unsupported_construct_counts: BTreeMap<&'static str, usize>,
543 pub ambient_inputs: Vec<String>,
545 pub provider_behavior_changed: bool,
547}
548
549#[derive(Debug, Clone, PartialEq, Eq)]
551#[non_exhaustive]
552pub struct PirGraph {
553 pub nodes: Vec<PirNode>,
555 pub edges: Vec<PirEdge>,
557 pub receipt: PirReceipt,
559}
560
561impl PirGraph {
562 #[must_use]
564 pub fn is_empty(&self) -> bool {
565 self.nodes.is_empty()
566 }
567
568 #[must_use]
574 pub fn node(&self, id: PirId) -> Option<&PirNode> {
575 self.nodes.get(id.index() as usize)
576 }
577}
578
579#[cfg(test)]
580mod tests {
581 use super::*;
582
583 #[test]
584 fn pir_context_has_stable_names() {
585 assert_eq!(PirContext::Scalar.name(), "Scalar");
586 assert_eq!(PirContext::List.name(), "List");
587 assert_eq!(PirContext::Void.name(), "Void");
588 assert_eq!(PirContext::Lvalue.name(), "Lvalue");
589 assert_eq!(PirContext::Unknown.name(), "Unknown");
590 }
591
592 #[test]
593 fn pir_anchor_kind_has_stable_names() {
594 assert_eq!(PirAnchorKind::ExplicitSource.name(), "ExplicitSource");
595 assert_eq!(PirAnchorKind::SourceBackedGenerated.name(), "SourceBackedGenerated");
596 assert_eq!(PirAnchorKind::GeneratedNoSource.name(), "GeneratedNoSource");
597 assert_eq!(PirAnchorKind::DynamicBoundary.name(), "DynamicBoundary");
598 assert_eq!(PirAnchorKind::AmbientInput.name(), "AmbientInput");
599 assert_eq!(PirAnchorKind::Unknown.name(), "Unknown");
600 }
601
602 #[test]
603 fn pir_anchor_kind_is_source_backed() {
604 assert!(PirAnchorKind::ExplicitSource.is_source_backed());
605 assert!(PirAnchorKind::SourceBackedGenerated.is_source_backed());
606 assert!(!PirAnchorKind::GeneratedNoSource.is_source_backed());
607 assert!(PirAnchorKind::DynamicBoundary.is_source_backed());
608 assert!(!PirAnchorKind::AmbientInput.is_source_backed());
609 assert!(!PirAnchorKind::Unknown.is_source_backed());
610 }
611
612 #[test]
613 fn pir_source_anchor_explicit_creates_anchored() {
614 let loc = SourceLocation { start: 0, end: 5 };
615 let anchor = PirSourceAnchor::explicit(loc, HirId::from_index(1));
616 assert!(anchor.is_anchored());
617 assert_eq!(anchor.kind, PirAnchorKind::ExplicitSource);
618 assert_eq!(anchor.range, Some(loc));
619 }
620
621 #[test]
622 fn pir_source_anchor_dynamic_boundary_creates_anchored() {
623 let loc = SourceLocation { start: 10, end: 20 };
624 let anchor = PirSourceAnchor::dynamic_boundary(loc, HirId::from_index(2));
625 assert!(anchor.is_anchored());
626 assert_eq!(anchor.kind, PirAnchorKind::DynamicBoundary);
627 assert_eq!(anchor.range, Some(loc));
628 }
629
630 #[test]
631 fn pir_callee_named_equality() {
632 let callee1 =
633 PirCallee::Named { name: "foo".to_string(), package: Some("Bar".to_string()) };
634 let callee2 =
635 PirCallee::Named { name: "foo".to_string(), package: Some("Bar".to_string()) };
636 assert_eq!(callee1, callee2);
637 }
638
639 #[test]
640 fn pir_callee_dynamic_equality() {
641 assert_eq!(PirCallee::Dynamic, PirCallee::Dynamic);
642 }
643
644 #[test]
645 fn pir_method_named_equality() {
646 assert_eq!(PirMethod::Named("foo".to_string()), PirMethod::Named("foo".to_string()));
647 }
648
649 #[test]
650 fn pir_operation_has_all_names() {
651 let expected = vec![
652 "Assign",
653 "Branch",
654 "Call",
655 "DynamicBoundary",
656 "LexicalRead",
657 "LexicalWrite",
658 "Loop",
659 "MethodCall",
660 "Modify",
661 "Return",
662 "StashModify",
663 "StashRead",
664 "StashWrite",
665 ];
666 let actual: Vec<_> = PirOperation::ALL_OPERATION_NAMES.to_vec();
667 assert_eq!(actual, expected);
668 }
669
670 #[test]
671 fn pir_operation_lexical_read_name() {
672 let op = PirOperation::LexicalRead {
673 name: LexicalName { sigil: "$".to_string(), name: "x".to_string() },
674 };
675 assert_eq!(op.name(), "LexicalRead");
676 }
677
678 #[test]
679 fn pir_operation_lexical_write_name() {
680 let op = PirOperation::LexicalWrite {
681 name: LexicalName { sigil: "$".to_string(), name: "x".to_string() },
682 };
683 assert_eq!(op.name(), "LexicalWrite");
684 }
685
686 #[test]
687 fn pir_operation_stash_read_name() {
688 let op = PirOperation::StashRead {
689 symbol: SymbolName { sigil: "$".to_string(), name: "x".to_string(), package: None },
690 };
691 assert_eq!(op.name(), "StashRead");
692 }
693
694 #[test]
695 fn pir_operation_stash_write_name() {
696 let op = PirOperation::StashWrite {
697 symbol: SymbolName {
698 sigil: "@".to_string(),
699 name: "items".to_string(),
700 package: Some("Acme".to_string()),
701 },
702 };
703 assert_eq!(op.name(), "StashWrite");
704 }
705
706 #[test]
707 fn pir_operation_assign_name() {
708 let op = PirOperation::Assign;
709 assert_eq!(op.name(), "Assign");
710 }
711
712 #[test]
713 fn pir_operation_call_name() {
714 let op = PirOperation::Call {
715 callee: PirCallee::Named { name: "foo".to_string(), package: None },
716 arg_count: 2,
717 };
718 assert_eq!(op.name(), "Call");
719 }
720
721 #[test]
722 fn pir_operation_method_call_name() {
723 let op = PirOperation::MethodCall {
724 receiver: PirReceiver::Expression { kind: "Variable" },
725 method: PirMethod::Named("foo".to_string()),
726 arg_count: 1,
727 };
728 assert_eq!(op.name(), "MethodCall");
729 }
730
731 #[test]
732 fn pir_operation_branch_name() {
733 let op = PirOperation::Branch { condition: None };
734 assert_eq!(op.name(), "Branch");
735 }
736
737 #[test]
738 fn pir_operation_loop_name() {
739 let op = PirOperation::Loop { condition: None };
740 assert_eq!(op.name(), "Loop");
741 }
742
743 #[test]
744 fn pir_operation_return_name() {
745 let op = PirOperation::Return;
746 assert_eq!(op.name(), "Return");
747 }
748
749 #[test]
750 fn pir_operation_dynamic_boundary_name() {
751 let op = PirOperation::DynamicBoundary {
752 kind: PirDynamicBoundaryKind::DynamicCallee,
753 reason: "test".to_string(),
754 };
755 assert_eq!(op.name(), "DynamicBoundary");
756 }
757
758 #[test]
759 fn pir_dynamic_boundary_kind_has_stable_names() {
760 assert_eq!(PirDynamicBoundaryKind::DynamicCallee.name(), "DynamicCallee");
761 assert_eq!(PirDynamicBoundaryKind::DynamicReceiver.name(), "DynamicReceiver");
762 assert_eq!(PirDynamicBoundaryKind::DynamicMethodName.name(), "DynamicMethodName");
763 assert_eq!(PirDynamicBoundaryKind::SymbolicReference.name(), "SymbolicReference");
764 assert_eq!(PirDynamicBoundaryKind::TypeglobAccess.name(), "TypeglobAccess");
765 assert_eq!(PirDynamicBoundaryKind::DynamicDereference.name(), "DynamicDereference");
766 assert_eq!(PirDynamicBoundaryKind::RuntimeStashMutation.name(), "RuntimeStashMutation");
767 assert_eq!(PirDynamicBoundaryKind::EvalExpression.name(), "EvalExpression");
768 assert_eq!(PirDynamicBoundaryKind::DoExpression.name(), "DoExpression");
769 assert_eq!(PirDynamicBoundaryKind::Autoload.name(), "Autoload");
770 assert_eq!(PirDynamicBoundaryKind::Unknown.name(), "Unknown");
771 }
772
773 #[test]
774 fn pir_edge_kind_has_stable_names() {
775 assert_eq!(PirEdgeKind::Fallthrough.name(), "Fallthrough");
776 assert_eq!(PirEdgeKind::Branch.name(), "Branch");
777 assert_eq!(PirEdgeKind::Loop.name(), "Loop");
778 assert_eq!(PirEdgeKind::Return.name(), "Return");
779 assert_eq!(PirEdgeKind::DynamicExit.name(), "DynamicExit");
780 assert_eq!(PirEdgeKind::Unknown.name(), "Unknown");
781 }
782
783 #[test]
784 fn pir_lowering_mode_has_stable_name() {
785 assert_eq!(PirLoweringMode::HirV0.name(), "HirV0");
786 }
787
788 #[test]
789 fn pir_anchor_coverage_total() {
790 let coverage = PirAnchorCoverage { anchored: 5, unanchored: 3 };
791 assert_eq!(coverage.total(), 8);
792 }
793
794 #[test]
795 fn pir_anchor_coverage_default() {
796 let coverage = PirAnchorCoverage::default();
797 assert_eq!(coverage.anchored, 0);
798 assert_eq!(coverage.unanchored, 0);
799 assert_eq!(coverage.total(), 0);
800 }
801
802 #[test]
803 fn pir_id_from_index_round_trip() {
804 let id = PirId::from_index(42);
805 assert_eq!(id.index(), 42);
806 }
807
808 #[test]
809 fn pir_graph_empty_returns_true_for_no_nodes() {
810 let graph = PirGraph {
811 nodes: vec![],
812 edges: vec![],
813 receipt: PirReceipt {
814 schema_version: 1,
815 source_identity: None,
816 lowering_mode: PirLoweringMode::HirV0,
817 node_count: 0,
818 edge_count: 0,
819 operation_counts: Default::default(),
820 context_counts: Default::default(),
821 source_anchor_coverage: Default::default(),
822 dynamic_boundary_counts: Default::default(),
823 unsupported_construct_counts: Default::default(),
824 ambient_inputs: vec![],
825 provider_behavior_changed: false,
826 },
827 };
828 assert!(graph.is_empty());
829 }
830
831 #[test]
832 fn pir_graph_empty_returns_false_for_nodes() {
833 let loc = SourceLocation { start: 0, end: 1 };
834 let node = PirNode {
835 id: PirId::from_index(0),
836 source_anchor: PirSourceAnchor::explicit(loc, HirId::from_index(1)),
837 operation: PirOperation::Assign,
838 context: PirContext::Void,
839 dynamic_boundary: None,
840 scope: None,
841 package_context: None,
842 };
843 let graph = PirGraph {
844 nodes: vec![node],
845 edges: vec![],
846 receipt: PirReceipt {
847 schema_version: 1,
848 source_identity: None,
849 lowering_mode: PirLoweringMode::HirV0,
850 node_count: 1,
851 edge_count: 0,
852 operation_counts: Default::default(),
853 context_counts: Default::default(),
854 source_anchor_coverage: Default::default(),
855 dynamic_boundary_counts: Default::default(),
856 unsupported_construct_counts: Default::default(),
857 ambient_inputs: vec![],
858 provider_behavior_changed: false,
859 },
860 };
861 assert!(!graph.is_empty());
862 }
863
864 #[test]
865 fn pir_graph_node_lookup() {
866 let loc = SourceLocation { start: 0, end: 1 };
867 let node = PirNode {
868 id: PirId::from_index(0),
869 source_anchor: PirSourceAnchor::explicit(loc, HirId::from_index(1)),
870 operation: PirOperation::Assign,
871 context: PirContext::Void,
872 dynamic_boundary: None,
873 scope: None,
874 package_context: None,
875 };
876 let graph = PirGraph {
877 nodes: vec![node.clone()],
878 edges: vec![],
879 receipt: PirReceipt {
880 schema_version: 1,
881 source_identity: None,
882 lowering_mode: PirLoweringMode::HirV0,
883 node_count: 1,
884 edge_count: 0,
885 operation_counts: Default::default(),
886 context_counts: Default::default(),
887 source_anchor_coverage: Default::default(),
888 dynamic_boundary_counts: Default::default(),
889 unsupported_construct_counts: Default::default(),
890 ambient_inputs: vec![],
891 provider_behavior_changed: false,
892 },
893 };
894 let found = graph.node(PirId::from_index(0));
895 assert_eq!(found, Some(&node));
896 }
897
898 #[test]
899 fn pir_graph_node_lookup_invalid_id() {
900 let graph = PirGraph {
901 nodes: vec![],
902 edges: vec![],
903 receipt: PirReceipt {
904 schema_version: 1,
905 source_identity: None,
906 lowering_mode: PirLoweringMode::HirV0,
907 node_count: 0,
908 edge_count: 0,
909 operation_counts: Default::default(),
910 context_counts: Default::default(),
911 source_anchor_coverage: Default::default(),
912 dynamic_boundary_counts: Default::default(),
913 unsupported_construct_counts: Default::default(),
914 ambient_inputs: vec![],
915 provider_behavior_changed: false,
916 },
917 };
918 let found = graph.node(PirId::from_index(42));
919 assert_eq!(found, None);
920 }
921
922 #[test]
923 fn lexical_name_structure() {
924 let name = LexicalName { sigil: "$".to_string(), name: "x".to_string() };
925 assert_eq!(name.sigil, "$");
926 assert_eq!(name.name, "x");
927 }
928
929 #[test]
930 fn symbol_name_with_package() {
931 let symbol = SymbolName {
932 sigil: "@".to_string(),
933 name: "items".to_string(),
934 package: Some("Acme".to_string()),
935 };
936 assert_eq!(symbol.sigil, "@");
937 assert_eq!(symbol.name, "items");
938 assert_eq!(symbol.package.as_deref(), Some("Acme"));
939 }
940
941 #[test]
942 fn pir_receiver_class() {
943 let receiver = PirReceiver::Class("Foo".to_string());
944 assert_eq!(format!("{:?}", receiver), "Class(\"Foo\")");
945 }
946
947 #[test]
948 fn pir_receiver_expression() {
949 let receiver = PirReceiver::Expression { kind: "Variable" };
950 assert_eq!(format!("{:?}", receiver), "Expression { kind: \"Variable\" }");
951 }
952
953 #[test]
954 fn pir_receiver_dynamic() {
955 assert_eq!(format!("{:?}", PirReceiver::Dynamic), "Dynamic");
956 }
957}