opendp_tooling/bootstrap/
docstring.rs1use std::{collections::HashMap, env, path::PathBuf};
2
3use darling::{Error, FromMeta, Result, ast::NestedMeta};
4use proc_macro2::Span;
5use quote::format_ident;
6use syn::{
7 AttrStyle, Attribute, Expr, ExprLit, ItemFn, Lit, LitStr, Meta, MetaNameValue, Path,
8 PathSegment, ReturnType, Type, TypePath,
9};
10
11use crate::{
12 Deprecation,
13 proven::filesystem::{get_src_dir, make_proof_link},
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 rust_path: Option<String>,
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.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(rust_path) = &rust_path {
100 description.push(String::new());
101 description.push(make_rustdoc_link(name, rust_path)?)
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 let pprint = |ty| {
254 quote::quote!(#ty)
255 .to_string()
256 .replace(" ", "")
257 .replace(",", ", ")
258 };
259
260 match ident {
261 i if i == "Fallible" => parse_supporting_elements(match arguments {
262 syn::PathArguments::AngleBracketed(ab) => {
263 if ab.args.len() != 1 {
264 return Err(Error::custom("Fallible needs one angle-bracketed argument")
265 .with_span(&ab.args));
266 }
267 match ab.args.first().expect("unreachable due to if statement") {
268 syn::GenericArgument::Type(ty) => ty,
269 arg => {
270 return Err(
271 Error::custom("argument to Fallible must to be a type").with_span(&arg)
272 );
273 }
274 }
275 }
276 arg => {
277 return Err(
278 Error::custom("Fallible needs an angle-bracketed argument").with_span(arg)
279 );
280 }
281 }),
282 i if i == "Transformation" || i == "Measurement" || i == "Function" => match arguments {
283 syn::PathArguments::AngleBracketed(ab) => {
284 let num_args = if i == "Function" { 2 } else { 4 };
285
286 if ab.args.len() != num_args {
287 return Err(Error::custom(format!(
288 "{i} needs {num_args} angle-bracketed arguments"
289 ))
290 .with_span(&ab.args));
291 }
292
293 let [input_domain, output_domain] = [&ab.args[0], &ab.args[1]];
294
295 let input_label = match i {
296 i if i == "Transformation" => "Domain:",
297 i if i == "Measurement" => "Domain:",
298 i if i == "Odometer" => "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 i if i == "Odometer" => match arguments {
337 syn::PathArguments::AngleBracketed(ab) => {
338 let [input_domain, input_metric, output_measure, query, answer] =
339 <[&_; 5]>::try_from(ab.args.iter().collect::<Vec<&_>>()).map_err(|_| {
340 Error::custom(format!("Odometer needs 5 angle-bracketed arguments"))
341 })?;
342
343 let lines = vec![
344 format!("* Input Domain `{}`", pprint(input_domain)),
345 format!("* Input Metric `{}`", pprint(input_metric)),
346 format!("* Output Measure `{}`", pprint(output_measure)),
347 format!("* Query `{}`", pprint(query)),
348 format!("* Answer `{}`", pprint(answer)),
349 ];
350
351 Ok(Some(lines.join("\n")))
352 }
353 arg => {
354 return Err(
355 Error::custom("Fallible needs an angle-bracketed argument").with_span(arg)
356 );
357 }
358 },
359 _ => Ok(None),
360 }
361}
362
363fn parse_doc_attribute(attr: Attribute) -> Result<String> {
365 let Meta::NameValue(MetaNameValue {
366 value: Expr::Lit(ExprLit {
367 lit: Lit::Str(v), ..
368 }),
369 ..
370 }) = attr.meta
371 else {
372 return Err(Error::custom("doc attribute must be a string literal").with_span(&attr));
373 };
374 Ok(v.value())
375}
376
377pub fn get_proof_path(
379 attr_args: &Vec<Meta>,
380 item_fn: &ItemFn,
381 proof_paths: &HashMap<String, Option<String>>,
382) -> Result<Option<String>> {
383 let BootstrapArguments {
384 name,
385 proof_path,
386 unproven,
387 ..
388 } = BootstrapArguments::from_attribute_args(
389 &attr_args
390 .iter()
391 .cloned()
392 .map(NestedMeta::Meta)
393 .collect::<Vec<_>>(),
394 )?;
395
396 let name = name.unwrap_or_else(|| item_fn.sig.ident.to_string());
397 if unproven && proof_path.is_some() {
398 return Err(Error::custom("proof_path is invalid when unproven"));
399 }
400 Ok(match proof_path {
401 Some(proof_path) => Some(proof_path),
402 None => match proof_paths.get(&name) {
403 Some(None) => {
404 return Err(Error::custom(format!(
405 "more than one file named {name}.tex. Please specify `proof_path = \"{{module}}/path/to/proof.tex\"` in the macro arguments."
406 )));
407 }
408 Some(proof_path) => proof_path.clone(),
409 None => None,
410 },
411 })
412}
413
414pub fn insert_proof_attribute(attributes: &mut Vec<Attribute>, proof_path: String) -> Result<()> {
416 let source_dir = get_src_dir()?;
417 let proof_path = PathBuf::from(proof_path);
418 let repo_path = PathBuf::from("rust/src");
419 let proof_link = format!(
420 " [(Proof Document)]({}) ",
421 make_proof_link(source_dir, proof_path, repo_path)?
422 );
423
424 let position = (attributes.iter())
425 .position(|attr| {
426 if attr.path().get_ident().map(ToString::to_string).as_deref() != Some("doc") {
427 return false;
428 }
429 if let Ok(comment) = parse_doc_attribute(attr.clone()) {
430 comment.starts_with(" # Proof Definition")
431 } else {
432 false
433 }
434 })
435 .map(|i| i + 1)
437 .unwrap_or_else(|| {
439 attributes.push(new_comment_attribute(" "));
440 attributes.push(new_comment_attribute(" # Proof Definition"));
441 attributes.len()
442 });
443
444 attributes.insert(position, new_comment_attribute(&proof_link));
445
446 Ok(())
447}
448
449fn new_comment_attribute(comment: &str) -> Attribute {
451 Attribute {
452 pound_token: Default::default(),
453 style: AttrStyle::Outer,
454 bracket_token: Default::default(),
455 meta: Meta::NameValue(MetaNameValue {
456 value: Expr::Lit(ExprLit {
457 lit: Lit::Str(LitStr::new(comment, Span::call_site())),
458 attrs: Vec::new(),
459 }),
460 path: Path::from(format_ident!("doc")),
461 eq_token: Default::default(),
462 }),
463 }
464}
465
466pub fn make_rustdoc_link(name: &str, path: &str) -> Result<String> {
467 let proof_uri = if let Ok(rustdoc_port) = std::env::var("OPENDP_RUSTDOC_PORT") {
469 format!("http://localhost:{rustdoc_port}")
470 } else {
471 let docs_uri =
473 env::var("OPENDP_REMOTE_RUSTDOC_URI").unwrap_or_else(|_| "https://docs.rs".to_string());
474
475 let mut version = env!("CARGO_PKG_VERSION");
477 if version.ends_with("-dev") {
478 version = "latest";
479 };
480
481 format!("{docs_uri}/opendp/{version}")
482 };
483
484 Ok(format!(
485 "[{name} in Rust documentation.]({proof_uri}/opendp/{path}.html)"
487 ))
488}