Skip to main content

rapx/verify/contract/
compound.rs

1//! User-defined compound-contract layer.
2//!
3//! Users (downloading a prebuilt `rapx` binary) can define *new* named safety
4//! contracts as boolean combinations of the primitive safety properties, and
5//! reference them from `#[rapx::requires(MyTag(...))]` — without recompiling
6//! `rapx`.
7//!
8//! A compound property is a DNF macro over primitive property calls:
9//!
10//! ```text
11//! MySafeRead(p: Ptr, T: Ty, n: Expr) {
12//!     NonNull(p) && Align(p, T) && Allocated(p, T, n)
13//! }
14//!
15//! StrOrBytes(s: Ptr, T: Ty, n: Expr) {
16//!     ValidCStr(s, n) || (Allocated(s, T, n) && Init(s, T, n))
17//! }
18//! ```
19//!
20//! The DSL only *composes* existing primitives; it cannot invent new primitive
21//! semantics (those live in `property_checker.rs`).  Expansion is a pure
22//! front-end that produces ordinary `Property` values consumed by the existing
23//! checker.
24
25use std::collections::{HashMap, HashSet};
26use std::sync::{OnceLock, RwLock};
27
28use rustc_hir::def_id::{CrateNum, LOCAL_CRATE};
29#[cfg(rapx_has_attr_ir)]
30use rustc_attr_ir::Attribute;
31#[cfg(not(rapx_has_attr_ir))]
32use rustc_hir::Attribute;
33use syn::Expr;
34use syn::visit_mut::{self, VisitMut};
35
36use super::types::{AtomProperty, ContractExpr, ContractKind, Property, PropertyArg, PropertyKind};
37
38/// A single argument in a compound body: a reference to a formal parameter, or a
39/// literal (kept as source text, re-parsed as `syn::Expr` at expansion time).
40#[derive(Debug, Clone, PartialEq, Eq, Hash)]
41pub(crate) enum CompoundArg {
42    Param(usize),
43    Lit(String),
44}
45
46/// The body of a compound property: a boolean tree over primitive-property
47/// calls (`Call` leaves), freely nested via `And`/`Or`.  The DSL `&&`/`||`
48/// grammar permits arbitrary nesting (an `And` may contain an `Or` and vice
49/// versa), so this is *not* constrained to DNF; `expand_compound_body`
50/// recurses over whatever shape was parsed.
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub(crate) enum CompoundBody {
53    And(Vec<CompoundBody>),
54    Or(Vec<CompoundBody>),
55    Call { tag: String, args: Vec<CompoundArg> },
56}
57
58/// A parsed compound-property declaration.
59#[derive(Debug, Clone)]
60pub(crate) struct CompoundSpec {
61    pub name: String,
62    pub params: Vec<String>,
63    pub param_tys: Vec<String>,
64    pub body: CompoundBody,
65    pub doc: Vec<String>,
66}
67
68/// Parse a source fragment containing block-shaped contract definitions
69/// (`Name(params) { body }`) into a list of `CompoundSpec`s.
70///
71/// This is the format produced by the `pred!` macro and used by the bundled
72/// `assets/*-compound-properties.rs` files:
73///
74/// ```text
75/// MySafeRead(p: Ptr, T: Ty, n: Expr) { NonNull(p) && Align(p, T) && Allocated(p, T, n) }
76/// ```
77///
78/// Each compound may be preceded by `///` doc lines (shown as the human-readable
79/// meaning in reports); `//` comments and blank lines are skipped.  The body
80/// supports `&&` (conjunction) and `||` (disjunction) of `Tag(arg, ...)` calls,
81/// plus `( ... )` grouping for a conjunction used as a single disjunct.
82pub(crate) fn parse_compounds(source: &str) -> Vec<CompoundSpec> {
83    let mut compounds = Vec::new();
84    let mut doc: Vec<String> = Vec::new();
85
86    let mut s = source.trim_start();
87    loop {
88        // Skip blank lines and `//` comments; collect `///` doc lines.
89        loop {
90            if let Some(r) = s.strip_prefix("///") {
91                let end = r.find('\n').unwrap_or(r.len());
92                doc.push(r[..end].trim().to_string());
93                s = r[end..].trim_start();
94            } else if let Some(r) = s.strip_prefix("//") {
95                let end = r.find('\n').unwrap_or(r.len());
96                s = r[end..].trim_start();
97            } else {
98                break;
99            }
100        }
101        if s.is_empty() {
102            break;
103        }
104
105        let (Some(mut compound), consumed) = parse_one_compound_block(s) else {
106            let preview: String = s.chars().take(80).collect();
107            rap_error!("failed to parse contract compound near: {preview}");
108            break;
109        };
110        compound.doc = std::mem::take(&mut doc);
111        compounds.push(compound);
112        s = s[consumed..].trim_start();
113    }
114
115    compounds
116}
117
118/// Parse a single leading `Name(p: Ptr, T: Ty, ...) { body }` block from `s`.
119/// Returns the `CompoundSpec` (without doc) and the number of bytes consumed through
120/// the closing `}`.
121///
122/// The block is emitted by the `pred!` proc-macro as a single line of
123/// space-separated tokens, so parameter names and role annotations (`Ptr`,
124/// `Ty`, `Expr`, `Ident`) are split on `:` and `,` after trimming whitespace.
125/// The braces delimit the body, so nested `==`/`<=` inside a `ValidNum`
126/// predicate cannot be confused with a definition separator.
127fn parse_one_compound_block(s: &str) -> (Option<CompoundSpec>, usize) {
128    let Some(open) = s.find('(') else {
129        return (None, 0);
130    };
131    let name = s[..open].trim().to_string();
132    if name.is_empty() {
133        return (None, 0);
134    }
135
136    // Params: match the first `)` — parameter annotations are `ident: ident`,
137    // so they never contain nested parens.
138    let Some(rel_close) = s[open + 1..].find(')') else {
139        return (None, 0);
140    };
141    let close = open + 1 + rel_close;
142    let params_str = &s[open + 1..close];
143
144    // Expect a `{` after the parameter list.
145    let after = s[close + 1..].trim_start();
146    if !after.starts_with('{') {
147        return (None, 0);
148    }
149    let brace_open = s.len() - after.len();
150    let body_start = brace_open + 1;
151
152    // Match braces to the closing `}` (the body may contain `if { } else { }`).
153    let bytes = s.as_bytes();
154    let mut depth = 1usize;
155    let mut j = body_start;
156    while j < bytes.len() {
157        match bytes[j] {
158            b'{' => depth += 1,
159            b'}' => {
160                depth -= 1;
161                if depth == 0 {
162                    break;
163                }
164            }
165            _ => {}
166        }
167        j += 1;
168    }
169    if depth != 0 {
170        return (None, 0);
171    }
172    let body = s[body_start..j].trim();
173
174    let (params, param_tys) = parse_equation_params(params_str);
175    let Some(body_ast) = super::pest_conv::parse_compound_body(body, &params) else {
176        return (None, 0);
177    };
178
179    let compound = CompoundSpec {
180        name,
181        params,
182        param_tys,
183        body: body_ast,
184        doc: Vec::new(),
185    };
186    (Some(compound), j + 1)
187}
188
189fn parse_equation_params(params_str: &str) -> (Vec<String>, Vec<String>) {
190    let mut params = Vec::new();
191    let mut param_tys = Vec::new();
192    for seg in params_str.split(',') {
193        let seg = seg.trim();
194        if seg.is_empty() {
195            continue;
196        }
197        match seg.split_once(':') {
198            Some((p, ty)) => {
199                params.push(p.trim().to_string());
200                param_tys.push(ty.trim().to_string());
201            }
202            None => {
203                params.push(seg.to_string());
204                param_tys.push(String::new());
205            }
206        }
207    }
208    (params, param_tys)
209}
210
211/// Render a `syn::Expr` back to source-like text.  `proc_macro2` stringifies
212/// tokens space-separated (`self . 0`, `size_of (T)`), so collapse the spaces
213/// around punctuation for a readable form.
214fn render_expr_src(e: &Expr) -> String {
215    quote::ToTokens::to_token_stream(e)
216        .to_string()
217        .replace(" . ", ".")
218        .replace(" ,", ",")
219        .replace(" (", "(")
220        .replace(" :: ", "::")
221}
222
223/// Resolve a call-site argument expression to its display form, following the
224/// compound parameter's declared role so internal placeholders (e.g. `Arg_0` from a
225/// JSON contract) render as the actual parameter name.
226fn resolve_arg_string<'tcx>(
227    tcx: rustc_middle::ty::TyCtxt<'tcx>,
228    def_id: rustc_hir::def_id::DefId,
229    param_ty: &str,
230    expr: &Expr,
231) -> String {
232    match param_ty {
233        "Ptr" => {
234            let arg = super::resolve::parse_target_arg(tcx, def_id, expr);
235            // For a resolved field place, show the field name (e.g. `ptr` in
236            // `self.ptr`) rather than the internal `arg1.0` form.
237            if let PropertyArg::Expr(ContractExpr::Place(place)) = &arg
238                && let Some(name) = super::place::field_name_from_place(tcx, def_id, place)
239            {
240                return name;
241            }
242            arg.display_for_report(tcx, None, Some(def_id))
243        }
244        "Ty" => super::resolve::parse_type(tcx, def_id, expr, "compound")
245            .map(|ty| ty.to_string())
246            .unwrap_or_else(|| render_expr_src(expr)),
247        "Expr" => {
248            let ce = super::resolve::expr_to_pest(tcx, def_id, expr);
249            super::render::display_expr_user_friendly(&ce, tcx, None, Some(def_id))
250        }
251        _ => render_expr_src(expr),
252    }
253}
254
255/// Substitute compound formal parameters with the concrete call-site arguments inside
256/// a literal expression (e.g. turn `size_of(T) * n` into `size_of(u32) * len`,
257/// or `p.unwrap_some()` into `head.unwrap_some()`).
258///
259/// Only a bare single-segment path that exactly matches a formal parameter name
260/// is replaced, so builtin function names (`size_of`), method names
261/// (`unwrap_some`) and field names are never rewritten.
262struct Subst<'a> {
263    params: &'a [String],
264    args: &'a [Expr],
265}
266
267impl VisitMut for Subst<'_> {
268    fn visit_expr_mut(&mut self, node: &mut Expr) {
269        if let Expr::Path(path) = node {
270            if path.qself.is_none()
271                && path.path.leading_colon.is_none()
272                && path.path.segments.len() == 1
273            {
274                let ident = path.path.segments[0].ident.to_string();
275                if let Some(i) = self.params.iter().position(|n| *n == ident) {
276                    if let Some(arg) = self.args.get(i) {
277                        // The substituted argument comes from the call site and
278                        // never refers to this compound's formals, so stop recursing.
279                        *node = arg.clone();
280                        return;
281                    }
282                }
283            }
284        }
285        visit_mut::visit_expr_mut(self, node);
286    }
287}
288
289/// Whether a compound parameter annotation matches a primitive argument role.
290fn compound_param_ty_matches_arg_kind(def_ty: &str, kind: super::spec::ArgKind) -> bool {
291    use super::spec::ArgKind;
292    matches!(
293        (def_ty, kind),
294        ("Ptr", ArgKind::Target)
295            | ("Ty", ArgKind::Ty)
296            | ("Expr", ArgKind::Expr)
297            | ("Ident", ArgKind::Ident)
298    )
299}
300
301/// Expand a `CompoundBody` into the property list it denotes.
302///
303/// `and` produces multiple `Property` values (the caller's `requires` list is
304/// already a conjunction); `or` produces a single `Property::Or` property
305/// whose `groups` encode the DNF groups.
306fn expand_compound_body<'tcx>(
307    tcx: rustc_middle::ty::TyCtxt<'tcx>,
308    def_id: rustc_hir::def_id::DefId,
309    body: &CompoundBody,
310    exprs: &[Expr],
311    params: &[String],
312    param_tys: &[String],
313) -> Vec<Property<'tcx>> {
314    match body {
315        CompoundBody::And(parts) => parts
316            .iter()
317            .flat_map(|p| expand_compound_body(tcx, def_id, p, exprs, params, param_tys))
318            .collect(),
319        CompoundBody::Or(parts) => {
320            let mut disjuncts: Vec<Property<'tcx>> = Vec::new();
321            for part in parts {
322                let conjuncts = expand_compound_body(tcx, def_id, part, exprs, params, param_tys);
323                if !conjuncts.is_empty() {
324                    disjuncts.push(Property::conjunction(conjuncts));
325                }
326            }
327            vec![Property::new_or(disjuncts)]
328        }
329        CompoundBody::Call { tag, args } => {
330            // Validate the compound's parameter annotations against the primitive's
331            // declared argument roles (e.g. a `Ptr` param used in a `Ty` slot).
332            if let Some(spec) = super::spec::find_spec(tag) {
333                match spec.build {
334                    // Variadic target list (Alias/Alive): every param must be Ptr.
335                    super::spec::BuildKind::Targets => {
336                        for (pos, a) in args.iter().enumerate() {
337                            if let CompoundArg::Param(i) = a
338                                && let Some(def_ty) = param_tys.get(*i)
339                                && !compound_param_ty_matches_arg_kind(
340                                    def_ty,
341                                    super::spec::ArgKind::Target,
342                                )
343                            {
344                                let pname = params.get(*i).map(String::as_str).unwrap_or("?");
345                                rap_warn!(
346                                    "contract compound type mismatch: `{tag}` arg {pos} expects \
347                                     {:?}, but param `{pname}` is annotated `{def_ty}`",
348                                    super::spec::ArgKind::Target
349                                );
350                            }
351                        }
352                    }
353                    // Accepts-anything placeholder: no constraints.
354                    super::spec::BuildKind::TobeSpecified => {}
355                    // Fixed-arity tag: match a form by call arity, then check each
356                    // positional parameter annotation against the declared role.
357                    _ => {
358                        if let Some(form) = spec.forms.iter().find(|f| f.len() == args.len()) {
359                            for (pos, a) in args.iter().enumerate() {
360                                if let CompoundArg::Param(i) = a
361                                    && let (Some(def_ty), Some(&arg_kind)) =
362                                        (param_tys.get(*i), form.get(pos))
363                                    && !compound_param_ty_matches_arg_kind(def_ty, arg_kind)
364                                {
365                                    let pname = params.get(*i).map(String::as_str).unwrap_or("?");
366                                    rap_warn!(
367                                        "contract compound type mismatch: `{tag}` arg {pos} expects \
368                                         {:?}, but param `{pname}` is annotated `{def_ty}`",
369                                        arg_kind
370                                    );
371                                }
372                            }
373                        }
374                    }
375                }
376            }
377
378            let mut resolved: Vec<Expr> = Vec::with_capacity(args.len());
379            for a in args {
380                match a {
381                    CompoundArg::Param(i) => {
382                        let Some(e) = exprs.get(*i) else {
383                            return vec![unknown_property()];
384                        };
385                        resolved.push(e.clone());
386                    }
387                    CompoundArg::Lit(s) => {
388                        let Ok(mut e) = syn::parse_str::<Expr>(s) else {
389                            return vec![unknown_property()];
390                        };
391                        Subst {
392                            params,
393                            args: exprs,
394                        }
395                        .visit_expr_mut(&mut e);
396                        resolved.push(e);
397                    }
398                }
399            }
400            // Recurse through the normal property parser so nested compounds and
401            // primitives are handled uniformly.
402            Property::parse_list(tcx, def_id, tag, &resolved)
403        }
404    }
405}
406
407fn unknown_property<'tcx>() -> Property<'tcx> {
408    Property::new_atom(PropertyKind::Unknown, Vec::new())
409}
410
411// ── Subsumption (one-way weakening) ───────────────────────────
412
413/// Cached builtin subsumption map (see [`subsumption_closure`]).
414fn builtin_subsumptions_map() -> &'static HashMap<String, CompoundSpec> {
415    static BUILTIN: OnceLock<HashMap<String, CompoundSpec>> = OnceLock::new();
416    BUILTIN.get_or_init(|| {
417        parse_compounds(include_str!("assets/std-subsumption.rs"))
418            .into_iter()
419            .map(|c| (c.name.clone(), c))
420            .collect()
421    })
422}
423
424/// Apply the builtin subsumption rules to an asserted fact: a stronger
425/// primitive also asserts its weaker consequences — a one-way weakening, unlike
426/// the `≡` compound equivalences above.  The rules are declared declaratively
427/// in `std-subsumption.rs` with the same `Name(params) { body }` syntax; the
428/// head is an existing primitive tag and the body a pure conjunction of weaker
429/// primitive calls whose parameters map positionally to the head's arguments
430/// (e.g. `Init(p, T, n) ⇒ Typed(p, T)`).  Pointer-validity primitives
431/// (`NonNull`/`Allocated`/`InBound`) are deliberately *not* implied: they are
432/// orthogonal requirements stated explicitly by contracts.
433///
434/// Returns the *transitive closure* of the subsumption relation applied to
435/// `atom`, deduplicated: each weaker consequence appears exactly once.
436pub(crate) fn subsumption_closure<'tcx>(atom: &AtomProperty<'tcx>) -> Vec<AtomProperty<'tcx>> {
437    let Some(head_tag) = super::spec::tag_name_for_kind(atom.kind) else {
438        return Vec::new();
439    };
440
441    // BFS over the subsumption graph, tracking `(tag, args)` pairs at the
442    // declared level, where `args` stay positionally relative to the original
443    // `atom` (substitution keeps them as `Param(i)`/`Lit`).  `CompoundArg` is
444    // `Eq + Hash`, so the pair is a valid dedup key.
445    let mut out: Vec<AtomProperty<'tcx>> = Vec::new();
446    let mut seen: HashSet<(String, Vec<CompoundArg>)> = HashSet::new();
447    let mut queue: Vec<(String, Vec<CompoundArg>)> = Vec::new();
448    if let Some(spec) = builtin_subsumptions_map().get(head_tag) {
449        queue.extend(flatten_subsumption_body(&spec.body));
450    }
451    let mut cursor = 0;
452    while cursor < queue.len() {
453        let (tag, args) = queue[cursor].clone();
454        cursor += 1;
455        if !seen.insert((tag.clone(), args.clone())) {
456            continue;
457        }
458        if let Some(kind) = super::spec::find_spec(&tag).map(|s| s.kind) {
459            if let Some(resolved) = resolve_subsumption_args(&args, atom) {
460                out.push(AtomProperty {
461                    kind,
462                    args: resolved,
463                    contract_kind: ContractKind::Precond,
464                    for_each: None,
465                    origin: None,
466                });
467            }
468        }
469        // Enqueue this tag's own consequences, substituting its params with the
470        // actual `args` passed to it (kept relative to `atom`).
471        if let Some(spec) = builtin_subsumptions_map().get(&tag) {
472            for (child_tag, child_args) in flatten_subsumption_body(&spec.body) {
473                let substituted: Vec<CompoundArg> = child_args
474                    .into_iter()
475                    .map(|a| match a {
476                        CompoundArg::Param(j) => args
477                            .get(j)
478                            .cloned()
479                            .unwrap_or(CompoundArg::Lit(String::new())),
480                        lit @ CompoundArg::Lit(_) => lit,
481                    })
482                    .collect();
483                queue.push((child_tag, substituted));
484            }
485        }
486    }
487    out
488}
489
490/// Resolve a subsumption body's `(tag, args)` — whose `args` are `Param(i)`
491/// indices into `atom`'s own arguments — into concrete `PropertyArg`s.
492fn resolve_subsumption_args<'tcx>(
493    args: &[CompoundArg],
494    atom: &AtomProperty<'tcx>,
495) -> Option<Vec<PropertyArg<'tcx>>> {
496    let mut resolved: Vec<PropertyArg<'tcx>> = Vec::with_capacity(args.len());
497    for a in args {
498        match a {
499            CompoundArg::Param(i) => match atom.args.get(*i) {
500                Some(pa) => resolved.push(pa.clone()),
501                None => return None,
502            },
503            CompoundArg::Lit(s) => {
504                rap_warn!("subsumption body literal `{s}` unsupported; skipping");
505                return None;
506            }
507        }
508    }
509    Some(resolved)
510}
511
512/// Flatten a subsumption body (a pure conjunction of primitive calls) into a
513/// list of `(tag, args)` pairs.
514fn flatten_subsumption_body(body: &CompoundBody) -> Vec<(String, Vec<CompoundArg>)> {
515    match body {
516        CompoundBody::And(parts) => parts.iter().flat_map(flatten_subsumption_body).collect(),
517        CompoundBody::Call { tag, args } => vec![(tag.clone(), args.clone())],
518        CompoundBody::Or(_) => {
519            rap_warn!("subsumption body must be a conjunction (no `||`); ignoring disjunction");
520            Vec::new()
521        }
522    }
523}
524
525// ── Registry ───────────────────────────────────────────────────
526
527/// Cached, shared builtin compound map (see [`builtin_compounds`]).
528fn builtin_compounds_map() -> &'static HashMap<String, CompoundSpec> {
529    static BUILTIN: OnceLock<HashMap<String, CompoundSpec>> = OnceLock::new();
530    BUILTIN.get_or_init(builtin_compounds)
531}
532
533/// Per-crate user compounds, registered from `#[rapx::def_property]` attributes.
534///
535/// Keyed by `CrateNum` so that compounds defined in one crate cannot leak into (or
536/// collide with) another crate analyzed in the same process.  A crate's own
537/// compounds shadow builtin compounds of the same name.
538fn user_compounds_map() -> &'static RwLock<HashMap<CrateNum, HashMap<String, CompoundSpec>>> {
539    static USER: OnceLock<RwLock<HashMap<CrateNum, HashMap<String, CompoundSpec>>>> =
540        OnceLock::new();
541    USER.get_or_init(|| RwLock::new(HashMap::new()))
542}
543
544/// Builtin compounds shipped with `rapx`: the standard compound safety properties
545/// (`std-compound-properties.rs`) plus user extensions
546/// (`user-compound-properties.rs`).
547fn builtin_compounds() -> HashMap<String, CompoundSpec> {
548    let mut map = HashMap::new();
549    for compound in parse_compounds(include_str!("assets/std-compound-properties.rs")) {
550        map.insert(compound.name.clone(), compound);
551    }
552    for compound in parse_compounds(include_str!("assets/user-compound-properties.rs")) {
553        map.insert(compound.name.clone(), compound);
554    }
555    map
556}
557
558/// Look up a compound property by name, in the given crate's namespace first,
559/// then the builtin namespace.
560pub(crate) fn find_compound(krate: CrateNum, name: &str) -> Option<CompoundSpec> {
561    if let Some(d) = user_compounds_map()
562        .read()
563        .ok()
564        .and_then(|t| t.get(&krate).and_then(|m| m.get(name).cloned()))
565    {
566        return Some(d);
567    }
568    builtin_compounds_map().get(name).cloned()
569}
570
571/// Expand a named compound against concrete argument expressions.
572pub(crate) fn expand_compound<'tcx>(
573    tcx: rustc_middle::ty::TyCtxt<'tcx>,
574    def_id: rustc_hir::def_id::DefId,
575    name: &str,
576    exprs: &[Expr],
577) -> Option<Vec<Property<'tcx>>> {
578    let compound = find_compound(def_id.krate, name)?;
579    if compound.params.len() != exprs.len() {
580        rap_warn!(
581            "contract compound `{name}` expects {} argument(s), got {}",
582            compound.params.len(),
583            exprs.len()
584        );
585        return None;
586    }
587    // Guard against self-referential compounds (direct or mutual) before recursing;
588    // otherwise `expand_compound_body` → `Property::parse_list` → `expand_compound` would
589    // recurse forever and overflow the stack.
590    if let Some(cycle) = find_compound_cycle(def_id.krate, name) {
591        rap_error!("contract compound cycle detected: {}", cycle.join(" -> "));
592        return None;
593    }
594    let mut props = expand_compound_body(
595        tcx,
596        def_id,
597        &compound.body,
598        exprs,
599        &compound.params,
600        &compound.param_tys,
601    );
602    // Tag expanded properties with the compound name, its full call-site arguments,
603    // and its doc-derived meaning so reports can show the compound as a single
604    // entry instead of the underlying primitives.
605    let arg_strings: Vec<String> = exprs
606        .iter()
607        .enumerate()
608        .map(|(i, e)| {
609            let param_ty = compound.param_tys.get(i).map(|s| s.as_str()).unwrap_or("");
610            resolve_arg_string(tcx, def_id, param_ty, e)
611        })
612        .collect();
613    let meaning = if compound.doc.is_empty() {
614        None
615    } else {
616        Some(compound.doc.join(" "))
617    };
618    for p in &mut props {
619        p.set_origin(name.to_string(), arg_strings.clone(), meaning.clone());
620    }
621    Some(props)
622}
623
624/// Return a cycle path (e.g. `["A", "B", "A"]`) if expanding `start` can reach
625/// itself again through compound-to-compound references, `None` otherwise.
626///
627/// Only edges that resolve to another registered compound are followed; calls to
628/// primitives (`Allocated`, `Align`, ...) terminate the walk.  The crate's own
629/// compounds are overlaid on the builtin namespace.
630pub(crate) fn find_compound_cycle(krate: CrateNum, start: &str) -> Option<Vec<String>> {
631    let mut combined = builtin_compounds_map().clone();
632    if let Ok(user) = user_compounds_map().read()
633        && let Some(crate_defs) = user.get(&krate)
634    {
635        for (name, compound) in crate_defs {
636            combined.insert(name.clone(), compound.clone());
637        }
638    }
639    find_cycle_in(start, &combined)
640}
641
642fn find_cycle_in(start: &str, table: &HashMap<String, CompoundSpec>) -> Option<Vec<String>> {
643    fn dfs(
644        name: &str,
645        table: &HashMap<String, CompoundSpec>,
646        path: &mut Vec<String>,
647        done: &mut HashSet<String>,
648    ) -> Option<Vec<String>> {
649        if let Some(pos) = path.iter().position(|n| n == name) {
650            let mut cycle: Vec<String> = path[pos..].to_vec();
651            cycle.push(name.to_string());
652            return Some(cycle);
653        }
654        if done.contains(name) {
655            return None;
656        }
657        let Some(compound) = table.get(name) else {
658            return None;
659        };
660        path.push(name.to_string());
661        for tag in compound_refs(&compound.body) {
662            if let Some(cycle) = dfs(&tag, table, path, done) {
663                return Some(cycle);
664            }
665        }
666        path.pop();
667        done.insert(name.to_string());
668        None
669    }
670
671    let mut path = Vec::new();
672    let mut done = HashSet::new();
673    dfs(start, table, &mut path, &mut done)
674}
675
676/// Collect the tag names referenced by a `CompoundBody`, in left-to-right order.
677fn compound_refs(body: &CompoundBody) -> Vec<String> {
678    let mut out = Vec::new();
679    collect_compound_refs(body, &mut out);
680    out
681}
682
683fn collect_compound_refs(body: &CompoundBody, out: &mut Vec<String>) {
684    match body {
685        CompoundBody::And(parts) | CompoundBody::Or(parts) => {
686            for part in parts {
687                collect_compound_refs(part, out);
688            }
689        }
690        CompoundBody::Call { tag, .. } => out.push(tag.clone()),
691    }
692}
693
694// ── Registration of user-defined compounds ─────────────────────────
695
696/// Parse compound-property declarations from `source` and insert them into the
697/// given crate's namespace.  Returns the number of compounds registered.
698pub(crate) fn register_compounds_from_source(krate: CrateNum, source: &str) -> usize {
699    let compounds = parse_compounds(source);
700    let n = compounds.len();
701    if n == 0 {
702        return 0;
703    }
704    let mut table = user_compounds_map()
705        .write()
706        .expect("compound table poisoned");
707    let entry = table.entry(krate).or_default();
708    for compound in compounds {
709        entry.insert(compound.name.clone(), compound);
710    }
711    n
712}
713
714// ── Procedural-macro contract definitions ─────────────────────
715
716/// Scan the local crate for `#[rapx::def_property("...")]` tool attributes
717/// (emitted by the `rapx_macros::pred` proc-macro) and register each embedded
718/// compound-property string.  Returns the number of compounds registered.
719pub(crate) fn register_compound_properties(tcx: rustc_middle::ty::TyCtxt<'_>) -> usize {
720    struct Visitor<'tcx> {
721        tcx: rustc_middle::ty::TyCtxt<'tcx>,
722        count: usize,
723    }
724
725    impl<'tcx> rustc_hir::intravisit::Visitor<'tcx> for Visitor<'tcx> {
726        fn visit_item(&mut self, item: &'tcx rustc_hir::Item<'tcx>) {
727            let attrs = self.tcx.hir_attrs(item.hir_id());
728            for attr in attrs {
729                if !is_def_property_attr(attr) {
730                    continue;
731                }
732                let attr_str = crate::compat::attribute_to_string(self.tcx, attr);
733                if let Some(def_str) = extract_def_property_string(&attr_str) {
734                    let n = register_compounds_from_source(LOCAL_CRATE, &def_str);
735                    if n > 0 {
736                        rap_info!(
737                            "rapx: registered {n} contract compound(s) from #[rapx::def_property]"
738                        );
739                    }
740                    self.count += n;
741                }
742            }
743            rustc_hir::intravisit::walk_item(self, item);
744        }
745    }
746
747    let mut v = Visitor { tcx, count: 0 };
748    tcx.hir_visit_all_item_likes_in_crate(&mut v);
749    v.count
750}
751
752/// Whether an attribute path is `rapx::def_property` (or the bare form with the
753/// tool prefix stripped).
754fn is_def_property_attr(attr: &Attribute) -> bool {
755    let path = attr.path();
756    if path.len() >= 2
757        && path[path.len() - 2].as_str() == "rapx"
758        && path[path.len() - 1].as_str() == "def_property"
759    {
760        return true;
761    }
762    path.len() == 1 && path[0].as_str() == "def_property"
763}
764
765/// Extract the string literal from a `#[rapx::def_property("compound ...")]`
766/// attribute's textual representation.
767fn extract_def_property_string(attr_str: &str) -> Option<String> {
768    struct OneAttr {
769        attr: syn::Attribute,
770    }
771    impl syn::parse::Parse for OneAttr {
772        fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
773            let attrs = syn::Attribute::parse_outer(input)?;
774            let attr = attrs
775                .into_iter()
776                .next()
777                .ok_or_else(|| input.error("expected one attribute"))?;
778            Ok(OneAttr { attr })
779        }
780    }
781
782    let one: OneAttr = syn::parse_str(attr_str).ok()?;
783    let syn::Meta::List(list) = one.attr.meta else {
784        return None;
785    };
786    let lit: syn::LitStr = syn::parse2(list.tokens).ok()?;
787    Some(lit.value())
788}