1use tatara_lisp::Sexp;
17use tatara_lisp_eval::Value;
18
19use blue_lang_check::{NameTable, Namespace};
20
21use crate::erase::erase_types;
22use crate::inputs::Inputs;
23use crate::uses::Entry;
24
25#[derive(Debug, thiserror::Error)]
27pub enum RunError {
28 #[error("parse error: {0}")]
29 Parse(String),
30 #[error("{} error(s) in the check stage:\n{}", .0.len(), .0.join("\n"))]
42 Types(Vec<String>),
43 #[error("the emitted tatara-lisp could not be read back: {0}")]
55 Lower(String),
56 #[error("runtime error: {0}")]
65 Eval(Box<crate::observe::Fault>),
66 #[error("import error: {0}")]
73 Import(String),
74}
75
76impl RunError {
77 #[must_use]
80 pub fn exit_status(&self) -> u8 {
81 match self {
82 RunError::Eval(fault) if fault.breach => crate::BREACH_EXIT,
83 _ => 1,
84 }
85 }
86}
87
88#[derive(Debug)]
90pub struct Run {
91 pub value: Value,
92 pub visited: usize,
96 pub typed_decls: usize,
98 pub seams: usize,
100}
101
102pub fn parse(src: &str) -> Result<Vec<Sexp>, RunError> {
104 parse_with_depth(src, blue_lang_syntax::MAX_EXPR_DEPTH)
105}
106
107pub fn parse_with_depth(src: &str, max_depth: usize) -> Result<Vec<Sexp>, RunError> {
114 blue_lang_syntax::parse_program_with_depth(src, max_depth)
115 .map_err(|e| RunError::Parse(e.to_string()))
116}
117
118pub fn parse_tree_with_depth(
126 src: &str,
127 max_depth: usize,
128) -> Result<Vec<blue_lang_syntax::Spanned>, RunError> {
129 blue_lang_syntax::parse_program_tree_with_depth(src, max_depth)
130 .map_err(|e| RunError::Parse(e.to_string()))
131}
132
133pub fn parse_tree(src: &str) -> Result<Vec<blue_lang_syntax::Spanned>, RunError> {
140 parse_tree_with_depth(src, blue_lang_syntax::MAX_EXPR_DEPTH)
141}
142
143pub fn run(src: &str) -> Result<Run, RunError> {
145 run_with_inputs(src, Inputs::new())
146}
147
148pub fn run_with_inputs(src: &str, inputs: Inputs) -> Result<Run, RunError> {
154 run_with_loader(src, inputs, &crate::uses::NoLoader)
155}
156
157pub fn run_with_loader(
165 src: &str,
166 inputs: Inputs,
167 loader: &dyn crate::uses::Loader,
168) -> Result<Run, RunError> {
169 run_in_surface(Entry::anonymous(src), inputs, loader, None)
170}
171
172pub fn run_in_surface(
189 entry: Entry<'_>,
190 inputs: Inputs,
191 loader: &dyn crate::uses::Loader,
192 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
193) -> Result<Run, RunError> {
194 run_framed(entry, inputs, loader, surface, None)
195}
196
197pub fn run_framed(
208 entry: Entry<'_>,
209 inputs: Inputs,
210 loader: &dyn crate::uses::Loader,
211 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
212 hold: Option<&crate::Hold>,
213) -> Result<Run, RunError> {
214 let (mut interp, mut prepared) = prepared_on(entry, inputs, loader, surface)?;
219 hold_to_frame(&mut interp, &mut prepared, hold)?;
220 let mut value = crate::timings::stage(crate::timings::Stage::Eval, || {
221 eval_prepared(&mut interp, &prepared, &mut ())
222 })?;
223 if let Some(v) = crate::timings::stage(crate::timings::Stage::Main, || {
224 call_main(&mut interp, &prepared, &mut ())
225 })? {
226 value = v;
227 }
228 Ok(Run {
229 value,
230 visited: prepared.visited,
231 typed_decls: prepared.typed_decls,
232 seams: prepared.seams,
233 })
234}
235
236fn hold_to_frame(
240 interp: &mut tatara_lisp_eval::Interpreter<()>,
241 prepared: &mut Prepared,
242 hold: Option<&crate::Hold>,
243) -> Result<(), RunError> {
244 let Some(hold) = hold else {
245 return Ok(());
246 };
247 let withheld = hold.rebind_scoped(&prepared.program, &mut prepared.erased);
248 let escapes = entry_escapes(&prepared.program, &withheld, &|cap| {
249 format!("this run does not allow; pass --allow {cap} to grant it")
250 });
251 if !escapes.is_empty() {
252 return Err(RunError::Types(escapes));
253 }
254 hold.bind(interp, "this run's frame");
255 crate::attach_node_held(interp, hold);
256 Ok(())
257}
258
259fn entry_escapes(
260 program: &crate::uses::ResolvedProgram,
261 refused: &[(&'static str, blue_lang_waku::Capability)],
262 grant: &dyn Fn(&str) -> String,
263) -> Vec<String> {
264 use crate::uses::ResolvedProgram;
265 let forms: Vec<tatara_lisp::Spanned> = program
266 .forms()
267 .iter()
268 .enumerate()
269 .filter(|(i, _)| program.owner_of(*i) == Some(ResolvedProgram::ENTRY))
270 .map(|(_, f)| f.clone())
271 .collect();
272 let label = |name: &str| {
273 refused
274 .iter()
275 .find(|(n, _)| *n == name)
276 .map(|(_, c)| c.label())
277 };
278 blue_lang_waku::free_names(&forms, &|name| label(name).is_none())
279 .into_iter()
280 .filter_map(|name| {
281 let cap = label(&name)?;
282 let at = program
283 .file(ResolvedProgram::ENTRY)
284 .and_then(|f| f.path.clone())
285 .map_or_else(|| "<entry>".to_string(), |p| p.display().to_string());
286 Some(format!(
287 "{at}: refused: `{name}` reaches the host ({cap}), which {}",
288 grant(cap)
289 ))
290 })
291 .collect()
292}
293
294fn runtime(inputs: Inputs) -> tatara_lisp_eval::Interpreter<()> {
296 let mut interp =
297 crate::timings::stage(crate::timings::Stage::Runtime, crate::interpreter_hostless);
298 crate::inputs::install_input_primitives(&mut interp, inputs);
299 interp
300}
301
302fn prepared_on(
304 entry: Entry<'_>,
305 inputs: Inputs,
306 loader: &dyn crate::uses::Loader,
307 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
308) -> Result<(tatara_lisp_eval::Interpreter<()>, Prepared), RunError> {
309 let interp = runtime(inputs);
310 let builtins = crate::timings::stage(crate::timings::Stage::Names, || builtin_names(&interp));
311 let prepared = prepare(entry, loader, surface, &builtins)?;
312 Ok((interp, prepared))
313}
314
315pub struct Compiled {
321 pub program: crate::uses::ResolvedProgram,
322 pub lowered: Vec<tatara_lisp::Spanned>,
323 pub erased: Vec<tatara_lisp::Spanned>,
324 pub main: Option<String>,
325}
326
327pub fn compile(
335 entry: Entry<'_>,
336 loader: &dyn crate::uses::Loader,
337 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
338 hold: Option<&crate::Hold>,
339) -> Result<Compiled, RunError> {
340 let (_, prepared) = prepared_on(entry, Inputs::new(), loader, surface)?;
341 if let Some(hold) = hold {
342 let escapes = entry_escapes(&prepared.program, &hold.withheld_host_names(), &|cap| {
343 format!("this bundle's policy does not allow; declare allow(%i[{cap}]) in the project's Bluefile and run `blue lock`")
344 });
345 if !escapes.is_empty() {
346 return Err(RunError::Types(escapes));
347 }
348 }
349 let main = entry_main(&prepared.program).map(|(top_level, _)| {
350 blue_lang_check::names::key(&namespace_of(&prepared.program, top_level), MAIN)
351 });
352 let Prepared {
353 program,
354 lowered,
355 erased,
356 ..
357 } = prepared;
358 Ok(Compiled {
359 program,
360 lowered,
361 erased,
362 main,
363 })
364}
365
366pub fn run_compiled(
376 erased: &[tatara_lisp::Spanned],
377 main: Option<&str>,
378 hold: &crate::Hold,
379 who: &'static str,
380) -> Result<Value, RunError> {
381 let mut interp = runtime(Inputs::new());
382 hold.bind(&mut interp, who);
383 crate::attach_node_held(&mut interp, hold);
384 let fault = |e: &tatara_lisp_eval::EvalError| {
385 RunError::Eval(Box::new(crate::observe::Fault::unplaced(e, erased)))
386 };
387 let mut value = Value::Nil;
388 for form in erased {
389 value = interp.eval_top_form(form, &mut ()).map_err(|e| fault(&e))?;
390 }
391 if let Some(key) = main {
392 let at = tatara_lisp::Span::synthetic();
393 let call = tatara_lisp::Spanned::new(
394 at,
395 tatara_lisp::SpannedForm::List(vec![tatara_lisp::Spanned::new(
396 at,
397 tatara_lisp::SpannedForm::Atom(tatara_lisp::Atom::Symbol(key.to_string())),
398 )]),
399 );
400 value = interp
401 .eval_top_form(&call, &mut ())
402 .map_err(|e| fault(&e))?;
403 }
404 Ok(value)
405}
406
407pub struct Observed {
410 pub definitions: std::sync::Arc<crate::observe::Definitions>,
411 pub outcome: Result<Run, RunError>,
413}
414
415pub struct Watched<'a> {
419 pub definitions: &'a std::sync::Arc<crate::observe::Definitions>,
420 pub program: &'a crate::uses::ResolvedProgram,
421 pub lowered: &'a [tatara_lisp::Spanned],
422 pub erased: &'a [tatara_lisp::Spanned],
423}
424
425pub fn run_observed(
436 entry: Entry<'_>,
437 inputs: Inputs,
438 loader: &dyn crate::uses::Loader,
439 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
440 watch: impl FnOnce(&Watched<'_>) -> std::sync::Arc<dyn crate::observe::CallObserver>,
441) -> Result<Observed, RunError> {
442 run_observed_framed(entry, inputs, loader, surface, None, watch)
443}
444
445pub fn run_observed_framed(
452 entry: Entry<'_>,
453 inputs: Inputs,
454 loader: &dyn crate::uses::Loader,
455 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
456 hold: Option<&crate::Hold>,
457 watch: impl FnOnce(&Watched<'_>) -> std::sync::Arc<dyn crate::observe::CallObserver>,
458) -> Result<Observed, RunError> {
459 let (mut interp, mut prepared) = prepared_on(entry, inputs, loader, surface)?;
460 hold_to_frame(&mut interp, &mut prepared, hold)?;
461 let definitions = std::sync::Arc::new(crate::observe::Definitions::of(
462 &prepared.program,
463 &prepared.erased,
464 ));
465 interp.set_call_observer(Some(watch(&Watched {
466 definitions: &definitions,
467 program: &prepared.program,
468 lowered: &prepared.lowered,
469 erased: &prepared.erased,
470 })));
471 let outcome = crate::timings::stage(crate::timings::Stage::Eval, || {
472 eval_prepared(&mut interp, &prepared, &mut ())
473 })
474 .and_then(|value| {
475 Ok(crate::timings::stage(crate::timings::Stage::Main, || {
476 call_main(&mut interp, &prepared, &mut ())
477 })?
478 .unwrap_or(value))
479 })
480 .map(|value| Run {
481 value,
482 visited: prepared.visited,
483 typed_decls: prepared.typed_decls,
484 seams: prepared.seams,
485 });
486 interp.set_call_observer(None);
487 Ok(Observed {
488 definitions,
489 outcome,
490 })
491}
492
493pub(crate) struct Prepared {
498 program: crate::uses::ResolvedProgram,
499 lowered: Vec<tatara_lisp::Spanned>,
500 erased: Vec<tatara_lisp::Spanned>,
501 visited: usize,
502 typed_decls: usize,
503 seams: usize,
504}
505
506impl Prepared {
507 pub(crate) fn erased(&self) -> &[tatara_lisp::Spanned] {
509 &self.erased
510 }
511
512 pub(crate) fn entry_namespace(&self) -> Namespace {
514 let first = (0..self.program.forms().len())
515 .find(|i| self.program.owner_of(*i) == Some(crate::uses::ResolvedProgram::ENTRY));
516 match first {
517 Some(i) => namespace_of(&self.program, i),
518 None => match self
519 .program
520 .file(crate::uses::ResolvedProgram::ENTRY)
521 .and_then(|f| f.package.clone())
522 {
523 Some(p) => Namespace::Bidama(p),
524 None => Namespace::File(String::new()),
525 },
526 }
527 }
528}
529
530pub(crate) fn prepare(
536 entry: Entry<'_>,
537 loader: &dyn crate::uses::Loader,
538 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
539 builtins: &NameTable,
540) -> Result<Prepared, RunError> {
541 let mut program = parse_and_resolve(entry, loader, surface)?;
542
543 program.retain(|f| !crate::uses::is_test_form(f));
556
557 let (outcome, names) = crate::timings::stage(crate::timings::Stage::Check, || {
572 check_stage(&program, builtins, false)
573 });
574 if !outcome.ok() {
575 return Err(RunError::Types(
576 outcome
577 .errors()
578 .map(|d| render(&program, d))
581 .collect(),
582 ));
583 }
584
585 let (lowered, erased) = crate::timings::stage(crate::timings::Stage::Erase, || {
599 let lowered = lower(&program, &names);
600 let erased = erase_types(&lowered);
601 (lowered, erased)
602 });
603
604 Ok(Prepared {
605 program,
606 lowered,
607 erased,
608 visited: outcome.stats.visited,
609 typed_decls: outcome.stats.typed_decls,
610 seams: outcome.seams.len(),
611 })
612}
613
614pub(crate) fn parse_and_resolve(
616 entry: Entry<'_>,
617 loader: &dyn crate::uses::Loader,
618 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
619) -> Result<crate::uses::ResolvedProgram, RunError> {
620 let forms = crate::timings::stage(crate::timings::Stage::Parse, || {
621 let mut forms = match surface {
622 Some(pack) => blue_lang_syntax::parse_program_tree_in(entry.text, pack)
623 .map_err(|e| RunError::Parse(e.to_string()))?,
624 None => loader
625 .parse(entry.text)
626 .map_err(|e| RunError::Parse(e.to_string()))?,
627 };
628 if surface.is_none() {
631 forms.extend(
632 blue_lang_syntax::doc::doctests(entry.text)
633 .map_err(|e| RunError::Parse(e.to_string()))?,
634 );
635 }
636 Ok::<_, RunError>(forms)
637 })?;
638
639 crate::timings::stage(crate::timings::Stage::Resolve, || {
650 crate::uses::resolve_uses(forms, entry, loader).map_err(RunError::Import)
651 })
652}
653
654pub(crate) fn check_stage(
667 program: &crate::uses::ResolvedProgram,
668 builtins: &NameTable,
669 with_tests: bool,
670) -> (blue_lang_check::Outcome, NameTable) {
671 use crate::uses::ResolvedProgram;
672 let forms = program.forms();
673 let is_entry = |i: usize| program.owner_of(i) == Some(ResolvedProgram::ENTRY);
674
675 let mut outcome = blue_lang_check::check_program_in(forms, &|a, b| {
677 program.owner_of(a) == program.owner_of(b)
678 });
679 outcome
680 .diagnostics
681 .extend(data_law_diagnostics(program, &is_entry));
682
683 let table = program_names(program, builtins);
686 let whole = |i: usize| with_tests && is_entry(i);
687 let (names, resolved) = blue_lang_check::names::check_names_whole(
688 forms,
689 &table,
690 &|i| namespace_of(program, i),
691 &is_entry,
692 &whole,
693 );
694 let stored = program
695 .file(ResolvedProgram::ENTRY)
696 .is_some_and(|f| f.path.is_some());
697 outcome.diagnostics.extend(names);
698 outcome.stats.names_resolved = resolved;
699 if stored {
700 let script = program
701 .file(ResolvedProgram::ENTRY)
702 .is_some_and(|f| f.package.is_none());
703 let found = blue_lang_check::form_rules::check(forms, &is_entry, script);
704 let silenced: Vec<tatara_lisp::Span> = found
705 .iter()
706 .filter(|d| d.code == blue_lang_check::Code::B0038)
707 .map(|d| d.span)
708 .collect();
709 outcome
710 .diagnostics
711 .retain(|d| !(d.code == blue_lang_check::Code::B0002 && silenced.contains(&d.span)));
712 outcome.diagnostics.extend(found);
713 }
714 outcome
715 .diagnostics
716 .extend(blue_lang_check::contracts::check(
717 forms,
718 &table,
719 &|i| namespace_of(program, i),
720 &is_entry,
721 &|i| {
722 program
723 .owner_of(i)
724 .and_then(|f| program.file(f))
725 .map(|f| f.text.as_str())
726 },
727 crate::docs::signatures(),
728 ));
729
730 outcome.diagnostics.extend(doc_diagnostics(program));
731
732 let mut waivers = Vec::new();
735 for file in program.files() {
736 let spans: Vec<(usize, tatara_lisp::Span)> = forms
737 .iter()
738 .enumerate()
739 .filter(|(i, _)| program.owner_of(*i) == Some(file.id))
740 .map(|(i, f)| (i, f.span))
741 .collect();
742 let (found, malformed) = blue_lang_check::waiver::collect(&file.text, &spans);
743 waivers.extend(found);
744 outcome.diagnostics.extend(malformed);
745 }
746 outcome.waived = blue_lang_check::waiver::apply(&mut outcome.diagnostics, waivers, &is_entry);
747 if !stored {
748 outcome.diagnostics.retain(|d| {
749 !matches!(
750 d.code,
751 blue_lang_check::Code::B0023
752 | blue_lang_check::Code::B0036
753 | blue_lang_check::Code::B0037
754 | blue_lang_check::Code::B0067
755 | blue_lang_check::Code::B0061
756 | blue_lang_check::Code::B0062
757 | blue_lang_check::Code::B0063
758 )
759 });
760 }
761 let judge = blue_lang_check::evolution::FormJudge::from_env().unwrap_or_default();
762 outcome
763 .diagnostics
764 .retain(|d| judge.reports(d.code, is_entry(d.top_level)));
765 let (enforced, census): (Vec<_>, Vec<_>) = std::mem::take(&mut outcome.diagnostics)
768 .into_iter()
769 .partition(|d| d.code.rule().active());
770 outcome.diagnostics = enforced;
771 outcome.census = census;
772 outcome
773 .diagnostics
774 .sort_by_key(|d| (d.top_level, d.span.start, d.code));
775 (outcome, table)
776}
777
778fn doc_diagnostics(program: &crate::uses::ResolvedProgram) -> Vec<blue_lang_check::Diagnostic> {
782 use crate::uses::ResolvedProgram;
783 let Some(file) = program.file(ResolvedProgram::ENTRY) else {
784 return Vec::new();
785 };
786 let Some(package) = file.package.as_deref() else {
787 return Vec::new();
788 };
789 let forms: Vec<(usize, &tatara_lisp::Spanned)> = program
790 .forms()
791 .iter()
792 .enumerate()
793 .filter(|(i, f)| {
794 program.owner_of(*i) == Some(ResolvedProgram::ENTRY)
795 && !blue_lang_syntax::doc::is_doctest(f)
796 })
797 .collect();
798 let graph = file.path.as_deref().map(crate::doc_sources::graph_meanings);
799 let meaning = |name: &str| {
800 graph
801 .as_ref()
802 .and_then(|g| g.get(&(package.to_string(), name.to_string())).cloned())
803 };
804 let names_line = |name: &str| crate::docs::doc_of(name).map(|d| d.doc.to_string());
805 blue_lang_check::doc_rules::check_file(
806 &file.text,
807 &forms,
808 package,
809 &blue_lang_check::doc_rules::Sources {
810 meaning: &meaning,
811 names_line: &names_line,
812 },
813 )
814}
815
816fn data_law_diagnostics(
817 program: &crate::uses::ResolvedProgram,
818 is_entry: &dyn Fn(usize) -> bool,
819) -> Vec<blue_lang_check::Diagnostic> {
820 use blue_lang_check::data_laws;
821 let forms = program.forms();
822 let fragments = data_laws::Fragments::collect(forms);
823 let frontend = program.frontend();
824 let mut out = Vec::new();
825 let schema = match frontend.and_then(|f| f.table.as_ref()) {
826 Some((label, text)) => match data_laws::Schema::read(text) {
827 Ok(s) => Some(s),
828 Err(why) => {
829 if let Some((i, first)) = forms.iter().enumerate().find(|(i, _)| is_entry(*i)) {
830 out.push(
831 blue_lang_check::Diagnostic::new(
832 blue_lang_check::Code::B0050,
833 format!("the generated schema table {label} does not read: {why}"),
834 first.span,
835 )
836 .at_top_level(i)
837 .with_help("regenerate it: `nix run .#regen`"),
838 );
839 }
840 None
841 }
842 },
843 None => None,
844 };
845 if fragments.is_empty() && schema.is_none() && !frontend.is_some_and(|f| f.view) {
846 return out;
847 }
848 out.extend(data_laws::check(
849 forms,
850 &data_laws::Context {
851 fragments: &fragments,
852 schema: schema.as_ref(),
853 is_entry,
854 view: frontend.is_some_and(|f| f.view),
855 },
856 ));
857 out
858}
859
860#[must_use]
873pub fn program_names(program: &crate::uses::ResolvedProgram, builtins: &NameTable) -> NameTable {
874 use blue_lang_check::Binding;
875 let mut table = NameTable::new();
876 table.add_program(program.forms(), |i| namespace_of(program, i));
877 let builtin_macro = |head: &str| {
878 builtins
879 .scopes()
880 .iter()
881 .any(|s| s.namespace == Namespace::Macro && s.get(head).is_some())
882 };
883 let mut expander: Option<tatara_lisp_eval::Interpreter<()>> = None;
884 for (i, form) in program.forms().iter().enumerate() {
885 let Some(head) = form
886 .as_list()
887 .and_then(|items| items.first())
888 .and_then(tatara_lisp::Spanned::as_symbol)
889 else {
890 continue;
891 };
892 if !builtin_macro(head) {
893 continue;
894 }
895 let interp = expander.get_or_insert_with(crate::interpreter_hostless);
896 let Ok(expanded) = interp.fully_expand(form, &mut ()) else {
899 continue;
900 };
901 let nullary = blue_lang_syntax::scope::nullary_definition(&expanded);
902 for (name, span, kind) in blue_lang_check::names::definitions_of(&expanded) {
903 table.define(
904 namespace_of(program, i),
905 Binding {
906 nullary: nullary == Some(name.as_str()),
907 name,
908 kind,
909 span: Some(span),
910 top_level: Some(i),
911 },
912 );
913 }
914 }
915 for scope in builtins.scopes() {
916 table.push(scope.clone());
917 }
918 table.spell_bare_like(builtins);
919 let form_file: Vec<usize> = (0..program.forms().len())
920 .map(|i| program.owner_of(i).map_or(usize::MAX, |f| f.index()))
921 .collect();
922 let mut imports: std::collections::BTreeMap<usize, blue_lang_check::names::FileImports> =
923 std::collections::BTreeMap::new();
924 for (file, import) in program.imports() {
925 let entry = imports.entry(file.index()).or_default();
926 entry.add(&import.package, import.names.iter().map(|(n, _)| n.clone()));
927 let at = import.span.start;
928 entry.use_at = Some(entry.use_at.map_or(at, |a| a.min(at)));
929 }
930 let facade_uses =
935 |pkg: &str,
936 imports: &std::collections::BTreeMap<usize, blue_lang_check::names::FileImports>| {
937 let defines = table.bidama(pkg).is_some_and(|s| !s.is_empty());
938 if defines {
939 return std::collections::BTreeSet::new();
940 }
941 program
942 .files()
943 .iter()
944 .filter(|f| f.package.as_deref() == Some(pkg))
945 .filter_map(|f| imports.get(&f.id.index()))
946 .flat_map(|i| i.uses.iter().cloned())
947 .collect::<std::collections::BTreeSet<String>>()
948 };
949 loop {
950 let mut grew = false;
951 let files: Vec<usize> = imports.keys().copied().collect();
952 for f in files {
953 let used: Vec<String> = imports[&f].uses.iter().cloned().collect();
954 for p in used {
955 for member in facade_uses(&p, &imports) {
956 if let Some(i) = imports.get_mut(&f) {
957 if i.uses.insert(member.clone()) {
958 i.via.insert(member, p.clone());
959 grew = true;
960 }
961 }
962 }
963 }
964 }
965 if !grew {
966 break;
967 }
968 }
969 let mut pkg_uses: std::collections::BTreeMap<String, std::collections::BTreeSet<String>> =
970 std::collections::BTreeMap::new();
971 for (file, import) in program.imports() {
972 if let Some(p) = program.file(*file).and_then(|f| f.package.clone()) {
973 pkg_uses
974 .entry(p)
975 .or_default()
976 .insert(import.package.clone());
977 }
978 }
979 table.attach_files(form_file, imports);
980 table.attach_package_uses(pkg_uses);
981 table.attach_needs(program.needs().clone());
982 let canonical = |p: &std::path::Path| {
986 std::fs::canonicalize(p)
987 .unwrap_or_else(|_| p.to_path_buf())
988 .display()
989 .to_string()
990 };
991 let mut file_paths = std::collections::BTreeMap::new();
992 let mut pkg_roots = std::collections::BTreeMap::new();
993 for f in program.files() {
994 if let Some(path) = &f.path {
995 file_paths.insert(f.id.index(), canonical(path));
996 if let (Some(pkg), Some(root)) =
997 (&f.package, path.parent().and_then(std::path::Path::parent))
998 {
999 pkg_roots
1000 .entry(pkg.clone())
1001 .or_insert_with(|| canonical(root));
1002 }
1003 }
1004 }
1005 table.attach_paths(file_paths, pkg_roots);
1006 let versions = program.versions().clone();
1007 table.apply_legacy(program.forms(), &|i| namespace_of(program, i), &|p| {
1008 versions.get(p).cloned()
1009 });
1010 table
1011}
1012
1013#[must_use]
1025pub fn lower(
1026 program: &crate::uses::ResolvedProgram,
1027 names: &NameTable,
1028) -> Vec<tatara_lisp::Spanned> {
1029 lower_with(program, names, true)
1030}
1031
1032#[must_use]
1033pub fn lower_with(
1034 program: &crate::uses::ResolvedProgram,
1035 names: &NameTable,
1036 simplify_chains: bool,
1037) -> Vec<tatara_lisp::Spanned> {
1038 let mut resolved = blue_lang_check::names::resolve_program(program.forms(), names, &|i| {
1039 namespace_of(program, i)
1040 });
1041 let simplified = if simplify_chains {
1042 chain_simplified(program.forms(), &resolved)
1043 } else {
1044 None
1045 };
1046 let forms: &[tatara_lisp::Spanned] = simplified.as_deref().unwrap_or(program.forms());
1047 if simplified.is_some() {
1048 resolved =
1049 blue_lang_check::names::resolve_program(forms, names, &|i| namespace_of(program, i));
1050 }
1051 resolved
1052 .resolved_tree(forms, blue_lang_check::names::Rule::Namespaced)
1053 .into_iter()
1054 .zip(forms)
1055 .enumerate()
1056 .map(|(i, (resolved, written))| {
1057 if crate::uses::use_target(written).is_some()
1058 || blue_lang_syntax::scope::legacy_target(written).is_some()
1059 {
1060 tatara_lisp::Spanned::new(written.span, tatara_lisp::SpannedForm::Nil)
1061 } else {
1062 let file = program.owner_of(i).and_then(|id| program.file(id));
1063 match file {
1064 Some(f) => locate_raises(&resolved, &raise_site(f)),
1065 None => resolved,
1066 }
1067 }
1068 })
1069 .collect()
1070}
1071
1072fn chain_simplified(
1073 forms: &[tatara_lisp::Spanned],
1074 resolved: &blue_lang_check::names::Resolved,
1075) -> Option<Vec<tatara_lisp::Spanned>> {
1076 let respell: std::collections::HashMap<usize, blue_lang_check::respell::Respelling> = resolved
1077 .respellings
1078 .iter()
1079 .filter(|(_, r)| matches!(r.respell, blue_lang_check::respell::Respell::Chain(_)))
1080 .cloned()
1081 .collect();
1082 if respell.is_empty() {
1083 return None;
1084 }
1085 let bare = std::collections::HashSet::new();
1086 let canon = Canon {
1087 bare: &bare,
1088 respell: &respell,
1089 };
1090 Some(forms.iter().map(|f| canon.form(f)).collect())
1091}
1092
1093fn raise_site(file: &crate::uses::SourceFile) -> impl Fn(tatara_lisp::Span) -> String + '_ {
1094 move |span| {
1095 let (line, _) =
1096 blue_lang_syntax::Span::line_col(&file.text, span.start.min(file.text.len()));
1097 let shown = file
1098 .path
1099 .as_ref()
1100 .map_or_else(|| "<source>".to_string(), |p| p.display().to_string());
1101 format!("{shown}:{line}")
1102 }
1103}
1104
1105fn locate_raises(
1106 form: &tatara_lisp::Spanned,
1107 site: &dyn Fn(tatara_lisp::Span) -> String,
1108) -> tatara_lisp::Spanned {
1109 use tatara_lisp::{Atom, Spanned, SpannedForm};
1110 let inner = match &form.form {
1111 SpannedForm::List(items) => {
1112 let mut out: Vec<Spanned> = items.iter().map(|i| locate_raises(i, site)).collect();
1113 let raising = items.first().and_then(Spanned::as_symbol) == Some("raise")
1114 && (3..=4).contains(&items.len());
1115 if raising {
1116 out[0] = Spanned::new(
1117 items[0].span,
1118 SpannedForm::Atom(Atom::Symbol(crate::errors::RAISE_AT.to_string())),
1119 );
1120 out.insert(
1121 1,
1122 Spanned::new(items[0].span, SpannedForm::Atom(Atom::Str(site(form.span)))),
1123 );
1124 }
1125 SpannedForm::List(out)
1126 }
1127 SpannedForm::Quote(_) | SpannedForm::Quasiquote(_) => return form.clone(),
1128 other => other.clone(),
1129 };
1130 Spanned::new(form.span, inner)
1131}
1132
1133#[must_use]
1135pub fn namespace_of(program: &crate::uses::ResolvedProgram, i: usize) -> Namespace {
1136 let file = program.owner_of(i).and_then(|id| program.file(id));
1137 match file {
1138 Some(f) => match &f.package {
1139 Some(p) => Namespace::Bidama(p.clone()),
1140 None => Namespace::File(
1141 f.path
1142 .as_ref()
1143 .map_or_else(|| "<anonymous>".to_string(), |p| p.display().to_string()),
1144 ),
1145 },
1146 None => Namespace::File("<unknown file>".to_string()),
1147 }
1148}
1149
1150pub const HARNESS_NAMES: &[&str] = &[blue_lang_syntax::LOWERED_ASSERT];
1164
1165#[must_use]
1175pub fn builtin_names<H: 'static>(interp: &tatara_lisp_eval::Interpreter<H>) -> NameTable {
1176 builtin_names_with(interp, &[])
1177}
1178
1179#[must_use]
1180pub fn builtin_names_with<H: 'static>(
1181 interp: &tatara_lisp_eval::Interpreter<H>,
1182 host: &[crate::host_surface::HostName],
1183) -> NameTable {
1184 use blue_lang_check::{Binding, Scope, ScopeKind};
1185 use tatara_lisp_eval::HeadBinding;
1186 let mut special = Scope::new(Namespace::SpecialForm);
1187 let mut macros = Scope::new(Namespace::Macro);
1188 let mut values = Scope::new(Namespace::Builtin);
1189 let mut names: Vec<std::sync::Arc<str>> = interp.reserved_head_names().into_iter().collect();
1194 names.extend(
1195 crate::inputs::INPUT_PRIMITIVES
1196 .iter()
1197 .filter(|n| interp.resolve_head(n).is_some())
1198 .map(|n| std::sync::Arc::from(*n)),
1199 );
1200 for name in names {
1201 let (scope, kind) = match interp.resolve_head(&name) {
1202 Some(HeadBinding::SpecialForm) => (&mut special, ScopeKind::SpecialForm),
1203 Some(HeadBinding::Macro) => (&mut macros, ScopeKind::Macro),
1204 _ => (&mut values, ScopeKind::Value),
1205 };
1206 let nullary = kind == ScopeKind::Value
1207 && interp.lookup_global(&name).is_some_and(|v| {
1208 nullary_value(&v)
1209 || (crate::docs::doc_of(&name).is_some_and(|d| d.signature == &*name)
1210 && matches!(
1211 v,
1212 tatara_lisp_eval::Value::NativeFn(_)
1213 | tatara_lisp_eval::Value::Closure(_)
1214 ))
1215 });
1216 scope.bind(Binding {
1217 name: name.to_string(),
1218 kind,
1219 span: None,
1220 top_level: None,
1221 nullary,
1222 });
1223 }
1224 for h in host {
1225 values.bind(Binding {
1226 name: h.name.clone(),
1227 kind: ScopeKind::Value,
1228 span: None,
1229 top_level: None,
1230 nullary: h.nullary(),
1231 });
1232 }
1233 let mut table = NameTable::new();
1234 let mut harness = Scope::new(Namespace::Harness);
1235 for n in HARNESS_NAMES {
1236 harness.bind(Binding {
1237 name: (*n).to_string(),
1238 kind: ScopeKind::Value,
1239 span: None,
1240 top_level: None,
1241 nullary: false,
1242 });
1243 }
1244 table.push(harness);
1245 table.push(special);
1246 table.push(macros);
1247 table.push(values);
1248 table.spell_bare_only(
1249 crate::docs::NAMES
1250 .iter()
1251 .filter(|d| d.signature == d.name)
1252 .map(|d| d.name.to_string())
1253 .chain(host.iter().filter(|h| h.nullary()).map(|h| h.name.clone()))
1254 .collect(),
1255 );
1256 table
1257}
1258
1259#[must_use]
1260pub fn nullary_value(value: &tatara_lisp_eval::Value) -> bool {
1261 match value {
1262 tatara_lisp_eval::Value::NativeFn(f) => {
1263 matches!(f.arity, tatara_lisp_eval::ffi::Arity::Exact(0))
1264 }
1265 tatara_lisp_eval::Value::Closure(c) => c.params.is_empty() && c.rest.is_none(),
1266 _ => false,
1267 }
1268}
1269
1270#[must_use]
1274pub fn render(program: &crate::uses::ResolvedProgram, d: &blue_lang_check::Diagnostic) -> String {
1275 let head = d.to_string();
1276 let mut out = program.locate(d.top_level, d.span, &head).to_string();
1277 if let Some(help) = &d.help {
1278 out.push_str("\n help: ");
1279 out.push_str(help);
1280 }
1281 let file = program
1282 .owner_of(d.top_level)
1283 .and_then(|id| program.file(id))
1284 .and_then(|f| f.path.as_ref());
1285 if let Some(cmd) =
1286 file.and_then(|p| blue_lang_check::evolution::fix_command(d, &p.display().to_string()))
1287 {
1288 out.push_str("\n fix: ");
1289 out.push_str(&cmd);
1290 }
1291 out
1292}
1293
1294#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1296pub enum Checking {
1297 Program,
1299 WithTests,
1302}
1303
1304pub struct Checked {
1306 pub program: crate::uses::ResolvedProgram,
1307 pub outcome: blue_lang_check::Outcome,
1308 pub names: NameTable,
1311}
1312
1313impl Checked {
1314 #[must_use]
1317 pub fn erased(&self) -> Vec<tatara_lisp::Spanned> {
1318 erase_types(&lower(&self.program, &self.names))
1319 }
1320
1321 #[must_use]
1324 pub fn evaluable(&self) -> Vec<tatara_lisp::Sexp> {
1325 lower(&self.program, &self.names)
1326 .iter()
1327 .map(tatara_lisp::Spanned::to_sexp)
1328 .collect()
1329 }
1330
1331 #[must_use]
1335 pub fn resolve(&self) -> blue_lang_check::names::Resolved {
1336 blue_lang_check::names::resolve_program(self.program.forms(), &self.names, &|i| {
1337 namespace_of(&self.program, i)
1338 })
1339 }
1340}
1341
1342impl Checked {
1343 #[must_use]
1344 pub fn call_explicit_forms(&self) -> Vec<tatara_lisp::Spanned> {
1345 self.resolve().call_explicit_tree(self.program.forms())
1346 }
1347
1348 #[must_use]
1349 pub fn canonical_entry(&self, src: &str) -> Option<Vec<tatara_lisp::Spanned>> {
1350 use crate::uses::ResolvedProgram;
1351 let program = &self.program;
1352 let entry: Vec<&tatara_lisp::Spanned> = program
1353 .forms()
1354 .iter()
1355 .enumerate()
1356 .filter(|(i, _)| program.owner_of(*i) == Some(ResolvedProgram::ENTRY))
1357 .map(|(_, f)| f)
1358 .filter(|f| !blue_lang_syntax::doc::is_doctest(f))
1359 .collect();
1360 let written = blue_lang_syntax::parse_program_tree(src).ok()?;
1361 let same = written.len() == entry.len()
1362 && written
1363 .iter()
1364 .zip(&entry)
1365 .all(|(w, e)| w.to_sexp() == e.to_sexp() && w.span == e.span);
1366 if !same {
1367 return None;
1368 }
1369 let resolved = self.resolve();
1370 let bare: std::collections::HashSet<usize> = resolved
1371 .parenthesized_nullary_calls()
1372 .into_iter()
1373 .filter(|r| program.owner_of(r.top_level) == Some(ResolvedProgram::ENTRY))
1374 .map(blue_lang_check::names::Reference::node_address)
1375 .collect();
1376 let waived: Vec<tatara_lisp::Span> = self
1377 .outcome
1378 .waived
1379 .iter()
1380 .map(|w| w.diagnostic.span)
1381 .collect();
1382 let respell: std::collections::HashMap<usize, blue_lang_check::respell::Respelling> =
1383 resolved
1384 .respellings
1385 .iter()
1386 .filter(|(_, r)| !waived.contains(&r.span))
1387 .cloned()
1388 .collect();
1389 let canon = Canon {
1390 bare: &bare,
1391 respell: &respell,
1392 };
1393 let forms: Vec<tatara_lisp::Spanned> = entry.into_iter().map(|f| canon.form(f)).collect();
1394 let script = program
1395 .file(ResolvedProgram::ENTRY)
1396 .is_some_and(|f| f.package.is_none());
1397 let forms = if script {
1398 blue_lang_check::form_rules::without_prefix(&forms).unwrap_or(forms)
1399 } else {
1400 forms
1401 };
1402 let forms = blue_lang_check::form_rules::without_main_guard(&forms).unwrap_or(forms);
1403 Some(blue_lang_check::form_rules::eta_reduced(&forms).unwrap_or(forms))
1404 }
1405}
1406
1407struct Canon<'a> {
1408 bare: &'a std::collections::HashSet<usize>,
1409 respell: &'a std::collections::HashMap<usize, blue_lang_check::respell::Respelling>,
1410}
1411
1412impl Canon<'_> {
1413 fn form(&self, form: &tatara_lisp::Spanned) -> tatara_lisp::Spanned {
1414 use tatara_lisp::{Spanned, SpannedForm};
1415 let inner = match &form.form {
1416 SpannedForm::List(items) => match items.as_slice() {
1417 [head] if self.bare.contains(&(std::ptr::from_ref(head) as usize)) => {
1418 return Spanned::new(form.span, head.form.clone());
1419 }
1420 [head, ..]
1421 if self
1422 .respell
1423 .contains_key(&(std::ptr::from_ref(head) as usize)) =>
1424 {
1425 let r = &self.respell[&(std::ptr::from_ref(head) as usize)];
1426 let walked: Vec<Spanned> = items.iter().map(|i| self.form(i)).collect();
1427 return blue_lang_check::respell::apply(form, &walked, r);
1428 }
1429 _ => SpannedForm::List(items.iter().map(|i| self.form(i)).collect()),
1430 },
1431 SpannedForm::Quote(i) => SpannedForm::Quote(Box::new(self.form(i))),
1432 SpannedForm::Quasiquote(i) => SpannedForm::Quasiquote(Box::new(self.form(i))),
1433 SpannedForm::Unquote(i) => SpannedForm::Unquote(Box::new(self.form(i))),
1434 SpannedForm::UnquoteSplice(i) => SpannedForm::UnquoteSplice(Box::new(self.form(i))),
1435 other => other.clone(),
1436 };
1437 Spanned::new(form.span, inner)
1438 }
1439}
1440
1441pub fn check_entry(
1457 entry: Entry<'_>,
1458 loader: &dyn crate::uses::Loader,
1459 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
1460 checking: Checking,
1461) -> Result<Checked, RunError> {
1462 let mut program = parse_and_resolve(entry, loader, surface)?;
1463 if checking == Checking::Program {
1464 program.retain(|f| !crate::uses::is_test_form(f));
1465 }
1466 let host = match entry.path {
1467 Some(path) => crate::host_surface::near(path)
1468 .map_err(|e| RunError::Import(e.to_string()))?
1469 .map(|s| s.functions)
1470 .unwrap_or_default(),
1471 None => Vec::new(),
1472 };
1473 let interp = runtime(Inputs::new());
1474 let builtins = crate::timings::stage(crate::timings::Stage::Names, || {
1475 builtin_names_with(&interp, &host)
1476 });
1477 let (outcome, names) = crate::timings::stage(crate::timings::Stage::Check, || {
1478 check_stage(&program, &builtins, checking == Checking::WithTests)
1479 });
1480 Ok(Checked {
1481 program,
1482 outcome,
1483 names,
1484 })
1485}
1486
1487#[must_use]
1488pub fn spelling_code(spelling: blue_lang_syntax::Spelling) -> blue_lang_check::Code {
1489 use blue_lang_check::Code;
1490 use blue_lang_syntax::Spelling;
1491 match spelling {
1492 Spelling::LogicalSymbol => Code::B0025,
1493 Spelling::Unless => Code::B0026,
1494 Spelling::ElseIf => Code::B0027,
1495 Spelling::CallForm => Code::B0028,
1496 Spelling::KeywordRocket => Code::B0029,
1497 Spelling::ParameterList => Code::B0030,
1498 Spelling::OperatorSpelling => Code::B0031,
1499 Spelling::TrailingComma => Code::B0032,
1500 Spelling::NegatedComparison => Code::B0034,
1501 Spelling::ElseNil => Code::B0035,
1502 Spelling::TryCatch => Code::B0037,
1503 Spelling::Apply => Code::B0042,
1504 Spelling::MapPun => Code::B0046,
1505 Spelling::NilTest => Code::B0047,
1506 Spelling::NewlineEscape => Code::B0065,
1507 Spelling::Index => Code::B0066,
1508 }
1509}
1510
1511#[must_use]
1512pub fn form_diagnostics(text: &str) -> Vec<blue_lang_check::Diagnostic> {
1513 match blue_lang_fmt::format_source_lossless(text) {
1514 Ok(formatted) => form_diagnostics_against(text, &formatted),
1515 Err(_) => Vec::new(),
1516 }
1517}
1518
1519#[must_use]
1520pub fn form_diagnostics_against(text: &str, formatted: &str) -> Vec<blue_lang_check::Diagnostic> {
1521 use blue_lang_check::{Code, Diagnostic};
1522 if formatted.trim_end() == text.trim_end() {
1523 return Vec::new();
1524 }
1525 let help = "`blue fmt --write FILE` writes it; no compile door writes a file (`BLUE_LAYOUT=rewrite` restores formatting in place)";
1526 let mut out: Vec<Diagnostic> = blue_lang_syntax::spelling_notes(text)
1527 .unwrap_or_default()
1528 .into_iter()
1529 .map(|n| {
1530 let message = if n.canonical.is_empty() {
1531 format!("`{}` is not written", n.written)
1532 } else {
1533 format!("`{}` is written `{}`", n.written, n.canonical)
1534 };
1535 Diagnostic::new(spelling_code(n.spelling), message, n.span)
1536 .at_top_level(0)
1537 .with_help(help)
1538 })
1539 .collect();
1540 out.extend(
1541 literal_diagnostics(text)
1542 .into_iter()
1543 .map(|d| d.with_help(help)),
1544 );
1545 if out.is_empty() {
1546 let at = text
1547 .lines()
1548 .zip(formatted.lines())
1549 .position(|(a, b)| a != b)
1550 .unwrap_or_else(|| text.lines().count().min(formatted.lines().count()));
1551 let start: usize = text.lines().take(at).map(|l| l.len() + 1).sum();
1552 let start = start.min(text.len());
1553 let end = text[start..].find('\n').map_or(text.len(), |e| start + e);
1554 out.push(
1555 Diagnostic::new(
1556 Code::B0024,
1557 format!("line {} is not laid out as blue writes it", at + 1),
1558 tatara_lisp::Span::new(start, end),
1559 )
1560 .at_top_level(0)
1561 .with_help(help),
1562 );
1563 }
1564 out.sort_by_key(|d| (d.span.start, d.code));
1565 out
1566}
1567
1568fn literal_diagnostics(text: &str) -> Vec<blue_lang_check::Diagnostic> {
1569 use blue_lang_syntax::{Atom, TokenKind};
1570 let Ok(toks) = blue_lang_syntax::lex(text) else {
1571 return Vec::new();
1572 };
1573 toks.iter()
1574 .filter_map(|t| {
1575 let atom = match &t.kind {
1576 TokenKind::Int(v) => Atom::Int(*v),
1577 TokenKind::Float(v) => Atom::Float(*v),
1578 TokenKind::Str(v) => Atom::Str(v.clone()),
1579 _ => return None,
1580 };
1581 let written = text.get(t.span.start..t.span.end)?;
1582 let canonical = blue_lang_fmt::render_literal(&atom);
1583 (written != canonical).then(|| {
1584 blue_lang_check::Diagnostic::new(
1585 blue_lang_check::Code::B0033,
1586 format!("`{written}` is written `{canonical}`"),
1587 t.span,
1588 )
1589 .at_top_level(0)
1590 })
1591 })
1592 .collect()
1593}
1594
1595#[must_use]
1599pub fn syntax_diagnostic(text: &str) -> Option<blue_lang_check::Diagnostic> {
1600 let e = blue_lang_syntax::parse_program_tree(text).err()?;
1601 Some(
1602 blue_lang_check::Diagnostic::new(blue_lang_check::Code::B0006, e.message, e.span)
1603 .at_top_level(0),
1604 )
1605}
1606
1607#[must_use]
1611pub fn display_keys(message: &str) -> String {
1612 let mut out = String::with_capacity(message.len());
1613 let mut parts = message.split('`');
1614 if let Some(first) = parts.next() {
1615 out.push_str(first);
1616 }
1617 for (i, part) in parts.enumerate() {
1618 out.push('`');
1619 if i % 2 == 0 {
1621 out.push_str(&crate::observe::display_key(part));
1622 } else {
1623 out.push_str(part);
1624 }
1625 }
1626 out
1627}
1628
1629pub(crate) fn eval_prepared<H: 'static>(
1631 interp: &mut tatara_lisp_eval::Interpreter<H>,
1632 prepared: &Prepared,
1633 host: &mut H,
1634) -> Result<Value, RunError> {
1635 let Prepared {
1636 program, erased, ..
1637 } = prepared;
1638
1639 let mut value = Value::Nil;
1653 for (top_level, form) in erased.iter().enumerate() {
1654 value = interp.eval_top_form(form, host).map_err(|e| {
1655 RunError::Eval(Box::new(crate::observe::Fault::of(
1662 program,
1663 erased,
1664 top_level,
1665 &e,
1666 &interp.take_trace(),
1667 )))
1668 })?;
1669 }
1670 Ok(value)
1671}
1672
1673pub const MAIN: &str = "main";
1674
1675#[must_use]
1676pub fn entry_main(program: &crate::uses::ResolvedProgram) -> Option<(usize, tatara_lisp::Span)> {
1677 use crate::uses::ResolvedProgram;
1678 let stored = program
1679 .file(ResolvedProgram::ENTRY)
1680 .is_some_and(|f| f.path.is_some());
1681 if !stored {
1682 return None;
1683 }
1684 program.forms().iter().enumerate().find_map(|(i, f)| {
1685 if program.owner_of(i) != Some(ResolvedProgram::ENTRY) {
1686 return None;
1687 }
1688 let items = f.as_list()?;
1689 let sig = items.get(1)?.as_list()?;
1690 let named_main = matches!(
1691 items.first().and_then(tatara_lisp::Spanned::as_symbol),
1692 Some("define" | "define-typed")
1693 ) && sig.len() == 1
1694 && sig[0].as_symbol() == Some(MAIN);
1695 named_main.then_some((i, sig[0].span))
1696 })
1697}
1698
1699pub(crate) fn call_main<H: 'static>(
1700 interp: &mut tatara_lisp_eval::Interpreter<H>,
1701 prepared: &Prepared,
1702 host: &mut H,
1703) -> Result<Option<Value>, RunError> {
1704 let Some((top_level, span)) = entry_main(&prepared.program) else {
1705 return Ok(None);
1706 };
1707 let ns = namespace_of(&prepared.program, top_level);
1708 let call = tatara_lisp::Spanned::new(
1709 span,
1710 tatara_lisp::SpannedForm::List(vec![tatara_lisp::Spanned::new(
1711 span,
1712 tatara_lisp::SpannedForm::Atom(tatara_lisp::Atom::Symbol(blue_lang_check::names::key(
1713 &ns, MAIN,
1714 ))),
1715 )]),
1716 );
1717 interp.eval_top_form(&call, host).map(Some).map_err(|e| {
1718 RunError::Eval(Box::new(crate::observe::Fault::of(
1719 &prepared.program,
1720 &prepared.erased,
1721 top_level,
1722 &e,
1723 &interp.take_trace(),
1724 )))
1725 })
1726}
1727
1728#[cfg(test)]
1744mod position_tests {
1745 use super::*;
1746 use crate::uses::{Entry, Loader};
1747 use std::collections::BTreeMap;
1748 use std::path::Path;
1749
1750 struct MemLoader(BTreeMap<&'static str, &'static str>);
1751
1752 impl Loader for MemLoader {
1753 fn load(&self, name: &str) -> Result<Vec<(String, String)>, String> {
1754 self.0
1755 .get(name)
1756 .map(|s| vec![(format!("{name}.b"), (*s).to_owned())])
1757 .ok_or_else(|| format!("no bidama named \"{name}\""))
1758 }
1759 }
1760
1761 const BAKUHATSU: &str = "\
1769def tsukawanai_a()
1770 1
1771end
1772
1773def tsukawanai_b()
1774 2
1775end
1776
1777# waive B0001: the fixture needs a raise at runtime
1778def bakuhatsu()
1779 kore_wa_sonzai_shinai()
1780end
1781
1782bakuhatsu";
1783
1784 const KOTAE: &str = "def kotae()\n 42\nend";
1785
1786 const BAKUHATSU2: &str =
1789 "# waive B0001: a raise at runtime\ndef yobu()\n kore_wa_sonzai_shinai()\nend";
1790
1791 fn loader() -> MemLoader {
1792 MemLoader(BTreeMap::from([
1793 ("kotae", KOTAE),
1794 ("bakuhatsu", BAKUHATSU),
1795 ("bakuhatsu2", BAKUHATSU2),
1796 ]))
1797 }
1798
1799 fn run_named(path: &str, src: &str) -> RunError {
1800 run_in_surface(
1801 Entry {
1802 path: Some(Path::new(path)),
1803 text: src,
1804 },
1805 Inputs::new(),
1806 &loader(),
1807 None,
1808 )
1809 .expect_err("the fixture must raise")
1810 }
1811
1812 const TWO_IMPORTS: &str =
1857 "use(\"kotae\")\n# waive B0015: kotae loads first on purpose\nuse(\"bakuhatsu\")\n";
1858
1859 #[test]
1860 fn a_raise_inside_an_imported_package_names_that_package() {
1861 let err = run_named("entry.b", TWO_IMPORTS);
1862 assert_eq!(
1863 err.to_string(),
1864 "runtime error: bakuhatsu.b:11:3: unbound symbol `kore_wa_sonzai_shinai`\n \
1865 at bakuhatsu::bakuhatsu (bakuhatsu.b:11:3)\n \
1866 at <top level> (bakuhatsu.b:14:1)"
1867 );
1868 }
1869
1870 #[test]
1878 fn line_11_column_3_of_the_fixture_is_the_failing_call() {
1879 let line = BAKUHATSU.lines().nth(10).expect("line 11 exists");
1880 assert_eq!(line, " kore_wa_sonzai_shinai()");
1881 assert_eq!(
1882 &line[2..],
1883 "kore_wa_sonzai_shinai()",
1884 "column 3 (1-indexed) is where the call starts"
1885 );
1886 let offset = BAKUHATSU.find("kore_wa_sonzai_shinai").expect("in fixture");
1889 assert!(
1890 offset > KOTAE.len(),
1891 "kotae.b could answer for byte {offset}"
1892 );
1893 assert!(
1894 offset > TWO_IMPORTS.len(),
1895 "the entry file could answer for byte {offset}"
1896 );
1897 }
1898
1899 #[test]
1918 fn a_raise_in_the_entry_file_names_the_entry_file() {
1919 let err = run_named(
1920 "honmono.b",
1921 "def f()\n 1\nend\n\n# waive B0001: a raise at runtime\nnani_mo_nai()\n",
1922 );
1923 assert_eq!(
1924 err.to_string(),
1925 "runtime error: honmono.b:6:1: unbound symbol `nani_mo_nai`"
1926 );
1927 }
1928
1929 #[test]
1943 fn a_raise_inside_an_annotated_def_still_names_its_line() {
1944 let err = run_named(
1945 "chuu.b",
1946 "# waive B0001: a raise at runtime\ndef f(a: Int) -> Int\n mada_nai(a)\nend\n\nf(1)\n",
1947 );
1948 assert_eq!(
1949 err.to_string(),
1950 "runtime error: chuu.b:3:3: unbound symbol `mada_nai`\n \
1951 at f (chuu.b:3:3)\n \
1952 at <top level> (chuu.b:6:1)"
1953 );
1954 }
1955
1956 #[test]
1974 fn a_raise_in_a_callee_from_another_file_names_the_callees_file() {
1975 let err = run_named(
1980 "yobidashi.b",
1981 "use(\"bakuhatsu2\")\n# pad: 47's in range\nbakuhatsu2::yobu\n",
1982 );
1983 assert_eq!(
1984 err.to_string(),
1985 "runtime error: bakuhatsu2.b:3:3: unbound symbol `kore_wa_sonzai_shinai`\n \
1986 at bakuhatsu2::yobu (bakuhatsu2.b:3:3)\n \
1987 at <top level> (yobidashi.b:3:1)"
1988 );
1989 }
1990}
1991
1992#[cfg(test)]
1993mod tests {
1994 use super::*;
1995
1996 fn int(src: &str) -> i64 {
1997 match run(src).unwrap_or_else(|e| panic!("{src:?}: {e}")).value {
1998 Value::Int(v) => v,
1999 other => panic!("{src:?} produced {other:?}"),
2000 }
2001 }
2002
2003 #[test]
2006 fn annotating_buys_analysis_and_changes_nothing_else() {
2007 let plain = run("def add(a, b)\n a + b\nend\nadd(2, 3)").expect("plain");
2008 let typed = run("def add(a: Int, b: Int) -> Int\n a + b\nend\nadd(2, 3)").expect("typed");
2009
2010 assert!(matches!(plain.value, Value::Int(5)));
2011 assert!(
2012 matches!(typed.value, Value::Int(5)),
2013 "the annotated program must compute the same answer"
2014 );
2015 assert_eq!(plain.visited, 0, "no annotations means no analysis");
2016 assert!(
2017 typed.visited > 0,
2018 "an annotation must actually buy analysis, not just decorate"
2019 );
2020 assert_eq!(plain.typed_decls, 0);
2021 assert_eq!(typed.typed_decls, 1);
2022 }
2023
2024 #[test]
2028 fn a_type_error_is_reported_and_the_program_does_not_run() {
2029 let err = run("def add(a: Int, b: Int) -> Str\n a + b\nend\nadd(1, 2)")
2030 .expect_err("a declared Str return from an Int body must be rejected");
2031 assert!(
2032 matches!(err, RunError::Types(ref d) if !d.is_empty()),
2033 "expected type diagnostics, got {err}"
2034 );
2035 }
2036
2037 #[test]
2040 fn the_same_program_without_annotations_runs() {
2041 assert_eq!(int("def add(a, b)\n a + b\nend\nadd(1, 2)"), 3);
2042 }
2043
2044 #[test]
2045 fn a_parse_error_is_reported_as_one() {
2046 assert!(matches!(run("def (").unwrap_err(), RunError::Parse(_)));
2047 }
2048
2049 #[test]
2052 fn a_runtime_error_is_reported_as_one() {
2053 let err = run("throw(error(\"boom\"))").expect_err("a raise");
2054 assert!(matches!(err, RunError::Eval(_)), "got {err}");
2055 }
2056
2057 #[test]
2060 fn the_pipeline_reaches_both_runtime_layers() {
2061 assert_eq!(int("6 % 3"), 0);
2062 assert_eq!(int("7 % 3"), 1);
2063 assert_eq!(int("2 + 3 * 4"), 14);
2064 }
2065
2066 #[test]
2076 fn the_deleted_round_trip_agreed_with_the_direct_lowering() {
2077 let corpus = [
2078 "def add(a, b)\n a + b\nend\nadd(2, 3)",
2079 "def fact(n)\n if n < 2\n 1\n else\n n * fact(n - 1)\n end\nend\nfact(5)",
2080 "def f(a, b)\n c = a + b\n c * 2\nend\nf(1, 2)",
2081 "defmacro sq(e)\n quote\n unquote(e) * unquote(e)\n end\nend\nsq(2 + 3)",
2082 "\"a string with spaces, a ( and a )\"",
2083 "def g(a: Int) -> Int\n a + 1\nend\ng(1)",
2084 "6 % 3",
2085 "1.5 + 2.25",
2086 ];
2087 for src in corpus {
2088 let erased = crate::to_sexps(&erase_types(&parse_tree(src).expect("parse")));
2091
2092 let direct: Vec<Sexp> = crate::lower_to_spanned(&erased)
2093 .iter()
2094 .map(tatara_lisp::Spanned::to_sexp)
2095 .collect();
2096 assert_eq!(direct, erased, "the direct lowering must be the identity");
2097
2098 let text = erased
2099 .iter()
2100 .map(ToString::to_string)
2101 .collect::<Vec<_>>()
2102 .join("\n");
2103 let round_tripped: Vec<Sexp> = tatara_lisp::read_spanned(&text)
2104 .unwrap_or_else(|e| panic!("{src:?}: the old path could not read back: {e:?}"))
2105 .iter()
2106 .map(tatara_lisp::Spanned::to_sexp)
2107 .collect();
2108 assert_eq!(
2109 round_tripped, erased,
2110 "{src:?}: the old print-and-reparse path changed the tree"
2111 );
2112 }
2113 }
2114
2115 #[test]
2133 fn the_round_trip_is_not_the_identity_in_general() {
2134 let tree = Sexp::List(vec![
2135 Sexp::Atom(tatara_lisp::Atom::Symbol("f".into())),
2136 Sexp::Atom(tatara_lisp::Atom::Symbol("a b".into())),
2137 ]);
2138 let text = tree.to_string();
2139 let back: Vec<Sexp> = tatara_lisp::read_spanned(&text)
2140 .expect("it reads back cleanly — that IS the problem")
2141 .iter()
2142 .map(tatara_lisp::Spanned::to_sexp)
2143 .collect();
2144 assert_ne!(
2145 back,
2146 vec![tree.clone()],
2147 "if print-then-read became inverse over symbols, the class would be \
2148 closed upstream and this test should be deleted rather than relaxed"
2149 );
2150 let direct: Vec<Sexp> = crate::lower_to_spanned(std::slice::from_ref(&tree))
2152 .iter()
2153 .map(tatara_lisp::Spanned::to_sexp)
2154 .collect();
2155 assert_eq!(direct, vec![tree]);
2156 }
2157}
2158
2159#[cfg(test)]
2160mod macro_tests {
2161 use super::*;
2162
2163 fn int(src: &str) -> i64 {
2164 match run(src).unwrap_or_else(|e| panic!("{src:?}: {e}")).value {
2165 Value::Int(v) => v,
2166 other => panic!("{src:?} produced {other:?}"),
2167 }
2168 }
2169
2170 #[test]
2172 fn a_macro_expands_and_runs() {
2173 assert_eq!(
2174 int("defmacro double(x)\n quote\n unquote(x) + unquote(x)\n end\nend\ndouble(21)"),
2175 42
2176 );
2177 }
2178
2179 #[test]
2182 fn a_macro_operates_on_syntax_not_values() {
2183 assert_eq!(
2184 int("defmacro sq(e)\n quote\n unquote(e) * unquote(e)\n end\nend\nsq(2 + 3)"),
2185 25,
2186 "the argument form `2 + 3` must be substituted twice"
2187 );
2188 }
2189
2190 #[test]
2209 fn a_runaway_macro_fails_the_compilation_rather_than_the_process() {
2210 let err =
2211 run("defmacro forever(x)\n quote\n forever(unquote(x))\n end\nend\nforever(1)")
2212 .expect_err("a self-referential macro must be rejected");
2213 let msg = err.to_string();
2214 assert!(
2215 msg.contains("forever") && msg.contains("expansion"),
2216 "the error must name the macro and the limit it hit: {msg}"
2217 );
2218 assert!(
2219 msg.contains("<anonymous>:6:1"),
2220 "and must place the call that ran away — line 6 is `forever(1)`: {msg}"
2221 );
2222 }
2223}
2224
2225#[cfg(test)]
2226mod input_tests {
2227 use super::*;
2228 use crate::inputs::{Declaration, Inputs};
2229
2230 const SCHEMA: &[u8] = b"3";
2232
2233 fn with_schema(src: &str) -> Result<Run, RunError> {
2234 let hash = Inputs::hash_of(SCHEMA);
2235 let mut inputs = Inputs::new();
2236 inputs
2237 .bind(
2238 &Declaration {
2239 name: "schema".to_string(),
2240 hash,
2241 },
2242 SCHEMA.to_vec(),
2243 )
2244 .expect("bind");
2245 run_with_inputs(src, inputs)
2246 }
2247
2248 fn decl_line() -> String {
2249 let mut s = String::from("definput(\"schema\", \"");
2250 s.push_str(&Inputs::hash_of(SCHEMA));
2251 s.push_str("\")\n");
2252 s
2253 }
2254
2255 #[test]
2260 fn a_macro_can_read_a_declared_build_input() {
2261 let src = decl_line() + "input(\"schema\")";
2262 let out = with_schema(&src).expect("run");
2263 assert!(
2264 matches!(out.value, Value::Str(ref s) if &**s == "3"),
2265 "got {:?}",
2266 out.value
2267 );
2268 }
2269
2270 #[test]
2274 fn an_undeclared_input_is_an_error() {
2275 let err = with_schema("input(\"not_declared\")").expect_err("must fail");
2276 let msg = err.to_string();
2277 assert!(msg.contains("not_declared"), "must name it: {msg}");
2278 assert!(msg.contains("definput"), "and say how to declare it: {msg}");
2279 }
2280
2281 #[cfg(not(feature = "sys"))]
2289 #[test]
2290 fn there_is_no_ambient_file_read() {
2291 for attempt in [
2292 "read_file(\"/etc/passwd\")",
2293 "File(\"/etc/passwd\")",
2294 "slurp(\"/etc/passwd\")",
2295 "open(\"/etc/passwd\")",
2296 ] {
2297 let err = with_schema(attempt).expect_err("must not resolve");
2298 assert!(
2299 err.to_string().contains("unbound"),
2300 "{attempt} must be UNBOUND — a capability removed by absence, \
2301 not guarded by a check: {err}"
2302 );
2303 }
2304 }
2305
2306 #[cfg(feature = "sys")]
2312 #[test]
2313 fn sys_read_file_is_the_trusted_cli_only_exception() {
2314 let err = with_schema("definitely_not_a_primitive(\"x\")").expect_err("must not resolve");
2315 assert!(err.to_string().contains("unbound"), "{err}");
2316 assert!(
2317 with_schema("read_file(\"/etc/passwd\")").is_ok(),
2318 "with `sys` on, read_file is the trusted CLI surface"
2319 );
2320 let out = with_schema("input(\"schema\")").expect("run");
2321 assert!(
2322 matches!(out.value, Value::Str(ref s) if &**s == "3"),
2323 "input() still binds beside the sys surface: {:?}",
2324 out.value
2325 );
2326 }
2327
2328 #[test]
2331 fn a_declared_input_with_no_bytes_supplied_fails() {
2332 let src = decl_line() + "input(\"schema\")";
2333 assert!(run(&src).is_err(), "no bytes were supplied");
2334 }
2335}
2336
2337#[cfg(test)]
2338mod tier2_tests {
2339 use super::*;
2340 use crate::inputs::{Declaration, Inputs};
2341
2342 #[test]
2354 fn a_macro_generates_code_from_a_schema() {
2355 let schema = b"7";
2356 let mut inputs = Inputs::new();
2357 inputs
2358 .bind(
2359 &Declaration {
2360 name: "arity".to_string(),
2361 hash: Inputs::hash_of(schema),
2362 },
2363 schema.to_vec(),
2364 )
2365 .expect("bind");
2366
2367 let mut src = String::from("definput(\"arity\", \"");
2370 src.push_str(&Inputs::hash_of(schema));
2371 src.push_str("\")\n");
2372 src.push_str(
2373 "defmacro from_schema()\n quote\n unquote(to_int(input(\"arity\")))\n end\nend\n\
2374 from_schema() * 6",
2375 );
2376
2377 let out = run_with_inputs(&src, inputs).expect("run");
2378 assert!(
2379 matches!(out.value, Value::Int(42)),
2380 "the schema's 7 must reach the generated code: got {:?}",
2381 out.value
2382 );
2383 }
2384}