Skip to main content

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}