Skip to content

Commit 9b00cd5

Browse files
fix: support verus_spec in verusdoc
Co-authored-by: Xinyi Wan <64517311+rikosellic@users.noreply.github.com>
1 parent b5e8d44 commit 9b00cd5

File tree

3 files changed

+214
-20
lines changed

3 files changed

+214
-20
lines changed

source/builtin_macros/src/attr_rewrite.rs

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
/// - Refer to `examples/syntax_attr.rs`.
3737
use proc_macro2::TokenStream;
3838
use quote::{ToTokens, quote, quote_spanned};
39+
use syn::parse::Parser;
3940
use syn::visit_mut::VisitMut;
4041
use syn::{Expr, Item, ItemConst, parse2, spanned::Spanned};
4142

@@ -487,9 +488,88 @@ pub(crate) fn rewrite_verus_spec_on_fun_or_loop(
487488
// To avoid misuse of the unverified function,
488489
// we add `requires false` and thus prevent verified function to use it.
489490
// Allow unverified code to use the function without changing in/output.
491+
let mut rustdoc_attrs: Vec<syn::Attribute> = vec![];
492+
if crate::rustdoc::env_rustdoc() {
493+
let mut verus_fun: verus_syn::ItemFn = syn_to_verus_syn(fun.clone());
494+
verus_fun.sig.spec = spec_attr.spec.clone();
495+
496+
// Set return variable name
497+
if let Some((verus_syn::Pat::Ident(pat_ident), _)) = &spec_attr.ret_pat {
498+
if let verus_syn::ReturnType::Type(_, _, opt_name, _) =
499+
&mut verus_fun.sig.output
500+
{
501+
*opt_name = Some(Box::new((
502+
verus_syn::token::Paren::default(),
503+
verus_syn::Pat::Ident(pat_ident.clone()),
504+
verus_syn::Token![:](pat_ident.span()),
505+
)));
506+
}
507+
}
508+
509+
crate::rustdoc::process_item_fn(&mut verus_fun);
510+
511+
for attr in &verus_fun.attrs {
512+
if attr.path().is_ident("doc")
513+
&& attr.to_token_stream().to_string().contains("verusdoc_special_attr")
514+
{
515+
if let Ok(doc_attrs) =
516+
syn::Attribute::parse_outer.parse(attr.to_token_stream().into())
517+
{
518+
rustdoc_attrs.extend(doc_attrs);
519+
}
520+
}
521+
}
522+
}
523+
490524
if let Some(with) = &spec_attr.spec.with {
491-
let extra_funs = rewrite_unverified_func(&mut fun, with.with.span(), erase);
525+
let mut extra_funs = rewrite_unverified_func(&mut fun, with.with.span(), erase);
526+
527+
if crate::rustdoc::env_rustdoc() {
528+
if let Some(unverified_fun) = extra_funs.last_mut() {
529+
unverified_fun.attrs.extend(rustdoc_attrs.clone());
530+
}
531+
fun.attrs.push(crate::syntax::mk_rust_attr_syn(
532+
with.with.span(),
533+
"doc",
534+
quote! {hidden},
535+
));
536+
}
492537
extra_funs.iter().for_each(|f| f.to_tokens(&mut new_stream));
538+
} else if crate::rustdoc::env_rustdoc() {
539+
fun.attrs.extend(rustdoc_attrs);
540+
}
541+
542+
// Inject doc attribute in rustdoc mode
543+
if crate::rustdoc::env_rustdoc() {
544+
let mut verus_fun: verus_syn::ItemFn = syn_to_verus_syn(fun.clone());
545+
verus_fun.sig.spec = spec_attr.spec.clone();
546+
547+
// Set return variable name
548+
if let Some((verus_syn::Pat::Ident(pat_ident), _)) = &spec_attr.ret_pat {
549+
if let verus_syn::ReturnType::Type(_, _, opt_name, _) =
550+
&mut verus_fun.sig.output
551+
{
552+
*opt_name = Some(Box::new((
553+
verus_syn::token::Paren::default(),
554+
verus_syn::Pat::Ident(pat_ident.clone()),
555+
verus_syn::Token![:](pat_ident.span()),
556+
)));
557+
}
558+
}
559+
560+
crate::rustdoc::process_item_fn(&mut verus_fun);
561+
562+
for attr in &verus_fun.attrs {
563+
if attr.path().is_ident("doc")
564+
&& attr.to_token_stream().to_string().contains("verusdoc_special_attr")
565+
{
566+
if let Ok(doc_attrs) =
567+
syn::Attribute::parse_outer.parse(attr.to_token_stream().into())
568+
{
569+
fun.attrs.extend(doc_attrs);
570+
}
571+
}
572+
}
493573
}
494574

495575
// Update function signature based on verus_spec.
@@ -820,6 +900,13 @@ fn rewrite_unverified_func(
820900
Some(syn::token::Semi { spans: [span] }),
821901
);
822902
unverified_fun.attrs_mut().push(mk_verus_attr_syn(span, quote! { external_body }));
903+
if !crate::rustdoc::env_rustdoc() {
904+
unverified_fun.attrs_mut().push(crate::syntax::mk_rust_attr_syn(
905+
span,
906+
"doc",
907+
quote! {hidden},
908+
));
909+
}
823910
if let Some(block) = unverified_fun.block_mut() {
824911
// For an unverified function, if it is in keep mode,
825912
// we erase the function body to avoid using

source/builtin_macros/src/rustdoc.rs

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,14 @@
2929
// some data explaining the function mode, param modes, and return mode.
3030

3131
use proc_macro2::Span;
32+
use quote::ToTokens;
3233
use std::iter::FromIterator;
3334
use verus_syn::punctuated::Punctuated;
3435
use verus_syn::spanned::Spanned;
3536
use verus_syn::token;
3637
use verus_syn::{
37-
AssumeSpecification, AttrStyle, Attribute, Block, Expr, ExprBlock, ExprPath, FnMode, Ident,
38-
ImplItemFn, ItemFn, Pat, PatIdent, Path, PathArguments, PathSegment, Publish, QSelf,
38+
AssumeSpecification, AttrStyle, Attribute, Block, Expr, ExprBlock, ExprPath, FnArg, FnMode,
39+
Ident, ImplItemFn, ItemFn, Pat, PatIdent, Path, PathArguments, PathSegment, Publish, QSelf,
3940
ReturnType, Signature, TraitItemFn, Type, TypeGroup, TypePath,
4041
};
4142

@@ -103,6 +104,10 @@ fn attr_for_sig(
103104

104105
v.push(encoded_sig_info(sig));
105106

107+
if let Some(with_spec) = &sig.spec.with {
108+
v.push(encoded_str("with", &format_with_spec(with_spec)));
109+
}
110+
106111
match &sig.spec.requires {
107112
Some(es) => {
108113
for expr in es.exprs.exprs.iter() {
@@ -330,6 +335,89 @@ fn encoded_str(kind: &str, data: &str) -> String {
330335
"```rust\n// verusdoc_special_attr ".to_string() + kind + "\n" + data + "\n```"
331336
}
332337

338+
fn format_with_spec(with_spec: &verus_syn::WithSpecOnFn) -> String {
339+
let mut lines: Vec<String> = vec![];
340+
341+
let inputs = format_fn_args(&with_spec.inputs);
342+
for input in inputs {
343+
let input = normalize_ws(input.trim());
344+
lines.push(format!(" {input},"));
345+
}
346+
347+
if let Some((_, outputs)) = &with_spec.outputs {
348+
lines.push(" ->".to_string());
349+
let outputs = format_pat_types(outputs);
350+
for output in outputs {
351+
let output = normalize_ws(output.trim());
352+
lines.push(format!(" {output},"));
353+
}
354+
}
355+
356+
lines.join("\n")
357+
}
358+
359+
fn format_pat_types(outputs: &Punctuated<verus_syn::PatType, verus_syn::Token![,]>) -> Vec<String> {
360+
if outputs.is_empty() {
361+
return vec![];
362+
}
363+
364+
outputs.iter().map(format_pat_type).collect()
365+
}
366+
367+
fn format_fn_args(inputs: &Punctuated<FnArg, verus_syn::Token![,]>) -> Vec<String> {
368+
if inputs.is_empty() {
369+
return vec![];
370+
}
371+
inputs.iter().map(format_fn_arg).collect()
372+
}
373+
374+
fn format_fn_arg(arg: &FnArg) -> String {
375+
let tracked = if arg.tracked.is_some() { "tracked " } else { "" };
376+
match &arg.kind {
377+
verus_syn::FnArgKind::Receiver(receiver) => {
378+
let s = normalize_ws(&receiver.to_token_stream().to_string());
379+
format!("{tracked}{s}")
380+
}
381+
verus_syn::FnArgKind::Typed(pt) => {
382+
let s = format_pat_type(pt);
383+
format!("{tracked}{s}")
384+
}
385+
}
386+
}
387+
388+
fn format_pat_type(pt: &verus_syn::PatType) -> String {
389+
let pat = normalize_ws(&pt.pat.to_token_stream().to_string());
390+
let ty = tighten_type_spacing(&normalize_ws(&pt.ty.to_token_stream().to_string()));
391+
format!("{pat}: {ty}")
392+
}
393+
394+
fn normalize_ws(s: &str) -> String {
395+
s.split_whitespace().collect::<Vec<&str>>().join(" ")
396+
}
397+
398+
fn tighten_type_spacing(s: &str) -> String {
399+
let bytes = s.as_bytes();
400+
let mut out = String::with_capacity(bytes.len());
401+
let mut i = 0usize;
402+
while i < bytes.len() {
403+
let b = bytes[i];
404+
if b == b' ' {
405+
let prev = if i > 0 { bytes[i - 1] } else { 0 };
406+
let next = if i + 1 < bytes.len() { bytes[i + 1] } else { 0 };
407+
let remove =
408+
matches!(prev, b'<' | b'(' | b',' | b':') || matches!(next, b'>' | b')' | b',');
409+
if !remove {
410+
out.push(' ');
411+
}
412+
i += 1;
413+
continue;
414+
}
415+
out.push(b as char);
416+
i += 1;
417+
}
418+
out
419+
}
420+
333421
/// Create an attr that looks like #[doc = "doc_str"]
334422
fn doc_attr_from_string(doc_str: &str, span: Span) -> Attribute {
335423
let path = Path {

source/verusdoc/src/main.rs

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ enum VerusDocAttr {
3333
}
3434

3535
// Types of spec clauses we handle.
36-
static SPEC_NAMES: [&str; 4] = ["requires", "ensures", "recommends", "body"];
36+
static SPEC_NAMES: [&str; 5] = ["with", "requires", "ensures", "recommends", "body"];
3737

3838
fn main() {
3939
// Manipulate the auto-generated files in `doc/` to clean them up to make
@@ -256,14 +256,26 @@ fn update_docblock(
256256
}
257257

258258
for spec_name in SPEC_NAMES.iter() {
259-
let code_blocks: Vec<NodeRef> = attrs
259+
let mut code_blocks: Vec<NodeRef> = attrs
260260
.iter()
261261
.filter_map(|a| match a {
262262
VerusDocAttr::Specification(s, nr) if s == spec_name => Some(nr.clone()),
263263
_ => None,
264264
})
265265
.collect();
266266

267+
// De-duplicate identical spec blocks (can happen with #[verus_spec] + rustdoc pass)
268+
let mut seen: Vec<String> = Vec::new();
269+
code_blocks.retain(|nr| {
270+
let text = nr.text_contents();
271+
if seen.iter().any(|t| t == &text) {
272+
false
273+
} else {
274+
seen.push(text);
275+
true
276+
}
277+
});
278+
267279
let is_body = spec_name == &"body";
268280

269281
if code_blocks.len() > 0 && !is_body {
@@ -289,22 +301,29 @@ fn update_docblock(
289301
}
290302

291303
// Add mode info to the signature
292-
293-
for attr in attrs.iter() {
294-
match attr {
295-
VerusDocAttr::ModeInfo(doc_mode_info) => {
296-
update_sig_info(
297-
docblock_elem,
298-
UpdateSigMode::DocSigInfo(doc_mode_info),
299-
opt_trait_info,
300-
);
301-
break;
302-
}
303-
VerusDocAttr::BroadcastGroup => {
304-
update_sig_info(docblock_elem, UpdateSigMode::BroadcastGroup, opt_trait_info);
305-
break;
304+
// If there are multiple ModeInfo attrs (caused by the `#[verus_spec]` macro expansion),
305+
// choose the one with a non-empty `ret_name`.
306+
let mode_infos: Vec<&DocSigInfo> = attrs
307+
.iter()
308+
.filter_map(|a| match a {
309+
VerusDocAttr::ModeInfo(info) => Some(info),
310+
_ => None,
311+
})
312+
.collect();
313+
let info_to_use =
314+
mode_infos.iter().find(|info| !info.ret_name.is_empty()).or_else(|| mode_infos.first());
315+
316+
if let Some(info) = info_to_use {
317+
update_sig_info(docblock_elem, UpdateSigMode::DocSigInfo(info), opt_trait_info);
318+
} else {
319+
for attr in attrs.iter() {
320+
match attr {
321+
VerusDocAttr::BroadcastGroup => {
322+
update_sig_info(docblock_elem, UpdateSigMode::BroadcastGroup, opt_trait_info);
323+
break;
324+
}
325+
_ => {}
306326
}
307-
_ => {}
308327
}
309328
}
310329
}

0 commit comments

Comments
 (0)