use crate::SourceLocation;
use crate::hir::{HirId, HirScopeId};
use perl_semantic_facts::AnchorId;
use std::collections::BTreeMap;
pub const PIR_RECEIPT_VERSION: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[non_exhaustive]
pub struct PirId {
index: u32,
}
impl PirId {
#[inline]
pub const fn from_index(index: u32) -> Self {
Self { index }
}
#[inline]
pub const fn index(self) -> u32 {
self.index
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PirAnchorKind {
ExplicitSource,
SourceBackedGenerated,
GeneratedNoSource,
DynamicBoundary,
AmbientInput,
Unknown,
}
impl PirAnchorKind {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::ExplicitSource => "ExplicitSource",
Self::SourceBackedGenerated => "SourceBackedGenerated",
Self::GeneratedNoSource => "GeneratedNoSource",
Self::DynamicBoundary => "DynamicBoundary",
Self::AmbientInput => "AmbientInput",
Self::Unknown => "Unknown",
}
}
#[must_use]
pub const fn is_source_backed(self) -> bool {
matches!(self, Self::ExplicitSource | Self::SourceBackedGenerated | Self::DynamicBoundary)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct PirSourceAnchor {
pub kind: PirAnchorKind,
pub range: Option<SourceLocation>,
pub anchor_id: Option<AnchorId>,
pub hir_item: Option<HirId>,
}
impl PirSourceAnchor {
#[must_use]
pub fn explicit(range: SourceLocation, hir_item: HirId) -> Self {
Self {
kind: PirAnchorKind::ExplicitSource,
range: Some(range),
anchor_id: Some(AnchorId(range.start as u64)),
hir_item: Some(hir_item),
}
}
#[must_use]
pub fn dynamic_boundary(range: SourceLocation, hir_item: HirId) -> Self {
Self {
kind: PirAnchorKind::DynamicBoundary,
range: Some(range),
anchor_id: Some(AnchorId(range.start as u64)),
hir_item: Some(hir_item),
}
}
#[must_use]
pub fn is_anchored(&self) -> bool {
self.range.is_some()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PirContext {
Scalar,
List,
Void,
Lvalue,
Unknown,
}
impl PirContext {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Scalar => "Scalar",
Self::List => "List",
Self::Void => "Void",
Self::Lvalue => "Lvalue",
Self::Unknown => "Unknown",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct LexicalName {
pub sigil: String,
pub name: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct SymbolName {
pub sigil: String,
pub name: String,
pub package: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum PirCallee {
Named {
name: String,
package: Option<String>,
},
Dynamic,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum PirReceiver {
Class(String),
Expression {
kind: &'static str,
},
Dynamic,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum PirMethod {
Named(String),
Dynamic,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PirDynamicBoundaryKind {
DynamicCallee,
DynamicReceiver,
DynamicMethodName,
SymbolicReference,
TypeglobAccess,
DynamicDereference,
RuntimeStashMutation,
EvalExpression,
DoExpression,
Autoload,
Unknown,
}
impl PirDynamicBoundaryKind {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::DynamicCallee => "DynamicCallee",
Self::DynamicReceiver => "DynamicReceiver",
Self::DynamicMethodName => "DynamicMethodName",
Self::SymbolicReference => "SymbolicReference",
Self::TypeglobAccess => "TypeglobAccess",
Self::DynamicDereference => "DynamicDereference",
Self::RuntimeStashMutation => "RuntimeStashMutation",
Self::EvalExpression => "EvalExpression",
Self::DoExpression => "DoExpression",
Self::Autoload => "Autoload",
Self::Unknown => "Unknown",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum PirOperation {
LexicalRead {
name: LexicalName,
},
LexicalWrite {
name: LexicalName,
},
StashRead {
symbol: SymbolName,
},
StashWrite {
symbol: SymbolName,
},
Modify {
name: LexicalName,
op: String,
},
StashModify {
symbol: SymbolName,
op: String,
},
Assign,
Call {
callee: PirCallee,
arg_count: usize,
},
MethodCall {
receiver: PirReceiver,
method: PirMethod,
arg_count: usize,
},
Branch {
condition: Option<PirId>,
},
Loop {
condition: Option<PirId>,
},
Return,
DynamicBoundary {
kind: PirDynamicBoundaryKind,
reason: String,
},
}
impl PirOperation {
#[must_use]
pub const fn name(&self) -> &'static str {
match self {
Self::LexicalRead { .. } => "LexicalRead",
Self::LexicalWrite { .. } => "LexicalWrite",
Self::StashRead { .. } => "StashRead",
Self::StashWrite { .. } => "StashWrite",
Self::Modify { .. } => "Modify",
Self::StashModify { .. } => "StashModify",
Self::Assign => "Assign",
Self::Call { .. } => "Call",
Self::MethodCall { .. } => "MethodCall",
Self::Branch { .. } => "Branch",
Self::Loop { .. } => "Loop",
Self::Return => "Return",
Self::DynamicBoundary { .. } => "DynamicBoundary",
}
}
pub const ALL_OPERATION_NAMES: &[&'static str] = &[
"Assign",
"Branch",
"Call",
"DynamicBoundary",
"LexicalRead",
"LexicalWrite",
"Loop",
"MethodCall",
"Modify",
"Return",
"StashModify",
"StashRead",
"StashWrite",
];
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct PirNode {
pub id: PirId,
pub source_anchor: PirSourceAnchor,
pub operation: PirOperation,
pub context: PirContext,
pub dynamic_boundary: Option<PirId>,
pub scope: Option<HirScopeId>,
pub package_context: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PirEdgeKind {
Fallthrough,
Branch,
Loop,
Return,
DynamicExit,
Unknown,
}
impl PirEdgeKind {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Fallthrough => "Fallthrough",
Self::Branch => "Branch",
Self::Loop => "Loop",
Self::Return => "Return",
Self::DynamicExit => "DynamicExit",
Self::Unknown => "Unknown",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct PirEdge {
pub from: PirId,
pub to: Option<PirId>,
pub kind: PirEdgeKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PirLoweringMode {
HirV0,
}
impl PirLoweringMode {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::HirV0 => "HirV0",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct PirAnchorCoverage {
pub anchored: usize,
pub unanchored: usize,
}
impl PirAnchorCoverage {
#[must_use]
pub const fn total(&self) -> usize {
self.anchored + self.unanchored
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct PirReceipt {
pub schema_version: u32,
pub source_identity: Option<String>,
pub lowering_mode: PirLoweringMode,
pub node_count: usize,
pub edge_count: usize,
pub operation_counts: BTreeMap<&'static str, usize>,
pub context_counts: BTreeMap<&'static str, usize>,
pub source_anchor_coverage: PirAnchorCoverage,
pub dynamic_boundary_counts: BTreeMap<&'static str, usize>,
pub unsupported_construct_counts: BTreeMap<&'static str, usize>,
pub ambient_inputs: Vec<String>,
pub provider_behavior_changed: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct PirGraph {
pub nodes: Vec<PirNode>,
pub edges: Vec<PirEdge>,
pub receipt: PirReceipt,
}
impl PirGraph {
#[must_use]
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
#[must_use]
pub fn node(&self, id: PirId) -> Option<&PirNode> {
self.nodes.get(id.index() as usize)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pir_context_has_stable_names() {
assert_eq!(PirContext::Scalar.name(), "Scalar");
assert_eq!(PirContext::List.name(), "List");
assert_eq!(PirContext::Void.name(), "Void");
assert_eq!(PirContext::Lvalue.name(), "Lvalue");
assert_eq!(PirContext::Unknown.name(), "Unknown");
}
#[test]
fn pir_anchor_kind_has_stable_names() {
assert_eq!(PirAnchorKind::ExplicitSource.name(), "ExplicitSource");
assert_eq!(PirAnchorKind::SourceBackedGenerated.name(), "SourceBackedGenerated");
assert_eq!(PirAnchorKind::GeneratedNoSource.name(), "GeneratedNoSource");
assert_eq!(PirAnchorKind::DynamicBoundary.name(), "DynamicBoundary");
assert_eq!(PirAnchorKind::AmbientInput.name(), "AmbientInput");
assert_eq!(PirAnchorKind::Unknown.name(), "Unknown");
}
#[test]
fn pir_anchor_kind_is_source_backed() {
assert!(PirAnchorKind::ExplicitSource.is_source_backed());
assert!(PirAnchorKind::SourceBackedGenerated.is_source_backed());
assert!(!PirAnchorKind::GeneratedNoSource.is_source_backed());
assert!(PirAnchorKind::DynamicBoundary.is_source_backed());
assert!(!PirAnchorKind::AmbientInput.is_source_backed());
assert!(!PirAnchorKind::Unknown.is_source_backed());
}
#[test]
fn pir_source_anchor_explicit_creates_anchored() {
let loc = SourceLocation { start: 0, end: 5 };
let anchor = PirSourceAnchor::explicit(loc, HirId::from_index(1));
assert!(anchor.is_anchored());
assert_eq!(anchor.kind, PirAnchorKind::ExplicitSource);
assert_eq!(anchor.range, Some(loc));
}
#[test]
fn pir_source_anchor_dynamic_boundary_creates_anchored() {
let loc = SourceLocation { start: 10, end: 20 };
let anchor = PirSourceAnchor::dynamic_boundary(loc, HirId::from_index(2));
assert!(anchor.is_anchored());
assert_eq!(anchor.kind, PirAnchorKind::DynamicBoundary);
assert_eq!(anchor.range, Some(loc));
}
#[test]
fn pir_callee_named_equality() {
let callee1 =
PirCallee::Named { name: "foo".to_string(), package: Some("Bar".to_string()) };
let callee2 =
PirCallee::Named { name: "foo".to_string(), package: Some("Bar".to_string()) };
assert_eq!(callee1, callee2);
}
#[test]
fn pir_callee_dynamic_equality() {
assert_eq!(PirCallee::Dynamic, PirCallee::Dynamic);
}
#[test]
fn pir_method_named_equality() {
assert_eq!(PirMethod::Named("foo".to_string()), PirMethod::Named("foo".to_string()));
}
#[test]
fn pir_operation_has_all_names() {
let expected = vec![
"Assign",
"Branch",
"Call",
"DynamicBoundary",
"LexicalRead",
"LexicalWrite",
"Loop",
"MethodCall",
"Modify",
"Return",
"StashModify",
"StashRead",
"StashWrite",
];
let actual: Vec<_> = PirOperation::ALL_OPERATION_NAMES.to_vec();
assert_eq!(actual, expected);
}
#[test]
fn pir_operation_lexical_read_name() {
let op = PirOperation::LexicalRead {
name: LexicalName { sigil: "$".to_string(), name: "x".to_string() },
};
assert_eq!(op.name(), "LexicalRead");
}
#[test]
fn pir_operation_lexical_write_name() {
let op = PirOperation::LexicalWrite {
name: LexicalName { sigil: "$".to_string(), name: "x".to_string() },
};
assert_eq!(op.name(), "LexicalWrite");
}
#[test]
fn pir_operation_stash_read_name() {
let op = PirOperation::StashRead {
symbol: SymbolName { sigil: "$".to_string(), name: "x".to_string(), package: None },
};
assert_eq!(op.name(), "StashRead");
}
#[test]
fn pir_operation_stash_write_name() {
let op = PirOperation::StashWrite {
symbol: SymbolName {
sigil: "@".to_string(),
name: "items".to_string(),
package: Some("Acme".to_string()),
},
};
assert_eq!(op.name(), "StashWrite");
}
#[test]
fn pir_operation_assign_name() {
let op = PirOperation::Assign;
assert_eq!(op.name(), "Assign");
}
#[test]
fn pir_operation_call_name() {
let op = PirOperation::Call {
callee: PirCallee::Named { name: "foo".to_string(), package: None },
arg_count: 2,
};
assert_eq!(op.name(), "Call");
}
#[test]
fn pir_operation_method_call_name() {
let op = PirOperation::MethodCall {
receiver: PirReceiver::Expression { kind: "Variable" },
method: PirMethod::Named("foo".to_string()),
arg_count: 1,
};
assert_eq!(op.name(), "MethodCall");
}
#[test]
fn pir_operation_branch_name() {
let op = PirOperation::Branch { condition: None };
assert_eq!(op.name(), "Branch");
}
#[test]
fn pir_operation_loop_name() {
let op = PirOperation::Loop { condition: None };
assert_eq!(op.name(), "Loop");
}
#[test]
fn pir_operation_return_name() {
let op = PirOperation::Return;
assert_eq!(op.name(), "Return");
}
#[test]
fn pir_operation_dynamic_boundary_name() {
let op = PirOperation::DynamicBoundary {
kind: PirDynamicBoundaryKind::DynamicCallee,
reason: "test".to_string(),
};
assert_eq!(op.name(), "DynamicBoundary");
}
#[test]
fn pir_dynamic_boundary_kind_has_stable_names() {
assert_eq!(PirDynamicBoundaryKind::DynamicCallee.name(), "DynamicCallee");
assert_eq!(PirDynamicBoundaryKind::DynamicReceiver.name(), "DynamicReceiver");
assert_eq!(PirDynamicBoundaryKind::DynamicMethodName.name(), "DynamicMethodName");
assert_eq!(PirDynamicBoundaryKind::SymbolicReference.name(), "SymbolicReference");
assert_eq!(PirDynamicBoundaryKind::TypeglobAccess.name(), "TypeglobAccess");
assert_eq!(PirDynamicBoundaryKind::DynamicDereference.name(), "DynamicDereference");
assert_eq!(PirDynamicBoundaryKind::RuntimeStashMutation.name(), "RuntimeStashMutation");
assert_eq!(PirDynamicBoundaryKind::EvalExpression.name(), "EvalExpression");
assert_eq!(PirDynamicBoundaryKind::DoExpression.name(), "DoExpression");
assert_eq!(PirDynamicBoundaryKind::Autoload.name(), "Autoload");
assert_eq!(PirDynamicBoundaryKind::Unknown.name(), "Unknown");
}
#[test]
fn pir_edge_kind_has_stable_names() {
assert_eq!(PirEdgeKind::Fallthrough.name(), "Fallthrough");
assert_eq!(PirEdgeKind::Branch.name(), "Branch");
assert_eq!(PirEdgeKind::Loop.name(), "Loop");
assert_eq!(PirEdgeKind::Return.name(), "Return");
assert_eq!(PirEdgeKind::DynamicExit.name(), "DynamicExit");
assert_eq!(PirEdgeKind::Unknown.name(), "Unknown");
}
#[test]
fn pir_lowering_mode_has_stable_name() {
assert_eq!(PirLoweringMode::HirV0.name(), "HirV0");
}
#[test]
fn pir_anchor_coverage_total() {
let coverage = PirAnchorCoverage { anchored: 5, unanchored: 3 };
assert_eq!(coverage.total(), 8);
}
#[test]
fn pir_anchor_coverage_default() {
let coverage = PirAnchorCoverage::default();
assert_eq!(coverage.anchored, 0);
assert_eq!(coverage.unanchored, 0);
assert_eq!(coverage.total(), 0);
}
#[test]
fn pir_id_from_index_round_trip() {
let id = PirId::from_index(42);
assert_eq!(id.index(), 42);
}
#[test]
fn pir_graph_empty_returns_true_for_no_nodes() {
let graph = PirGraph {
nodes: vec![],
edges: vec![],
receipt: PirReceipt {
schema_version: 1,
source_identity: None,
lowering_mode: PirLoweringMode::HirV0,
node_count: 0,
edge_count: 0,
operation_counts: Default::default(),
context_counts: Default::default(),
source_anchor_coverage: Default::default(),
dynamic_boundary_counts: Default::default(),
unsupported_construct_counts: Default::default(),
ambient_inputs: vec![],
provider_behavior_changed: false,
},
};
assert!(graph.is_empty());
}
#[test]
fn pir_graph_empty_returns_false_for_nodes() {
let loc = SourceLocation { start: 0, end: 1 };
let node = PirNode {
id: PirId::from_index(0),
source_anchor: PirSourceAnchor::explicit(loc, HirId::from_index(1)),
operation: PirOperation::Assign,
context: PirContext::Void,
dynamic_boundary: None,
scope: None,
package_context: None,
};
let graph = PirGraph {
nodes: vec![node],
edges: vec![],
receipt: PirReceipt {
schema_version: 1,
source_identity: None,
lowering_mode: PirLoweringMode::HirV0,
node_count: 1,
edge_count: 0,
operation_counts: Default::default(),
context_counts: Default::default(),
source_anchor_coverage: Default::default(),
dynamic_boundary_counts: Default::default(),
unsupported_construct_counts: Default::default(),
ambient_inputs: vec![],
provider_behavior_changed: false,
},
};
assert!(!graph.is_empty());
}
#[test]
fn pir_graph_node_lookup() {
let loc = SourceLocation { start: 0, end: 1 };
let node = PirNode {
id: PirId::from_index(0),
source_anchor: PirSourceAnchor::explicit(loc, HirId::from_index(1)),
operation: PirOperation::Assign,
context: PirContext::Void,
dynamic_boundary: None,
scope: None,
package_context: None,
};
let graph = PirGraph {
nodes: vec![node.clone()],
edges: vec![],
receipt: PirReceipt {
schema_version: 1,
source_identity: None,
lowering_mode: PirLoweringMode::HirV0,
node_count: 1,
edge_count: 0,
operation_counts: Default::default(),
context_counts: Default::default(),
source_anchor_coverage: Default::default(),
dynamic_boundary_counts: Default::default(),
unsupported_construct_counts: Default::default(),
ambient_inputs: vec![],
provider_behavior_changed: false,
},
};
let found = graph.node(PirId::from_index(0));
assert_eq!(found, Some(&node));
}
#[test]
fn pir_graph_node_lookup_invalid_id() {
let graph = PirGraph {
nodes: vec![],
edges: vec![],
receipt: PirReceipt {
schema_version: 1,
source_identity: None,
lowering_mode: PirLoweringMode::HirV0,
node_count: 0,
edge_count: 0,
operation_counts: Default::default(),
context_counts: Default::default(),
source_anchor_coverage: Default::default(),
dynamic_boundary_counts: Default::default(),
unsupported_construct_counts: Default::default(),
ambient_inputs: vec![],
provider_behavior_changed: false,
},
};
let found = graph.node(PirId::from_index(42));
assert_eq!(found, None);
}
#[test]
fn lexical_name_structure() {
let name = LexicalName { sigil: "$".to_string(), name: "x".to_string() };
assert_eq!(name.sigil, "$");
assert_eq!(name.name, "x");
}
#[test]
fn symbol_name_with_package() {
let symbol = SymbolName {
sigil: "@".to_string(),
name: "items".to_string(),
package: Some("Acme".to_string()),
};
assert_eq!(symbol.sigil, "@");
assert_eq!(symbol.name, "items");
assert_eq!(symbol.package.as_deref(), Some("Acme"));
}
#[test]
fn pir_receiver_class() {
let receiver = PirReceiver::Class("Foo".to_string());
assert_eq!(format!("{:?}", receiver), "Class(\"Foo\")");
}
#[test]
fn pir_receiver_expression() {
let receiver = PirReceiver::Expression { kind: "Variable" };
assert_eq!(format!("{:?}", receiver), "Expression { kind: \"Variable\" }");
}
#[test]
fn pir_receiver_dynamic() {
assert_eq!(format!("{:?}", PirReceiver::Dynamic), "Dynamic");
}
}