use std::fmt;
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use super::super::alias::AliasEntryId;
use super::super::scope::ScopeId;
use crate::graph::unified::edge::id::EdgeId;
use crate::graph::unified::file::id::FileId;
use crate::graph::unified::node::id::NodeId;
use crate::graph::unified::resolution::{ResolutionMode, SymbolCandidateBucket};
use crate::graph::unified::string::id::StringId;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ResolutionStep {
EnterFileScope {
file: FileId,
},
EnterScope {
scope: ScopeId,
},
LookupInBucket {
bucket: SymbolCandidateBucket,
},
LookupInAliasTable {
scope: ScopeId,
},
LookupInShadowChain {
scope: ScopeId,
},
ConsiderCandidate {
node: NodeId,
rank: u16,
},
FollowAlias {
alias: AliasEntryId,
from: StringId,
to: StringId,
},
ShadowedBy {
outer: ScopeId,
inner: ScopeId,
by_node: NodeId,
},
FollowImportEdge {
edge: EdgeId,
},
FollowExportEdge {
edge: EdgeId,
from: StringId,
to: StringId,
},
FollowAttributeLookup {
receiver: NodeId,
attribute: StringId,
},
FilterByVisibility {
candidate: NodeId,
reason: VisibilityReason,
},
ApplyResolutionMode {
mode: ResolutionMode,
},
TieBreak {
reason: TieBreakReason,
},
Rejected {
node: NodeId,
reason: RejectionReason,
},
Chose {
node: NodeId,
},
Ambiguous {
candidates: SmallVec<[NodeId; 4]>,
},
Unresolved {
symbol: StringId,
reason: UnresolvedReason,
},
}
impl fmt::Display for ResolutionStep {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ResolutionStep::EnterFileScope { file } => {
write!(f, "enter file scope ({file})")
}
ResolutionStep::EnterScope { scope } => {
write!(f, "enter scope ({scope:?})")
}
ResolutionStep::LookupInBucket { bucket } => {
write!(f, "lookup in bucket: {bucket:?}")
}
ResolutionStep::LookupInAliasTable { scope } => {
write!(f, "lookup in alias table (scope {scope:?})")
}
ResolutionStep::LookupInShadowChain { scope } => {
write!(f, "lookup in shadow chain (scope {scope:?})")
}
ResolutionStep::ConsiderCandidate { node, rank } => {
write!(f, "consider candidate {node} at rank {rank}")
}
ResolutionStep::FollowAlias { alias, from, to } => {
write!(f, "follow alias {:?}: {from} → {to}", alias)
}
ResolutionStep::ShadowedBy {
outer,
inner,
by_node,
} => {
write!(
f,
"shadowed by {by_node} (outer scope {outer:?}, inner scope {inner:?})"
)
}
ResolutionStep::FollowImportEdge { edge } => {
write!(f, "follow import edge {edge}")
}
ResolutionStep::FollowExportEdge { edge, from, to } => {
write!(f, "follow export edge {edge}: {from} → {to}")
}
ResolutionStep::FollowAttributeLookup {
receiver,
attribute,
} => {
write!(f, "follow attribute lookup .{attribute} on {receiver}")
}
ResolutionStep::FilterByVisibility { candidate, reason } => {
write!(f, "filter by visibility: {candidate} rejected ({reason:?})")
}
ResolutionStep::ApplyResolutionMode { mode } => {
write!(f, "apply resolution mode: {mode:?}")
}
ResolutionStep::TieBreak { reason } => {
write!(f, "tie-break: {reason:?}")
}
ResolutionStep::Rejected { node, reason } => {
write!(f, "rejected {node}: {reason:?}")
}
ResolutionStep::Chose { node } => {
write!(f, "chose {node}")
}
ResolutionStep::Ambiguous { candidates } => {
write!(f, "ambiguous: {} candidates", candidates.len())
}
ResolutionStep::Unresolved { symbol, reason } => {
write!(f, "unresolved {symbol}: {reason:?}")
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum VisibilityReason {
Private,
Sealed,
Internal,
FileScoped,
ModulePath,
CrateBoundary,
Protected,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TieBreakReason {
NarrowerScope,
ShorterPath,
ExplicitImport,
EarlierDeclaration,
LanguagePriority,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RejectionReason {
OutOfScope,
Shadowed,
WrongKind,
PrivateVisibility,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum UnresolvedReason {
NotInAnyScope,
AllCandidatesRejected,
AmbiguousWithoutTieBreak,
FileNotIndexed,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::unified::node::id::NodeId;
#[test]
fn chose_round_trips_through_eq() {
let step = ResolutionStep::Chose {
node: NodeId::new(42, 1),
};
assert_eq!(step.clone(), step);
}
#[test]
fn ambiguous_round_trips_through_postcard() {
let mut candidates = SmallVec::<[NodeId; 4]>::new();
candidates.push(NodeId::new(1, 1));
candidates.push(NodeId::new(2, 1));
candidates.push(NodeId::new(3, 1));
let step = ResolutionStep::Ambiguous { candidates };
let bytes = postcard::to_allocvec(&step).expect("serialize");
let restored: ResolutionStep = postcard::from_bytes(&bytes).expect("deserialize");
assert_eq!(restored, step);
}
#[test]
fn smallvec_grows_past_inline_capacity() {
let mut candidates = SmallVec::<[NodeId; 4]>::new();
for idx in 0..8 {
candidates.push(NodeId::new(idx, 1));
}
assert_eq!(candidates.len(), 8);
let step = ResolutionStep::Ambiguous {
candidates: candidates.clone(),
};
let bytes = postcard::to_allocvec(&step).expect("serialize");
let restored: ResolutionStep = postcard::from_bytes(&bytes).expect("deserialize");
if let ResolutionStep::Ambiguous { candidates: got } = restored {
assert_eq!(got.len(), 8);
} else {
panic!("expected Ambiguous variant after round-trip");
}
}
#[test]
fn every_variant_serializes_through_postcard() {
use super::super::super::alias::AliasEntryId;
use super::super::super::scope::ScopeId;
use crate::graph::unified::edge::id::EdgeId;
use crate::graph::unified::file::id::FileId;
use crate::graph::unified::resolution::{ResolutionMode, SymbolCandidateBucket};
use crate::graph::unified::string::id::StringId;
let steps = vec![
ResolutionStep::EnterFileScope {
file: FileId::new(0),
},
ResolutionStep::EnterScope {
scope: ScopeId::new(1, 1),
},
ResolutionStep::LookupInBucket {
bucket: SymbolCandidateBucket::ExactQualified,
},
ResolutionStep::LookupInAliasTable {
scope: ScopeId::new(2, 1),
},
ResolutionStep::LookupInShadowChain {
scope: ScopeId::new(3, 1),
},
ResolutionStep::ConsiderCandidate {
node: NodeId::new(4, 1),
rank: 0,
},
ResolutionStep::FollowAlias {
alias: AliasEntryId(0),
from: StringId::new(1),
to: StringId::new(2),
},
ResolutionStep::ShadowedBy {
outer: ScopeId::new(1, 1),
inner: ScopeId::new(2, 1),
by_node: NodeId::new(5, 1),
},
ResolutionStep::FollowImportEdge {
edge: EdgeId::new(0),
},
ResolutionStep::FollowExportEdge {
edge: EdgeId::new(1),
from: StringId::new(3),
to: StringId::new(4),
},
ResolutionStep::FollowAttributeLookup {
receiver: NodeId::new(6, 1),
attribute: StringId::new(5),
},
ResolutionStep::FilterByVisibility {
candidate: NodeId::new(7, 1),
reason: VisibilityReason::Private,
},
ResolutionStep::ApplyResolutionMode {
mode: ResolutionMode::Strict,
},
ResolutionStep::TieBreak {
reason: TieBreakReason::NarrowerScope,
},
ResolutionStep::Rejected {
node: NodeId::new(8, 1),
reason: RejectionReason::OutOfScope,
},
ResolutionStep::Chose {
node: NodeId::new(9, 1),
},
ResolutionStep::Ambiguous {
candidates: SmallVec::from_slice(&[NodeId::new(10, 1), NodeId::new(11, 1)]),
},
ResolutionStep::Unresolved {
symbol: StringId::new(6),
reason: UnresolvedReason::NotInAnyScope,
},
];
assert_eq!(steps.len(), 18, "must have exactly 18 variants");
let bytes = postcard::to_allocvec(&steps).expect("serialize");
let restored: Vec<ResolutionStep> = postcard::from_bytes(&bytes).expect("deserialize");
assert_eq!(restored, steps);
}
#[test]
fn all_variants_display_non_empty_and_contains_variant_keyword() {
use super::super::super::alias::AliasEntryId;
use super::super::super::scope::ScopeId;
use crate::graph::unified::edge::id::EdgeId;
use crate::graph::unified::file::id::FileId;
use crate::graph::unified::resolution::{ResolutionMode, SymbolCandidateBucket};
use crate::graph::unified::string::id::StringId;
let steps: Vec<(ResolutionStep, &str)> = vec![
(
ResolutionStep::EnterFileScope {
file: FileId::new(0),
},
"enter file scope",
),
(
ResolutionStep::EnterScope {
scope: ScopeId::new(1, 1),
},
"enter scope",
),
(
ResolutionStep::LookupInBucket {
bucket: SymbolCandidateBucket::ExactQualified,
},
"lookup in bucket",
),
(
ResolutionStep::LookupInAliasTable {
scope: ScopeId::new(2, 1),
},
"lookup in alias table",
),
(
ResolutionStep::LookupInShadowChain {
scope: ScopeId::new(3, 1),
},
"lookup in shadow chain",
),
(
ResolutionStep::ConsiderCandidate {
node: NodeId::new(4, 1),
rank: 0,
},
"consider candidate",
),
(
ResolutionStep::FollowAlias {
alias: AliasEntryId(0),
from: StringId::new(1),
to: StringId::new(2),
},
"follow alias",
),
(
ResolutionStep::ShadowedBy {
outer: ScopeId::new(1, 1),
inner: ScopeId::new(2, 1),
by_node: NodeId::new(5, 1),
},
"shadowed by",
),
(
ResolutionStep::FollowImportEdge {
edge: EdgeId::new(0),
},
"follow import edge",
),
(
ResolutionStep::FollowExportEdge {
edge: EdgeId::new(1),
from: StringId::new(3),
to: StringId::new(4),
},
"follow export edge",
),
(
ResolutionStep::FollowAttributeLookup {
receiver: NodeId::new(6, 1),
attribute: StringId::new(5),
},
"follow attribute lookup",
),
(
ResolutionStep::FilterByVisibility {
candidate: NodeId::new(7, 1),
reason: VisibilityReason::Private,
},
"filter by visibility",
),
(
ResolutionStep::ApplyResolutionMode {
mode: ResolutionMode::Strict,
},
"apply resolution mode",
),
(
ResolutionStep::TieBreak {
reason: TieBreakReason::NarrowerScope,
},
"tie-break",
),
(
ResolutionStep::Rejected {
node: NodeId::new(8, 1),
reason: RejectionReason::OutOfScope,
},
"rejected",
),
(
ResolutionStep::Chose {
node: NodeId::new(9, 1),
},
"chose",
),
(
ResolutionStep::Ambiguous {
candidates: SmallVec::from_slice(&[NodeId::new(10, 1), NodeId::new(11, 1)]),
},
"ambiguous",
),
(
ResolutionStep::Unresolved {
symbol: StringId::new(6),
reason: UnresolvedReason::NotInAnyScope,
},
"unresolved",
),
];
assert_eq!(steps.len(), 18, "must cover all 18 ResolutionStep variants");
for (step, keyword) in &steps {
let text = format!("{step}");
assert!(
!text.is_empty(),
"Display output for step {step:?} must be non-empty"
);
assert!(
text.to_lowercase().contains(keyword),
"Display output for step {step:?} must contain keyword {:?}, got: {:?}",
keyword,
text
);
}
}
#[test]
fn all_subenum_variants_round_trip() {
macro_rules! round_trip {
($val:expr) => {{
let bytes = postcard::to_allocvec(&$val).expect("serialize");
let restored = postcard::from_bytes(&bytes).expect("deserialize");
assert_eq!($val, restored);
}};
}
round_trip!(VisibilityReason::Private);
round_trip!(VisibilityReason::Sealed);
round_trip!(VisibilityReason::Internal);
round_trip!(VisibilityReason::FileScoped);
round_trip!(VisibilityReason::ModulePath);
round_trip!(VisibilityReason::CrateBoundary);
round_trip!(VisibilityReason::Protected);
round_trip!(TieBreakReason::NarrowerScope);
round_trip!(TieBreakReason::ShorterPath);
round_trip!(TieBreakReason::ExplicitImport);
round_trip!(TieBreakReason::EarlierDeclaration);
round_trip!(TieBreakReason::LanguagePriority);
round_trip!(RejectionReason::OutOfScope);
round_trip!(RejectionReason::Shadowed);
round_trip!(RejectionReason::WrongKind);
round_trip!(RejectionReason::PrivateVisibility);
round_trip!(UnresolvedReason::NotInAnyScope);
round_trip!(UnresolvedReason::AllCandidatesRejected);
round_trip!(UnresolvedReason::AmbiguousWithoutTieBreak);
round_trip!(UnresolvedReason::FileNotIndexed);
}
}