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