opendp_tooling/bootstrap/
docstring.rs1use std::{collections::HashMap, env, path::PathBuf};
2
3use darling::{Error, FromMeta, Result};
4use proc_macro2::{Literal, Punct, Spacing, TokenStream, TokenTree};
5use quote::format_ident;
6use syn::{
7 AttrStyle, Attribute, AttributeArgs, ItemFn, Lit, Meta, MetaNameValue, Path, PathSegment,
8 ReturnType, Type, TypePath,
9};
10
11use crate::{
12 proven::filesystem::{get_src_dir, make_proof_link},
13 Deprecation,
14};
15
16use super::arguments::BootstrapArguments;
17
18#[derive(Debug, Default)]
19pub struct BootstrapDocstring {
20 pub description: Option<String>,
21 pub arguments: HashMap<String, String>,
22 pub generics: HashMap<String, String>,
23 pub returns: Option<String>,
24 pub deprecated: Option<Deprecation>,
25}
26
27#[derive(Debug, FromMeta, Clone)]
28pub struct DeprecationArguments {
29 pub since: Option<String>,
30 pub note: Option<String>,
31}
32
33impl BootstrapDocstring {
34 pub fn from_attrs(
35 name: &String,
36 attrs: Vec<Attribute>,
37 output: &ReturnType,
38 path: Option<(&str, &str)>,
39 features: Vec<String>,
40 ) -> Result<BootstrapDocstring> {
41 let deprecated = attrs
44 .iter()
45 .find(|attr| {
46 attr.path.get_ident().map(ToString::to_string).as_deref() == Some("deprecated")
47 })
48 .map(|attr| {
49 let meta = DeprecationArguments::from_meta(&attr.parse_meta()?)?;
50 Result::Ok(Deprecation {
51 since: meta.since.ok_or_else(|| {
52 Error::custom("`since` must be specified").with_span(&attr)
53 })?,
54 note: meta.note.ok_or_else(|| {
55 Error::custom("`note` must be specified").with_span(&attr)
56 })?,
57 })
58 })
59 .transpose()?;
60
61 let mut doc_sections = parse_docstring_sections(attrs)?;
62
63 const HONEST_SECTION: &str = "Why honest-but-curious?";
64 const HONEST_FEATURE: &str = "honest-but-curious";
65 let has_honest_section = doc_sections.keys().any(|key| key == HONEST_SECTION);
66 let has_honest_feature = features
67 .clone()
68 .into_iter()
69 .any(|feature| feature == HONEST_FEATURE);
70 if has_honest_feature && !has_honest_section {
71 let msg = format!(
72 "{name} requires \"{HONEST_FEATURE}\" but is missing \"{HONEST_SECTION}\" section"
73 );
74 return Err(Error::custom(msg));
75 }
76 if has_honest_section && !has_honest_feature {
77 let msg = format!(
78 "{name} has \"{HONEST_SECTION}\" section but is missing \"{HONEST_FEATURE}\" feature"
79 );
80 return Err(Error::custom(msg));
81 }
82
83 if let Some(sup_elements) = parse_sig_output(output)? {
84 doc_sections.insert("Supporting Elements".to_string(), sup_elements);
85 }
86
87 let mut description = Vec::from_iter(doc_sections.remove("Description"));
88
89 if !features.is_empty() {
90 let features_list = features
91 .into_iter()
92 .map(|f| format!("`{f}`"))
93 .collect::<Vec<_>>()
94 .join(", ");
95 description.push(format!("\n\nRequired features: {features_list}"));
96 }
97
98 if let Some((module, name)) = &path {
100 description.push(String::new());
101 description.push(make_rustdoc_link(module, name)?)
102 }
103
104 let mut add_section_to_description = |section_name: &str| {
105 doc_sections.remove(section_name).map(|section| {
106 description.push(format!("\n**{section_name}:**\n"));
107 description.push(section)
108 })
109 };
110 add_section_to_description(HONEST_SECTION);
112 add_section_to_description("Citations");
113 add_section_to_description("Supporting Elements");
114 add_section_to_description("Proof Definition");
115
116 Ok(BootstrapDocstring {
117 description: if description.is_empty() {
118 None
119 } else {
120 Some(description.join("\n").trim().to_string())
121 },
122 arguments: doc_sections
123 .remove("Arguments")
124 .map(parse_docstring_args)
125 .unwrap_or_else(HashMap::new),
126 generics: doc_sections
127 .remove("Generics")
128 .map(parse_docstring_args)
129 .unwrap_or_else(HashMap::new),
130 returns: doc_sections.remove("Returns"),
131 deprecated,
132 })
133 }
134}
135
136fn parse_docstring_args(args: String) -> HashMap<String, String> {
151 let mut args = args
153 .split("\n")
154 .map(ToString::to_string)
155 .collect::<Vec<_>>();
156
157 args.push("* `".to_string());
159
160 (args.iter().enumerate())
162 .filter_map(|(i, v)| v.starts_with("* `").then(|| i))
163 .collect::<Vec<usize>>()
164 .windows(2)
166 .map(|window| {
167 let mut splitter = args[window[0]].splitn(2, " - ").map(str::to_string);
169 let name = splitter.next().unwrap();
170 let name = name[3..name.len() - 1].to_string();
171
172 let description = vec![splitter.next().unwrap_or_else(String::new)]
174 .into_iter()
175 .chain(
176 args[window[0] + 1..window[1]]
177 .iter()
178 .map(|v| v.trim().to_string()),
179 )
180 .collect::<Vec<String>>()
181 .join("\n")
182 .trim()
183 .to_string();
184 (name, description)
185 })
186 .collect::<HashMap<String, String>>()
187}
188
189fn parse_docstring_sections(attrs: Vec<Attribute>) -> Result<HashMap<String, String>> {
193 let mut docstrings = (attrs.into_iter())
194 .filter(|v| v.path.get_ident().map(ToString::to_string).as_deref() == Some("doc"))
195 .map(parse_doc_attribute)
196 .collect::<Result<Vec<_>>>()?
197 .into_iter()
198 .filter_map(|v| {
199 if v.is_empty() {
200 Some(String::new())
201 } else {
202 v.starts_with(" ").then(|| v[1..].to_string())
203 }
204 })
205 .collect::<Vec<String>>();
206
207 docstrings.insert(0, "# Description".to_string());
209 docstrings.push("# End".to_string());
210
211 Ok(docstrings
212 .iter()
213 .enumerate()
214 .filter_map(|(i, v)| v.starts_with("# ").then(|| i))
215 .collect::<Vec<usize>>()
216 .windows(2)
217 .map(|window| {
218 (
219 docstrings[window[0]]
220 .strip_prefix("# ")
221 .expect("won't panic (because of filter)")
222 .to_string(),
223 docstrings[window[0] + 1..window[1]]
224 .to_vec()
225 .join("\n")
226 .trim()
227 .to_string(),
228 )
229 })
230 .collect())
231}
232
233fn parse_sig_output(output: &ReturnType) -> Result<Option<String>> {
235 match output {
236 ReturnType::Default => Ok(None),
237 ReturnType::Type(_, ty) => parse_supporting_elements(&*ty),
238 }
239}
240
241fn parse_supporting_elements(ty: &Type) -> Result<Option<String>> {
242 let PathSegment { ident, arguments } = match &ty {
243 syn::Type::Path(TypePath {
244 path: Path { segments, .. },
245 ..
246 }) => segments.last().ok_or_else(|| {
247 Error::custom("return type cannot be an empty path").with_span(&segments)
248 })?,
249 _ => return Ok(None),
250 };
251
252 match ident {
253 i if i == "Fallible" => parse_supporting_elements(match arguments {
254 syn::PathArguments::AngleBracketed(ab) => {
255 if ab.args.len() != 1 {
256 return Err(Error::custom("Fallible needs one angle-bracketed argument")
257 .with_span(&ab.args));
258 }
259 match ab.args.first().expect("unreachable due to if statement") {
260 syn::GenericArgument::Type(ty) => ty,
261 arg => {
262 return Err(
263 Error::custom("argument to Fallible must to be a type").with_span(&arg)
264 )
265 }
266 }
267 }
268 arg => {
269 return Err(
270 Error::custom("Fallible needs an angle-bracketed argument").with_span(arg)
271 )
272 }
273 }),
274 i if i == "Transformation" || i == "Measurement" || i == "Function" => {
275 match arguments {
276 syn::PathArguments::AngleBracketed(ab) => {
277 let num_args = if i == "Function" { 2 } else { 4 };
278
279 if ab.args.len() != num_args {
280 return Err(Error::custom(format!(
281 "{i} needs {num_args} angle-bracketed arguments"
282 ))
283 .with_span(&ab.args));
284 }
285
286 let [input_domain, output_domain] = [&ab.args[0], &ab.args[1]];
287
288 let pprint = |ty| {
290 quote::quote!(#ty)
291 .to_string()
292 .replace(" ", "")
293 .replace(",", ", ")
294 };
295
296 let input_label = match i {
297 i if i == "Transformation" => "Domain:",
298 i if i == "Measurement" => "Domain:",
299 i if i == "Function" => "Type: ",
300 _ => unreachable!(),
301 };
302
303 let output_label = match i {
304 i if i == "Transformation" => "Domain:",
305 i if i == "Measurement" => "Type: ",
306 i if i == "Function" => "Type: ",
307 _ => unreachable!(),
308 };
309
310 let mut lines = vec![
311 format!("* Input {} `{}`", input_label, pprint(input_domain)),
312 format!("* Output {} `{}`", output_label, pprint(output_domain)),
313 ];
314
315 if i != "Function" {
316 let output_distance = match i {
317 i if i == "Transformation" => "Metric: ",
318 i if i == "Measurement" => "Measure:",
319 _ => unreachable!(),
320 };
321 let [input_metric, output_metmeas] = [&ab.args[2], &ab.args[3]];
322 lines.extend([
323 format!("* Input Metric: `{}`", pprint(input_metric)),
324 format!("* Output {} `{}`", output_distance, pprint(output_metmeas)),
325 ]);
326 }
327
328 Ok(Some(lines.join("\n")))
329 }
330 arg => {
331 return Err(
332 Error::custom("Fallible needs an angle-bracketed argument").with_span(arg)
333 )
334 }
335 }
336 }
337 _ => Ok(None),
338 }
339}
340
341fn parse_doc_attribute(attr: Attribute) -> Result<String> {
343 match attr.parse_meta()? {
344 Meta::NameValue(MetaNameValue {
345 lit: Lit::Str(v), ..
346 }) => Ok(v.value()),
347 _ => Err(Error::custom("doc attribute must be a string literal").with_span(&attr)),
348 }
349}
350
351pub fn get_proof_path(
353 attr_args: &AttributeArgs,
354 item_fn: &ItemFn,
355 proof_paths: &HashMap<String, Option<String>>,
356) -> Result<Option<String>> {
357 let BootstrapArguments {
358 name,
359 proof_path,
360 unproven,
361 ..
362 } = BootstrapArguments::from_attribute_args(&attr_args)?;
363
364 let name = name.unwrap_or_else(|| item_fn.sig.ident.to_string());
365 if unproven && proof_path.is_some() {
366 return Err(Error::custom("proof_path is invalid when unproven"));
367 }
368 Ok(match proof_path {
369 Some(proof_path) => Some(proof_path),
370 None => match proof_paths.get(&name) {
371 Some(None) => return Err(Error::custom(format!("more than one file named {name}.tex. Please specify `proof_path = \"{{module}}/path/to/proof.tex\"` in the macro arguments."))),
372 Some(proof_path) => proof_path.clone(),
373 None => None
374 }
375 })
376}
377
378pub fn insert_proof_attribute(attributes: &mut Vec<Attribute>, proof_path: String) -> Result<()> {
380 let source_dir = get_src_dir()?;
381 let proof_path = PathBuf::from(proof_path);
382 let repo_path = PathBuf::from("rust/src");
383 let proof_link = format!(
384 " [(Proof Document)]({}) ",
385 make_proof_link(source_dir, proof_path, repo_path)?
386 );
387
388 let position = (attributes.iter())
389 .position(|attr| {
390 if attr.path.get_ident().map(ToString::to_string).as_deref() != Some("doc") {
391 return false;
392 }
393 if let Ok(comment) = parse_doc_attribute(attr.clone()) {
394 comment.starts_with(" # Proof Definition")
395 } else {
396 false
397 }
398 })
399 .map(|i| i + 1)
401 .unwrap_or_else(|| {
403 attributes.push(new_comment_attribute(" "));
404 attributes.push(new_comment_attribute(" # Proof Definition"));
405 attributes.len()
406 });
407
408 attributes.insert(position, new_comment_attribute(&proof_link));
409
410 Ok(())
411}
412
413fn new_comment_attribute(comment: &str) -> Attribute {
415 Attribute {
416 pound_token: Default::default(),
417 style: AttrStyle::Outer,
418 bracket_token: Default::default(),
419 path: Path::from(format_ident!("doc")),
420 tokens: TokenStream::from_iter(
421 [
422 TokenTree::Punct(Punct::new('=', Spacing::Alone)),
423 TokenTree::Literal(Literal::string(comment)),
424 ]
425 .into_iter(),
426 ),
427 }
428}
429
430pub fn make_rustdoc_link(module: &str, name: &str) -> Result<String> {
431 let proof_uri = if let Ok(rustdoc_port) = std::env::var("OPENDP_RUSTDOC_PORT") {
433 format!("http://localhost:{rustdoc_port}")
434 } else {
435 let docs_uri =
437 env::var("OPENDP_REMOTE_RUSTDOC_URI").unwrap_or_else(|_| "https://docs.rs".to_string());
438
439 let mut version = env!("CARGO_PKG_VERSION");
441 if version.ends_with("-dev") {
442 version = "latest";
443 };
444
445 format!("{docs_uri}/opendp/{version}")
446 };
447
448 Ok(format!(
449 "[{name} in Rust documentation.]({proof_uri}/opendp/{module}/fn.{name}.html)"
451 ))
452}