Skip to main content

backhopper_core/model/
resolver_coverage.rs

1// Copyright (C) 2026 Michael S. Klishin and Contributors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3// See LICENSE-APACHE and LICENSE-MIT for details.
4
5//! Which symbol classes the analyzer resolves, and the classifier from a
6//! symbol to its class.
7//!
8//! A symbol-presence taxonomy across mechanisms: macro, record, local
9//! call, include, and behaviour resolve on the target tree; qualified
10//! call and type through the snapshot. The fold routes a break by class
11//! against the producing row's coverage: a checked class is a resolver
12//! bug, an unchecked one a coverage gap.
13
14use std::collections::BTreeSet;
15
16use serde::{Deserialize, Serialize};
17
18use crate::model::names::vocabulary;
19use crate::model::symbol::SymbolKind;
20
21vocabulary!(
22    /// A class of symbol whose presence a backport can dangle on.
23    #[derive(Hash, PartialOrd, Ord)]
24    pub enum ResolverClass: "resolver class" {
25        Macro => "macro",
26        Include => "include",
27        Behaviour => "behaviour",
28        QualifiedCall => "qualified_call",
29        Record => "record",
30        LocalCall => "local_call",
31        Type => "type",
32        IndirectCall => "indirect_call",
33    }
34);
35
36impl ResolverClass {
37    /// Whether the analyzer resolves this class. Macro, include,
38    /// behaviour, record, local call, and indirect call resolve on the
39    /// target tree; qualified call and type resolve through the
40    /// snapshot and self-pin.
41    // exhaustive so a newly added class fails to compile here rather than silently return true
42    #[must_use]
43    pub fn is_covered(self) -> bool {
44        match self {
45            ResolverClass::Macro
46            | ResolverClass::Include
47            | ResolverClass::Behaviour
48            | ResolverClass::QualifiedCall
49            | ResolverClass::Record
50            | ResolverClass::LocalCall
51            | ResolverClass::Type
52            | ResolverClass::IndirectCall => true,
53        }
54    }
55
56    /// The class a symbol falls in. One class per kind, so the producer
57    /// and the consumer agree without a hand-copied table: a function is
58    /// a qualified call, a type its own class, a callback its behaviour.
59    #[must_use]
60    pub fn of_symbol_kind(kind: &SymbolKind) -> ResolverClass {
61        match kind {
62            SymbolKind::Macro { .. } => ResolverClass::Macro,
63            SymbolKind::Record { .. } => ResolverClass::Record,
64            SymbolKind::Behaviour { .. } | SymbolKind::Callback { .. } => ResolverClass::Behaviour,
65            SymbolKind::Type { .. } => ResolverClass::Type,
66            SymbolKind::Function { .. } | SymbolKind::FunctionAnyArity { .. } => {
67                ResolverClass::QualifiedCall
68            }
69        }
70    }
71}
72
73/// The set of classes a producer checks, stamped on a verdict envelope
74/// so a corpus row self-describes its producer's coverage. Stores only
75/// the checked set: `unchecked` is derived, so a class can never be in
76/// both or neither.
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
79pub struct ResolverCoverage {
80    checked: BTreeSet<ResolverClass>,
81}
82
83impl ResolverCoverage {
84    /// Coverage of the running binary.
85    #[must_use]
86    pub fn current() -> Self {
87        Self {
88            checked: ResolverClass::ALL
89                .iter()
90                .copied()
91                .filter(|c| c.is_covered())
92                .collect(),
93        }
94    }
95
96    /// Whether this producer checked `class`: the fold's bug-vs-gap test.
97    #[must_use]
98    pub fn is_checked(&self, class: ResolverClass) -> bool {
99        self.checked.contains(&class)
100    }
101
102    /// Classes this producer did not check: the calibration sampler steers
103    /// toward candidates touching one.
104    pub fn unchecked(&self) -> impl Iterator<Item = ResolverClass> + '_ {
105        ResolverClass::ALL
106            .iter()
107            .copied()
108            .filter(|c| !self.checked.contains(c))
109    }
110}