rapx/verify/contract/attr.rs
1//! Parsing utilities for `#[rapx::requires(...)]` outer attributes.
2//!
3//! This module converts a raw `#[rapx::requires(...)]` attribute string into a
4//! structured representation that the verification analysis can consume without
5//! depending on `syn` expression details in later stages.
6//!
7//! The currently supported shape is:
8//!
9//! ```text
10//! #[rapx::requires(property_call, kind = "...")]
11//! ```
12//!
13//! where `kind = "..."` applies to the property in the same attribute.
14
15use syn::{
16 Expr, Lit, Result as SynResult, Token, Type,
17 parse::{Parse, ParseStream},
18};
19
20use quote::ToTokens;
21
22/// The raw syntactic form of a property call `tag(arg0, arg1, ...)` parsed
23/// from an attribute — the *unevaluated* stage, before semantic resolution
24/// into a [`Property`](crate::verify::contract::Property).
25#[derive(Debug, Clone)]
26pub(crate) struct AttrProperty {
27 /// The property name extracted from the call target.
28 pub tag: String,
29 /// The positional arguments passed to the property call.
30 pub args: Vec<Expr>,
31 /// Optional `kind` metadata associated with this property.
32 pub kind: Option<String>,
33}
34
35impl Parse for AttrProperty {
36 /// Parse a single property item from a `requires` attribute argument list.
37 ///
38 /// Supported forms:
39 /// - `nonzero(x)`
40 /// - `nonzero(x), kind = "ptr"`
41 fn parse(input: ParseStream<'_>) -> SynResult<Self> {
42 let mut property = parse_property_head(input)?;
43
44 if input.peek(Token![,]) {
45 let fork = input.fork();
46 let _: Token![,] = fork.parse()?;
47 if fork.peek(syn::Ident) && fork.peek2(Token![=]) {
48 let _: Token![,] = input.parse()?;
49 let ident: syn::Ident = input.parse()?;
50 let _: Token![=] = input.parse()?;
51 let value: Expr = input.parse()?;
52
53 if ident == "kind" {
54 if let Expr::Lit(ref expr_lit) = value
55 && let Lit::Str(ref kind) = expr_lit.lit
56 {
57 property.kind = Some(kind.value());
58 } else {
59 return Err(syn::Error::new_spanned(
60 value,
61 "RAPx requires attribute kind must be a string literal",
62 ));
63 }
64 } else {
65 return Err(syn::Error::new(
66 ident.span(),
67 "unsupported named RAPx requires attribute argument",
68 ));
69 }
70 }
71 }
72
73 Ok(property)
74 }
75}
76
77/// A thin wrapper that allows parsing exactly one outer attribute from a string.
78struct RequireOuterAttribute {
79 attr: syn::Attribute,
80}
81
82impl Parse for RequireOuterAttribute {
83 /// Parse exactly one outer attribute.
84 fn parse(input: ParseStream<'_>) -> SynResult<Self> {
85 Ok(Self {
86 attr: input
87 .call(syn::Attribute::parse_outer)?
88 .into_iter()
89 .next()
90 .ok_or_else(|| input.error("expected exactly one outer attribute"))?,
91 })
92 }
93}
94
95/// Parse a raw attribute string into a structured `requires` property.
96///
97/// Returns `Ok(None)` when the attribute does not match `rapx::<expected_name>`
98/// or when it is not a list attribute.
99pub(crate) fn parse_rapx_attr(
100 attr_str: &str,
101 expected_name: &str,
102) -> SynResult<Option<AttrProperty>> {
103 let attr_str = strip_lifetime_ticks(attr_str);
104 // Parse the raw string into a single outer attribute node.
105 let attr = syn::parse_str::<RequireOuterAttribute>(&attr_str)?.attr;
106 if !is_expected_syn_rapx_attr(&attr, expected_name) {
107 return Ok(None);
108 }
109
110 // Only list-style attributes carry an argument list.
111 let syn::Meta::List(meta_list) = &attr.meta else {
112 return Ok(None);
113 };
114
115 let property = meta_list.parse_args::<AttrProperty>()?;
116 Ok(Some(property))
117}
118
119/// Check whether an attribute path is exactly `rapx::<expected_name>`.
120fn is_expected_syn_rapx_attr(attr: &syn::Attribute, expected_name: &str) -> bool {
121 let mut segments = attr.path().segments.iter();
122 matches!(
123 (segments.next(), segments.next(), segments.next()),
124 (Some(first), Some(second), None)
125 if first.ident == "rapx" && second.ident == expected_name
126 )
127}
128
129/// Parse a property call head `tag(arg0, arg1, ...)`.
130///
131/// The argument list is parsed position-by-position rather than as a single
132/// `Expr::Call`, because property arguments may be generic *types* (e.g.
133/// `ValidTransmute(T, Option<NonZero<T>>)`) that `syn` cannot parse as value
134/// expressions.
135fn parse_property_head(input: ParseStream<'_>) -> SynResult<AttrProperty> {
136 let path: syn::Path = input.parse()?;
137 let tag = path
138 .segments
139 .last()
140 .map(|seg| seg.ident.to_string())
141 .ok_or_else(|| syn::Error::new_spanned(&path, "missing property name"))?;
142
143 let content;
144 syn::parenthesized!(content in input);
145
146 let mut args: Vec<Expr> = Vec::new();
147 while !content.is_empty() {
148 args.push(parse_property_arg(&content)?);
149 if content.is_empty() {
150 break;
151 }
152 content.parse::<Token![,]>()?;
153 }
154
155 Ok(AttrProperty {
156 tag,
157 args,
158 kind: None,
159 })
160}
161
162/// Parse a single property argument as an `Expr`.
163///
164/// Arguments are usually types (`Option<NonZero<T>>`, `[T; N]`, `T`), which
165/// `syn` cannot parse as value expressions (it would read `<`/`>` as comparison
166/// operators), so we try `Type` first and wrap its token stream as
167/// `Expr::Verbatim`. Plain path types (single- or multi-segment identifiers
168/// without generics) are kept as `Expr::Path` so downstream place/ident
169/// resolution still recognises them. Arguments that are genuine expressions
170/// (`0`, `x + 1`, `ptr.0`) fail type parsing and fall back to `Expr`.
171fn parse_property_arg(input: ParseStream<'_>) -> SynResult<Expr> {
172 // Arguments that begin with a parenthesised group are tuple/parenthesised
173 // expressions (e.g. the disjunctive grouping `(Align(head, T), ValidPtr(...))`
174 // inside `any`), never types. syn's `Type` parser is fooled by these: it
175 // parses `(Align(head, T))` as a `Type::Paren` around just `Align` and
176 // silently leaves the rest of the group unparsed, which then surfaces as a
177 // spurious `unexpected token, expected `)`` error. Parse them directly as
178 // expressions instead.
179 if input.peek(syn::token::Paren) {
180 return input.parse::<Expr>();
181 }
182
183 let fork = input.fork();
184 if fork.parse::<Type>().is_ok() && (fork.is_empty() || fork.peek(Token![,])) {
185 let ty: Type = input.parse()?;
186 return Ok(type_to_arg_expr(ty));
187 }
188 input.parse::<Expr>()
189}
190
191/// Convert a parsed argument `Type` back into the `Expr` form expected by the
192/// property builder: plain paths stay `Expr::Path`, everything else (generics,
193/// arrays, tuples, references) becomes `Expr::Verbatim`.
194fn type_to_arg_expr(ty: Type) -> Expr {
195 if let Type::Path(type_path) = &ty
196 && type_path.qself.is_none()
197 && type_path
198 .path
199 .segments
200 .iter()
201 .all(|s| matches!(s.arguments, syn::PathArguments::None))
202 {
203 return Expr::Path(syn::ExprPath {
204 attrs: Vec::new(),
205 qself: None,
206 path: type_path.path.clone(),
207 });
208 }
209 Expr::Verbatim(ty.to_token_stream())
210}
211
212/// Strips the leading `'` from Rust lifetime tokens so that `syn` can
213/// parse them as regular identifier expressions inside attribute arguments.
214/// For example, `'a` becomes `a`, `'static` becomes `static`.
215///
216/// String literals (`"..."`) and char literals (`'x'`) are copied verbatim so
217/// their contents are never altered.
218fn strip_lifetime_ticks(s: &str) -> String {
219 let chars: Vec<char> = s.chars().collect();
220 let mut out = String::with_capacity(s.len());
221 let mut i = 0;
222 while i < chars.len() {
223 match chars[i] {
224 '"' => {
225 out.push('"');
226 i += 1;
227 while i < chars.len() {
228 let c = chars[i];
229 out.push(c);
230 i += 1;
231 if c == '\\' && i < chars.len() {
232 out.push(chars[i]);
233 i += 1;
234 } else if c == '"' {
235 break;
236 }
237 }
238 }
239 '\'' => {
240 // `'static` is a Rust keyword, so it cannot be parsed as a
241 // plain ident after the tick is stripped. Map it to a
242 // non-keyword token that `resolve_region_name` recognises.
243 let rest: String = chars[i + 1..].iter().collect();
244 if rest.starts_with("static")
245 && chars.get(i + 1 + "static".len()).is_none_or(|&c| !c.is_ascii_alphanumeric() && c != '_')
246 {
247 out.push_str("static_lifetime");
248 i += 1 + "static".len();
249 continue;
250 }
251 let char_literal = match chars.get(i + 1) {
252 Some('\\') => chars.get(i + 3) == Some(&'\''),
253 Some(_) => chars.get(i + 2) == Some(&'\''),
254 None => false,
255 };
256 if char_literal {
257 out.push('\'');
258 }
259 i += 1;
260 }
261 c => {
262 out.push(c);
263 i += 1;
264 }
265 }
266 }
267 out
268}