callgraph_verify/lib.rs
1//! Pure-byte call-graph + fact verifier for the Rust rebuild.
2//!
3//! When a rebuild function is tagged `origin: 0xADDR`, it claims to be a 1:1
4//! port of the function at that VA in the target binary. This crate establishes
5//! ground truth by disassembling the actual bytes (no Ghidra, no decompiled C)
6//! and lets a port declare what it should structurally match:
7//!
8//! * direct CALL targets (the call graph)
9//! * struct field offsets (`[reg+0xNN]` accesses)
10//! * global / static VAs (`[0x00bXXXXX]` data refs)
11//! * vtable / immediate VAs (`MOV [esi],0x00a7e38c`)
12//! * virtual-call slots (`CALL [reg+0x84]`)
13//! * IAT import targets (`CALL [0x00a28xxx]`)
14//!
15//! Use the [`verify_callgraph!`] macro to turn declarations into `#[test]`s.
16
17pub mod disasm;
18pub mod pe;
19pub mod skel;
20
21pub use disasm::{analyze, reg_params, resolve_thunk, tail_referenced, ArgKind, CallGraph, CallSite};
22pub use pe::Pe;
23pub use skel::{cfg_fingerprint, CfgFingerprint};
24
25use std::collections::BTreeSet;
26
27/// One declared expectation for a function, checked against the binary's facts.
28#[derive(Default)]
29pub struct Spec {
30 pub addr: u64,
31 calls: Option<(Vec<u64>, bool)>, // (declared, exact?)
32 fields: Vec<u64>,
33 globals: Vec<u64>,
34 vtables: Vec<u64>, // in-image immediates (vtable VAs, fn ptrs, string addrs)
35 vcall_slots: Vec<u64>,
36 iat: Vec<u64>,
37}
38
39/// The cached `.text` instruction-start sets, keyed by the PE's unique id
40/// plus `(image_base, text_rva, text_vsize)`. Each entry holds an `Arc` so the
41/// cached set can be cloned out without holding the MutexGuard. The id (not
42/// layout alone) is the key because two same-layout PEs with different bytes
43/// must never share a set — the input-ignoring-cache class of BUG #9.
44type TextStartsCache = std::sync::Mutex<
45 std::collections::HashMap<(u64, u64, u32, u32), std::sync::OnceLock<TextStarts>>,
46>;
47type TextStarts = std::sync::Arc<std::collections::BTreeSet<u64>>;
48
49/// The cached set of EVERY rel32 branch target (CALL/JMP/Jcc) whose opcode sits
50/// at a real instruction start in `.text`, keyed by PE id + layout. Built in
51/// ONE linear pass per PE instance so [`has_direct_branch_target`] is a hash
52/// lookup — the per-target scan it replaced walked the whole (multi-MB)
53/// `.text` blob on every call, making the macro's inline-marker and
54/// vtable-dispatch checks O(fns × markers × |.text|) per build. NOTE: the
55/// steady-state path must not touch the `.text` bytes at all — a content
56/// fingerprint per call (an earlier draft hashed 4 MB every lookup) is just a
57/// cheaper rescan and reproduces the same quadratic build time.
58type BranchTargetsCache = std::sync::Mutex<
59 std::collections::HashMap<(u64, u64, u32, u32), std::sync::OnceLock<BranchTargets>>,
60>;
61type BranchTargets = std::sync::Arc<std::collections::HashSet<u32>>;
62
63fn branch_targets_cache() -> &'static BranchTargetsCache {
64 static CACHE: std::sync::OnceLock<BranchTargetsCache> = std::sync::OnceLock::new();
65 CACHE.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
66}
67
68fn text_starts_cache() -> &'static TextStartsCache {
69 static CACHE: std::sync::OnceLock<TextStartsCache> = std::sync::OnceLock::new();
70 CACHE.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
71}
72
73/// Decode the entire `.text` section once and return the set of VAs that are
74/// the start of a valid x86 instruction. Used by [`has_direct_branch_target`]
75/// to gate its byte-scan so a `0xE8` byte sitting inside some other
76/// instruction's encoding (i.e. NOT at an instruction start) is rejected — the
77/// call/jmp pattern there is a spurious coincidence, not a real branch.
78///
79/// Steady-state is lock-free: the cache stores `&'static OnceLock` values, and
80/// once `get_or_init` has populated one, repeated `get()` calls return
81/// `&'static BTreeSet`s without touching the mutex. The mutex is only acquired
82/// on the very first request per `(image_base, text_rva, text_vsize)` triple.
83fn text_instruction_starts(pe: &Pe) -> TextStarts {
84 let key = || -> Option<(u64, u64, u32, u32)> {
85 let text = pe.sections().iter().find(|s| s.name == ".text")?;
86 Some((
87 pe.id(),
88 pe.image_base,
89 text.virtual_address,
90 text.virtual_size,
91 ))
92 };
93 let key = match key() {
94 Some(k) => k,
95 None => {
96 // No .text section: return an empty Arc.
97 static EMPTY: std::sync::OnceLock<TextStarts> = std::sync::OnceLock::new();
98 return EMPTY
99 .get_or_init(|| std::sync::Arc::new(std::collections::BTreeSet::new()))
100 .clone();
101 }
102 };
103 let cache = text_starts_cache();
104 // Fast path: lock-free read once the value exists. `Mutex::lock` returns
105 // `LockResult<MutexGuard>`; we ignore poisoning — `OnceLock::get_or_init`
106 // never panics inside the closure.
107 let mut lock = cache.lock().unwrap_or_else(|e| e.into_inner());
108 let entry = lock.entry(key).or_default();
109 let once = entry.get_or_init(|| {
110 use iced_x86::{Decoder, DecoderOptions};
111 let text = pe
112 .sections()
113 .iter()
114 .find(|s| s.name == ".text")
115 .expect("key was Some");
116 let text_va = pe.image_base + text.virtual_address as u64;
117 let bytes = match pe.read(text_va, text.virtual_size as usize) {
118 Some(b) => b,
119 None => return std::sync::Arc::new(std::collections::BTreeSet::new()),
120 };
121 let mut dec = Decoder::with_ip(32, bytes, text_va, DecoderOptions::NONE);
122 let mut starts = std::collections::BTreeSet::new();
123 starts.insert(text_va);
124 while dec.can_decode() {
125 let ip = dec.ip();
126 starts.insert(ip);
127 let _ = dec.decode();
128 }
129 std::sync::Arc::new(starts)
130 });
131 once.clone()
132}
133
134/// Does ANY instruction in `.text` statically target `target` via a relative branch
135/// (CALL/JMP/Jcc rel32)? Used by the macro to warn on vtable-only dispatch.
136///
137/// Implementation: the set of all such targets is built in ONE pass over `.text`
138/// (cached per PE — see [`branch_targets_cache`]) and this call is a hash lookup.
139/// The one-pass walk decodes `.text` once (also cached) to learn every valid
140/// instruction start, then byte-scans for `0xE8`/`0xE9`/`0x0F 0x8x` and only
141/// records a hit as a real branch if its opcode byte sits AT an instruction
142/// start. Without that gate (BUG #3), a `0xE8` byte anywhere inside another
143/// instruction's encoding was treated as a CALL, computing a junk rel32 target
144/// that could coincidentally equal `target` — a false positive that suppressed
145/// legitimate VTABLE-ONLY DISPATCH warnings.
146pub fn has_direct_branch_target(pe: &Pe, target: u64) -> bool {
147 all_branch_targets(pe).contains(&(target as u32))
148}
149
150/// Every rel32 branch target of `.text`, computed once per PE instance.
151/// Accept-set identical to the per-target scan this replaces: rel32
152/// CALL/JMP/Jcc opcodes gated on real instruction starts. Keyed by the PE's
153/// unique id — the steady-state lookup never touches the `.text` bytes.
154fn all_branch_targets(pe: &Pe) -> BranchTargets {
155 let Some(text) = pe.sections().iter().find(|s| s.name == ".text") else {
156 // No .text section: every lookup misses against an empty set.
157 static EMPTY: std::sync::OnceLock<BranchTargets> = std::sync::OnceLock::new();
158 return EMPTY
159 .get_or_init(|| std::sync::Arc::new(std::collections::HashSet::new()))
160 .clone();
161 };
162 let text_va = pe.image_base + text.virtual_address as u64;
163 let key = (
164 pe.id(),
165 pe.image_base,
166 text.virtual_address,
167 text.virtual_size,
168 );
169 let cache = branch_targets_cache();
170 let mut lock = cache.lock().unwrap_or_else(|e| e.into_inner());
171 let entry = lock.entry(key).or_default();
172 entry
173 .get_or_init(|| {
174 let blob = match pe.read(text_va, text.virtual_size as usize) {
175 Some(b) => b,
176 None => return std::sync::Arc::new(std::collections::HashSet::new()),
177 };
178 let starts = text_instruction_starts(pe);
179 let mut targets = std::collections::HashSet::new();
180 let mut i = 0;
181 while i + 4 < blob.len() {
182 let rel_at = match blob[i] {
183 0xE8 | 0xE9 => i + 1, // CALL rel32 / JMP rel32
184 0x0F if i + 6 <= blob.len() && (0x80..=0x8F).contains(&blob[i + 1]) => i + 2,
185 _ => {
186 i += 1;
187 continue;
188 }
189 };
190 // Gate: the branch opcode must sit at a real instruction start.
191 // Spurious `0xE8` bytes inside other instructions are ignored.
192 // (BUG #3 fix.)
193 let ip_va = text_va + i as u64;
194 if !starts.contains(&ip_va) {
195 i += 1;
196 continue;
197 }
198 if rel_at + 4 > blob.len() {
199 i += 1;
200 continue;
201 }
202 let rel = i32::from_le_bytes([
203 blob[rel_at],
204 blob[rel_at + 1],
205 blob[rel_at + 2],
206 blob[rel_at + 3],
207 ]) as i64;
208 let t = (text_va + (rel_at as u64 + 4)).wrapping_add(rel as u64) as u32;
209 targets.insert(t);
210 i += 1;
211 }
212 std::sync::Arc::new(targets)
213 })
214 .clone()
215}
216
217impl Spec {
218 pub fn new(addr: u64) -> Spec {
219 Spec {
220 addr,
221 ..Default::default()
222 }
223 }
224 /// Declared CALL set must EQUAL the binary's (flags hallucinated + missing).
225 pub fn calls_exact(mut self, t: &[u64]) -> Spec {
226 self.calls = Some((t.to_vec(), true));
227 self
228 }
229 /// No declared CALL may be absent (hallucination-only; tolerates inlined/unported).
230 pub fn calls_superset(mut self, t: &[u64]) -> Spec {
231 self.calls = Some((t.to_vec(), false));
232 self
233 }
234 /// Each declared field offset must be touched by the binary (present-check).
235 pub fn fields(mut self, t: &[u64]) -> Spec {
236 self.fields = t.to_vec();
237 self
238 }
239 pub fn globals(mut self, t: &[u64]) -> Spec {
240 self.globals = t.to_vec();
241 self
242 }
243 pub fn vtables(mut self, t: &[u64]) -> Spec {
244 self.vtables = t.to_vec();
245 self
246 }
247 pub fn vcall_slots(mut self, t: &[u64]) -> Spec {
248 self.vcall_slots = t.to_vec();
249 self
250 }
251 pub fn iat(mut self, t: &[u64]) -> Spec {
252 self.iat = t.to_vec();
253 self
254 }
255
256 /// Check this spec against the binary; `Err` lists every violation.
257 pub fn check(&self, pe: &Pe) -> Result<(), String> {
258 let cg = analyze(pe, self.addr)?;
259 let mut v = Vec::new();
260
261 if let Some((declared, exact)) = &self.calls {
262 let real = cg.call_targets();
263 let dset: BTreeSet<u64> = declared.iter().copied().collect();
264 for h in dset.difference(&real) {
265 v.push(format!(
266 "HALLUCINATED declared CALL {h:#010x} — binary never directly calls it"
267 ));
268 }
269 if *exact {
270 for m in real.difference(&dset) {
271 v.push(format!(
272 "MISSING binary CALLs {m:#010x} — not declared"
273 ));
274 }
275 }
276 }
277 // The rest are present-checks: a declared item the binary doesn't exhibit
278 // is the hallucination; we don't require enumerating the binary's full set.
279 present_check("field offset", &self.fields, &cg.field_offsets, &mut v);
280 present_check("global", &self.globals, &cg.globals, &mut v);
281 present_check("vtable/imm addr", &self.vtables, &cg.imm_addrs, &mut v);
282 present_check("vcall slot", &self.vcall_slots, &cg.vcall_slots, &mut v);
283 present_check("IAT import", &self.iat, &cg.iat_calls, &mut v);
284
285 if v.is_empty() {
286 Ok(())
287 } else {
288 Err(format!(
289 "{:#010x}: {} violation(s) vs pure-byte ground truth:\n {}\n (indirect calls: {}, indirect jumps: {})",
290 self.addr,
291 v.len(),
292 v.join("\n "),
293 cg.indirect_calls,
294 cg.indirect_jumps,
295 ))
296 }
297 }
298
299 /// Load `<manifest_dir>/<exe>` and panic on any violation (for `#[test]`s).
300 pub fn check_at(&self, manifest_dir: &str, exe: &str) {
301 let path = std::path::Path::new(manifest_dir).join(exe);
302 let pe = Pe::load(path.to_str().unwrap())
303 .unwrap_or_else(|e| panic!("load {}: {e}", path.display()));
304 if let Err(msg) = self.check(&pe) {
305 panic!("{msg}");
306 }
307 }
308}
309
310fn present_check(kind: &str, declared: &[u64], real: &BTreeSet<u64>, out: &mut Vec<String>) {
311 for d in declared {
312 if !real.contains(d) {
313 out.push(format!(
314 "HALLUCINATED declared {kind} {d:#x} — binary never references it"
315 ));
316 }
317 }
318}
319
320/// Back-compat thin diff over direct calls only (used by the CLI / build gate).
321#[derive(Debug)]
322pub struct Diff {
323 pub entry: u64,
324 pub hallucinated: Vec<u64>,
325 pub missing: Vec<u64>,
326 pub indirect_calls: u32,
327 pub indirect_jumps: u32,
328}
329
330impl Diff {
331 pub fn is_clean(&self) -> bool {
332 self.hallucinated.is_empty() && self.missing.is_empty()
333 }
334}
335
336pub fn diff_calls(
337 pe: &Pe,
338 entry: u64,
339 declared: &[u64],
340 expect_complete: bool,
341) -> Result<Diff, String> {
342 let cg = analyze(pe, entry)?;
343 let real = cg.call_targets();
344 let dset: BTreeSet<u64> = declared.iter().copied().collect();
345 Ok(Diff {
346 entry,
347 hallucinated: dset.difference(&real).copied().collect(),
348 missing: if expect_complete {
349 real.difference(&dset).copied().collect()
350 } else {
351 Vec::new()
352 },
353 indirect_calls: cg.indirect_calls,
354 indirect_jumps: cg.indirect_jumps,
355 })
356}
357
358/// Result of diffing a Rust function's *resolved body calls* against the binary.
359pub struct CorpusDiff {
360 pub addr: u64,
361 /// Rust calls a ported fn the binary never calls (and didn't inline) — invented.
362 pub hallucinated: Vec<(String, Vec<u64>)>,
363 /// Rust calls a ported fn the binary inlines — reclassified faithful.
364 pub inlined_ok: Vec<String>,
365 /// Rust DIRECTLY calls a method the binary dispatches through a known (declared) vtable —
366 /// the faithful form is a `vcall`. Once the vtable is declared, the call CAN be made
367 /// virtually, so it MUST be one.
368 pub vtable_dispatch: Vec<(String, Vec<u64>)>,
369 /// Binary calls a ported fn the Rust body doesn't (often inlined; review).
370 pub missing: Vec<u64>,
371 pub indirect_calls: u32,
372}
373
374fn is_inlined_into(pe: &Pe, callee: u64, caller_fp: &BTreeSet<u64>) -> bool {
375 match analyze(pe, callee) {
376 Ok(cg) => {
377 let fp = cg.footprint();
378 // Footprint-subset is only meaningful evidence with >= 2 distinct
379 // in-image references. A SINGLE shared global (an inlined refcount
380 // accessor) or a single shared call target is a subset of almost any
381 // caller that touches the same thing — that's coincidence, not
382 // inlining, and accepting it would excuse an invented call. Trivial
383 // single-element callees can't be footprint-confirmed, so the
384 // faithful port reproduces the inlined work inside the caller —
385 // it does not call the helper.
386 fp.len() >= 2 && fp.is_subset(caller_fp)
387 }
388 Err(_) => false,
389 }
390}
391
392/// Diff the calls a Rust body makes (already resolved to ported addresses) against
393/// the binary's real call set at `own_addrs`. `corpus` is the set of all ported
394/// origin addresses (only calls within it are diffable). Mirrors the logic of
395/// `tools/analysis/verify_callgraph.py`, but on bytes instead of Ghidra `.asm`.
396pub fn diff_body_calls(
397 pe: &Pe,
398 own_addrs: &[u64],
399 corpus: &BTreeSet<u64>,
400 rust_idents: &[(String, Vec<u64>)],
401 vtable_methods: &BTreeSet<u64>,
402 miss_satisfied: &BTreeSet<u64>,
403) -> Result<CorpusDiff, String> {
404 let primary = *own_addrs.first().ok_or("no origin address")?;
405 let mut bin_calls = BTreeSet::new();
406 let mut wide_extras = BTreeSet::new();
407 let mut footprint = BTreeSet::new();
408 let mut indirect = 0u32;
409 let mut tail_targets: BTreeSet<u64> = BTreeSet::new();
410 for &a in own_addrs {
411 let cg = analyze(pe, a)?;
412 bin_calls.extend(cg.call_targets());
413 // A tail-call `JMP 0xADDR` out of the body is a real call edge (e.g. a
414 // cache-wrapper that tail-calls the base getter), so count it as a call —
415 // a faithful port spells it as an ordinary call.
416 bin_calls.extend(&cg.tail_calls);
417 tail_targets.extend(&cg.tail_calls);
418 // Code-pointer immediates widen acceptance only (folded into
419 // `bin_calls_wide` below, never into the MISSING requirement); staged here
420 // so the walk stays single-pass.
421 wide_extras.extend(
422 cg.imm_addrs
423 .iter()
424 .copied()
425 .filter(|&x| pe.is_executable_section_va(x)),
426 );
427 footprint.extend(cg.footprint());
428 indirect += cg.indirect_calls;
429 }
430 // Wide acceptance set: `bin_calls` plus, per function, its executable-section
431 // code-pointer immediates and one level of each tail-callee's direct calls.
432 // - A code-pointer immediate (`PUSH 0xADDR` / `MOV reg,0xADDR` with ADDR in a
433 // section flagged EXECUTE/CODE) is a function the binary hands to a higher-order
434 // helper that then INVOKES it — a CRT `_EH_vector_constructor_iterator` over an
435 // element array, a `qsort` comparator, a registered callback, etc. A faithful
436 // port that spells the per-element / callback work as a direct call to that
437 // function is not a hallucination. The characteristics-based filter
438 // (`is_executable_section_va`, NOT the permissive `is_executable_va`) excludes
439 // `.rdata` vtable VAs and string addresses — those are data, not call targets.
440 // - A tail-call `JMP G` transfers control to G, so the calls G makes are reachable
441 // as part of THIS function's execution. MSVC frequently splits a function across
442 // non-contiguous chunks linked by a `JMP`, or emits a tiny entry stub that
443 // tail-jumps to a shared body (e.g. `0x00590110` → `JMP 0x0058da00`, whose body
444 // `CALL 0x0058d1c0`; or the ICF-folded stubs that all `JMP 0x004524c0`). The
445 // decompiler shows that body inlined into the entry, so a faithful port spells
446 // those as ordinary calls.
447 // Both folds only ever ADD acceptable spellings (HALLUCINATED direction). They must
448 // NOT feed the MISSING requirement: the binary issues no CALL to an imm-addr, and
449 // no CALL here to the tail target's callees — demanding them forces ports to
450 // duplicate the tail target's body (previously misdiagnosed as E013 with an
451 // unactionable "add the call" fix; the suggested call was already spelled).
452 let mut bin_calls_wide = bin_calls.clone();
453 bin_calls_wide.extend(wide_extras);
454 for t in tail_targets {
455 if let Ok(tcg) = analyze(pe, t) {
456 bin_calls_wide.extend(tcg.call_targets());
457 }
458 }
459 // Follow one level of jmp-thunks: a call to a `JMP X` thunk (tail-jump stub / ICF forwarder)
460 // really targets X, so a faithful direct Rust call to X is not a hallucination.
461 // Resolved over the WIDE set (as before this split) so acceptance is unchanged;
462 // the results stay wide-only — a thunk is never itself a MISSING requirement.
463 let thunked: Vec<u64> = bin_calls_wide
464 .iter()
465 .filter_map(|&t| resolve_thunk(pe, t))
466 .collect();
467 bin_calls_wide.extend(thunked);
468 let own: BTreeSet<u64> = own_addrs.iter().copied().collect();
469 let bin_ported: BTreeSet<u64> = bin_calls
470 .intersection(corpus)
471 .copied()
472 .filter(|a| !own.contains(a))
473 .collect();
474
475 let (mut hallucinated, mut inlined_ok, mut vtable_dispatch, mut resolved) =
476 (Vec::new(), Vec::new(), Vec::new(), BTreeSet::new());
477 for (ident, addrs) in rust_idents {
478 let aset: Vec<u64> = addrs.iter().copied().filter(|a| !own.contains(a)).collect();
479 if aset.is_empty() {
480 continue;
481 }
482 resolved.extend(&aset);
483 // Satisfied-direction check reads the WIDE set: a body edge the binary
484 // reaches only through a tail-jump or a higher-order immediate is faithful.
485 // (`bin_calls` below stays narrow: MISSING must only require edges the binary
486 // actually CALLs here.)
487 if aset.iter().any(|a| bin_calls_wide.contains(a)) {
488 continue; // satisfied — binary makes (one of) this call
489 }
490 if aset.iter().any(|a| is_inlined_into(pe, *a, &footprint)) {
491 inlined_ok.push(ident.clone());
492 continue; // faithful: binary inlined the callee (footprint-confirmed)
493 }
494 // Known (declared) vtable method the binary doesn't directly call → it is dispatched
495 // virtually; the faithful form is a `vcall`. The vtable exists, so the call CAN and
496 // MUST be virtual — no other form is faithful.
497 if aset.iter().any(|a| vtable_methods.contains(a)) {
498 vtable_dispatch.push((ident.clone(), aset));
499 continue;
500 }
501 hallucinated.push((ident.clone(), aset));
502 }
503 // A MISSING is a binary call to a ported fn the body doesn't spell. But the
504 // verifier can only SEE a spelling when the body call name resolves through
505 // the strict table split + DENY filter — a qualified path `Self::m(..)` /
506 // `Type::m(..)` denotes an impl method yet resolves against free_fns only,
507 // and a generically-named ported fn (`mul`, `new`) is excluded outright, so
508 // its MISSING would be UNSATISFIABLE no matter how faithfully the body
509 // spells it. `miss_satisfied` (both tables, DENY-inclusive, split-agnostic)
510 // marks those addresses as satisfiable-by-name. Used ONLY here — never to
511 // excuse a HALLUCINATED, which keeps the strict split + DENY filter.
512 let missing: Vec<u64> = bin_ported
513 .difference(&resolved)
514 .filter(|a| !miss_satisfied.contains(a))
515 .copied()
516 .collect();
517 Ok(CorpusDiff {
518 addr: primary,
519 hallucinated,
520 inlined_ok,
521 vtable_dispatch,
522 missing,
523 indirect_calls: indirect,
524 })
525}
526
527/// Declare expected facts for `origin:` functions and generate one `#[test]` per
528/// function, checked against the real binary.
529///
530/// Two forms:
531///
532/// ```ignore
533/// verify_callgraph! {
534/// exe: "../../target.exe";
535///
536/// // 1) shorthand — direct calls only
537/// fn set_name @ 0x006ff420 => exact [0x00401f00, 0x00401f20, 0x00984b6a];
538///
539/// // 2) block — any subset of clauses, each `;`-terminated
540/// fn ni_node_ctor @ 0x0070b780 {
541/// calls: superset [0x004b2d30];
542/// fields: [0xbc, 0xc0, 0xc4, 0xc8]; // struct offsets it must touch
543/// vtables: [0x00a7e38c]; // NiNode vtable VA installed
544/// vcall_slots: [0x84]; // virtual call slot, if any
545/// }
546/// }
547/// ```
548///
549/// CALL clauses: `exact` (equality) or `superset` (hallucination-only). The other
550/// clauses are present-checks: each declared value must appear in the binary's
551/// harvested set, else it's a hallucination.
552#[macro_export]
553macro_rules! verify_callgraph {
554 (exe: $exe:expr; $($body:tt)*) => {
555 $crate::verify_callgraph!(@each $exe; $($body)*);
556 };
557
558 // shorthand: direct calls, exact
559 (@each $exe:expr;
560 $(#[$m:meta])* fn $name:ident @ $addr:expr => exact [ $($t:expr),* $(,)? ] ; $($rest:tt)*) => {
561 $(#[$m])*
562 #[test]
563 fn $name() {
564 $crate::Spec::new($addr).calls_exact(&[$($t),*]).check_at(env!("CARGO_MANIFEST_DIR"), $exe);
565 }
566 $crate::verify_callgraph!(@each $exe; $($rest)*);
567 };
568
569 // shorthand: direct calls, superset
570 (@each $exe:expr;
571 $(#[$m:meta])* fn $name:ident @ $addr:expr => superset [ $($t:expr),* $(,)? ] ; $($rest:tt)*) => {
572 $(#[$m])*
573 #[test]
574 fn $name() {
575 $crate::Spec::new($addr).calls_superset(&[$($t),*]).check_at(env!("CARGO_MANIFEST_DIR"), $exe);
576 }
577 $crate::verify_callgraph!(@each $exe; $($rest)*);
578 };
579
580 // block form: any subset of `;`-terminated clauses
581 (@each $exe:expr;
582 $(#[$m:meta])* fn $name:ident @ $addr:literal { $($clauses:tt)* } $($rest:tt)*) => {
583 $(#[$m])*
584 #[test]
585 fn $name() {
586 let spec = $crate::Spec::new($addr);
587 let spec = $crate::verify_callgraph!(@clauses spec; $($clauses)*);
588 spec.check_at(env!("CARGO_MANIFEST_DIR"), $exe);
589 }
590 $crate::verify_callgraph!(@each $exe; $($rest)*);
591 };
592
593 (@each $exe:expr;) => {};
594
595 // clause munchers — each consumes one `;`-terminated clause, returns the builder
596 (@clauses $s:expr; calls: exact [ $($t:expr),* $(,)? ] ; $($r:tt)*) => {
597 $crate::verify_callgraph!(@clauses $s.calls_exact(&[$($t),*]); $($r)*)
598 };
599 (@clauses $s:expr; calls: superset [ $($t:expr),* $(,)? ] ; $($r:tt)*) => {
600 $crate::verify_callgraph!(@clauses $s.calls_superset(&[$($t),*]); $($r)*)
601 };
602 (@clauses $s:expr; fields: [ $($t:expr),* $(,)? ] ; $($r:tt)*) => {
603 $crate::verify_callgraph!(@clauses $s.fields(&[$($t),*]); $($r)*)
604 };
605 (@clauses $s:expr; globals: [ $($t:expr),* $(,)? ] ; $($r:tt)*) => {
606 $crate::verify_callgraph!(@clauses $s.globals(&[$($t),*]); $($r)*)
607 };
608 (@clauses $s:expr; vtables: [ $($t:expr),* $(,)? ] ; $($r:tt)*) => {
609 $crate::verify_callgraph!(@clauses $s.vtables(&[$($t),*]); $($r)*)
610 };
611 (@clauses $s:expr; vcall_slots: [ $($t:expr),* $(,)? ] ; $($r:tt)*) => {
612 $crate::verify_callgraph!(@clauses $s.vcall_slots(&[$($t),*]); $($r)*)
613 };
614 (@clauses $s:expr; iat: [ $($t:expr),* $(,)? ] ; $($r:tt)*) => {
615 $crate::verify_callgraph!(@clauses $s.iat(&[$($t),*]); $($r)*)
616 };
617 (@clauses $s:expr;) => { $s };
618}