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