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 (outcome, names) = pipeline::check_stage(&program, &self.builtins, false);
532 let place = |top_level: usize, span: Span, message: &str| -> String {
533 let in_code = (program.owner_of(top_level) == Some(ResolvedProgram::ENTRY)
534 && !span.is_synthetic())
535 .then(|| in_code(&fresh_at, &forms, span))
536 .flatten();
537 match in_code {
538 Some(at) => {
539 let (line, col) = Span::line_col(code, at.min(code.len()));
540 format!("{CODE_LABEL}:{line}:{col}: {message}")
541 }
542 None => program.locate(top_level, span, message).to_string(),
543 }
544 };
545 if !outcome.ok() {
546 let code = outcome
547 .errors()
548 .next()
549 .map_or_else(|| "check".to_string(), |d| d.code.to_string());
550 let message = outcome
551 .errors()
552 .map(|d| {
553 let mut line = place(d.top_level, d.span, &d.to_string());
554 if let Some(help) = &d.help {
555 line.push_str("\n help: ");
556 line.push_str(help);
557 }
558 line
559 })
560 .collect::<Vec<_>>()
561 .join("\n");
562 return Err(Stop::Error(Failure::new(code, message)));
563 }
564 let erased = crate::erase::erase_types(&pipeline::lower(&program, &names));
565
566 let mut imported = Vec::new();
569 let mut asked: Vec<(usize, usize)> = Vec::new();
570 let mut new_packages = BTreeSet::new();
571 for (i, form) in program.forms().iter().enumerate() {
572 let owner = program.owner_of(i);
573 if owner == Some(ResolvedProgram::ENTRY) {
574 if let Some((order, _, _)) = fresh_at.get(&form.span.start) {
575 asked.push((*order, i));
576 }
577 continue;
578 }
579 let package = owner
580 .and_then(|o| program.file(o))
581 .and_then(|f| f.package.clone());
582 if let Some(p) = package.filter(|p| !self.packages.contains(p)) {
583 new_packages.insert(p);
584 imported.push(i);
585 }
586 }
587 asked.sort_unstable();
588
589 let limit = budget
590 .or(self.budget)
591 .or(crate::execution_bounds().max_steps);
592 let _ = self.interp.set_budget(tatara_lisp_eval::vm::Budget {
593 fuel: limit,
594 max_depth: Some(crate::execution_bounds().max_call_depth),
595 quantum: None,
596 });
597 self.host.reached = None;
598
599 let mut value = Value::Nil;
600 let mut ran: BTreeSet<usize> = BTreeSet::new();
601 let mut failed = None;
602 for &i in &imported {
603 if let Err(e) = self.interp.eval_top_form(&erased[i], &mut self.host) {
604 failed = Some((i, e));
605 break;
606 }
607 }
608 if failed.is_none() {
609 self.packages.extend(new_packages);
610 for &(order, i) in &asked {
611 let step = match action {
612 Action::Expand(step) => {
613 expand_form(&mut self.interp, &mut self.host, &erased[i], step)
614 }
615 Action::Evaluate | Action::Load => {
616 self.interp.eval_top_form(&erased[i], &mut self.host)
617 }
618 };
619 match step {
620 Ok(v) => {
621 value = v;
622 ran.insert(order);
623 }
624 Err(e) => {
625 failed = Some((i, e));
626 break;
627 }
628 }
629 }
630 }
631 let stop = failed.map(|(i, e)| self.classify(&e, &erased, |span, msg| place(i, span, msg)));
632
633 match action {
634 Action::Load => ctx.forms = forms.into_iter().map(Form::settled).collect(),
635 Action::Evaluate => {
636 for f in forms {
637 if f.persists() && f.fresh.is_some_and(|(order, _)| ran.contains(&order)) {
638 splice(&mut ctx.forms, f.settled());
639 }
640 }
641 }
642 Action::Expand(_) => {}
643 }
644 if action != Action::Expand(Step::One) && action != Action::Expand(Step::All) {
645 ctx.program = Some(program);
646 ctx.names = Some(names);
647 }
648 if let Some((reach, name)) = self.host.reached.take() {
649 return Err(Stop::Volatile {
650 reach,
651 name: name.to_string(),
652 });
653 }
654 match stop {
655 Some(s) => Err(s),
656 None => Ok(value),
657 }
658 }
659
660 fn classify(
661 &self,
662 e: &EvalError,
663 erased: &[Spanned],
664 place: impl Fn(Span, &str) -> String,
665 ) -> Stop {
666 match e {
667 EvalError::Halted => {
668 self.interrupt.store(false, Ordering::SeqCst);
669 Stop::Interrupted
670 }
671 EvalError::BudgetExceeded {
672 dimension: tatara_lisp_eval::error::BudgetDimension::Fuel,
673 limit,
674 ..
675 } => Stop::Exhausted { limit: *limit },
676 other => {
677 let at = other.span().unwrap_or_else(Span::synthetic);
678 let message = pipeline::display_keys(&crate::messages::describe(other, erased));
679 Stop::Error(Failure::new("runtime", place(at, &message)))
680 }
681 }
682 }
683}
684
685#[derive(Clone, Copy, Debug, PartialEq, Eq)]
686enum Action {
687 Load,
688 Evaluate,
689 Expand(Step),
690}
691
692fn in_code(
695 fresh_at: &BTreeMap<usize, (usize, Option<usize>, usize)>,
696 forms: &[Form],
697 span: Span,
698) -> Option<usize> {
699 let (start, (_, code_off, i)) = fresh_at.range(..=span.start).next_back()?;
700 let len = forms[*i].text.len() - forms[*i].at;
701 (span.start < start + len).then(|| (*code_off).map(|c| c + (span.start - start)))?
702}
703
704fn expand_form(
705 interp: &mut Interpreter<Host>,
706 host: &mut Host,
707 form: &Spanned,
708 step: Step,
709) -> tatara_lisp_eval::Result<Value> {
710 let head = match step {
711 Step::One => "macroexpand-1",
712 Step::All => "macroexpand",
713 };
714 let call = Sexp::List(vec![
715 Sexp::Atom(Atom::Symbol(head.to_string())),
716 Sexp::List(vec![
717 Sexp::Atom(Atom::Symbol("quote".to_string())),
718 form.to_sexp(),
719 ]),
720 ]);
721 interp.eval_spanned(&Spanned::from_sexp_synthetic(&call), host)
722}
723
724fn run_failure(e: RunError) -> Failure {
725 let code = match &e {
726 RunError::Parse(_) | RunError::Lower(_) => "parse",
727 RunError::Import(_) => "import",
728 RunError::Types(_) => "check",
729 RunError::Eval(_) => "runtime",
730 };
731 Failure::new(code, e.to_string())
732}
733
734fn build(frame: &Waku, interrupt: &Arc<AtomicBool>, host: &mut Host) -> Interpreter<Host> {
737 let mut interp = crate::interpreter(host);
738 capture_output(&mut interp);
739 for cap in Capability::host_effects() {
740 if frame.reach.grants(cap) {
741 continue;
742 }
743 for name in cap.names() {
744 if interp.lookup_global(name).is_none() {
745 continue;
746 }
747 interp.register_fn(name, Arity::Any, move |_: &[Value], h: &mut Host, span| {
748 h.reached.get_or_insert((cap, name));
749 Err(EvalError::native_fn(
750 name,
751 format!(
752 "refused: `{name}` reaches the host ({}), which this session's frame does not grant",
753 cap.label()
754 ),
755 span,
756 ))
757 });
758 }
759 }
760 crate::attach_node_in_frame(&mut interp, frame);
762 interp.set_interrupt(Arc::clone(interrupt));
763 interp
764}
765
766fn capture_output(interp: &mut Interpreter<Host>) {
769 interp.register_fn(
770 "write_stdout",
771 Arity::Exact(1),
772 |a: &[Value], h: &mut Host, s| match &a[0] {
773 Value::Str(t) => {
774 h.write(t);
775 Ok(Value::Nil)
776 }
777 other => Err(EvalError::type_mismatch("string", other.type_name(), s)),
778 },
779 );
780 interp.register_fn(
781 "display",
782 Arity::Exact(1),
783 |a: &[Value], h: &mut Host, _| {
784 h.write(&a[0].to_string());
785 Ok(Value::Nil)
786 },
787 );
788 interp.register_fn("print", Arity::Exact(1), |a: &[Value], h: &mut Host, _| {
789 h.write(&format!("{}\n", a[0]));
790 Ok(Value::Nil)
791 });
792 interp.register_fn(
793 "newline",
794 Arity::Exact(0),
795 |_: &[Value], h: &mut Host, _| {
796 h.write("\n");
797 Ok(Value::Nil)
798 },
799 );
800 interp.register_fn("println", Arity::Any, |a: &[Value], h: &mut Host, _| {
801 let line = a
802 .iter()
803 .map(ToString::to_string)
804 .collect::<Vec<_>>()
805 .join(" ");
806 h.write(&format!("{line}\n"));
807 Ok(Value::Nil)
808 });
809}
810
811fn split(text: &str) -> Result<Vec<Form>, blue_lang_syntax::ParseError> {
813 let tree = blue_lang_syntax::parse_program_tree(text)?;
814 let mut out = Vec::with_capacity(tree.len());
815 let mut prev = 0;
816 let last = tree.len().saturating_sub(1);
817 for (i, form) in tree.iter().enumerate() {
818 let end = if i == last { text.len() } else { form.span.end };
819 out.push(Form {
820 text: text[prev..end].to_string(),
821 at: form.span.start - prev,
822 defines: blue_lang_syntax::scope::definitions_of(form)
823 .into_iter()
824 .map(|(n, _, _)| n)
825 .collect(),
826 is_use: blue_lang_syntax::scope::use_target(form).is_some()
827 || blue_lang_syntax::scope::legacy_target(form).is_some(),
828 fresh: None,
829 });
830 prev = end;
831 }
832 Ok(out)
833}
834
835#[must_use]
838pub fn incomplete(code: &str) -> bool {
839 match blue_lang_syntax::parse_program_tree(code) {
840 Ok(_) => false,
841 Err(e) => e.span.end >= code.trim_end().len() || e.message.starts_with("unterminated"),
842 }
843}
844
845#[must_use]
847pub fn render(v: &Value) -> String {
848 match crate::literal::value_literal(v) {
849 Some(Sexp::Atom(Atom::Symbol(s))) => unkey_name(&s),
850 Some(s) => crate::literal::format(&unkey(s)),
851 None => match v {
852 Value::Closure(_) => "<function>".to_string(),
853 Value::NativeFn(f) => format!("<builtin {}>", f.name),
854 other => other.to_string(),
855 },
856 }
857}
858
859fn unkey(s: Sexp) -> Sexp {
862 match s {
863 Sexp::Atom(Atom::Symbol(n)) => Sexp::Atom(Atom::Symbol(unkey_name(&n))),
864 Sexp::List(items) => Sexp::List(items.into_iter().map(unkey).collect()),
865 other => other,
866 }
867}
868
869fn unkey_name(n: &str) -> String {
870 let root = format!(
871 "{}{}",
872 blue_lang_check::names::ROOT_QUALIFIER,
873 blue_lang_syntax::QUALIFIER
874 );
875 n.strip_prefix(&root).unwrap_or(n).to_string()
876}
877
878fn spelled(ns: &Namespace, name: &str) -> String {
880 match ns {
881 Namespace::Bidama(p) => format!("{p}::{name}"),
882 _ => name.to_string(),
883 }
884}
885
886fn definition_doc(program: &ResolvedProgram, i: usize) -> (Option<String>, Option<String>) {
889 let Some(form) = program.forms().get(i) else {
890 return (None, None);
891 };
892 let rendered = crate::literal::format(&unkey(form.to_sexp()));
893 let signature = rendered.lines().next().map(str::to_string);
894 let text = program
895 .owner_of(i)
896 .and_then(|o| program.file(o))
897 .map(|f| f.text.as_str())
898 .unwrap_or_default();
899 let before = &text[..form.span.start.min(text.len())];
900 let mut lines: Vec<&str> = before
901 .lines()
902 .rev()
903 .skip_while(|l| l.trim().is_empty() && !before.ends_with('\n'))
904 .take_while(|l| l.trim_start().starts_with('#'))
905 .collect();
906 lines.reverse();
907 let doc = lines
908 .iter()
909 .map(|l| l.trim_start().trim_start_matches('#').trim())
910 .filter(|l| !l.starts_with("waive "))
911 .collect::<Vec<_>>()
912 .join(" ");
913 (signature, (!doc.is_empty()).then_some(doc))
914}
915
916fn canonical(p: &Path) -> PathBuf {
917 std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf())
918}
919
920#[cfg(test)]
921mod tests;