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::symbol::SymbolKind;
19
20/// A class of symbol whose presence a backport can dangle on.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
22#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
23#[serde(rename_all = "snake_case")]
24pub enum ResolverClass {
25    Macro,
26    Include,
27    Behaviour,
28    QualifiedCall,
29    Record,
30    LocalCall,
31    Type,
32    IndirectCall,
33}
34
35impl ResolverClass {
36    /// Stable label matching the serde spelling, for text roll-ups.
37    #[must_use]
38    pub fn as_str(self) -> &'static str {
39        match self {
40            ResolverClass::Macro => "macro",
41            ResolverClass::Include => "include",
42            ResolverClass::Behaviour => "behaviour",
43            ResolverClass::QualifiedCall => "qualified_call",
44            ResolverClass::Record => "record",
45            ResolverClass::LocalCall => "local_call",
46            ResolverClass::Type => "type",
47            ResolverClass::IndirectCall => "indirect_call",
48        }
49    }
50
51    pub const ALL: [ResolverClass; 8] = [
52        ResolverClass::Macro,
53        ResolverClass::Include,
54        ResolverClass::Behaviour,
55        ResolverClass::QualifiedCall,
56        ResolverClass::Record,
57        ResolverClass::LocalCall,
58        ResolverClass::Type,
59        ResolverClass::IndirectCall,
60    ];
61
62    /// Whether the analyzer resolves this class. Macro, include,
63    /// behaviour, record, local call, and indirect call resolve on the
64    /// target tree; qualified call and type resolve through the
65    /// snapshot and self-pin.
66    // every class is covered today: the arm exists so a newly added,
67    // not-yet-resolved class fails to compile here rather than silently
68    // returning true
69    #[must_use]
70    pub fn is_covered(self) -> bool {
71        match self {
72            ResolverClass::Macro
73            | ResolverClass::Include
74            | ResolverClass::Behaviour
75            | ResolverClass::QualifiedCall
76            | ResolverClass::Record
77            | ResolverClass::LocalCall
78            | ResolverClass::Type
79            | ResolverClass::IndirectCall => true,
80        }
81    }
82
83    /// The class a symbol falls in. One class per kind, so the producer
84    /// and the consumer agree without a hand-copied table: a function is
85    /// a qualified call, a type its own class, a callback its behaviour.
86    #[must_use]
87    pub fn of_symbol_kind(kind: &SymbolKind) -> ResolverClass {
88        match kind {
89            SymbolKind::Macro { .. } => ResolverClass::Macro,
90            SymbolKind::Record { .. } => ResolverClass::Record,
91            SymbolKind::Behaviour { .. } | SymbolKind::Callback { .. } => ResolverClass::Behaviour,
92            SymbolKind::Type { .. } => ResolverClass::Type,
93            SymbolKind::Function { .. } | SymbolKind::FunctionAnyArity { .. } => {
94                ResolverClass::QualifiedCall
95            }
96        }
97    }
98}
99
100/// The set of classes a producer checks, stamped on a verdict envelope
101/// so a corpus row self-describes its producer's coverage. Stores only
102/// the checked set: `unchecked` is derived, so a class can never be in
103/// both or neither.
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
105#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
106pub struct ResolverCoverage {
107    checked: BTreeSet<ResolverClass>,
108}
109
110impl ResolverCoverage {
111    /// Coverage of the running binary.
112    #[must_use]
113    pub fn current() -> Self {
114        Self {
115            checked: ResolverClass::ALL
116                .into_iter()
117                .filter(|c| c.is_covered())
118                .collect(),
119        }
120    }
121
122    /// Whether this producer checked `class`: the fold's bug-vs-gap test.
123    #[must_use]
124    pub fn is_checked(&self, class: ResolverClass) -> bool {
125        self.checked.contains(&class)
126    }
127
128    /// Classes this producer did not check: the calibration sampler steers
129    /// toward candidates touching one.
130    pub fn unchecked(&self) -> impl Iterator<Item = ResolverClass> + '_ {
131        ResolverClass::ALL
132            .into_iter()
133            .filter(|c| !self.checked.contains(c))
134    }
135}