1use tatara_lisp::{Atom, Span, Spanned, SpannedForm};
44
45use crate::ParseError;
46
47pub const DOC_HEAD: &str = "blue-doc";
49
50pub const MODULEDOC_HEAD: &str = "blue-moduledoc";
52
53pub const DOCTEST_PREFIX: &str = "@doc ";
55
56#[must_use]
58pub fn documented(form: &Spanned) -> Option<(&str, &Spanned)> {
59 match form.as_list()? {
60 [head, text, def] if head.as_symbol() == Some(DOC_HEAD) => Some((text.as_string()?, def)),
61 _ => None,
62 }
63}
64
65#[must_use]
67pub fn moduledoc_text(form: &Spanned) -> Option<&str> {
68 match form.as_list()? {
69 [head, text] if head.as_symbol() == Some(MODULEDOC_HEAD) => text.as_string(),
70 _ => None,
71 }
72}
73
74#[must_use]
76pub fn module_doc(forms: &[Spanned]) -> Option<(&str, Span)> {
77 forms
78 .iter()
79 .find_map(|f| moduledoc_text(f).map(|t| (t, f.span)))
80}
81
82#[must_use]
84pub fn is_doc_node(form: &Spanned) -> bool {
85 documented(form).is_some() || moduledoc_text(form).is_some()
86}
87
88#[must_use]
91pub fn strip(forms: Vec<Spanned>) -> Vec<Spanned> {
92 forms
93 .into_iter()
94 .filter(|f| moduledoc_text(f).is_none())
95 .map(|f| match f.form {
96 SpannedForm::List(mut items)
97 if items.len() == 3 && items[0].as_symbol() == Some(DOC_HEAD) =>
98 {
99 let def = items.pop().expect("three items");
100 Spanned::new(f.span, def.form)
101 }
102 form => Spanned::new(f.span, form),
103 })
104 .collect()
105}
106
107#[must_use]
113pub fn redocument(src: &str, forms: Vec<Spanned>) -> Vec<Spanned> {
114 if forms.iter().any(is_doc_node) {
115 return forms;
116 }
117 let Ok(documented_forms) = crate::parse_program_documented(src) else {
118 return forms;
119 };
120 let mut out = Vec::with_capacity(forms.len() + 1);
121 for f in &documented_forms {
122 if moduledoc_text(f).is_some() {
123 out.push(f.clone());
124 }
125 }
126 for f in forms {
127 let wrapper = documented_forms
128 .iter()
129 .find(|d| d.span == f.span && documented(d).is_some());
130 match wrapper.and_then(|w| w.as_list()) {
131 Some([head, text, def]) => out.push(Spanned::new(
132 f.span,
133 SpannedForm::List(vec![
134 head.clone(),
135 text.clone(),
136 Spanned::new(def.span, f.form),
137 ]),
138 )),
139 _ => out.push(f),
140 }
141 }
142 out
143}
144
145#[must_use]
147pub fn defined_name(def: &Spanned) -> Option<&str> {
148 let items = def.as_list()?;
149 if items.first()?.as_symbol()? == "defmacro" {
150 return items.get(1)?.as_symbol();
151 }
152 crate::scope::defined_name_node(items)?.as_symbol()
153}
154
155#[must_use]
157pub fn is_definition(form: &Spanned) -> bool {
158 defined_name(form).is_some()
159}
160
161#[derive(Clone, Debug, PartialEq, Eq)]
163pub enum Step {
164 Run(String),
166 Check { expr: String, value: String },
168}
169
170#[derive(Clone, Debug, PartialEq, Eq)]
173pub struct Example {
174 pub steps: Vec<(usize, Step)>,
176}
177
178#[must_use]
180pub fn examples(text: &str) -> Vec<Example> {
181 let mut out = Vec::new();
182 let mut current: Vec<(usize, Step)> = Vec::new();
183 let flush = |current: &mut Vec<(usize, Step)>, out: &mut Vec<Example>| {
184 if current.iter().any(|(_, s)| matches!(s, Step::Check { .. })) {
185 out.push(Example {
186 steps: std::mem::take(current),
187 });
188 } else {
189 current.clear();
190 }
191 };
192 for (i, line) in text.lines().enumerate() {
193 let indented = line.starts_with([' ', '\t']) && !line.trim().is_empty();
194 if !indented {
195 flush(&mut current, &mut out);
196 continue;
197 }
198 let code = line.trim();
199 let step = match code.rsplit_once(" #=> ") {
200 Some((expr, value)) => Step::Check {
201 expr: expr.trim().to_string(),
202 value: value.trim().to_string(),
203 },
204 None => Step::Run(code.to_string()),
205 };
206 current.push((i, step));
207 }
208 flush(&mut current, &mut out);
209 out
210}
211
212#[must_use]
214pub fn intent(text: &str) -> String {
215 text.lines()
216 .take_while(|l| !l.trim().is_empty() && !l.starts_with([' ', '\t']))
217 .map(str::trim)
218 .collect::<Vec<_>>()
219 .join(" ")
220}
221
222#[must_use]
224pub fn is_doctest(form: &Spanned) -> bool {
225 match form.as_list() {
226 Some([head, name, _]) => {
227 head.as_symbol() == Some("deftest")
228 && name
229 .as_string()
230 .is_some_and(|n| n.starts_with(DOCTEST_PREFIX))
231 }
232 _ => false,
233 }
234}
235
236pub fn doctests(src: &str) -> Result<Vec<Spanned>, ParseError> {
243 let forms = crate::parse_program_documented(src)?;
244 let mut out = Vec::new();
245 for form in &forms {
246 let (subject, text, text_span) = if let Some((text, def)) = documented(form) {
247 let Some(name) = defined_name(def) else {
248 continue;
249 };
250 let span = form.as_list().map_or(form.span, |items| items[1].span);
251 (format!("`{name}`"), text, span)
252 } else if let Some(text) = moduledoc_text(form) {
253 ("the module".to_string(), text, form.span)
254 } else {
255 continue;
256 };
257 let mut from = text_span.start;
258 for (k, example) in examples(text).iter().enumerate() {
259 out.push(doctest(
260 src,
261 text,
262 &mut from,
263 text_span.end,
264 &subject,
265 k + 1,
266 example,
267 )?);
268 }
269 }
270 Ok(out)
271}
272
273fn doctest(
277 src: &str,
278 text: &str,
279 from: &mut usize,
280 until: usize,
281 subject: &str,
282 k: usize,
283 example: &Example,
284) -> Result<Spanned, ParseError> {
285 let fail = |at: Span, why: &str| {
286 ParseError::new(format!("{subject}'s example {k} {why}"), at).located(src)
287 };
288 let mut stmts = Vec::new();
289 for (line_no, step) in &example.steps {
290 let written = text.lines().nth(*line_no).unwrap_or("").trim();
291 let at = next_line(src, from, until, written)
292 .ok_or_else(|| fail(Span::new(*from, *from), "is not where its text is"))?;
293 match step {
294 Step::Run(code) => {
295 stmts.push(snippet(src, code, at).map_err(|e| fail(e, "does not parse"))?)
296 }
297 Step::Check { expr, value } => {
298 let marker = at + written.rfind(" #=> ").unwrap_or(0);
299 let after = &written[written.rfind(" #=> ").map_or(0, |i| i + 5)..];
300 let value_at = marker + 5 + (after.len() - after.trim_start().len());
301 let e = snippet(src, expr, at).map_err(|e| fail(e, "does not parse"))?;
302 let v = snippet(src, value, value_at).map_err(|e| fail(e, "does not parse"))?;
303 let op = Span::new(marker + 1, marker + 4);
304 let compared = Span::new(e.span.start, v.span.end);
305 let cmp = Spanned::new(
306 compared,
307 SpannedForm::List(vec![symbol(op, "equal?"), e, v]),
308 );
309 let quoted = Spanned::new(compared, SpannedForm::Quote(Box::new(cmp.clone())));
310 stmts.push(Spanned::new(
311 compared,
312 SpannedForm::List(vec![symbol(op, crate::LOWERED_ASSERT), quoted, cmp]),
313 ));
314 }
315 }
316 }
317 let first = stmts.first().map_or(*from, |s| s.span.start);
318 let last = stmts.last().map_or(*from, |s| s.span.end);
319 let whole = Span::new(first, last);
320 let body = if stmts.len() == 1 {
321 stmts.remove(0)
322 } else {
323 let mut items = vec![symbol(Span::new(first, first), "begin")];
324 items.extend(stmts);
325 Spanned::new(whole, SpannedForm::List(items))
326 };
327 Ok(Spanned::new(
328 whole,
329 SpannedForm::List(vec![
330 symbol(Span::new(first, first), "deftest"),
331 text_node(
332 Span::new(first, first),
333 format!("{DOCTEST_PREFIX}{subject} example {k}"),
334 ),
335 body,
336 ]),
337 ))
338}
339
340fn next_line(src: &str, from: &mut usize, until: usize, written: &str) -> Option<usize> {
343 let region = src.get(*from..until.min(src.len()))?;
344 let mut offset = *from;
345 for line in region.split_inclusive('\n') {
346 if line.trim() == written {
347 let code = offset + (line.len() - line.trim_start().len());
348 *from = offset + line.len();
349 return Some(code);
350 }
351 offset += line.len();
352 }
353 None
354}
355
356fn snippet(src: &str, code: &str, at: usize) -> Result<Spanned, Span> {
359 let here = Span::new(at, at + code.len());
360 let mut forms = crate::parse::parse_snippet(code).map_err(|_| here)?;
361 if forms.len() != 1 || src.get(here.start..here.end) != Some(code) {
362 return Err(here);
363 }
364 Ok(shift(forms.remove(0), at))
365}
366
367fn symbol(span: Span, name: &str) -> Spanned {
368 Spanned::new(span, SpannedForm::Atom(Atom::Symbol(name.into())))
369}
370
371fn shift(node: Spanned, by: usize) -> Spanned {
372 let span = if node.span.is_synthetic() {
373 node.span
374 } else {
375 Span::new(node.span.start + by, node.span.end + by)
376 };
377 let form = match node.form {
378 SpannedForm::List(items) => {
379 SpannedForm::List(items.into_iter().map(|i| shift(i, by)).collect())
380 }
381 SpannedForm::Quote(x) => SpannedForm::Quote(Box::new(shift(*x, by))),
382 SpannedForm::Quasiquote(x) => SpannedForm::Quasiquote(Box::new(shift(*x, by))),
383 SpannedForm::Unquote(x) => SpannedForm::Unquote(Box::new(shift(*x, by))),
384 SpannedForm::UnquoteSplice(x) => SpannedForm::UnquoteSplice(Box::new(shift(*x, by))),
385 other => other,
386 };
387 Spanned::new(span, form)
388}
389
390pub(crate) fn text_node(span: Span, text: String) -> Spanned {
392 Spanned::new(span, SpannedForm::Atom(Atom::Str(text.into())))
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398
399 const SRC: &str = "@moduledoc \"\"\"\n m does a thing.\n\n f(1) #=> 2\n \"\"\"\n\n@doc \"\"\"\nf adds one.\n\n x = 1\n f(x) #=> 2\n f(2) #=> 3\n\"\"\"\ndef f(n)\n n + 1\nend\n";
400
401 #[test]
402 fn the_documented_tree_holds_both_kinds_and_the_stripped_one_neither() {
403 let doc = crate::parse_program_documented(SRC).unwrap();
404 assert_eq!(doc.len(), 2);
405 assert_eq!(
406 module_doc(&doc).map(|(t, _)| t),
407 Some("m does a thing.\n\n f(1) #=> 2")
408 );
409 let (text, def) = documented(&doc[1]).unwrap();
410 assert!(text.starts_with("f adds one."));
411 assert_eq!(defined_name(def), Some("f"));
412 let plain = crate::parse_program_tree(SRC).unwrap();
413 assert_eq!(plain.len(), 1);
414 assert_eq!(plain[0].to_sexp().to_string(), "(define (f n) (+ n 1))");
415 assert_eq!(plain[0].span, doc[1].span);
416 }
417
418 #[test]
419 fn redocument_restores_what_strip_took() {
420 let doc = crate::parse_program_documented(SRC).unwrap();
421 let plain = crate::parse_program_tree(SRC).unwrap();
422 assert_eq!(redocument(SRC, plain), doc);
423 }
424
425 #[test]
426 fn examples_and_intent_are_read_from_the_text() {
427 let text = "f adds one,\nto a number.\n\n x = 1\n f(x) #=> 2\n\nprose\n\n g(1)\n";
428 assert_eq!(intent(text), "f adds one, to a number.");
429 let ex = examples(text);
430 assert_eq!(ex.len(), 1, "a paragraph with no `#=>` is not an example");
431 assert_eq!(
432 ex[0].steps,
433 vec![
434 (3, Step::Run("x = 1".into())),
435 (
436 4,
437 Step::Check {
438 expr: "f(x)".into(),
439 value: "2".into()
440 }
441 )
442 ]
443 );
444 }
445
446 #[test]
447 fn each_example_is_one_test_spanned_at_its_lines() {
448 let tests = doctests(SRC).unwrap();
449 assert_eq!(tests.len(), 2);
450 assert!(tests.iter().all(is_doctest));
451 assert_eq!(
452 tests[1].to_sexp().to_string(),
453 "(deftest \"@doc `f` example 1\" (begin (define x 1) (blue-assert '(equal? (f x) 2) (equal? (f x) 2)) (blue-assert '(equal? (f 2) 3) (equal? (f 2) 3))))"
454 );
455 let at = tests[1].span;
456 assert_eq!(
457 &SRC[at.start..at.end],
458 "x = 1\n f(x) #=> 2\n f(2) #=> 3"
459 );
460 fn refs(n: &Spanned, out: &mut Vec<Span>) {
461 if n.as_symbol() == Some("f") {
462 out.push(n.span);
463 }
464 if let Some(items) = n.as_list() {
465 for i in items {
466 refs(i, out);
467 }
468 }
469 }
470 let mut found = Vec::new();
471 refs(&tests[1], &mut found);
472 assert!(!found.is_empty());
473 for s in found {
474 assert_eq!(&SRC[s.start..s.end], "f");
475 }
476 }
477
478 #[test]
479 fn a_doc_must_document_a_definition_and_a_moduledoc_must_lead() {
480 let e = crate::parse_program_documented("@doc \"\"\"\nx\n\"\"\"\nf(1)\n").unwrap_err();
481 assert!(
482 e.message.contains("documents the definition below it"),
483 "{e}"
484 );
485 let e =
486 crate::parse_program_documented("x = 1\n@moduledoc \"\"\"\nm\n\"\"\"\n").unwrap_err();
487 assert!(e.message.contains("first form"), "{e}");
488 let e =
489 crate::parse_program_documented("def f()\n @doc \"\"\"\n x\n \"\"\"\n 1\nend\n")
490 .unwrap_err();
491 assert!(e.message.contains("top level"), "{e}");
492 }
493}