Skip to main content

rapx/verify/contract/
json.rs

1//! The bundled JSON contract front-end: loading, lookup, and conversion.
2//!
3//! Three embedded JSON assets provide out-of-the-box contracts: std function
4//! `requires` contracts (`std-api-requires.json`), std type invariants
5//! (`std-type-invariants.json`), and std auto-trait `ensures` obligations
6//! (`std-trait-ensures.json`).  Lookup uses exact, path-stripped, and wildcard
7//! fallback so trait-method impls and re-exported paths resolve correctly.
8//! Entries are then converted into [`Property`] values via [`entry_to_property`].
9
10use rustc_hir::def_id::DefId;
11use rustc_middle::ty::TyCtxt;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::sync::OnceLock;
15use syn::Expr;
16
17use crate::helpers::name::public_def_path;
18
19use super::types::{Property, PropertyKind};
20
21/// Structure of JSON entries.
22///
23/// When the `any` field is present, the entry represents a disjunction (logical
24/// OR) of property groups.  Each element in `any` is either a single
25/// [`JsonProperty`] (one disjunct) or an array of entries (a conjunction group —
26/// all must hold).
27///
28/// JSON format for `any` (flat OR):
29/// ```json
30/// {
31///   "any": [
32///     {"tag": "Trait", "args": ["T", "Copy"]},
33///     {"tag": "Alias", "args": ["T", "return"]}
34///   ]
35/// }
36/// ```
37///
38/// JSON format for `any` with conjunction group (null-guard):
39/// ```json
40/// {
41///   "any": [
42///     {"tag": "Null", "args": ["head"]},
43///     [
44///       {"tag": "Align", "args": ["head", "Node"]},
45///       {"tag": "ValidPtr", "args": ["head", "Node", "1"]}
46///     ]
47///   ]
48/// }
49/// ```
50#[derive(Debug, Serialize, Deserialize, Clone)]
51pub(crate) struct JsonProperty {
52    #[serde(default)]
53    pub tag: String,
54    #[serde(default)]
55    pub args: Vec<String>,
56    #[serde(default)]
57    pub kind: Option<String>,
58    /// The list of disjuncts (OR alternatives) when this entry is a disjunction.
59    /// Each element is either a single entry or a conjunction group.
60    #[serde(default)]
61    pub any: Option<Vec<AnyItem>>,
62}
63
64/// One disjunct inside a JSON `any` entry.
65///
66/// `Single` is one property; `And` is a conjunction of properties
67/// (all must hold together, forming one OR alternative).
68#[derive(Debug, Serialize, Deserialize, Clone)]
69#[serde(untagged)]
70pub(crate) enum AnyItem {
71    Single(JsonProperty),
72    And(Vec<JsonProperty>),
73}
74
75/// Looks up backup contracts for a standard-library function by its normalized path.
76/// For trait-method impls, resolves to the trait method's path first so that
77/// all impls share the same contracts.
78///
79/// After exact-path lookup, first strips impl-block type segments, then falls
80/// back to wildcard patterns by progressively replacing the tail segment with
81/// `*`.  For example, for `core::slice::<impl [T]>::as_chunks`, the fallback
82/// chain is:
83///
84/// 1. `core::slice::<impl [T]>::as_chunks`  (exact)
85/// 2. `core::slice::as_chunks`              (impl segments stripped)
86/// 3. `core::slice::<impl [T]>::*`          (all methods of `[T]`)
87/// 4. `core::slice::*`                      (all functions in slice module)
88/// 5. `core::*`                             (anything in core crate)
89pub(crate) fn get_std_contracts_from_json(
90    tcx: TyCtxt<'_>,
91    def_id: DefId,
92) -> Option<&'static [JsonProperty]> {
93    let lookup_def_id = resolve_trait_method(tcx, def_id);
94    let cleaned_path_name = public_def_path(tcx, lookup_def_id);
95    let db = load_std_contracts_json();
96
97    // Exact match first.
98    if let Some(entries) = db.get(&cleaned_path_name) {
99        return Some(entries.as_slice());
100    }
101
102    // Strip intra-path type segments that appear in impl blocks.
103    // E.g. `core::slice::[T]::as_chunks_unchecked` → `core::slice::as_chunks_unchecked`.
104    {
105        let stripped: Vec<&str> = cleaned_path_name
106            .split("::")
107            .filter(|s| !s.starts_with('[') && !s.starts_with('<'))
108            .collect();
109        if stripped.len() != cleaned_path_name.matches("::").count() + 1 {
110            let stripped_path = stripped.join("::");
111            if let Some(entries) = db.get(&stripped_path) {
112                return Some(entries.as_slice());
113            }
114        }
115    }
116
117    // Wildcard fallback: progressively replace tail segments with `*`.
118    let mut segments: Vec<&str> = cleaned_path_name.split("::").collect();
119    for i in (1..segments.len()).rev() {
120        segments.truncate(i + 1);
121        segments[i] = "*";
122        let pattern = segments.join("::");
123        if let Some(entries) = db.get(&pattern) {
124            return Some(entries.as_slice());
125        }
126    }
127
128    // Try bare `*` for any function.
129    if let Some(entries) = db.get("*") {
130        return Some(entries.as_slice());
131    }
132
133    None
134}
135
136/// Whether the JSON contract database has an entry for `def_id` — including an
137/// *empty* one, which means "no safety contract is needed" (e.g. `fmt::new`,
138/// whose arguments are all references).
139pub(crate) fn std_contracts_has_entry(tcx: TyCtxt<'_>, def_id: DefId) -> bool {
140    get_std_contracts_from_json(tcx, def_id).is_some()
141}
142
143/// If `def_id` is a trait-method implementation, returns the corresponding
144/// trait method's [`DefId`]; otherwise returns `def_id` unchanged.
145fn resolve_trait_method(tcx: TyCtxt<'_>, def_id: DefId) -> DefId {
146    if let Some(assoc_item) = tcx.opt_associated_item(def_id) {
147        if let Some(trait_def_id) = assoc_item.trait_item_def_id() {
148            return trait_def_id;
149        }
150    }
151    def_id
152}
153
154/// Lazily loads the backup contract database for standard-library APIs.
155fn load_std_contracts_json() -> &'static HashMap<String, Vec<JsonProperty>> {
156    static STD_CONTRACTS: OnceLock<HashMap<String, Vec<JsonProperty>>> = OnceLock::new();
157    STD_CONTRACTS.get_or_init(|| {
158        serde_json::from_str(include_str!("assets/std-api-requires.json"))
159            .expect("failed to parse verify std contracts backup")
160    })
161}
162
163/// Serialisation-friendly struct for the type-invariants JSON.
164#[derive(Debug, Serialize, Deserialize, Clone)]
165pub(crate) struct TypeInvariantEntry {
166    pub invariants: Vec<JsonProperty>,
167}
168
169/// Returns the std-type-invariants database, mapping a type path key
170/// (e.g. `"core::num::nonzero::NonZero"`) to its invariant entries.
171pub(crate) fn get_std_type_invariants() -> &'static HashMap<String, TypeInvariantEntry> {
172    static TYPE_INVARIANTS: OnceLock<HashMap<String, TypeInvariantEntry>> = OnceLock::new();
173    TYPE_INVARIANTS.get_or_init(|| {
174        serde_json::from_str(include_str!("assets/std-type-invariants.json"))
175            .expect("failed to parse std type invariants")
176    })
177}
178
179/// Lazily loads the std auto-trait `ensures` obligation database.
180fn load_trait_ensures_json() -> &'static HashMap<String, Vec<JsonProperty>> {
181    static TRAIT_ENSURES: OnceLock<HashMap<String, Vec<JsonProperty>>> = OnceLock::new();
182    TRAIT_ENSURES.get_or_init(|| {
183        serde_json::from_str(include_str!("assets/std-trait-ensures.json"))
184            .expect("failed to parse std trait ensures")
185    })
186}
187
188/// Returns the `ensures` obligation template for a marker trait such as
189/// `Send`/`Sync`, keyed by the trait's def path (e.g. `"core::marker::Send"`).
190///
191/// The returned entries are templates: `ty:Self` placeholders are resolved to
192/// the concrete implementing type by the caller (`TraitEnsurance` collection).
193///
194/// Matching falls back to the trait's short name so that `std`/`core` re-exports
195/// (`std::marker::Send` vs `core::marker::Send`) resolve to the same entry.
196pub(crate) fn query_trait_ensures(tcx: TyCtxt<'_>, trait_def_id: DefId) -> Vec<JsonProperty> {
197    let db = load_trait_ensures_json();
198    let key = tcx.def_path_str(trait_def_id);
199    if let Some(entries) = db.get(&key) {
200        return entries.clone();
201    }
202    let short = key.rsplit("::").next().unwrap_or(&key).to_string();
203    for (k, entries) in db.iter() {
204        if k.rsplit("::").next() == Some(short.as_str()) {
205            return entries.clone();
206        }
207    }
208    Vec::new()
209}
210
211// ── Entry conversion & argument normalization ───────────────────────────────
212
213/// Convert a single [`JsonProperty`] from JSON into the properties it denotes.
214///
215/// Resolves named parameter references (e.g. `"src"` → `"Arg_0"`), normalizes
216/// explicit JSON tokens (`arg:`, `const:`, `ty:`), and delegates to
217/// [`Property::parse_list`] for tag-based parsing.  A single entry may expand to
218/// several properties (via a compound property or `any`), hence the `Vec` return.
219pub(crate) fn entry_to_property<'tcx>(
220    tcx: TyCtxt<'tcx>,
221    def_id: DefId,
222    entry: &JsonProperty,
223    param_names: &[String],
224    has_names: bool,
225) -> Vec<Property<'tcx>> {
226    if let Some(disjuncts) = &entry.any {
227        if disjuncts.len() >= 2 {
228            let mut prop = any_entry_to_property(tcx, def_id, disjuncts, param_names, has_names);
229            prop.apply_kind(entry.kind.as_deref());
230            return vec![prop];
231        }
232        rap_error!(
233            "JSON any entry requires at least 2 disjuncts, got {}",
234            disjuncts.len()
235        );
236        return Vec::new();
237    }
238
239    let exprs = resolve_json_args(&entry.args, param_names, has_names, &entry.tag);
240    if exprs.len() != entry.args.len() {
241        rap_error!(
242            "Parse JSON API args error: Failed to parse arg '{:?}' for tag {}",
243            entry.args,
244            entry.tag
245        );
246        return Vec::new();
247    }
248
249    let properties = Property::parse_list(tcx, def_id, entry.tag.as_str(), &exprs);
250    let mut result = Vec::new();
251    for mut property in properties {
252        property.apply_kind(entry.kind.as_deref());
253        if matches!(property.kind(), Some(PropertyKind::Unknown)) {
254            rap_debug!(
255                "skip unsupported std safety contract tag '{}' for callee {:?}",
256                entry.tag,
257                def_id
258            );
259            continue;
260        }
261        result.push(property);
262    }
263    result
264}
265
266/// Parse an `any` disjunction entry from JSON into a `Property::Or` property.
267///
268/// Each element of `disjuncts` is an [`AnyItem`]:
269/// - `Single(entry)` → one-property disjunct
270/// - `And(entries)` → conjunction group (all entries must hold for this disjunct)
271fn any_entry_to_property<'tcx>(
272    tcx: TyCtxt<'tcx>,
273    def_id: DefId,
274    disjuncts: &[AnyItem],
275    param_names: &[String],
276    has_names: bool,
277) -> Property<'tcx> {
278    let mut or_disjuncts: Vec<Property<'tcx>> = Vec::new();
279    for item in disjuncts {
280        match item {
281            AnyItem::Single(entry) => {
282                let group = resolve_entry_group(tcx, def_id, entry, param_names, has_names, false);
283                if !group.is_empty() {
284                    or_disjuncts.push(Property::conjunction(group));
285                }
286            }
287            AnyItem::And(entries) => {
288                let mut group: Vec<Property<'tcx>> = Vec::new();
289                for entry in entries {
290                    group.extend(resolve_entry_group(
291                        tcx,
292                        def_id,
293                        entry,
294                        param_names,
295                        has_names,
296                        true,
297                    ));
298                }
299                if !group.is_empty() {
300                    or_disjuncts.push(Property::conjunction(group));
301                }
302            }
303        }
304    }
305    Property::new_or(or_disjuncts)
306}
307
308/// Resolve a single JSON `any` entry into its property group (empty on error).
309fn resolve_entry_group<'tcx>(
310    tcx: TyCtxt<'tcx>,
311    def_id: DefId,
312    entry: &JsonProperty,
313    param_names: &[String],
314    has_names: bool,
315    in_group: bool,
316) -> Vec<Property<'tcx>> {
317    if entry.any.is_some() {
318        if in_group {
319            rap_error!("Nested 'any' inside 'any' group is not supported");
320        } else {
321            rap_error!("Nested 'any' inside 'any' is not supported in JSON contracts");
322        }
323        return Vec::new();
324    }
325    let exprs = resolve_json_args(&entry.args, param_names, has_names, &entry.tag);
326    if exprs.len() != entry.args.len() {
327        if in_group {
328            rap_error!(
329                "Parse any group entry arg error: failed to parse '{:?}' for tag {}",
330                entry.args,
331                entry.tag
332            );
333        } else {
334            rap_error!(
335                "Parse any entry arg error: Failed to parse arg '{:?}' for tag {}",
336                entry.args,
337                entry.tag
338            );
339        }
340        return Vec::new();
341    }
342    let props = Property::parse_list(tcx, def_id, entry.tag.as_str(), &exprs);
343    let mut group = Vec::new();
344    for mut prop in props {
345        prop.apply_kind(entry.kind.as_deref());
346        group.push(prop);
347    }
348    group
349}
350
351/// Resolve JSON contract argument strings to parsed [`syn::Expr`] values.
352///
353/// Handles:
354/// - Named parameter resolution (e.g. `"src"` → `"arg:0"`)
355/// - Explicit token normalization (`arg:`, `const:`, `ty:` prefixes)
356/// - Lifetime stripping (`'a` → `a`)
357pub(crate) fn resolve_json_args(
358    args: &[String],
359    param_names: &[String],
360    has_names: bool,
361    tag: &str,
362) -> Vec<Expr> {
363    let mut exprs: Vec<Expr> = Vec::new();
364    for arg_str in args {
365        let resolved = if has_names {
366            resolve_json_param_name(arg_str, param_names)
367        } else {
368            arg_str.clone()
369        };
370        let normalized_arg = normalize_json_contract_arg(&resolved);
371        match syn::parse_str::<Expr>(&normalized_arg) {
372            Ok(expr) => exprs.push(expr),
373            Err(_) => {
374                if let Some(lifetime) = normalized_arg.strip_prefix('\'') {
375                    if lifetime.chars().all(|c| c.is_alphabetic() || c == '_') {
376                        match syn::parse_str::<Expr>(lifetime) {
377                            Ok(expr) => exprs.push(expr),
378                            Err(_) => {
379                                rap_error!(
380                                    "JSON Contract Error: Failed to parse lifetime \
381                                     '{}' as Rust Expr for tag {}",
382                                    arg_str,
383                                    tag
384                                );
385                            }
386                        }
387                    } else {
388                        rap_error!(
389                            "JSON Contract Error: Failed to parse arg '{}' as Rust Expr for tag {}",
390                            arg_str,
391                            tag
392                        );
393                    }
394                } else {
395                    rap_error!(
396                        "JSON Contract Error: Failed to parse arg '{}' as Rust Expr for tag {}",
397                        arg_str,
398                        tag
399                    );
400                }
401            }
402        }
403    }
404    exprs
405}
406
407/// Resolve a simple parameter-name reference in a JSON contract arg string to
408/// the `arg:N` positional form.  Complex expressions (containing function
409/// calls, field access, etc.) are left unchanged — they are handled later by
410/// the expression parser which already knows how to resolve named parameters.
411pub(crate) fn resolve_json_param_name(arg: &str, param_names: &[String]) -> String {
412    if arg.starts_with("arg:")
413        || arg.starts_with("const:")
414        || arg.starts_with("ty:")
415        || arg.contains('(')
416        || arg.contains('.')
417        || arg.contains("::")
418        || arg.contains(' ')
419        || arg.starts_with('\'')
420    {
421        return arg.to_string();
422    }
423    if let Some(pos) = param_names.iter().position(|n| n == arg) {
424        format!("arg:{pos}")
425    } else {
426        arg.to_string()
427    }
428}
429
430/// Convert explicit JSON contract tokens into the expression syntax accepted by
431/// the existing property parser.
432///
433/// Supported explicit tokens:
434/// - `arg:N` names callee argument `N` and becomes internal `Arg_N`.
435/// - `const:N` names an integer constant and becomes `N`.
436/// - `ty:T` names a type parameter/type identifier and becomes `T`.
437///
438/// Unprefixed strings are kept unchanged for compatibility with older entries
439/// such as `"0"`, `"T"`, and `"1"`.
440pub(crate) fn normalize_json_contract_arg(arg: &str) -> String {
441    let bytes = arg.as_bytes();
442    let mut out = String::with_capacity(arg.len());
443    let mut i = 0;
444
445    while i < bytes.len() {
446        if arg[i..].starts_with("arg:") {
447            let start = i + "arg:".len();
448            let end = scan_while(arg, start, |ch| ch.is_ascii_digit());
449            if end > start {
450                out.push_str("Arg_");
451                out.push_str(&arg[start..end]);
452                i = end;
453                continue;
454            }
455        }
456
457        if arg[i..].starts_with("const:") {
458            let start = i + "const:".len();
459            let end = scan_while(arg, start, is_contract_token_char);
460            if end > start {
461                out.push_str(&arg[start..end]);
462                i = end;
463                continue;
464            }
465        }
466
467        if arg[i..].starts_with("ty:") {
468            let start = i + "ty:".len();
469            let end = scan_while(arg, start, is_contract_token_char);
470            if end > start {
471                out.push_str(&arg[start..end]);
472                i = end;
473                continue;
474            }
475        }
476
477        let ch = arg[i..].chars().next().unwrap();
478        out.push(ch);
479        i += ch.len_utf8();
480    }
481
482    out
483}
484
485fn scan_while(arg: &str, mut index: usize, predicate: impl Fn(char) -> bool) -> usize {
486    while index < arg.len() {
487        let ch = arg[index..].chars().next().unwrap();
488        if !predicate(ch) {
489            break;
490        }
491        index += ch.len_utf8();
492    }
493    index
494}
495
496fn is_contract_token_char(ch: char) -> bool {
497    ch.is_ascii_alphanumeric() || ch == '_' || ch == ':'
498}
499
500/// Query contracts for a function from the bundled JSON backup database.
501///
502/// Uses [`get_std_contracts_from_json`] for lookup with wildcard fallback,
503/// then parses each entry into a [`Property`] via [`entry_to_property`].
504pub(crate) fn query_json_contracts<'tcx>(tcx: TyCtxt<'tcx>, def_id: DefId) -> Vec<Property<'tcx>> {
505    let Some(entries) = get_std_contracts_from_json(tcx, def_id) else {
506        return Vec::new();
507    };
508    let (param_names, _) = crate::helpers::name::parse_signature(tcx, def_id);
509    let has_names = !param_names.is_empty() && !param_names[0].chars().all(|c| c.is_ascii_digit());
510
511    let mut results = Vec::new();
512    for entry in entries {
513        results.extend(entry_to_property(
514            tcx,
515            def_id,
516            entry,
517            &param_names,
518            has_names,
519        ));
520    }
521    results
522}