1use 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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
41pub(crate) enum CompoundArg {
42 Param(usize),
43 Lit(String),
44}
45
46#[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#[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
68pub(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 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
118fn 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 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 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 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, ¶ms) 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
211fn 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
223fn 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 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
255struct 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 *node = arg.clone();
280 return;
281 }
282 }
283 }
284 }
285 visit_mut::visit_expr_mut(self, node);
286 }
287}
288
289fn 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
301fn 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 if let Some(spec) = super::spec::find_spec(tag) {
333 match spec.build {
334 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 super::spec::BuildKind::TobeSpecified => {}
355 _ => {
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 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
411fn 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
424pub(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 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 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
490fn 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
512fn 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
525fn 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
533fn 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
544fn 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
558pub(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
571pub(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 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 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
624pub(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
676fn 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
694pub(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
714pub(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
752fn 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
765fn 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}