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}