Skip to main content

qcode/
obligation.rs

1//! Stable identity for a reconstruction obligation: an unresolved control
2//! transfer that reconstruction still owes an answer for.
3//!
4//! This module holds *only* the identity. The obligation database, its states,
5//! and its evidence live in `qcode_analysis` — they are derived analysis state
6//! and must not become part of serialized qcode bodies (see the roadmap's
7//! "Reconstruction facts and hypotheses" section).
8//!
9//! Like [`DiscoveryKey`](crate::discovery::DiscoveryKey), the key is built from
10//! stable binary addresses rather than context-local `FunctionId` / `BlockId` /
11//! `InstructionId` values. Obligations are produced while analyzing a disposable
12//! optimized clone and consumed by the scheduler against the persistent clean
13//! context, so an arena ID from one context would be meaningless — or worse,
14//! silently valid — in the other.
15
16use crate::discovery::Address;
17
18/// The kind of control transfer an obligation stands for.
19///
20/// Part of the key, not just a payload: the same address cannot host both, but
21/// keeping the kind in the identity means a record carries what it is without a
22/// lookup, and it matches [`DiscoveryKind`](crate::discovery::DiscoveryKind)'s
23/// precedent of typing the work item.
24#[derive(
25    Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
26)]
27pub enum TransferKind {
28    /// An indirect branch — `Mnemonic::BranchInd`. Resolvable today by
29    /// `handle_jump_tables`.
30    IndirectBranch,
31    /// An indirect call — `Mnemonic::CallInd`. No resolver exists yet; these
32    /// obligations stay pending until candidate-target analysis lands.
33    IndirectCall,
34}
35
36impl TransferKind {
37    /// Whether any pass can currently attempt to resolve this kind of transfer.
38    ///
39    /// `IndirectCall` obligations are recorded for completeness and reporting
40    /// but are inert: nothing attempts them, so a permanently-pending indirect
41    /// call is expected, not a bug. Reporting uses this to avoid presenting
42    /// inert obligations as reconstruction failures.
43    pub fn has_resolver(self) -> bool {
44        match self {
45            TransferKind::IndirectBranch => true,
46            TransferKind::IndirectCall => false,
47        }
48    }
49}
50
51/// Stable identity of a reconstruction obligation.
52///
53/// Keyed on the address of the transfer *instruction*, not its block. A block's
54/// start address is not stable under the straight-line merging the optimized
55/// clone performs (the jump-table pass already works around this — see its
56/// `dispatch_source_addr` helper), whereas the instruction address is fixed by
57/// the binary.
58#[derive(
59    Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
60)]
61pub struct ObligationKey {
62    /// Address of the indirect transfer instruction itself.
63    pub site: Address,
64    pub kind: TransferKind,
65}
66
67impl ObligationKey {
68    pub fn branch(site: Address) -> Self {
69        Self {
70            site,
71            kind: TransferKind::IndirectBranch,
72        }
73    }
74
75    pub fn call(site: Address) -> Self {
76        Self {
77            site,
78            kind: TransferKind::IndirectCall,
79        }
80    }
81}
82
83impl std::fmt::Display for ObligationKey {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        let kind = match self.kind {
86            TransferKind::IndirectBranch => "branch",
87            TransferKind::IndirectCall => "call",
88        };
89        write!(f, "{kind}@{:#x}", self.site)
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn kind_participates_in_identity() {
99        assert_ne!(ObligationKey::branch(0x1000), ObligationKey::call(0x1000));
100    }
101
102    #[test]
103    fn same_site_and_kind_is_the_same_obligation() {
104        assert_eq!(ObligationKey::branch(0x1000), ObligationKey::branch(0x1000));
105    }
106
107    #[test]
108    fn only_indirect_branches_have_a_resolver_today() {
109        assert!(TransferKind::IndirectBranch.has_resolver());
110        assert!(!TransferKind::IndirectCall.has_resolver());
111    }
112
113    #[test]
114    fn ordering_groups_by_site_then_kind() {
115        let mut keys = vec![
116            ObligationKey::call(0x2000),
117            ObligationKey::branch(0x2000),
118            ObligationKey::branch(0x1000),
119        ];
120        keys.sort();
121        assert_eq!(
122            keys,
123            vec![
124                ObligationKey::branch(0x1000),
125                ObligationKey::branch(0x2000),
126                ObligationKey::call(0x2000),
127            ]
128        );
129    }
130
131    #[test]
132    fn display_is_stable_and_readable() {
133        assert_eq!(
134            ObligationKey::branch(0x401a60).to_string(),
135            "branch@0x401a60"
136        );
137        assert_eq!(ObligationKey::call(0x401a60).to_string(), "call@0x401a60");
138    }
139}