use rustc_hir::def_id::DefId;
use rustc_middle::ty::TyCtxt;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::OnceLock;
use syn::Expr;
use crate::helpers::name::get_cleaned_def_path_name;
use super::types::{Property, PropertyKind};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub(crate) struct JsonProperty {
#[serde(default)]
pub tag: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub kind: Option<String>,
#[serde(default)]
pub any: Option<Vec<AnyItem>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(untagged)]
pub(crate) enum AnyItem {
Single(JsonProperty),
And(Vec<JsonProperty>),
}
pub(crate) fn get_std_contracts_from_json(
tcx: TyCtxt<'_>,
def_id: DefId,
) -> &'static [JsonProperty] {
let lookup_def_id = resolve_trait_method(tcx, def_id);
let cleaned_path_name = get_cleaned_def_path_name(tcx, lookup_def_id);
let db = load_std_contracts_json();
if let Some(entries) = db.get(&cleaned_path_name) {
return entries.as_slice();
}
{
let stripped: Vec<&str> = cleaned_path_name
.split("::")
.filter(|s| !s.starts_with('[') && !s.starts_with('<'))
.collect();
if stripped.len() != cleaned_path_name.matches("::").count() + 1 {
let stripped_path = stripped.join("::");
if let Some(entries) = db.get(&stripped_path) {
return entries.as_slice();
}
}
}
let mut segments: Vec<&str> = cleaned_path_name.split("::").collect();
for i in (1..segments.len()).rev() {
segments.truncate(i + 1);
segments[i] = "*";
let pattern = segments.join("::");
if let Some(entries) = db.get(&pattern) {
return entries.as_slice();
}
}
if let Some(entries) = db.get("*") {
return entries.as_slice();
}
&[]
}
fn resolve_trait_method(tcx: TyCtxt<'_>, def_id: DefId) -> DefId {
if let Some(assoc_item) = tcx.opt_associated_item(def_id) {
if let Some(trait_def_id) = assoc_item.trait_item_def_id() {
return trait_def_id;
}
}
def_id
}
fn load_std_contracts_json() -> &'static HashMap<String, Vec<JsonProperty>> {
static STD_CONTRACTS: OnceLock<HashMap<String, Vec<JsonProperty>>> = OnceLock::new();
STD_CONTRACTS.get_or_init(|| {
serde_json::from_str(include_str!("assets/std-api-requires.json"))
.expect("failed to parse verify std contracts backup")
})
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub(crate) struct TypeInvariantEntry {
pub invariants: Vec<JsonProperty>,
}
pub(crate) fn get_std_type_invariants() -> &'static HashMap<String, TypeInvariantEntry> {
static TYPE_INVARIANTS: OnceLock<HashMap<String, TypeInvariantEntry>> = OnceLock::new();
TYPE_INVARIANTS.get_or_init(|| {
serde_json::from_str(include_str!("assets/std-type-invariants.json"))
.expect("failed to parse std type invariants")
})
}
fn load_trait_ensures_json() -> &'static HashMap<String, Vec<JsonProperty>> {
static TRAIT_ENSURES: OnceLock<HashMap<String, Vec<JsonProperty>>> = OnceLock::new();
TRAIT_ENSURES.get_or_init(|| {
serde_json::from_str(include_str!("assets/std-trait-ensures.json"))
.expect("failed to parse std trait ensures")
})
}
pub(crate) fn query_trait_ensures(tcx: TyCtxt<'_>, trait_def_id: DefId) -> Vec<JsonProperty> {
let db = load_trait_ensures_json();
let key = tcx.def_path_str(trait_def_id);
if let Some(entries) = db.get(&key) {
return entries.clone();
}
let short = key.rsplit("::").next().unwrap_or(&key).to_string();
for (k, entries) in db.iter() {
if k.rsplit("::").next() == Some(short.as_str()) {
return entries.clone();
}
}
Vec::new()
}
pub(crate) fn entry_to_property<'tcx>(
tcx: TyCtxt<'tcx>,
def_id: DefId,
entry: &JsonProperty,
param_names: &[String],
has_names: bool,
) -> Vec<Property<'tcx>> {
if let Some(disjuncts) = &entry.any {
if disjuncts.len() >= 2 {
let mut prop =
any_entry_to_property(tcx, def_id, disjuncts, param_names, has_names);
prop.apply_kind(entry.kind.as_deref());
return vec![prop];
}
rap_error!(
"JSON any entry requires at least 2 disjuncts, got {}",
disjuncts.len()
);
return Vec::new();
}
let exprs = resolve_json_args(&entry.args, param_names, has_names, &entry.tag);
if exprs.len() != entry.args.len() {
rap_error!(
"Parse JSON API args error: Failed to parse arg '{:?}' for tag {}",
entry.args,
entry.tag
);
return Vec::new();
}
let properties = Property::parse_list(tcx, def_id, entry.tag.as_str(), &exprs);
let mut result = Vec::new();
for mut property in properties {
property.apply_kind(entry.kind.as_deref());
if matches!(property.kind(), Some(PropertyKind::Unknown)) {
rap_debug!(
"skip unsupported std safety contract tag '{}' for callee {:?}",
entry.tag,
def_id
);
continue;
}
result.push(property);
}
result
}
fn any_entry_to_property<'tcx>(
tcx: TyCtxt<'tcx>,
def_id: DefId,
disjuncts: &[AnyItem],
param_names: &[String],
has_names: bool,
) -> Property<'tcx> {
let mut or_disjuncts: Vec<Property<'tcx>> = Vec::new();
for item in disjuncts {
match item {
AnyItem::Single(entry) => {
let group = resolve_entry_group(tcx, def_id, entry, param_names, has_names, false);
if !group.is_empty() {
or_disjuncts.push(Property::conjunction(group));
}
}
AnyItem::And(entries) => {
let mut group: Vec<Property<'tcx>> = Vec::new();
for entry in entries {
group.extend(resolve_entry_group(
tcx, def_id, entry, param_names, has_names, true,
));
}
if !group.is_empty() {
or_disjuncts.push(Property::conjunction(group));
}
}
}
}
Property::new_or(or_disjuncts)
}
fn resolve_entry_group<'tcx>(
tcx: TyCtxt<'tcx>,
def_id: DefId,
entry: &JsonProperty,
param_names: &[String],
has_names: bool,
in_group: bool,
) -> Vec<Property<'tcx>> {
if entry.any.is_some() {
if in_group {
rap_error!("Nested 'any' inside 'any' group is not supported");
} else {
rap_error!("Nested 'any' inside 'any' is not supported in JSON contracts");
}
return Vec::new();
}
let exprs = resolve_json_args(&entry.args, param_names, has_names, &entry.tag);
if exprs.len() != entry.args.len() {
if in_group {
rap_error!(
"Parse any group entry arg error: failed to parse '{:?}' for tag {}",
entry.args,
entry.tag
);
} else {
rap_error!(
"Parse any entry arg error: Failed to parse arg '{:?}' for tag {}",
entry.args,
entry.tag
);
}
return Vec::new();
}
let props = Property::parse_list(tcx, def_id, entry.tag.as_str(), &exprs);
let mut group = Vec::new();
for mut prop in props {
prop.apply_kind(entry.kind.as_deref());
group.push(prop);
}
group
}
pub(crate) fn resolve_json_args(
args: &[String],
param_names: &[String],
has_names: bool,
tag: &str,
) -> Vec<Expr> {
let mut exprs: Vec<Expr> = Vec::new();
for arg_str in args {
let resolved = if has_names {
resolve_json_param_name(arg_str, param_names)
} else {
arg_str.clone()
};
let normalized_arg = normalize_json_contract_arg(&resolved);
match syn::parse_str::<Expr>(&normalized_arg) {
Ok(expr) => exprs.push(expr),
Err(_) => {
if let Some(lifetime) = normalized_arg.strip_prefix('\'') {
if lifetime.chars().all(|c| c.is_alphabetic() || c == '_') {
match syn::parse_str::<Expr>(lifetime) {
Ok(expr) => exprs.push(expr),
Err(_) => {
rap_error!(
"JSON Contract Error: Failed to parse lifetime \
'{}' as Rust Expr for tag {}",
arg_str,
tag
);
}
}
} else {
rap_error!(
"JSON Contract Error: Failed to parse arg '{}' as Rust Expr for tag {}",
arg_str,
tag
);
}
} else {
rap_error!(
"JSON Contract Error: Failed to parse arg '{}' as Rust Expr for tag {}",
arg_str,
tag
);
}
}
}
}
exprs
}
pub(crate) fn resolve_json_param_name(arg: &str, param_names: &[String]) -> String {
if arg.starts_with("arg:")
|| arg.starts_with("const:")
|| arg.starts_with("ty:")
|| arg.contains('(')
|| arg.contains('.')
|| arg.contains("::")
|| arg.contains(' ')
|| arg.starts_with('\'')
{
return arg.to_string();
}
if let Some(pos) = param_names.iter().position(|n| n == arg) {
format!("arg:{pos}")
} else {
arg.to_string()
}
}
pub(crate) fn normalize_json_contract_arg(arg: &str) -> String {
let bytes = arg.as_bytes();
let mut out = String::with_capacity(arg.len());
let mut i = 0;
while i < bytes.len() {
if arg[i..].starts_with("arg:") {
let start = i + "arg:".len();
let end = scan_while(arg, start, |ch| ch.is_ascii_digit());
if end > start {
out.push_str("Arg_");
out.push_str(&arg[start..end]);
i = end;
continue;
}
}
if arg[i..].starts_with("const:") {
let start = i + "const:".len();
let end = scan_while(arg, start, is_contract_token_char);
if end > start {
out.push_str(&arg[start..end]);
i = end;
continue;
}
}
if arg[i..].starts_with("ty:") {
let start = i + "ty:".len();
let end = scan_while(arg, start, is_contract_token_char);
if end > start {
out.push_str(&arg[start..end]);
i = end;
continue;
}
}
let ch = arg[i..].chars().next().unwrap();
out.push(ch);
i += ch.len_utf8();
}
out
}
fn scan_while(arg: &str, mut index: usize, predicate: impl Fn(char) -> bool) -> usize {
while index < arg.len() {
let ch = arg[index..].chars().next().unwrap();
if !predicate(ch) {
break;
}
index += ch.len_utf8();
}
index
}
fn is_contract_token_char(ch: char) -> bool {
ch.is_ascii_alphanumeric() || ch == '_' || ch == ':'
}
pub(crate) fn query_json_contracts<'tcx>(tcx: TyCtxt<'tcx>, def_id: DefId) -> Vec<Property<'tcx>> {
let entries = get_std_contracts_from_json(tcx, def_id);
if entries.is_empty() {
return Vec::new();
}
let (param_names, _) = crate::helpers::name::parse_signature(tcx, def_id);
let has_names = !param_names.is_empty() && !param_names[0].chars().all(|c| c.is_ascii_digit());
let mut results = Vec::new();
for entry in entries {
results.extend(entry_to_property(
tcx,
def_id,
entry,
¶m_names,
has_names,
));
}
results
}