rapx/verify/
target.rs

1use crate::analysis::Analysis;
2use rustc_hir::{
3    Attribute, BodyId, FnDecl, ItemKind,
4    def_id::{DefId, LocalDefId},
5    intravisit::{FnKind, Visitor},
6};
7use rustc_middle::{
8    hir::nested_filter,
9    ty::{TyCtxt, TyKind},
10};
11use rustc_span::{Span, Symbol};
12use std::collections::HashMap;
13use syn::Expr;
14
15use super::{
16    assets_parser::*,
17    attr_parser::parse_rapx_attr,
18    contract::Property,
19    helpers::{collect_unsafe_callsites, get_unsafe_callees},
20    path::{PathExtractor, PathStart},
21};
22
23/// A list of parsed `requires` contracts.
24pub type FnContracts<'tcx> = Vec<Property<'tcx>>;
25
26/// A list of parsed struct invariants.
27pub type StructInvariants<'tcx> = Vec<Property<'tcx>>;
28
29/// Collected verification data for a function annotated with `#[rapx::verify]`.
30#[derive(Clone)]
31pub struct FunctionTarget<'tcx> {
32    /// Function marked with `#[rapx::verify]` and selected as a target to verify.
33    pub def_id: DefId,
34    /// Owning struct definition when this target to verify is an associated method.
35    pub owner_struct_def_id: Option<DefId>,
36    /// Parsed `requires` contracts for each unsafe callee reachable from this target.
37    pub callee_requires: HashMap<DefId, FnContracts<'tcx>>,
38}
39
40/// Collected verification data for a struct that owns methods marked with `#[rapx::verify]`.
41pub struct StructTarget<'tcx> {
42    /// Struct that owns one or more methods selected as targets to verify.
43    pub def_id: DefId,
44    /// Parsed `invariant` contracts attached to the struct.
45    pub invariants: StructInvariants<'tcx>,
46    /// Methods of this struct selected as targets to verify.
47    pub function_targets: Vec<FunctionTarget<'tcx>>,
48}
49
50/// Visitor that collects targets annotated with `#[rapx::verify]`.
51pub struct VerifyTargetCollector<'tcx> {
52    tcx: TyCtxt<'tcx>,
53    /// All function targets to verify collected from the current crate.
54    pub function_targets: Vec<FunctionTarget<'tcx>>,
55    /// All struct targets to verify collected from the current crate.
56    pub struct_targets: HashMap<DefId, StructTarget<'tcx>>,
57    /// Cached contracts for each callee function so repeated callees are parsed once.
58    fn_contract_cache: HashMap<DefId, FnContracts<'tcx>>,
59}
60
61impl<'tcx> VerifyTargetCollector<'tcx> {
62    /// Creates a new collector for the current type context.
63    pub fn new(tcx: TyCtxt<'tcx>) -> Self {
64        VerifyTargetCollector {
65            tcx,
66            function_targets: Vec::new(),
67            struct_targets: HashMap::new(),
68            fn_contract_cache: HashMap::new(),
69        }
70    }
71
72    /// Returns whether the given definition belongs to a standard Rust crate.
73    fn is_std_crate_def_id(&self, def_id: DefId) -> bool {
74        matches!(
75            self.tcx.crate_name(def_id.krate).as_str(),
76            "core" | "std" | "alloc"
77        )
78    }
79
80    /// Returns (and caches) the contracts for an unsafe callee.
81    ///
82    /// Contracts are resolved with the following priority:
83    /// 1. Inline RAPx annotations attached to the callee.
84    /// 2. If no annotations are found and the callee belongs to the standard
85    ///    library, fall back to the bundled JSON contract database.
86    ///
87    /// Results are memoized in `fn_contract_cache` to avoid recomputation.
88    fn get_fn_contracts(&mut self, callee_def_id: DefId) -> FnContracts<'tcx> {
89        let is_std = self.is_std_crate_def_id(callee_def_id);
90        self.fn_contract_cache
91            .entry(callee_def_id)
92            .or_insert_with(|| {
93                // Try to collect contracts from inline RAPx annotations first.
94                let mut requires = get_contract_from_annotation(self.tcx, callee_def_id);
95
96                // If no annotation is found and this is a std item,
97                // fall back to the precomputed JSON contracts.
98                if requires.is_empty() && is_std {
99                    requires = get_contract_from_entry(
100                        self.tcx,
101                        callee_def_id,
102                        get_std_contracts_from_assets(self.tcx, callee_def_id),
103                    );
104                }
105
106                requires
107            })
108            // `entry` returns a mutable reference; clone to return an owned value.
109            .clone()
110    }
111
112    /// Checks whether a local function has the exact tool attribute `#[rapx::verify]`.
113    fn has_rapx_verify_attr(&self, def_id: LocalDefId) -> bool {
114        let hir_id = self.tcx.local_def_id_to_hir_id(def_id);
115
116        let rapx = Symbol::intern("rapx");
117        let verify = Symbol::intern("verify");
118
119        let attrs = self.tcx.hir_attrs(hir_id);
120
121        attrs.iter().any(|attr| {
122            if attr.is_doc_comment().is_some() {
123                return false;
124            }
125
126            let path = attr.path();
127
128            path.len() == 2 && path[0] == rapx && path[1] == verify
129        })
130    }
131
132    /// Builds a function target to verify from a function definition.
133    fn build_function_target(&mut self, def_id: DefId) -> FunctionTarget<'tcx> {
134        let unsafe_callees = get_unsafe_callees(self.tcx, def_id);
135        let callee_requires = unsafe_callees
136            .iter()
137            .map(|callee_def_id| (*callee_def_id, self.get_fn_contracts(*callee_def_id)))
138            .collect();
139
140        FunctionTarget {
141            def_id,
142            owner_struct_def_id: self.get_owner_struct_def_id(def_id),
143            callee_requires,
144        }
145    }
146
147    /// Returns the owning struct definition when the function is a method on a struct.
148    fn get_owner_struct_def_id(&self, def_id: DefId) -> Option<DefId> {
149        let assoc_item = self.tcx.opt_associated_item(def_id)?;
150        let impl_id = assoc_item.impl_container(self.tcx)?;
151        let self_ty = self.tcx.type_of(impl_id).skip_binder();
152
153        match self_ty.kind() {
154            TyKind::Adt(adt_def, _) => Some(adt_def.did()),
155            _ => None,
156        }
157    }
158
159    /// Adds a function target and updates its owning struct target when applicable.
160    fn push_function_target(&mut self, function_target: FunctionTarget<'tcx>) {
161        self.function_targets.push(function_target.clone());
162
163        if let Some(struct_def_id) = function_target.owner_struct_def_id {
164            self.struct_targets
165                .entry(struct_def_id)
166                .or_insert_with(|| StructTarget {
167                    def_id: struct_def_id,
168                    invariants: get_struct_invariants_from_annotation(
169                        self.tcx,
170                        struct_def_id,
171                        function_target.def_id,
172                    ),
173                    function_targets: Vec::new(),
174                })
175                .function_targets
176                .push(function_target);
177        }
178    }
179}
180
181impl<'tcx> Visitor<'tcx> for VerifyTargetCollector<'tcx> {
182    type NestedFilter = nested_filter::OnlyBodies;
183
184    fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt {
185        self.tcx
186    }
187
188    /// Visits each function body and records those annotated with `#[rapx::verify]`.
189    ///
190    /// For every function target to verify, this also computes its unsafe callees
191    /// and the safety preconditions required by those callees.
192    fn visit_fn(
193        &mut self,
194        _fk: FnKind<'tcx>,
195        _fd: &'tcx FnDecl<'tcx>,
196        _b: BodyId,
197        _span: Span,
198        id: LocalDefId,
199    ) -> Self::Result {
200        if self.has_rapx_verify_attr(id) {
201            let def_id = id.to_def_id();
202            let function_target = self.build_function_target(def_id);
203            self.push_function_target(function_target);
204        }
205    }
206}
207
208/// Analysis pass that finds all targets annotated with `#[rapx::verify]`.
209pub struct PrepareTargets<'tcx> {
210    tcx: TyCtxt<'tcx>,
211}
212
213impl<'tcx> Analysis for PrepareTargets<'tcx> {
214    fn name(&self) -> &'static str {
215        "Verify Identify Targets Analysis"
216    }
217
218    /// Runs the collection pass and logs targets, struct invariants, unsafe callees, and contracts.
219    fn run(&mut self) {
220        rap_info!("======== #[rapx::verify] identify targets ========");
221        let mut collector = VerifyTargetCollector::new(self.tcx);
222        self.tcx.hir_visit_all_item_likes_in_crate(&mut collector);
223
224        for function_target in collector
225            .function_targets
226            .iter()
227            .filter(|target| target.owner_struct_def_id.is_none())
228        {
229            self.log_function_target(function_target, false);
230        }
231
232        let mut struct_ids: Vec<_> = collector.struct_targets.keys().copied().collect();
233        struct_ids.sort_by_key(|def_id| self.tcx.def_path_str(*def_id));
234
235        for struct_def_id in struct_ids {
236            let Some(struct_target) = collector.struct_targets.get(&struct_def_id) else {
237                continue;
238            };
239            let struct_path = self.tcx.def_path_str(struct_target.def_id);
240            rap_info!(
241                "[rapx::verify::identify-targets] struct target: {} (DefId: {:?})",
242                struct_path,
243                struct_target.def_id
244            );
245
246            if struct_target.invariants.is_empty() {
247                rap_info!("  struct invariants: <none>");
248            } else {
249                for property in &struct_target.invariants {
250                    rap_info!(
251                        "  struct invariant: kind={:?}, args={:?}",
252                        property.kind,
253                        property.args
254                    );
255                }
256            }
257
258            for function_target in &struct_target.function_targets {
259                self.log_function_target(function_target, true);
260            }
261        }
262
263        let total_free_function_targets = collector
264            .function_targets
265            .iter()
266            .filter(|target| target.owner_struct_def_id.is_none())
267            .count();
268        let total_method_targets = collector
269            .function_targets
270            .iter()
271            .filter(|target| target.owner_struct_def_id.is_some())
272            .count();
273        let total_struct_targets = collector.struct_targets.len();
274
275        rap_info!(
276            "total: {} free function target(s) to verify, {} method target(s) to verify, {} struct target(s) to verify",
277            total_free_function_targets,
278            total_method_targets,
279            total_struct_targets
280        );
281        rap_info!("=======================================");
282    }
283
284    fn reset(&mut self) {}
285}
286
287impl<'tcx> PrepareTargets<'tcx> {
288    /// Creates a new analysis instance.
289    pub fn new(tcx: TyCtxt<'tcx>) -> Self {
290        PrepareTargets { tcx }
291    }
292
293    /// Logs one function target and all contracts collected from its unsafe callees.
294    fn log_function_target(&self, target: &FunctionTarget<'tcx>, nested_under_struct: bool) {
295        let target_path = self.tcx.def_path_str(target.def_id);
296        let prefix = if nested_under_struct {
297            "  method target"
298        } else {
299            "[rapx::verify::identify-targets] function target"
300        };
301        rap_info!("{}: {} (DefId: {:?})", prefix, target_path, target.def_id);
302
303        if let Some(struct_def_id) = target.owner_struct_def_id {
304            let struct_path = self.tcx.def_path_str(struct_def_id);
305            rap_info!(
306                "    owner struct: {} (DefId: {:?})",
307                struct_path,
308                struct_def_id
309            );
310        }
311
312        if target.callee_requires.is_empty() {
313            rap_info!("    unsafe callees: <none>");
314            return;
315        }
316
317        let mut unsafe_callee_ids: Vec<_> = target.callee_requires.keys().copied().collect();
318        unsafe_callee_ids.sort_by_key(|def_id| self.tcx.def_path_str(*def_id));
319
320        for unsafe_callee_def_id in unsafe_callee_ids {
321            let unsafe_callee_path = self.tcx.def_path_str(unsafe_callee_def_id);
322            rap_info!(
323                "    unsafe callee: {} (DefId: {:?})",
324                unsafe_callee_path,
325                unsafe_callee_def_id
326            );
327
328            match target.callee_requires.get(&unsafe_callee_def_id) {
329                Some(requires) if !requires.is_empty() => {
330                    for property in requires {
331                        rap_info!(
332                            "      safety contract: kind={:?}, args={:?}",
333                            property.kind,
334                            property.args
335                        );
336                    }
337                }
338                _ => {
339                    rap_info!("      safety contract: <none>");
340                }
341            }
342        }
343
344        self.log_function_paths(target);
345    }
346
347    /// Logs unsafe callsites and loop-aware path skeletons for one target.
348    fn log_function_paths(&self, target: &FunctionTarget<'tcx>) {
349        let callsites = collect_unsafe_callsites(self.tcx, target.def_id);
350        if callsites.is_empty() {
351            rap_info!("    unsafe callsites: <none>");
352            return;
353        }
354
355        let result = PathExtractor::new(self.tcx, target.def_id, callsites).run();
356
357        rap_info!("    detected loop(s): {}", result.loops().len());
358        for loop_info in result.loops() {
359            let body: Vec<_> = loop_info
360                .blocks
361                .iter()
362                .map(|bb| format!("bb{}", bb.as_usize()))
363                .collect();
364            let exits: Vec<_> = loop_info
365                .exits
366                .iter()
367                .map(|exit| format!("bb{}->bb{}", exit.from.as_usize(), exit.to.as_usize()))
368                .collect();
369            rap_info!(
370                "      loop #{}: header=bb{}, body={:?}, exits={:?}",
371                loop_info.id.index(),
372                loop_info.header.as_usize(),
373                body,
374                exits
375            );
376        }
377
378        for (display_index, callsite) in result.callsites().iter().enumerate() {
379            rap_info!(
380                "    unsafe callsite #{}: {} at bb{} ({} arg(s))",
381                display_index,
382                callsite.callee_name(self.tcx),
383                callsite.block.as_usize(),
384                callsite.args.len()
385            );
386
387            let mut callsite_paths: Vec<_> = result.paths_for(callsite.location()).iter().collect();
388            callsite_paths.sort_by_key(|path| path.describe());
389
390            if callsite_paths.is_empty() {
391                rap_info!("      paths: <none>");
392                continue;
393            }
394
395            for (path_idx, path) in callsite_paths.iter().enumerate() {
396                let kind = match path.start {
397                    PathStart::FunctionEntry => "entry",
398                    PathStart::LoopHeader { loop_id } => {
399                        rap_info!(
400                            "      path {} kind: loop-header(loop #{})",
401                            path_idx,
402                            loop_id.index()
403                        );
404                        rap_info!("      path {}: {}", path_idx, path.describe());
405                        continue;
406                    }
407                };
408                rap_info!("      path {} kind: {}", path_idx, kind);
409                rap_info!("      path {}: {}", path_idx, path.describe());
410            }
411        }
412    }
413}
414
415/// Builds contracts from backup JSON entries.
416///
417/// Each entry stores property-expression arguments that are passed directly into
418/// `Property::new`. The target information is resolved by `Property` itself
419/// from those arguments.
420fn get_contract_from_entry<'tcx>(
421    tcx: TyCtxt<'tcx>,
422    def_id: DefId,
423    contract_entries: &[PropertyEntry],
424) -> FnContracts<'tcx> {
425    let mut results = Vec::new();
426    for entry in contract_entries {
427        if entry.args.is_empty() {
428            continue;
429        }
430
431        let mut exprs: Vec<Expr> = Vec::new();
432        for arg_str in &entry.args {
433            match syn::parse_str::<Expr>(arg_str) {
434                Ok(expr) => exprs.push(expr),
435                Err(_) => {
436                    rap_error!(
437                        "JSON Contract Error: Failed to parse arg '{}' as Rust Expr for tag {}",
438                        arg_str,
439                        entry.tag
440                    );
441                }
442            }
443        }
444
445        if exprs.len() != entry.args.len() {
446            rap_error!(
447                "Parse std API args error: Failed to parse arg '{:?}'",
448                entry.args
449            );
450            continue;
451        }
452
453        let property = Property::new(tcx, def_id, entry.tag.as_str(), &exprs);
454        results.push(property);
455    }
456    results
457}
458
459/// Returns whether an attribute is exactly `#[rapx::requires(...)]`.
460fn is_rapx_requires_attr(attr: &Attribute) -> bool {
461    matches!(
462        attr,
463        Attribute::Unparsed(tool_attr)
464            if tool_attr.path.segments.len() == 2
465                && tool_attr.path.segments[0].as_str() == "rapx"
466                && tool_attr.path.segments[1].as_str() == "requires"
467    )
468}
469
470/// Collects properties from `#[rapx::requires(...)]` attributes.
471fn collect_properties_from_requires_attrs<'tcx>(
472    tcx: TyCtxt<'tcx>,
473    attrs: impl IntoIterator<Item = &'tcx Attribute>,
474    property_def_id: DefId,
475    parse_error_label: &str,
476) -> Vec<Property<'tcx>> {
477    let mut results = Vec::new();
478
479    for attr in attrs {
480        if !is_rapx_requires_attr(attr) {
481            continue;
482        }
483
484        let attr_str = rustc_hir_pretty::attribute_to_string(&tcx, attr);
485        let parsed = match parse_rapx_attr(attr_str.as_str(), "requires") {
486            Ok(parsed) => parsed,
487            Err(err) => {
488                rap_error!(
489                    "Failed to parse RAPx {} attr '{}': {}",
490                    parse_error_label,
491                    attr_str,
492                    err
493                );
494                continue;
495            }
496        };
497
498        results.extend(parsed.properties.into_iter().map(|property| {
499            Property::new(tcx, property_def_id, property.tag.as_str(), &property.args)
500        }));
501    }
502
503    results
504}
505
506/// Parses `requires` contracts from source-level RAPx annotations attached to a definition.
507fn get_contract_from_annotation<'tcx>(tcx: TyCtxt<'tcx>, def_id: DefId) -> FnContracts<'tcx> {
508    collect_properties_from_requires_attrs(tcx, tcx.get_all_attrs(def_id), def_id, "requires")
509}
510
511/// Parses struct invariants from source-level RAPx annotations attached to a struct definition.
512fn get_struct_invariants_from_annotation<'tcx>(
513    tcx: TyCtxt<'tcx>,
514    struct_def_id: DefId,
515    context_def_id: DefId,
516) -> StructInvariants<'tcx> {
517    let Some(local_def_id) = struct_def_id.as_local() else {
518        return Vec::new();
519    };
520
521    let item = tcx.hir_expect_item(local_def_id);
522    if !matches!(item.kind, ItemKind::Struct(..)) {
523        return Vec::new();
524    }
525
526    collect_properties_from_requires_attrs(
527        tcx,
528        tcx.get_all_attrs(struct_def_id),
529        context_def_id,
530        "invariant",
531    )
532}