1use std::fmt;
4
5use crate::mib::{ImportResolution, Mib, ModuleId, Oid, Symbol, UnresolvedRef};
6use crate::types::{ResolutionDomain, ResolverStrictness};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
10pub enum ResolutionCandidateKind {
11 Object,
13 Notification,
15 Group,
17 Compliance,
19 Capability,
21 Type,
23 Node,
25}
26
27impl From<Symbol> for ResolutionCandidateKind {
28 fn from(symbol: Symbol) -> Self {
29 match symbol {
30 Symbol::Object(_) => Self::Object,
31 Symbol::Notification(_) => Self::Notification,
32 Symbol::Group(_) => Self::Group,
33 Symbol::Compliance(_) => Self::Compliance,
34 Symbol::Capability(_) => Self::Capability,
35 Symbol::Type(_) => Self::Type,
36 Symbol::Node(_) => Self::Node,
37 }
38 }
39}
40
41impl fmt::Display for ResolutionCandidateKind {
42 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
43 formatter.write_str(match self {
44 Self::Object => "object",
45 Self::Notification => "notification",
46 Self::Group => "group",
47 Self::Compliance => "compliance",
48 Self::Capability => "capability",
49 Self::Type => "type",
50 Self::Node => "node",
51 })
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct ResolutionCandidate {
58 pub module: ModuleId,
60 pub module_name: String,
62 pub source_label: Option<String>,
64 pub last_updated: String,
66 pub kind: ResolutionCandidateKind,
68 pub symbol: Symbol,
70 pub oid: Option<Oid>,
72 pub applicable: bool,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct ResolutionScope {
79 pub module: ModuleId,
81 pub module_name: String,
83 pub source_label: Option<String>,
85 pub last_updated: String,
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub struct ResolutionFallbackPolicy {
92 pub intrinsic: bool,
94 pub constrained: bool,
96 pub global: bool,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum ResolutionStrategy {
103 Local,
105 DirectImport,
107 ForwardedImport,
109 PartialImport,
111 AliasImport,
113 IntrinsicFallback,
115 ConstrainedFallback,
117 GlobalFallback,
119 UniqueCandidate,
121}
122
123impl fmt::Display for ResolutionStrategy {
124 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
125 formatter.write_str(match self {
126 Self::Local => "local definition",
127 Self::DirectImport => "direct import",
128 Self::ForwardedImport => "forwarded import",
129 Self::PartialImport => "partial import",
130 Self::AliasImport => "import alias",
131 Self::IntrinsicFallback => "intrinsic fallback",
132 Self::ConstrainedFallback => "constrained fallback",
133 Self::GlobalFallback => "global fallback",
134 Self::UniqueCandidate => "unique unscoped candidate",
135 })
136 }
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub enum ResolutionOutcome {
147 Resolved,
149 Ambiguous,
151 Missing,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq)]
157pub struct ResolutionTarget {
158 pub candidate: ResolutionCandidate,
160 pub strategy: ResolutionStrategy,
162}
163
164#[derive(Debug, Clone)]
166pub struct ResolutionTrace {
167 pub query: String,
169 pub symbol: String,
171 pub domain: ResolutionDomain,
173 pub scope: Option<ResolutionScope>,
175 pub strictness: ResolverStrictness,
177 pub fallbacks: ResolutionFallbackPolicy,
179 pub candidates: Vec<ResolutionCandidate>,
181 pub import: Option<ImportResolution>,
183 pub outcome: ResolutionOutcome,
185 pub target: Option<ResolutionTarget>,
187 pub unresolved: Vec<UnresolvedRef>,
191}
192
193#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
195pub enum ResolutionTraceError {
196 #[error("symbol query is empty")]
198 EmptyQuery,
199 #[error("invalid qualified symbol query: {0}")]
201 InvalidQualifiedQuery(String),
202 #[error("qualified query scope {query_scope:?} conflicts with --module {explicit_scope:?}")]
204 ConflictingScope {
205 query_scope: String,
207 explicit_scope: String,
209 },
210 #[error("module scope not found: {0}")]
212 ModuleNotFound(String),
213 #[error("module scope {module:?} is ambiguous across loaded sources: {candidates:?}")]
215 AmbiguousModuleScope {
216 module: String,
218 candidates: Vec<ResolutionScope>,
220 },
221}
222
223impl Mib {
224 pub fn trace_symbol(
273 &self,
274 query: &str,
275 module_scope: Option<&str>,
276 domain: ResolutionDomain,
277 ) -> Result<ResolutionTrace, ResolutionTraceError> {
278 let (qualified_scope, symbol_name) = parse_query(query)?;
279 let scope_name = match (qualified_scope, module_scope) {
280 (Some(query_scope), Some(explicit_scope)) if query_scope != explicit_scope => {
281 return Err(ResolutionTraceError::ConflictingScope {
282 query_scope: query_scope.to_owned(),
283 explicit_scope: explicit_scope.to_owned(),
284 });
285 }
286 (Some(query_scope), _) => Some(query_scope),
287 (None, Some(explicit_scope)) => Some(explicit_scope),
288 (None, None) => None,
289 };
290 let scope = scope_name
291 .map(|name| self.unique_resolution_scope(name))
292 .transpose()?;
293 let candidates = self.resolution_candidates(symbol_name, domain);
294 let fallback_domain = super::resolver::rules::fallback_domain(domain, symbol_name);
295 let fallbacks = ResolutionFallbackPolicy {
296 intrinsic: super::resolver::rules::intrinsic_foundation_module(
297 fallback_domain,
298 symbol_name,
299 )
300 .is_some(),
301 constrained: !super::resolver::rules::constrained_foundation_modules(
302 fallback_domain,
303 self.resolver_strictness(),
304 )
305 .is_empty(),
306 global: super::resolver::rules::allows_global_fallback(
307 fallback_domain,
308 self.resolver_strictness(),
309 ),
310 };
311
312 let (target, import) = match &scope {
313 Some(scope) => self.resolve_trace_in_scope(scope.module, symbol_name, domain),
314 None => {
315 let applicable = candidates
316 .iter()
317 .filter(|candidate| candidate.applicable)
318 .collect::<Vec<_>>();
319 if applicable.len() == 1 {
320 (
321 Some(ResolutionTarget {
322 candidate: applicable[0].clone(),
323 strategy: ResolutionStrategy::UniqueCandidate,
324 }),
325 None,
326 )
327 } else {
328 (None, None)
329 }
330 }
331 };
332 let outcome = if target.is_some() {
333 ResolutionOutcome::Resolved
334 } else if scope.is_none()
335 && candidates
336 .iter()
337 .filter(|candidate| candidate.applicable)
338 .count()
339 > 1
340 {
341 ResolutionOutcome::Ambiguous
342 } else {
343 ResolutionOutcome::Missing
344 };
345
346 let mut unresolved = self
347 .unresolved()
348 .iter()
349 .filter(|reference| reference.symbol == symbol_name)
350 .cloned()
351 .collect::<Vec<_>>();
352 unresolved.sort_by(|left, right| {
353 left.module
354 .cmp(&right.module)
355 .then((left.kind as u8).cmp(&(right.kind as u8)))
356 .then(left.reason.cmp(&right.reason))
357 });
358
359 Ok(ResolutionTrace {
360 query: query.to_owned(),
361 symbol: symbol_name.to_owned(),
362 domain,
363 scope,
364 strictness: self.resolver_strictness(),
365 fallbacks,
366 candidates,
367 import,
368 outcome,
369 target,
370 unresolved,
371 })
372 }
373
374 fn unique_resolution_scope(
375 &self,
376 module_name: &str,
377 ) -> Result<ResolutionScope, ResolutionTraceError> {
378 let mut scopes = self
379 .modules_slice()
380 .iter()
381 .enumerate()
382 .filter(|(_, module)| module.name() == module_name)
383 .map(|(index, _)| self.resolution_scope(ModuleId::new(index as u32)))
384 .collect::<Vec<_>>();
385 scopes.sort_by(|left, right| {
386 left.source_label
387 .cmp(&right.source_label)
388 .then_with(|| right.last_updated.cmp(&left.last_updated))
389 .then(left.module.cmp(&right.module))
390 });
391 match scopes.len() {
392 0 => Err(ResolutionTraceError::ModuleNotFound(module_name.to_owned())),
393 1 => Ok(scopes.remove(0)),
394 _ => Err(ResolutionTraceError::AmbiguousModuleScope {
395 module: module_name.to_owned(),
396 candidates: scopes,
397 }),
398 }
399 }
400
401 fn resolution_scope(&self, module: ModuleId) -> ResolutionScope {
402 let handle = self.module_by_id(module);
403 ResolutionScope {
404 module,
405 module_name: handle.name().to_owned(),
406 source_label: handle.source_label().map(str::to_owned),
407 last_updated: handle.last_updated().to_owned(),
408 }
409 }
410
411 fn resolution_candidates(
412 &self,
413 name: &str,
414 domain: ResolutionDomain,
415 ) -> Vec<ResolutionCandidate> {
416 let mut candidates = self
417 .modules_slice()
418 .iter()
419 .enumerate()
420 .flat_map(|(index, module)| {
421 module.symbols(name).into_iter().map(move |symbol| {
422 self.resolution_candidate(ModuleId::new(index as u32), symbol, domain, name)
423 })
424 })
425 .collect::<Vec<_>>();
426 candidates.sort_by(|left, right| {
427 left.module_name
428 .cmp(&right.module_name)
429 .then(left.source_label.cmp(&right.source_label))
430 .then_with(|| right.last_updated.cmp(&left.last_updated))
431 .then(left.kind.cmp(&right.kind))
432 .then(left.module.cmp(&right.module))
433 });
434 candidates
435 }
436
437 fn resolution_candidate(
438 &self,
439 module: ModuleId,
440 symbol: Symbol,
441 domain: ResolutionDomain,
442 name: &str,
443 ) -> ResolutionCandidate {
444 let handle = self.module_by_id(module);
445 ResolutionCandidate {
446 module,
447 module_name: handle.name().to_owned(),
448 source_label: handle.source_label().map(str::to_owned),
449 last_updated: handle.last_updated().to_owned(),
450 kind: symbol.into(),
451 symbol,
452 oid: symbol
453 .node(self)
454 .map(|node| self.node_by_id(node).oid().clone()),
455 applicable: symbol_matches_domain(symbol, domain, name),
456 }
457 }
458
459 fn resolve_trace_in_scope(
460 &self,
461 scope: ModuleId,
462 name: &str,
463 domain: ResolutionDomain,
464 ) -> (Option<ResolutionTarget>, Option<ImportResolution>) {
465 let fallback_domain = super::resolver::rules::fallback_domain(domain, name);
466
467 if domain == ResolutionDomain::Oid
469 && let Some(module_name) =
470 super::resolver::rules::intrinsic_foundation_module(fallback_domain, name)
471 && let Some(target) = self.foundation_candidate(module_name, name, domain)
472 {
473 let import = self.module_data(scope).import_resolution(name).cloned();
474 return (
475 Some(ResolutionTarget {
476 candidate: target,
477 strategy: ResolutionStrategy::IntrinsicFallback,
478 }),
479 import,
480 );
481 }
482
483 if let Some(local) = self.domain_candidate_in_module(scope, name, domain) {
484 return (
485 Some(ResolutionTarget {
486 candidate: local,
487 strategy: ResolutionStrategy::Local,
488 }),
489 None,
490 );
491 }
492
493 let import = self.module_data(scope).import_resolution(name).cloned();
494 if let Some(resolution) = &import
495 && let Some(target_module) = resolution.target
496 && let Some(imported) = self.domain_candidate_in_module(target_module, name, domain)
497 {
498 let strategy = match resolution.mode {
499 crate::mib::ImportResolutionMode::Direct => ResolutionStrategy::DirectImport,
500 crate::mib::ImportResolutionMode::Alias => ResolutionStrategy::AliasImport,
501 crate::mib::ImportResolutionMode::Forwarded => ResolutionStrategy::ForwardedImport,
502 crate::mib::ImportResolutionMode::Partial => ResolutionStrategy::PartialImport,
503 crate::mib::ImportResolutionMode::Unresolved
504 | crate::mib::ImportResolutionMode::Cycle => {
505 unreachable!("an unresolved import cannot retain a target")
506 }
507 };
508 return (
509 Some(ResolutionTarget {
510 candidate: imported,
511 strategy,
512 }),
513 import,
514 );
515 }
516
517 if let Some(module_name) =
518 super::resolver::rules::intrinsic_foundation_module(fallback_domain, name)
519 && let Some(target) = self.foundation_candidate(module_name, name, domain)
520 {
521 return (
522 Some(ResolutionTarget {
523 candidate: target,
524 strategy: ResolutionStrategy::IntrinsicFallback,
525 }),
526 import,
527 );
528 }
529 for &module_name in super::resolver::rules::constrained_foundation_modules(
530 fallback_domain,
531 self.resolver_strictness(),
532 ) {
533 if let Some(target) = self.foundation_candidate(module_name, name, domain) {
534 return (
535 Some(ResolutionTarget {
536 candidate: target,
537 strategy: ResolutionStrategy::ConstrainedFallback,
538 }),
539 import,
540 );
541 }
542 }
543 if super::resolver::rules::allows_global_fallback(
544 fallback_domain,
545 self.resolver_strictness(),
546 ) && let Some(target) = self.global_domain_candidate(name, domain)
547 {
548 return (
549 Some(ResolutionTarget {
550 candidate: target,
551 strategy: ResolutionStrategy::GlobalFallback,
552 }),
553 import,
554 );
555 }
556 (None, import)
557 }
558
559 fn domain_candidate_in_module(
560 &self,
561 module: ModuleId,
562 name: &str,
563 domain: ResolutionDomain,
564 ) -> Option<ResolutionCandidate> {
565 let data = self.module_data(module);
566 let symbol = match domain {
567 ResolutionDomain::Type => Symbol::Type(data.type_by_name(name)?),
568 ResolutionDomain::Oid
569 | ResolutionDomain::GroupMember
570 | ResolutionDomain::Conformance => data
571 .symbols(name)
572 .into_iter()
573 .find(|symbol| !matches!(symbol, Symbol::Type(_)))?,
574 ResolutionDomain::Object | ResolutionDomain::NotificationObject => {
575 Symbol::Object(data.object_by_name(name)?)
576 }
577 ResolutionDomain::Index if super::resolver::rules::is_bare_index_type(name) => {
578 Symbol::Type(data.type_by_name(name)?)
579 }
580 ResolutionDomain::Index => Symbol::Object(data.object_by_name(name)?),
581 };
582 Some(self.resolution_candidate(module, symbol, domain, name))
583 }
584
585 fn foundation_candidate(
586 &self,
587 module_name: &str,
588 name: &str,
589 domain: ResolutionDomain,
590 ) -> Option<ResolutionCandidate> {
591 let module = self
592 .modules_slice()
593 .iter()
594 .enumerate()
595 .rev()
596 .find(|(_, module)| module.name() == module_name)
597 .map(|(index, _)| ModuleId::new(index as u32))?;
598 self.domain_candidate_in_module(module, name, domain)
599 }
600
601 fn global_domain_candidate(
602 &self,
603 name: &str,
604 domain: ResolutionDomain,
605 ) -> Option<ResolutionCandidate> {
606 match domain {
607 ResolutionDomain::Object | ResolutionDomain::Index => {
608 if domain == ResolutionDomain::Index
609 && super::resolver::rules::is_bare_index_type(name)
610 {
611 return None;
612 }
613 let symbol = Symbol::Object(self.object_by_name(name)?);
614 let module = symbol.module(self)?;
615 Some(self.resolution_candidate(module, symbol, domain, name))
616 }
617 ResolutionDomain::GroupMember | ResolutionDomain::Conformance => {
618 for (index, module) in self.modules_slice().iter().enumerate() {
619 if module.node_by_name(name).is_none() {
620 continue;
621 }
622 return self.domain_candidate_in_module(
623 ModuleId::new(index as u32),
624 name,
625 domain,
626 );
627 }
628 None
629 }
630 ResolutionDomain::NotificationObject => {
631 for (index, module) in self.modules_slice().iter().enumerate() {
632 let Some(node) = module.node_by_name(name) else {
633 continue;
634 };
635 let symbol = self.symbol_for_resolved_node(node);
636 if !matches!(symbol, Symbol::Object(_)) {
637 return None;
638 }
639 let module = symbol.module(self).unwrap_or(ModuleId::new(index as u32));
640 return Some(self.resolution_candidate(module, symbol, domain, name));
641 }
642 None
643 }
644 ResolutionDomain::Type | ResolutionDomain::Oid => None,
645 }
646 }
647
648 fn symbol_for_resolved_node(&self, node: crate::mib::NodeId) -> Symbol {
649 let data = self.node_data(node);
650 if let Some(object) = data.object {
651 Symbol::Object(object)
652 } else if let Some(notification) = data.notification {
653 Symbol::Notification(notification)
654 } else if let Some(group) = data.group {
655 Symbol::Group(group)
656 } else if let Some(compliance) = data.compliance {
657 Symbol::Compliance(compliance)
658 } else if let Some(capability) = data.capability {
659 Symbol::Capability(capability)
660 } else {
661 Symbol::Node(node)
662 }
663 }
664}
665
666fn symbol_matches_domain(symbol: Symbol, domain: ResolutionDomain, name: &str) -> bool {
667 match domain {
668 ResolutionDomain::Type => matches!(symbol, Symbol::Type(_)),
669 ResolutionDomain::Object | ResolutionDomain::NotificationObject => {
670 matches!(symbol, Symbol::Object(_))
671 }
672 ResolutionDomain::Index if super::resolver::rules::is_bare_index_type(name) => {
673 matches!(symbol, Symbol::Type(_))
674 }
675 ResolutionDomain::Index => matches!(symbol, Symbol::Object(_)),
676 ResolutionDomain::Oid | ResolutionDomain::GroupMember | ResolutionDomain::Conformance => {
677 !matches!(symbol, Symbol::Type(_))
678 }
679 }
680}
681
682fn parse_query(query: &str) -> Result<(Option<&str>, &str), ResolutionTraceError> {
683 if query.is_empty() {
684 return Err(ResolutionTraceError::EmptyQuery);
685 }
686 let Some((module, symbol)) = query.split_once("::") else {
687 return Ok((None, query));
688 };
689 if module.is_empty() || symbol.is_empty() || symbol.contains("::") {
690 return Err(ResolutionTraceError::InvalidQualifiedQuery(
691 query.to_owned(),
692 ));
693 }
694 Ok((Some(module), symbol))
695}