1use std::collections::{BTreeMap, BTreeSet};
33use std::io::Write;
34use std::path::{Path, PathBuf};
35use std::sync::atomic::{AtomicBool, Ordering};
36use std::sync::Arc;
37
38use blue_lang_check::{NameTable, Namespace};
39use blue_lang_waku::{Capability, Reach, Waku, When, Where};
40use tatara_lisp::{Atom, Sexp, Span, Spanned};
41use tatara_lisp_eval::ffi::Arity;
42use tatara_lisp_eval::{EvalError, Interpreter, Value};
43
44use crate::pipeline::{self, RunError};
45use crate::uses::{Entry, Loader, ResolvedProgram};
46
47#[cfg(feature = "sys")]
48pub mod protocol;
49
50#[must_use]
53pub fn pure_frame() -> Waku {
54 frame_granting(&[])
55}
56
57#[must_use]
59pub fn frame_granting(host: &[Capability]) -> Waku {
60 Waku {
61 reach: Reach::only(
62 Capability::ALL
63 .into_iter()
64 .filter(|c| !c.is_host_effect() || host.contains(c)),
65 ),
66 when: When::Anytime,
67 place: Where::Process,
68 }
69}
70
71pub struct Config {
73 pub frame: Waku,
74 pub budget: Option<usize>,
77 pub loader: Box<dyn Loader + Send>,
78 pub interrupt: Arc<AtomicBool>,
80 pub live: Option<Box<dyn Write + Send>>,
82}
83
84impl Config {
85 #[must_use]
87 pub fn new(loader: Box<dyn Loader + Send>) -> Self {
88 Self {
89 frame: pure_frame(),
90 budget: None,
91 loader,
92 interrupt: Arc::new(AtomicBool::new(false)),
93 live: None,
94 }
95 }
96}
97
98#[derive(Clone, Debug, PartialEq, Eq)]
100pub enum Stop {
101 Error(Failure),
102 Volatile {
104 reach: Capability,
105 name: String,
106 },
107 Exhausted {
109 limit: usize,
110 },
111 Interrupted,
112}
113
114#[derive(Clone, Debug, PartialEq, Eq)]
116pub struct Failure {
117 pub code: String,
120 pub message: String,
121}
122
123impl Failure {
124 fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
125 Self {
126 code: code.into(),
127 message: message.into(),
128 }
129 }
130}
131
132#[derive(Debug)]
134pub struct Outcome<T> {
135 pub result: Result<T, Stop>,
136 pub out: String,
137}
138
139#[derive(Clone, Debug, PartialEq, Eq)]
141pub struct Item {
142 pub name: String,
144 pub namespace: String,
145 pub signature: Option<String>,
146 pub doc: Option<String>,
147}
148
149#[derive(Clone, Copy, Debug, PartialEq, Eq)]
151pub enum Load {
152 Whole,
154 Definitions,
157}
158
159#[derive(Clone, Copy, Debug, PartialEq, Eq)]
161pub enum Step {
162 One,
163 All,
164}
165
166#[derive(Default)]
168pub struct Host {
169 out: String,
170 live: Option<Box<dyn Write + Send>>,
171 reached: Option<(Capability, &'static str)>,
172}
173
174impl Host {
175 fn write(&mut self, text: &str) {
176 match &mut self.live {
177 Some(w) => {
178 let _ = w.write_all(text.as_bytes()).and_then(|()| w.flush());
179 }
180 None => self.out.push_str(text),
181 }
182 }
183}
184
185#[derive(Clone, Debug)]
187struct Form {
188 text: String,
191 at: usize,
193 defines: Vec<String>,
194 is_use: bool,
195 fresh: Option<(usize, Option<usize>)>,
198}
199
200impl Form {
201 fn persists(&self) -> bool {
203 !self.defines.is_empty() || self.is_use
204 }
205
206 fn settled(mut self) -> Self {
207 self.fresh = None;
208 self
209 }
210}
211
212fn splice(forms: &mut Vec<Form>, form: Form) {
215 let same = |f: &Form| f.defines.iter().any(|d| form.defines.contains(d));
216 match forms.iter().position(same) {
217 Some(first) => {
218 let mut i = forms.len();
219 while i > first + 1 {
220 i -= 1;
221 if same(&forms[i]) {
222 forms.remove(i);
223 }
224 }
225 forms[first] = form;
226 }
227 None => forms.push(form),
228 }
229}
230
231#[derive(Default)]
233struct Context {
234 path: Option<PathBuf>,
235 forms: Vec<Form>,
236 program: Option<ResolvedProgram>,
238 names: Option<NameTable>,
239}
240
241pub struct Session {
243 interp: Interpreter<Host>,
244 host: Host,
245 frame: Waku,
246 budget: Option<usize>,
247 loader: Box<dyn Loader + Send>,
248 interrupt: Arc<AtomicBool>,
249 builtins: NameTable,
252 contexts: BTreeMap<Option<PathBuf>, Context>,
253 packages: BTreeSet<String>,
255}
256
257const CODE_LABEL: &str = "<eval>";
258
259impl Session {
260 #[must_use]
261 pub fn new(config: Config) -> Self {
262 let mut host = Host {
263 live: config.live,
264 ..Host::default()
265 };
266 let interp = build(&config.frame, &config.interrupt, &mut host);
267 let builtins = pipeline::builtin_names(&interp);
268 Self {
269 interp,
270 host,
271 frame: config.frame,
272 budget: config.budget,
273 loader: config.loader,
274 interrupt: config.interrupt,
275 builtins,
276 contexts: BTreeMap::new(),
277 packages: BTreeSet::new(),
278 }
279 }
280
281 #[must_use]
283 pub fn interrupt_flag(&self) -> Arc<AtomicBool> {
284 Arc::clone(&self.interrupt)
285 }
286
287 #[must_use]
288 pub fn frame(&self) -> &Waku {
289 &self.frame
290 }
291
292 pub fn reset(&mut self) {
294 let live = self.host.live.take();
295 self.host = Host {
296 live,
297 ..Host::default()
298 };
299 self.interp = build(&self.frame, &self.interrupt, &mut self.host);
300 self.contexts.clear();
301 self.packages.clear();
302 }
303
304 pub fn load(&mut self, file: &Path, text: Option<&str>, how: Load) -> Outcome<Vec<String>> {
308 let result = self.load_inner(file, text, how);
309 self.finish(result)
310 }
311
312 pub fn eval(
316 &mut self,
317 code: &str,
318 file: Option<&Path>,
319 budget: Option<usize>,
320 ) -> Outcome<String> {
321 let result = self
322 .context_key(file)
323 .and_then(|key| self.run_code(&key, code, budget, Action::Evaluate))
324 .map(|v| render(&v));
325 self.finish(result)
326 }
327
328 pub fn expand(&mut self, code: &str, file: Option<&Path>, step: Step) -> Outcome<String> {
331 let result = self
332 .context_key(file)
333 .and_then(|key| self.run_code(&key, code, None, Action::Expand(step)))
334 .and_then(|v| match crate::literal::value_to_sexp(&v) {
335 Some(s) => Ok(crate::literal::format(&unkey(s))),
336 None => Err(Stop::Error(Failure::new(
337 "runtime",
338 "the expansion is not a form",
339 ))),
340 });
341 self.finish(result)
342 }
343
344 #[must_use]
347 pub fn complete(&self, prefix: &str, file: Option<&Path>) -> Vec<Item> {
348 let mut seen = BTreeSet::new();
349 let mut out = Vec::new();
350 let (table, ctx) = self.table(file);
351 for (ns, b) in table.all() {
352 let name = spelled(ns, &b.name);
353 let spellable = name
354 .chars()
355 .next()
356 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
357 && !name.contains('-')
358 && !name.contains('%');
359 if !spellable || !name.starts_with(prefix) || !seen.insert(name.clone()) {
360 continue;
361 }
362 out.push(self.describe(ns, b, name, ctx));
363 }
364 out
365 }
366
367 #[must_use]
371 pub fn doc(&self, name: &str, file: Option<&Path>) -> Option<Item> {
372 let (table, ctx) = self.table(file);
373 let (want_ns, bare) = match name.split_once("::") {
374 Some((blue_lang_syntax::BUILTIN_QUALIFIER, bare)) => (Some(Namespace::Builtin), bare),
375 Some((pkg, bare)) => (Some(Namespace::Bidama(pkg.to_string())), bare),
376 None => (None, name),
377 };
378 table
379 .all()
380 .find(|(ns, b)| {
381 b.name == bare
382 && want_ns.as_ref().is_none_or(|w| w == *ns)
383 && !matches!(ns, Namespace::Harness)
384 })
385 .map(|(ns, b)| self.describe(ns, b, spelled(ns, &b.name), ctx))
386 }
387
388 fn table(&self, file: Option<&Path>) -> (&NameTable, Option<&Context>) {
389 let key = file.map(canonical);
390 let ctx = self.contexts.get(&key);
391 match ctx.and_then(|c| c.names.as_ref()) {
392 Some(t) => (t, ctx),
393 None => (&self.builtins, ctx),
394 }
395 }
396
397 fn describe(
398 &self,
399 ns: &Namespace,
400 b: &blue_lang_check::Binding,
401 name: String,
402 ctx: Option<&Context>,
403 ) -> Item {
404 let (signature, doc) = match (b.top_level, ctx.and_then(|c| c.program.as_ref())) {
405 (Some(i), Some(program)) => definition_doc(program, i),
406 _ => match crate::docs::doc_of(&b.name) {
407 Some(d) => (Some(d.signature.to_string()), Some(d.doc.to_string())),
408 None => (None, None),
409 },
410 };
411 Item {
412 name,
413 namespace: ns.to_string(),
414 signature,
415 doc,
416 }
417 }
418
419 fn finish<T>(&mut self, result: Result<T, Stop>) -> Outcome<T> {
420 Outcome {
421 result,
422 out: std::mem::take(&mut self.host.out),
423 }
424 }
425
426 fn context_key(&mut self, file: Option<&Path>) -> Result<Option<PathBuf>, Stop> {
427 let key = file.map(canonical);
428 if let Some(path) = &key {
429 if !self.contexts.contains_key(&key) {
430 self.load_inner(path, None, Load::Definitions)?;
431 }
432 }
433 self.contexts.entry(key.clone()).or_default();
434 Ok(key)
435 }
436
437 fn load_inner(
438 &mut self,
439 file: &Path,
440 text: Option<&str>,
441 how: Load,
442 ) -> Result<Vec<String>, Stop> {
443 let path = canonical(file);
444 let text = match text {
445 Some(t) => t.to_string(),
446 None => std::fs::read_to_string(&path).map_err(|e| {
447 Stop::Error(Failure::new(
448 "not-found",
449 format!("{}: {e}", file.display()),
450 ))
451 })?,
452 };
453 let mut forms = split(&text)
454 .map_err(|e| Stop::Error(Failure::new("parse", format!("{}:{e}", file.display()))))?;
455 for (order, f) in forms.iter_mut().enumerate() {
456 if how == Load::Whole || f.persists() {
457 f.fresh = Some((order, None));
458 }
459 }
460 let defined: Vec<String> = forms.iter().flat_map(|f| f.defines.clone()).collect();
461 let key = Some(path.clone());
462 let mut ctx = Context {
463 path: Some(path),
464 ..Context::default()
465 };
466 let run = self.execute(&mut ctx, forms, "", None, Action::Load);
467 self.contexts.insert(key, ctx);
470 run.map(|_| defined)
471 }
472
473 fn run_code(
474 &mut self,
475 key: &Option<PathBuf>,
476 code: &str,
477 budget: Option<usize>,
478 action: Action,
479 ) -> Result<Value, Stop> {
480 let new = split(code)
481 .map_err(|e| Stop::Error(Failure::new("parse", format!("{CODE_LABEL}:{e}"))))?;
482 let mut ctx = self.contexts.remove(key).unwrap_or_default();
483 let mut forms = ctx.forms.clone();
484 let mut code_at = 0;
485 for (order, mut form) in new.into_iter().enumerate() {
486 form.fresh = Some((order, Some(code_at + form.at)));
487 code_at += form.text.len();
488 form.text.insert(0, '\n');
489 form.at += 1;
490 if form.defines.is_empty() {
491 forms.push(form);
492 } else {
493 splice(&mut forms, form);
494 }
495 }
496 let result = self.execute(&mut ctx, forms, code, budget, action);
497 self.contexts.insert(key.clone(), ctx);
498 result
499 }
500
501 fn execute(
506 &mut self,
507 ctx: &mut Context,
508 forms: Vec<Form>,
509 code: &str,
510 budget: Option<usize>,
511 action: Action,
512 ) -> Result<Value, Stop> {
513 let mut text = String::new();
514 let mut fresh_at: BTreeMap<usize, (usize, Option<usize>, usize)> = BTreeMap::new();
517 for (i, f) in forms.iter().enumerate() {
518 if let Some((order, code_off)) = f.fresh {
519 fresh_at.insert(text.len() + f.at, (order, code_off, i));
520 }
521 text.push_str(&f.text);
522 }
523
524 let entry = Entry {
525 path: ctx.path.as_deref(),
526 text: &text,
527 };
528 let mut program = pipeline::parse_and_resolve(entry, self.loader.as_ref(), None)
529 .map_err(|e| Stop::Error(run_failure(e)))?;
530 program.retain(|f| !crate::uses::is_test_form(f));
531 let (mut outcome, names) = pipeline::check_stage(&program, &self.builtins, false);
532 outcome.diagnostics.retain(|d| {
533 !matches!(
534 d.code,
535 blue_lang_check::Code::B0023
536 | blue_lang_check::Code::B0036
537 | blue_lang_check::Code::B0037
538 )
539 });
540 let place = |top_level: usize, span: Span, message: &str| -> String {
541 let in_code = (program.owner_of(top_level) == Some(ResolvedProgram::ENTRY)
542 && !span.is_synthetic())
543 .then(|| in_code(&fresh_at, &forms, span))
544 .flatten();
545 match in_code {
546 Some(at) => {
547 let (line, col) = Span::line_col(code, at.min(code.len()));
548 format!("{CODE_LABEL}:{line}:{col}: {message}")
549 }
550 None => program.locate(top_level, span, message).to_string(),
551 }
552 };
553 if !outcome.ok() {
554 let code = outcome
555 .errors()
556 .next()
557 .map_or_else(|| "check".to_string(), |d| d.code.to_string());
558 let message = outcome
559 .errors()
560 .map(|d| {
561 let mut line = place(d.top_level, d.span, &d.to_string());
562 if let Some(help) = &d.help {
563 line.push_str("\n help: ");
564 line.push_str(help);
565 }
566 line
567 })
568 .collect::<Vec<_>>()
569 .join("\n");
570 return Err(Stop::Error(Failure::new(code, message)));
571 }
572 let erased = crate::erase::erase_types(&pipeline::lower(&program, &names));
573
574 let mut imported = Vec::new();
577 let mut asked: Vec<(usize, usize)> = Vec::new();
578 let mut new_packages = BTreeSet::new();
579 for (i, form) in program.forms().iter().enumerate() {
580 let owner = program.owner_of(i);
581 if owner == Some(ResolvedProgram::ENTRY) {
582 if let Some((order, _, _)) = fresh_at.get(&form.span.start) {
583 asked.push((*order, i));
584 }
585 continue;
586 }
587 let package = owner
588 .and_then(|o| program.file(o))
589 .and_then(|f| f.package.clone());
590 if let Some(p) = package.filter(|p| !self.packages.contains(p)) {
591 new_packages.insert(p);
592 imported.push(i);
593 }
594 }
595 asked.sort_unstable();
596
597 let limit = budget
598 .or(self.budget)
599 .or(crate::execution_bounds().max_steps);
600 let _ = self.interp.set_budget(tatara_lisp_eval::vm::Budget {
601 fuel: limit,
602 max_depth: Some(crate::execution_bounds().max_call_depth),
603 quantum: None,
604 });
605 self.host.reached = None;
606
607 let mut value = Value::Nil;
608 let mut ran: BTreeSet<usize> = BTreeSet::new();
609 let mut failed = None;
610 for &i in &imported {
611 if let Err(e) = self.interp.eval_top_form(&erased[i], &mut self.host) {
612 failed = Some((i, e));
613 break;
614 }
615 }
616 if failed.is_none() {
617 self.packages.extend(new_packages);
618 for &(order, i) in &asked {
619 let step = match action {
620 Action::Expand(step) => {
621 expand_form(&mut self.interp, &mut self.host, &erased[i], step)
622 }
623 Action::Evaluate | Action::Load => {
624 self.interp.eval_top_form(&erased[i], &mut self.host)
625 }
626 };
627 match step {
628 Ok(v) => {
629 value = v;
630 ran.insert(order);
631 }
632 Err(e) => {
633 failed = Some((i, e));
634 break;
635 }
636 }
637 }
638 }
639 let stop = failed.map(|(i, e)| self.classify(&e, &erased, |span, msg| place(i, span, msg)));
640
641 match action {
642 Action::Load => ctx.forms = forms.into_iter().map(Form::settled).collect(),
643 Action::Evaluate => {
644 for f in forms {
645 if f.persists() && f.fresh.is_some_and(|(order, _)| ran.contains(&order)) {
646 splice(&mut ctx.forms, f.settled());
647 }
648 }
649 }
650 Action::Expand(_) => {}
651 }
652 if action != Action::Expand(Step::One) && action != Action::Expand(Step::All) {
653 ctx.program = Some(program);
654 ctx.names = Some(names);
655 }
656 if let Some((reach, name)) = self.host.reached.take() {
657 return Err(Stop::Volatile {
658 reach,
659 name: name.to_string(),
660 });
661 }
662 match stop {
663 Some(s) => Err(s),
664 None => Ok(value),
665 }
666 }
667
668 fn classify(
669 &self,
670 e: &EvalError,
671 erased: &[Spanned],
672 place: impl Fn(Span, &str) -> String,
673 ) -> Stop {
674 match e {
675 EvalError::Halted => {
676 self.interrupt.store(false, Ordering::SeqCst);
677 Stop::Interrupted
678 }
679 EvalError::BudgetExceeded {
680 dimension: tatara_lisp_eval::error::BudgetDimension::Fuel,
681 limit,
682 ..
683 } => Stop::Exhausted { limit: *limit },
684 other => {
685 let at = other.span().unwrap_or_else(Span::synthetic);
686 let message = pipeline::display_keys(&crate::messages::describe(other, erased));
687 Stop::Error(Failure::new("runtime", place(at, &message)))
688 }
689 }
690 }
691}
692
693#[derive(Clone, Copy, Debug, PartialEq, Eq)]
694enum Action {
695 Load,
696 Evaluate,
697 Expand(Step),
698}
699
700fn in_code(
703 fresh_at: &BTreeMap<usize, (usize, Option<usize>, usize)>,
704 forms: &[Form],
705 span: Span,
706) -> Option<usize> {
707 let (start, (_, code_off, i)) = fresh_at.range(..=span.start).next_back()?;
708 let len = forms[*i].text.len() - forms[*i].at;
709 (span.start < start + len).then(|| (*code_off).map(|c| c + (span.start - start)))?
710}
711
712fn expand_form(
713 interp: &mut Interpreter<Host>,
714 host: &mut Host,
715 form: &Spanned,
716 step: Step,
717) -> tatara_lisp_eval::Result<Value> {
718 let head = match step {
719 Step::One => "macroexpand-1",
720 Step::All => "macroexpand",
721 };
722 let call = Sexp::List(vec![
723 Sexp::Atom(Atom::Symbol(head.to_string())),
724 Sexp::List(vec![
725 Sexp::Atom(Atom::Symbol("quote".to_string())),
726 form.to_sexp(),
727 ]),
728 ]);
729 interp.eval_spanned(&Spanned::from_sexp_synthetic(&call), host)
730}
731
732fn run_failure(e: RunError) -> Failure {
733 let code = match &e {
734 RunError::Parse(_) | RunError::Lower(_) => "parse",
735 RunError::Import(_) => "import",
736 RunError::Types(_) => "check",
737 RunError::Eval(_) => "runtime",
738 };
739 Failure::new(code, e.to_string())
740}
741
742fn build(frame: &Waku, interrupt: &Arc<AtomicBool>, host: &mut Host) -> Interpreter<Host> {
745 let mut interp = crate::interpreter(host);
746 capture_output(&mut interp);
747 for cap in Capability::host_effects() {
748 if frame.reach.grants(cap) {
749 continue;
750 }
751 for name in cap.names() {
752 if interp.lookup_global(name).is_none() {
753 continue;
754 }
755 interp.register_fn(name, Arity::Any, move |_: &[Value], h: &mut Host, span| {
756 h.reached.get_or_insert((cap, name));
757 Err(EvalError::native_fn(
758 name,
759 format!(
760 "refused: `{name}` reaches the host ({}), which this session's frame does not grant",
761 cap.label()
762 ),
763 span,
764 ))
765 });
766 }
767 }
768 crate::attach_node_in_frame(&mut interp, frame);
770 interp.set_interrupt(Arc::clone(interrupt));
771 interp
772}
773
774fn capture_output(interp: &mut Interpreter<Host>) {
777 interp.register_fn(
778 "write_stdout",
779 Arity::Exact(1),
780 |a: &[Value], h: &mut Host, s| match &a[0] {
781 Value::Str(t) => {
782 h.write(t);
783 Ok(Value::Nil)
784 }
785 other => Err(EvalError::type_mismatch("string", other.type_name(), s)),
786 },
787 );
788 interp.register_fn(
789 "display",
790 Arity::Exact(1),
791 |a: &[Value], h: &mut Host, _| {
792 h.write(&a[0].to_string());
793 Ok(Value::Nil)
794 },
795 );
796 interp.register_fn("print", Arity::Exact(1), |a: &[Value], h: &mut Host, _| {
797 h.write(&format!("{}\n", a[0]));
798 Ok(Value::Nil)
799 });
800 interp.register_fn(
801 "newline",
802 Arity::Exact(0),
803 |_: &[Value], h: &mut Host, _| {
804 h.write("\n");
805 Ok(Value::Nil)
806 },
807 );
808 interp.register_fn("println", Arity::Any, |a: &[Value], h: &mut Host, _| {
809 let line = a
810 .iter()
811 .map(ToString::to_string)
812 .collect::<Vec<_>>()
813 .join(" ");
814 h.write(&format!("{line}\n"));
815 Ok(Value::Nil)
816 });
817}
818
819fn split(text: &str) -> Result<Vec<Form>, blue_lang_syntax::ParseError> {
821 let tree = blue_lang_syntax::parse_program_tree(text)?;
822 let mut out = Vec::with_capacity(tree.len());
823 let mut prev = 0;
824 let last = tree.len().saturating_sub(1);
825 for (i, form) in tree.iter().enumerate() {
826 let end = if i == last { text.len() } else { form.span.end };
827 out.push(Form {
828 text: text[prev..end].to_string(),
829 at: form.span.start - prev,
830 defines: blue_lang_syntax::scope::definitions_of(form)
831 .into_iter()
832 .map(|(n, _, _)| n)
833 .collect(),
834 is_use: blue_lang_syntax::scope::use_target(form).is_some()
835 || blue_lang_syntax::scope::legacy_target(form).is_some(),
836 fresh: None,
837 });
838 prev = end;
839 }
840 Ok(out)
841}
842
843#[must_use]
846pub fn incomplete(code: &str) -> bool {
847 match blue_lang_syntax::parse_program_tree(code) {
848 Ok(_) => false,
849 Err(e) => e.span.end >= code.trim_end().len() || e.message.starts_with("unterminated"),
850 }
851}
852
853#[must_use]
855pub fn render(v: &Value) -> String {
856 match crate::literal::value_literal(v) {
857 Some(Sexp::Atom(Atom::Symbol(s))) => unkey_name(&s),
858 Some(s) => crate::literal::format(&unkey(s)),
859 None => match v {
860 Value::Closure(_) => "<function>".to_string(),
861 Value::NativeFn(f) => format!("<builtin {}>", f.name),
862 other => other.to_string(),
863 },
864 }
865}
866
867fn unkey(s: Sexp) -> Sexp {
870 match s {
871 Sexp::Atom(Atom::Symbol(n)) => Sexp::Atom(Atom::Symbol(unkey_name(&n))),
872 Sexp::List(items) => Sexp::List(items.into_iter().map(unkey).collect()),
873 other => other,
874 }
875}
876
877fn unkey_name(n: &str) -> String {
878 let root = format!(
879 "{}{}",
880 blue_lang_check::names::ROOT_QUALIFIER,
881 blue_lang_syntax::QUALIFIER
882 );
883 n.strip_prefix(&root).unwrap_or(n).to_string()
884}
885
886fn spelled(ns: &Namespace, name: &str) -> String {
888 match ns {
889 Namespace::Bidama(p) => format!("{p}::{name}"),
890 _ => name.to_string(),
891 }
892}
893
894fn definition_doc(program: &ResolvedProgram, i: usize) -> (Option<String>, Option<String>) {
897 let Some(form) = program.forms().get(i) else {
898 return (None, None);
899 };
900 let rendered = crate::literal::format(&unkey(form.to_sexp()));
901 let signature = rendered.lines().next().map(str::to_string);
902 let text = program
903 .owner_of(i)
904 .and_then(|o| program.file(o))
905 .map(|f| f.text.as_str())
906 .unwrap_or_default();
907 let before = &text[..form.span.start.min(text.len())];
908 let mut lines: Vec<&str> = before
909 .lines()
910 .rev()
911 .skip_while(|l| l.trim().is_empty() && !before.ends_with('\n'))
912 .take_while(|l| l.trim_start().starts_with('#'))
913 .collect();
914 lines.reverse();
915 let doc = lines
916 .iter()
917 .map(|l| l.trim_start().trim_start_matches('#').trim())
918 .filter(|l| !l.starts_with("waive "))
919 .collect::<Vec<_>>()
920 .join(" ");
921 (signature, (!doc.is_empty()).then_some(doc))
922}
923
924fn canonical(p: &Path) -> PathBuf {
925 std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf())
926}
927
928#[cfg(test)]
929mod tests;