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
76#[derive(Debug)]
78pub struct Run {
79 pub value: Value,
80 pub visited: usize,
84 pub typed_decls: usize,
86 pub seams: usize,
88}
89
90pub fn parse(src: &str) -> Result<Vec<Sexp>, RunError> {
92 parse_with_depth(src, blue_lang_syntax::MAX_EXPR_DEPTH)
93}
94
95pub fn parse_with_depth(src: &str, max_depth: usize) -> Result<Vec<Sexp>, RunError> {
102 blue_lang_syntax::parse_program_with_depth(src, max_depth)
103 .map_err(|e| RunError::Parse(e.to_string()))
104}
105
106pub fn parse_tree_with_depth(
114 src: &str,
115 max_depth: usize,
116) -> Result<Vec<blue_lang_syntax::Spanned>, RunError> {
117 blue_lang_syntax::parse_program_tree_with_depth(src, max_depth)
118 .map_err(|e| RunError::Parse(e.to_string()))
119}
120
121pub fn parse_tree(src: &str) -> Result<Vec<blue_lang_syntax::Spanned>, RunError> {
128 parse_tree_with_depth(src, blue_lang_syntax::MAX_EXPR_DEPTH)
129}
130
131pub fn run(src: &str) -> Result<Run, RunError> {
133 run_with_inputs(src, Inputs::new())
134}
135
136pub fn run_with_inputs(src: &str, inputs: Inputs) -> Result<Run, RunError> {
142 run_with_loader(src, inputs, &crate::uses::NoLoader)
143}
144
145pub fn run_with_loader(
153 src: &str,
154 inputs: Inputs,
155 loader: &dyn crate::uses::Loader,
156) -> Result<Run, RunError> {
157 run_in_surface(Entry::anonymous(src), inputs, loader, None)
158}
159
160pub fn run_in_surface(
177 entry: Entry<'_>,
178 inputs: Inputs,
179 loader: &dyn crate::uses::Loader,
180 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
181) -> Result<Run, RunError> {
182 run_framed(entry, inputs, loader, surface, None)
183}
184
185pub fn run_framed(
196 entry: Entry<'_>,
197 inputs: Inputs,
198 loader: &dyn crate::uses::Loader,
199 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
200 hold: Option<&crate::Hold>,
201) -> Result<Run, RunError> {
202 let mut interp =
207 crate::timings::stage(crate::timings::Stage::Runtime, crate::interpreter_hostless);
208 crate::inputs::install_input_primitives(&mut interp, inputs);
209 let builtins = crate::timings::stage(crate::timings::Stage::Names, || builtin_names(&interp));
210 let prepared = prepare(entry, loader, surface, &builtins)?;
211 hold_to_frame(&mut interp, &prepared, hold)?;
212 let mut value = crate::timings::stage(crate::timings::Stage::Eval, || {
213 eval_prepared(&mut interp, &prepared, &mut ())
214 })?;
215 if let Some(v) = crate::timings::stage(crate::timings::Stage::Main, || {
216 call_main(&mut interp, &prepared, &mut ())
217 })? {
218 value = v;
219 }
220 Ok(Run {
221 value,
222 visited: prepared.visited,
223 typed_decls: prepared.typed_decls,
224 seams: prepared.seams,
225 })
226}
227
228fn hold_to_frame(
231 interp: &mut tatara_lisp_eval::Interpreter<()>,
232 prepared: &Prepared,
233 hold: Option<&crate::Hold>,
234) -> Result<(), RunError> {
235 let Some(hold) = hold else {
236 return Ok(());
237 };
238 let escapes = entry_escapes(&prepared.program, &hold.withheld_host_names());
239 if !escapes.is_empty() {
240 return Err(RunError::Types(escapes));
241 }
242 hold.bind(interp, "this run's frame");
243 crate::attach_node_held(interp, hold);
244 Ok(())
245}
246
247fn entry_escapes(
248 program: &crate::uses::ResolvedProgram,
249 refused: &[(&'static str, blue_lang_waku::Capability)],
250) -> Vec<String> {
251 use crate::uses::ResolvedProgram;
252 let forms: Vec<tatara_lisp::Spanned> = program
253 .forms()
254 .iter()
255 .enumerate()
256 .filter(|(i, _)| program.owner_of(*i) == Some(ResolvedProgram::ENTRY))
257 .map(|(_, f)| f.clone())
258 .collect();
259 let label = |name: &str| {
260 refused
261 .iter()
262 .find(|(n, _)| *n == name)
263 .map(|(_, c)| c.label())
264 };
265 blue_lang_waku::free_names(&forms, &|name| label(name).is_none())
266 .into_iter()
267 .filter_map(|name| {
268 let cap = label(&name)?;
269 let at = program
270 .file(ResolvedProgram::ENTRY)
271 .and_then(|f| f.path.clone())
272 .map_or_else(|| "<entry>".to_string(), |p| p.display().to_string());
273 Some(format!(
274 "{at}: refused: `{name}` reaches the host ({cap}), which this run does not allow; pass --allow {cap} to grant it"
275 ))
276 })
277 .collect()
278}
279
280pub struct Observed {
283 pub definitions: std::sync::Arc<crate::observe::Definitions>,
284 pub outcome: Result<Run, RunError>,
286}
287
288pub struct Watched<'a> {
292 pub definitions: &'a std::sync::Arc<crate::observe::Definitions>,
293 pub program: &'a crate::uses::ResolvedProgram,
294 pub lowered: &'a [tatara_lisp::Spanned],
295 pub erased: &'a [tatara_lisp::Spanned],
296}
297
298pub fn run_observed(
309 entry: Entry<'_>,
310 inputs: Inputs,
311 loader: &dyn crate::uses::Loader,
312 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
313 watch: impl FnOnce(&Watched<'_>) -> std::sync::Arc<dyn crate::observe::CallObserver>,
314) -> Result<Observed, RunError> {
315 run_observed_framed(entry, inputs, loader, surface, None, watch)
316}
317
318pub fn run_observed_framed(
325 entry: Entry<'_>,
326 inputs: Inputs,
327 loader: &dyn crate::uses::Loader,
328 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
329 hold: Option<&crate::Hold>,
330 watch: impl FnOnce(&Watched<'_>) -> std::sync::Arc<dyn crate::observe::CallObserver>,
331) -> Result<Observed, RunError> {
332 let mut interp =
333 crate::timings::stage(crate::timings::Stage::Runtime, crate::interpreter_hostless);
334 crate::inputs::install_input_primitives(&mut interp, inputs);
335 let builtins = crate::timings::stage(crate::timings::Stage::Names, || builtin_names(&interp));
336 let prepared = prepare(entry, loader, surface, &builtins)?;
337 hold_to_frame(&mut interp, &prepared, hold)?;
338 let definitions = std::sync::Arc::new(crate::observe::Definitions::of(
339 &prepared.program,
340 &prepared.erased,
341 ));
342 interp.set_call_observer(Some(watch(&Watched {
343 definitions: &definitions,
344 program: &prepared.program,
345 lowered: &prepared.lowered,
346 erased: &prepared.erased,
347 })));
348 let outcome = crate::timings::stage(crate::timings::Stage::Eval, || {
349 eval_prepared(&mut interp, &prepared, &mut ())
350 })
351 .and_then(|value| {
352 Ok(crate::timings::stage(crate::timings::Stage::Main, || {
353 call_main(&mut interp, &prepared, &mut ())
354 })?
355 .unwrap_or(value))
356 })
357 .map(|value| Run {
358 value,
359 visited: prepared.visited,
360 typed_decls: prepared.typed_decls,
361 seams: prepared.seams,
362 });
363 interp.set_call_observer(None);
364 Ok(Observed {
365 definitions,
366 outcome,
367 })
368}
369
370pub(crate) struct Prepared {
375 program: crate::uses::ResolvedProgram,
376 lowered: Vec<tatara_lisp::Spanned>,
377 erased: Vec<tatara_lisp::Spanned>,
378 visited: usize,
379 typed_decls: usize,
380 seams: usize,
381}
382
383impl Prepared {
384 pub(crate) fn erased(&self) -> &[tatara_lisp::Spanned] {
386 &self.erased
387 }
388
389 pub(crate) fn entry_namespace(&self) -> Namespace {
391 let first = (0..self.program.forms().len())
392 .find(|i| self.program.owner_of(*i) == Some(crate::uses::ResolvedProgram::ENTRY));
393 match first {
394 Some(i) => namespace_of(&self.program, i),
395 None => match self
396 .program
397 .file(crate::uses::ResolvedProgram::ENTRY)
398 .and_then(|f| f.package.clone())
399 {
400 Some(p) => Namespace::Bidama(p),
401 None => Namespace::File(String::new()),
402 },
403 }
404 }
405}
406
407pub(crate) fn prepare(
413 entry: Entry<'_>,
414 loader: &dyn crate::uses::Loader,
415 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
416 builtins: &NameTable,
417) -> Result<Prepared, RunError> {
418 let mut program = parse_and_resolve(entry, loader, surface)?;
419
420 program.retain(|f| !crate::uses::is_test_form(f));
433
434 let (outcome, names) = crate::timings::stage(crate::timings::Stage::Check, || {
449 check_stage(&program, builtins, false)
450 });
451 if !outcome.ok() {
452 return Err(RunError::Types(
453 outcome
454 .errors()
455 .map(|d| render(&program, d))
458 .collect(),
459 ));
460 }
461
462 let (lowered, erased) = crate::timings::stage(crate::timings::Stage::Erase, || {
476 let lowered = lower(&program, &names);
477 let erased = erase_types(&lowered);
478 (lowered, erased)
479 });
480
481 Ok(Prepared {
482 program,
483 lowered,
484 erased,
485 visited: outcome.stats.visited,
486 typed_decls: outcome.stats.typed_decls,
487 seams: outcome.seams.len(),
488 })
489}
490
491pub(crate) fn parse_and_resolve(
493 entry: Entry<'_>,
494 loader: &dyn crate::uses::Loader,
495 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
496) -> Result<crate::uses::ResolvedProgram, RunError> {
497 let forms = crate::timings::stage(crate::timings::Stage::Parse, || {
498 let mut forms = match surface {
499 Some(pack) => blue_lang_syntax::parse_program_tree_in(entry.text, pack)
500 .map_err(|e| RunError::Parse(e.to_string()))?,
501 None => loader
502 .parse(entry.text)
503 .map_err(|e| RunError::Parse(e.to_string()))?,
504 };
505 if surface.is_none() {
508 forms.extend(
509 blue_lang_syntax::doc::doctests(entry.text)
510 .map_err(|e| RunError::Parse(e.to_string()))?,
511 );
512 }
513 Ok::<_, RunError>(forms)
514 })?;
515
516 crate::timings::stage(crate::timings::Stage::Resolve, || {
527 crate::uses::resolve_uses(forms, entry, loader).map_err(RunError::Import)
528 })
529}
530
531pub(crate) fn check_stage(
544 program: &crate::uses::ResolvedProgram,
545 builtins: &NameTable,
546 with_tests: bool,
547) -> (blue_lang_check::Outcome, NameTable) {
548 use crate::uses::ResolvedProgram;
549 let forms = program.forms();
550 let is_entry = |i: usize| program.owner_of(i) == Some(ResolvedProgram::ENTRY);
551
552 let mut outcome = blue_lang_check::check_program_in(forms, &|a, b| {
554 program.owner_of(a) == program.owner_of(b)
555 });
556 outcome
557 .diagnostics
558 .extend(data_law_diagnostics(program, &is_entry));
559
560 let table = program_names(program, builtins);
563 let whole = |i: usize| with_tests && is_entry(i);
564 let (names, resolved) = blue_lang_check::names::check_names_whole(
565 forms,
566 &table,
567 &|i| namespace_of(program, i),
568 &is_entry,
569 &whole,
570 );
571 let stored = program
572 .file(ResolvedProgram::ENTRY)
573 .is_some_and(|f| f.path.is_some());
574 outcome.diagnostics.extend(names);
575 outcome.stats.names_resolved = resolved;
576 if stored {
577 let script = program
578 .file(ResolvedProgram::ENTRY)
579 .is_some_and(|f| f.package.is_none());
580 let found = blue_lang_check::form_rules::check(forms, &is_entry, script);
581 let silenced: Vec<tatara_lisp::Span> = found
582 .iter()
583 .filter(|d| d.code == blue_lang_check::Code::B0038)
584 .map(|d| d.span)
585 .collect();
586 outcome
587 .diagnostics
588 .retain(|d| !(d.code == blue_lang_check::Code::B0002 && silenced.contains(&d.span)));
589 outcome.diagnostics.extend(found);
590 }
591 outcome
592 .diagnostics
593 .extend(blue_lang_check::contracts::check(
594 forms,
595 &table,
596 &|i| namespace_of(program, i),
597 &is_entry,
598 &|i| {
599 program
600 .owner_of(i)
601 .and_then(|f| program.file(f))
602 .map(|f| f.text.as_str())
603 },
604 crate::docs::signatures(),
605 ));
606
607 outcome.diagnostics.extend(doc_diagnostics(program));
608
609 let mut waivers = Vec::new();
612 for file in program.files() {
613 let spans: Vec<(usize, tatara_lisp::Span)> = forms
614 .iter()
615 .enumerate()
616 .filter(|(i, _)| program.owner_of(*i) == Some(file.id))
617 .map(|(i, f)| (i, f.span))
618 .collect();
619 let (found, malformed) = blue_lang_check::waiver::collect(&file.text, &spans);
620 waivers.extend(found);
621 outcome.diagnostics.extend(malformed);
622 }
623 outcome.waived = blue_lang_check::waiver::apply(&mut outcome.diagnostics, waivers, &is_entry);
624 if !stored {
625 outcome.diagnostics.retain(|d| {
626 !matches!(
627 d.code,
628 blue_lang_check::Code::B0023
629 | blue_lang_check::Code::B0036
630 | blue_lang_check::Code::B0037
631 | blue_lang_check::Code::B0067
632 | blue_lang_check::Code::B0061
633 | blue_lang_check::Code::B0062
634 | blue_lang_check::Code::B0063
635 )
636 });
637 }
638 let judge = blue_lang_check::evolution::FormJudge::from_env().unwrap_or_default();
639 outcome
640 .diagnostics
641 .retain(|d| judge.reports(d.code, is_entry(d.top_level)));
642 let (enforced, census): (Vec<_>, Vec<_>) = std::mem::take(&mut outcome.diagnostics)
645 .into_iter()
646 .partition(|d| d.code.rule().active());
647 outcome.diagnostics = enforced;
648 outcome.census = census;
649 outcome
650 .diagnostics
651 .sort_by_key(|d| (d.top_level, d.span.start, d.code));
652 (outcome, table)
653}
654
655fn doc_diagnostics(program: &crate::uses::ResolvedProgram) -> Vec<blue_lang_check::Diagnostic> {
659 use crate::uses::ResolvedProgram;
660 let Some(file) = program.file(ResolvedProgram::ENTRY) else {
661 return Vec::new();
662 };
663 let Some(package) = file.package.as_deref() else {
664 return Vec::new();
665 };
666 let forms: Vec<(usize, &tatara_lisp::Spanned)> = program
667 .forms()
668 .iter()
669 .enumerate()
670 .filter(|(i, f)| {
671 program.owner_of(*i) == Some(ResolvedProgram::ENTRY)
672 && !blue_lang_syntax::doc::is_doctest(f)
673 })
674 .collect();
675 let graph = file.path.as_deref().map(crate::doc_sources::graph_meanings);
676 let meaning = |name: &str| {
677 graph
678 .as_ref()
679 .and_then(|g| g.get(&(package.to_string(), name.to_string())).cloned())
680 };
681 let names_line = |name: &str| crate::docs::doc_of(name).map(|d| d.doc.to_string());
682 blue_lang_check::doc_rules::check_file(
683 &file.text,
684 &forms,
685 package,
686 &blue_lang_check::doc_rules::Sources {
687 meaning: &meaning,
688 names_line: &names_line,
689 },
690 )
691}
692
693fn data_law_diagnostics(
694 program: &crate::uses::ResolvedProgram,
695 is_entry: &dyn Fn(usize) -> bool,
696) -> Vec<blue_lang_check::Diagnostic> {
697 use blue_lang_check::data_laws;
698 let forms = program.forms();
699 let fragments = data_laws::Fragments::collect(forms);
700 let frontend = program.frontend();
701 let mut out = Vec::new();
702 let schema = match frontend.and_then(|f| f.table.as_ref()) {
703 Some((label, text)) => match data_laws::Schema::read(text) {
704 Ok(s) => Some(s),
705 Err(why) => {
706 if let Some((i, first)) = forms.iter().enumerate().find(|(i, _)| is_entry(*i)) {
707 out.push(
708 blue_lang_check::Diagnostic::new(
709 blue_lang_check::Code::B0050,
710 format!("the generated schema table {label} does not read: {why}"),
711 first.span,
712 )
713 .at_top_level(i)
714 .with_help("regenerate it: `nix run .#regen`"),
715 );
716 }
717 None
718 }
719 },
720 None => None,
721 };
722 if fragments.is_empty() && schema.is_none() && !frontend.is_some_and(|f| f.view) {
723 return out;
724 }
725 out.extend(data_laws::check(
726 forms,
727 &data_laws::Context {
728 fragments: &fragments,
729 schema: schema.as_ref(),
730 is_entry,
731 view: frontend.is_some_and(|f| f.view),
732 },
733 ));
734 out
735}
736
737#[must_use]
750pub fn program_names(program: &crate::uses::ResolvedProgram, builtins: &NameTable) -> NameTable {
751 use blue_lang_check::Binding;
752 let mut table = NameTable::new();
753 table.add_program(program.forms(), |i| namespace_of(program, i));
754 let builtin_macro = |head: &str| {
755 builtins
756 .scopes()
757 .iter()
758 .any(|s| s.namespace == Namespace::Macro && s.get(head).is_some())
759 };
760 let mut expander: Option<tatara_lisp_eval::Interpreter<()>> = None;
761 for (i, form) in program.forms().iter().enumerate() {
762 let Some(head) = form
763 .as_list()
764 .and_then(|items| items.first())
765 .and_then(tatara_lisp::Spanned::as_symbol)
766 else {
767 continue;
768 };
769 if !builtin_macro(head) {
770 continue;
771 }
772 let interp = expander.get_or_insert_with(crate::interpreter_hostless);
773 let Ok(expanded) = interp.fully_expand(form, &mut ()) else {
776 continue;
777 };
778 let nullary = blue_lang_syntax::scope::nullary_definition(&expanded);
779 for (name, span, kind) in blue_lang_check::names::definitions_of(&expanded) {
780 table.define(
781 namespace_of(program, i),
782 Binding {
783 nullary: nullary == Some(name.as_str()),
784 name,
785 kind,
786 span: Some(span),
787 top_level: Some(i),
788 },
789 );
790 }
791 }
792 for scope in builtins.scopes() {
793 table.push(scope.clone());
794 }
795 table.spell_bare_like(builtins);
796 let form_file: Vec<usize> = (0..program.forms().len())
797 .map(|i| program.owner_of(i).map_or(usize::MAX, |f| f.index()))
798 .collect();
799 let mut imports: std::collections::BTreeMap<usize, blue_lang_check::names::FileImports> =
800 std::collections::BTreeMap::new();
801 for (file, import) in program.imports() {
802 let entry = imports.entry(file.index()).or_default();
803 entry.add(&import.package, import.names.iter().map(|(n, _)| n.clone()));
804 let at = import.span.start;
805 entry.use_at = Some(entry.use_at.map_or(at, |a| a.min(at)));
806 }
807 let facade_uses =
812 |pkg: &str,
813 imports: &std::collections::BTreeMap<usize, blue_lang_check::names::FileImports>| {
814 let defines = table.bidama(pkg).is_some_and(|s| !s.is_empty());
815 if defines {
816 return std::collections::BTreeSet::new();
817 }
818 program
819 .files()
820 .iter()
821 .filter(|f| f.package.as_deref() == Some(pkg))
822 .filter_map(|f| imports.get(&f.id.index()))
823 .flat_map(|i| i.uses.iter().cloned())
824 .collect::<std::collections::BTreeSet<String>>()
825 };
826 loop {
827 let mut grew = false;
828 let files: Vec<usize> = imports.keys().copied().collect();
829 for f in files {
830 let used: Vec<String> = imports[&f].uses.iter().cloned().collect();
831 for p in used {
832 for member in facade_uses(&p, &imports) {
833 if let Some(i) = imports.get_mut(&f) {
834 if i.uses.insert(member.clone()) {
835 i.via.insert(member, p.clone());
836 grew = true;
837 }
838 }
839 }
840 }
841 }
842 if !grew {
843 break;
844 }
845 }
846 let mut pkg_uses: std::collections::BTreeMap<String, std::collections::BTreeSet<String>> =
847 std::collections::BTreeMap::new();
848 for (file, import) in program.imports() {
849 if let Some(p) = program.file(*file).and_then(|f| f.package.clone()) {
850 pkg_uses
851 .entry(p)
852 .or_default()
853 .insert(import.package.clone());
854 }
855 }
856 table.attach_files(form_file, imports);
857 table.attach_package_uses(pkg_uses);
858 table.attach_needs(program.needs().clone());
859 let canonical = |p: &std::path::Path| {
863 std::fs::canonicalize(p)
864 .unwrap_or_else(|_| p.to_path_buf())
865 .display()
866 .to_string()
867 };
868 let mut file_paths = std::collections::BTreeMap::new();
869 let mut pkg_roots = std::collections::BTreeMap::new();
870 for f in program.files() {
871 if let Some(path) = &f.path {
872 file_paths.insert(f.id.index(), canonical(path));
873 if let (Some(pkg), Some(root)) =
874 (&f.package, path.parent().and_then(std::path::Path::parent))
875 {
876 pkg_roots
877 .entry(pkg.clone())
878 .or_insert_with(|| canonical(root));
879 }
880 }
881 }
882 table.attach_paths(file_paths, pkg_roots);
883 let versions = program.versions().clone();
884 table.apply_legacy(program.forms(), &|i| namespace_of(program, i), &|p| {
885 versions.get(p).cloned()
886 });
887 table
888}
889
890#[must_use]
902pub fn lower(
903 program: &crate::uses::ResolvedProgram,
904 names: &NameTable,
905) -> Vec<tatara_lisp::Spanned> {
906 lower_with(program, names, true)
907}
908
909#[must_use]
910pub fn lower_with(
911 program: &crate::uses::ResolvedProgram,
912 names: &NameTable,
913 simplify_chains: bool,
914) -> Vec<tatara_lisp::Spanned> {
915 let mut resolved = blue_lang_check::names::resolve_program(program.forms(), names, &|i| {
916 namespace_of(program, i)
917 });
918 let simplified = if simplify_chains {
919 chain_simplified(program.forms(), &resolved)
920 } else {
921 None
922 };
923 let forms: &[tatara_lisp::Spanned] = simplified.as_deref().unwrap_or(program.forms());
924 if simplified.is_some() {
925 resolved =
926 blue_lang_check::names::resolve_program(forms, names, &|i| namespace_of(program, i));
927 }
928 resolved
929 .resolved_tree(forms, blue_lang_check::names::Rule::Namespaced)
930 .into_iter()
931 .zip(forms)
932 .enumerate()
933 .map(|(i, (resolved, written))| {
934 if crate::uses::use_target(written).is_some()
935 || blue_lang_syntax::scope::legacy_target(written).is_some()
936 {
937 tatara_lisp::Spanned::new(written.span, tatara_lisp::SpannedForm::Nil)
938 } else {
939 let file = program.owner_of(i).and_then(|id| program.file(id));
940 match file {
941 Some(f) => locate_raises(&resolved, &raise_site(f)),
942 None => resolved,
943 }
944 }
945 })
946 .collect()
947}
948
949fn chain_simplified(
950 forms: &[tatara_lisp::Spanned],
951 resolved: &blue_lang_check::names::Resolved,
952) -> Option<Vec<tatara_lisp::Spanned>> {
953 let respell: std::collections::HashMap<usize, blue_lang_check::respell::Respelling> = resolved
954 .respellings
955 .iter()
956 .filter(|(_, r)| matches!(r.respell, blue_lang_check::respell::Respell::Chain(_)))
957 .cloned()
958 .collect();
959 if respell.is_empty() {
960 return None;
961 }
962 let bare = std::collections::HashSet::new();
963 let canon = Canon {
964 bare: &bare,
965 respell: &respell,
966 };
967 Some(forms.iter().map(|f| canon.form(f)).collect())
968}
969
970fn raise_site(file: &crate::uses::SourceFile) -> impl Fn(tatara_lisp::Span) -> String + '_ {
971 move |span| {
972 let (line, _) =
973 blue_lang_syntax::Span::line_col(&file.text, span.start.min(file.text.len()));
974 let shown = file
975 .path
976 .as_ref()
977 .map_or_else(|| "<source>".to_string(), |p| p.display().to_string());
978 format!("{shown}:{line}")
979 }
980}
981
982fn locate_raises(
983 form: &tatara_lisp::Spanned,
984 site: &dyn Fn(tatara_lisp::Span) -> String,
985) -> tatara_lisp::Spanned {
986 use tatara_lisp::{Atom, Spanned, SpannedForm};
987 let inner = match &form.form {
988 SpannedForm::List(items) => {
989 let mut out: Vec<Spanned> = items.iter().map(|i| locate_raises(i, site)).collect();
990 let raising = items.first().and_then(Spanned::as_symbol) == Some("raise")
991 && (3..=4).contains(&items.len());
992 if raising {
993 out[0] = Spanned::new(
994 items[0].span,
995 SpannedForm::Atom(Atom::Symbol(crate::errors::RAISE_AT.to_string())),
996 );
997 out.insert(
998 1,
999 Spanned::new(items[0].span, SpannedForm::Atom(Atom::Str(site(form.span)))),
1000 );
1001 }
1002 SpannedForm::List(out)
1003 }
1004 SpannedForm::Quote(_) | SpannedForm::Quasiquote(_) => return form.clone(),
1005 other => other.clone(),
1006 };
1007 Spanned::new(form.span, inner)
1008}
1009
1010#[must_use]
1012pub fn namespace_of(program: &crate::uses::ResolvedProgram, i: usize) -> Namespace {
1013 let file = program.owner_of(i).and_then(|id| program.file(id));
1014 match file {
1015 Some(f) => match &f.package {
1016 Some(p) => Namespace::Bidama(p.clone()),
1017 None => Namespace::File(
1018 f.path
1019 .as_ref()
1020 .map_or_else(|| "<anonymous>".to_string(), |p| p.display().to_string()),
1021 ),
1022 },
1023 None => Namespace::File("<unknown file>".to_string()),
1024 }
1025}
1026
1027pub const HARNESS_NAMES: &[&str] = &[blue_lang_syntax::LOWERED_ASSERT];
1041
1042#[must_use]
1052pub fn builtin_names<H: 'static>(interp: &tatara_lisp_eval::Interpreter<H>) -> NameTable {
1053 builtin_names_with(interp, &[])
1054}
1055
1056#[must_use]
1057pub fn builtin_names_with<H: 'static>(
1058 interp: &tatara_lisp_eval::Interpreter<H>,
1059 host: &[crate::host_surface::HostName],
1060) -> NameTable {
1061 use blue_lang_check::{Binding, Scope, ScopeKind};
1062 use tatara_lisp_eval::HeadBinding;
1063 let mut special = Scope::new(Namespace::SpecialForm);
1064 let mut macros = Scope::new(Namespace::Macro);
1065 let mut values = Scope::new(Namespace::Builtin);
1066 let mut names: Vec<std::sync::Arc<str>> = interp.reserved_head_names().into_iter().collect();
1071 names.extend(
1072 crate::inputs::INPUT_PRIMITIVES
1073 .iter()
1074 .filter(|n| interp.resolve_head(n).is_some())
1075 .map(|n| std::sync::Arc::from(*n)),
1076 );
1077 for name in names {
1078 let (scope, kind) = match interp.resolve_head(&name) {
1079 Some(HeadBinding::SpecialForm) => (&mut special, ScopeKind::SpecialForm),
1080 Some(HeadBinding::Macro) => (&mut macros, ScopeKind::Macro),
1081 _ => (&mut values, ScopeKind::Value),
1082 };
1083 let nullary = kind == ScopeKind::Value
1084 && interp.lookup_global(&name).is_some_and(|v| {
1085 nullary_value(&v)
1086 || (crate::docs::doc_of(&name).is_some_and(|d| d.signature == &*name)
1087 && matches!(
1088 v,
1089 tatara_lisp_eval::Value::NativeFn(_)
1090 | tatara_lisp_eval::Value::Closure(_)
1091 ))
1092 });
1093 scope.bind(Binding {
1094 name: name.to_string(),
1095 kind,
1096 span: None,
1097 top_level: None,
1098 nullary,
1099 });
1100 }
1101 for h in host {
1102 values.bind(Binding {
1103 name: h.name.clone(),
1104 kind: ScopeKind::Value,
1105 span: None,
1106 top_level: None,
1107 nullary: h.nullary(),
1108 });
1109 }
1110 let mut table = NameTable::new();
1111 let mut harness = Scope::new(Namespace::Harness);
1112 for n in HARNESS_NAMES {
1113 harness.bind(Binding {
1114 name: (*n).to_string(),
1115 kind: ScopeKind::Value,
1116 span: None,
1117 top_level: None,
1118 nullary: false,
1119 });
1120 }
1121 table.push(harness);
1122 table.push(special);
1123 table.push(macros);
1124 table.push(values);
1125 table.spell_bare_only(
1126 crate::docs::NAMES
1127 .iter()
1128 .filter(|d| d.signature == d.name)
1129 .map(|d| d.name.to_string())
1130 .chain(host.iter().filter(|h| h.nullary()).map(|h| h.name.clone()))
1131 .collect(),
1132 );
1133 table
1134}
1135
1136#[must_use]
1137pub fn nullary_value(value: &tatara_lisp_eval::Value) -> bool {
1138 match value {
1139 tatara_lisp_eval::Value::NativeFn(f) => {
1140 matches!(f.arity, tatara_lisp_eval::ffi::Arity::Exact(0))
1141 }
1142 tatara_lisp_eval::Value::Closure(c) => c.params.is_empty() && c.rest.is_none(),
1143 _ => false,
1144 }
1145}
1146
1147#[must_use]
1151pub fn render(program: &crate::uses::ResolvedProgram, d: &blue_lang_check::Diagnostic) -> String {
1152 let head = d.to_string();
1153 let mut out = program.locate(d.top_level, d.span, &head).to_string();
1154 if let Some(help) = &d.help {
1155 out.push_str("\n help: ");
1156 out.push_str(help);
1157 }
1158 let file = program
1159 .owner_of(d.top_level)
1160 .and_then(|id| program.file(id))
1161 .and_then(|f| f.path.as_ref());
1162 if let Some(cmd) =
1163 file.and_then(|p| blue_lang_check::evolution::fix_command(d, &p.display().to_string()))
1164 {
1165 out.push_str("\n fix: ");
1166 out.push_str(&cmd);
1167 }
1168 out
1169}
1170
1171#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1173pub enum Checking {
1174 Program,
1176 WithTests,
1179}
1180
1181pub struct Checked {
1183 pub program: crate::uses::ResolvedProgram,
1184 pub outcome: blue_lang_check::Outcome,
1185 pub names: NameTable,
1188}
1189
1190impl Checked {
1191 #[must_use]
1194 pub fn erased(&self) -> Vec<tatara_lisp::Spanned> {
1195 erase_types(&lower(&self.program, &self.names))
1196 }
1197
1198 #[must_use]
1201 pub fn evaluable(&self) -> Vec<tatara_lisp::Sexp> {
1202 lower(&self.program, &self.names)
1203 .iter()
1204 .map(tatara_lisp::Spanned::to_sexp)
1205 .collect()
1206 }
1207
1208 #[must_use]
1212 pub fn resolve(&self) -> blue_lang_check::names::Resolved {
1213 blue_lang_check::names::resolve_program(self.program.forms(), &self.names, &|i| {
1214 namespace_of(&self.program, i)
1215 })
1216 }
1217}
1218
1219impl Checked {
1220 #[must_use]
1221 pub fn call_explicit_forms(&self) -> Vec<tatara_lisp::Spanned> {
1222 self.resolve().call_explicit_tree(self.program.forms())
1223 }
1224
1225 #[must_use]
1226 pub fn canonical_entry(&self, src: &str) -> Option<Vec<tatara_lisp::Spanned>> {
1227 use crate::uses::ResolvedProgram;
1228 let program = &self.program;
1229 let entry: Vec<&tatara_lisp::Spanned> = program
1230 .forms()
1231 .iter()
1232 .enumerate()
1233 .filter(|(i, _)| program.owner_of(*i) == Some(ResolvedProgram::ENTRY))
1234 .map(|(_, f)| f)
1235 .filter(|f| !blue_lang_syntax::doc::is_doctest(f))
1236 .collect();
1237 let written = blue_lang_syntax::parse_program_tree(src).ok()?;
1238 let same = written.len() == entry.len()
1239 && written
1240 .iter()
1241 .zip(&entry)
1242 .all(|(w, e)| w.to_sexp() == e.to_sexp() && w.span == e.span);
1243 if !same {
1244 return None;
1245 }
1246 let resolved = self.resolve();
1247 let bare: std::collections::HashSet<usize> = resolved
1248 .parenthesized_nullary_calls()
1249 .into_iter()
1250 .filter(|r| program.owner_of(r.top_level) == Some(ResolvedProgram::ENTRY))
1251 .map(blue_lang_check::names::Reference::node_address)
1252 .collect();
1253 let waived: Vec<tatara_lisp::Span> = self
1254 .outcome
1255 .waived
1256 .iter()
1257 .map(|w| w.diagnostic.span)
1258 .collect();
1259 let respell: std::collections::HashMap<usize, blue_lang_check::respell::Respelling> =
1260 resolved
1261 .respellings
1262 .iter()
1263 .filter(|(_, r)| !waived.contains(&r.span))
1264 .cloned()
1265 .collect();
1266 let canon = Canon {
1267 bare: &bare,
1268 respell: &respell,
1269 };
1270 let forms: Vec<tatara_lisp::Spanned> = entry.into_iter().map(|f| canon.form(f)).collect();
1271 let script = program
1272 .file(ResolvedProgram::ENTRY)
1273 .is_some_and(|f| f.package.is_none());
1274 let forms = if script {
1275 blue_lang_check::form_rules::without_prefix(&forms).unwrap_or(forms)
1276 } else {
1277 forms
1278 };
1279 let forms = blue_lang_check::form_rules::without_main_guard(&forms).unwrap_or(forms);
1280 Some(blue_lang_check::form_rules::eta_reduced(&forms).unwrap_or(forms))
1281 }
1282}
1283
1284struct Canon<'a> {
1285 bare: &'a std::collections::HashSet<usize>,
1286 respell: &'a std::collections::HashMap<usize, blue_lang_check::respell::Respelling>,
1287}
1288
1289impl Canon<'_> {
1290 fn form(&self, form: &tatara_lisp::Spanned) -> tatara_lisp::Spanned {
1291 use tatara_lisp::{Spanned, SpannedForm};
1292 let inner = match &form.form {
1293 SpannedForm::List(items) => match items.as_slice() {
1294 [head] if self.bare.contains(&(std::ptr::from_ref(head) as usize)) => {
1295 return Spanned::new(form.span, head.form.clone());
1296 }
1297 [head, ..]
1298 if self
1299 .respell
1300 .contains_key(&(std::ptr::from_ref(head) as usize)) =>
1301 {
1302 let r = &self.respell[&(std::ptr::from_ref(head) as usize)];
1303 let walked: Vec<Spanned> = items.iter().map(|i| self.form(i)).collect();
1304 return blue_lang_check::respell::apply(form, &walked, r);
1305 }
1306 _ => SpannedForm::List(items.iter().map(|i| self.form(i)).collect()),
1307 },
1308 SpannedForm::Quote(i) => SpannedForm::Quote(Box::new(self.form(i))),
1309 SpannedForm::Quasiquote(i) => SpannedForm::Quasiquote(Box::new(self.form(i))),
1310 SpannedForm::Unquote(i) => SpannedForm::Unquote(Box::new(self.form(i))),
1311 SpannedForm::UnquoteSplice(i) => SpannedForm::UnquoteSplice(Box::new(self.form(i))),
1312 other => other.clone(),
1313 };
1314 Spanned::new(form.span, inner)
1315 }
1316}
1317
1318pub fn check_entry(
1334 entry: Entry<'_>,
1335 loader: &dyn crate::uses::Loader,
1336 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
1337 checking: Checking,
1338) -> Result<Checked, RunError> {
1339 let mut program = parse_and_resolve(entry, loader, surface)?;
1340 if checking == Checking::Program {
1341 program.retain(|f| !crate::uses::is_test_form(f));
1342 }
1343 let host = match entry.path {
1344 Some(path) => crate::host_surface::near(path)
1345 .map_err(|e| RunError::Import(e.to_string()))?
1346 .map(|s| s.functions)
1347 .unwrap_or_default(),
1348 None => Vec::new(),
1349 };
1350 let mut interp =
1351 crate::timings::stage(crate::timings::Stage::Runtime, crate::interpreter_hostless);
1352 crate::inputs::install_input_primitives(&mut interp, Inputs::new());
1353 let builtins = crate::timings::stage(crate::timings::Stage::Names, || {
1354 builtin_names_with(&interp, &host)
1355 });
1356 let (outcome, names) = crate::timings::stage(crate::timings::Stage::Check, || {
1357 check_stage(&program, &builtins, checking == Checking::WithTests)
1358 });
1359 Ok(Checked {
1360 program,
1361 outcome,
1362 names,
1363 })
1364}
1365
1366#[must_use]
1367pub fn spelling_code(spelling: blue_lang_syntax::Spelling) -> blue_lang_check::Code {
1368 use blue_lang_check::Code;
1369 use blue_lang_syntax::Spelling;
1370 match spelling {
1371 Spelling::LogicalSymbol => Code::B0025,
1372 Spelling::Unless => Code::B0026,
1373 Spelling::ElseIf => Code::B0027,
1374 Spelling::CallForm => Code::B0028,
1375 Spelling::KeywordRocket => Code::B0029,
1376 Spelling::ParameterList => Code::B0030,
1377 Spelling::OperatorSpelling => Code::B0031,
1378 Spelling::TrailingComma => Code::B0032,
1379 Spelling::NegatedComparison => Code::B0034,
1380 Spelling::ElseNil => Code::B0035,
1381 Spelling::TryCatch => Code::B0037,
1382 Spelling::Apply => Code::B0042,
1383 Spelling::MapPun => Code::B0046,
1384 Spelling::NilTest => Code::B0047,
1385 Spelling::NewlineEscape => Code::B0065,
1386 Spelling::Index => Code::B0066,
1387 }
1388}
1389
1390#[must_use]
1391pub fn form_diagnostics(text: &str) -> Vec<blue_lang_check::Diagnostic> {
1392 match blue_lang_fmt::format_source_lossless(text) {
1393 Ok(formatted) => form_diagnostics_against(text, &formatted),
1394 Err(_) => Vec::new(),
1395 }
1396}
1397
1398#[must_use]
1399pub fn form_diagnostics_against(text: &str, formatted: &str) -> Vec<blue_lang_check::Diagnostic> {
1400 use blue_lang_check::{Code, Diagnostic};
1401 if formatted.trim_end() == text.trim_end() {
1402 return Vec::new();
1403 }
1404 let help = "`blue fmt --write FILE` writes it; no compile door writes a file (`BLUE_LAYOUT=rewrite` restores formatting in place)";
1405 let mut out: Vec<Diagnostic> = blue_lang_syntax::spelling_notes(text)
1406 .unwrap_or_default()
1407 .into_iter()
1408 .map(|n| {
1409 let message = if n.canonical.is_empty() {
1410 format!("`{}` is not written", n.written)
1411 } else {
1412 format!("`{}` is written `{}`", n.written, n.canonical)
1413 };
1414 Diagnostic::new(spelling_code(n.spelling), message, n.span)
1415 .at_top_level(0)
1416 .with_help(help)
1417 })
1418 .collect();
1419 out.extend(
1420 literal_diagnostics(text)
1421 .into_iter()
1422 .map(|d| d.with_help(help)),
1423 );
1424 if out.is_empty() {
1425 let at = text
1426 .lines()
1427 .zip(formatted.lines())
1428 .position(|(a, b)| a != b)
1429 .unwrap_or_else(|| text.lines().count().min(formatted.lines().count()));
1430 let start: usize = text.lines().take(at).map(|l| l.len() + 1).sum();
1431 let start = start.min(text.len());
1432 let end = text[start..].find('\n').map_or(text.len(), |e| start + e);
1433 out.push(
1434 Diagnostic::new(
1435 Code::B0024,
1436 format!("line {} is not laid out as blue writes it", at + 1),
1437 tatara_lisp::Span::new(start, end),
1438 )
1439 .at_top_level(0)
1440 .with_help(help),
1441 );
1442 }
1443 out.sort_by_key(|d| (d.span.start, d.code));
1444 out
1445}
1446
1447fn literal_diagnostics(text: &str) -> Vec<blue_lang_check::Diagnostic> {
1448 use blue_lang_syntax::{Atom, TokenKind};
1449 let Ok(toks) = blue_lang_syntax::lex(text) else {
1450 return Vec::new();
1451 };
1452 toks.iter()
1453 .filter_map(|t| {
1454 let atom = match &t.kind {
1455 TokenKind::Int(v) => Atom::Int(*v),
1456 TokenKind::Float(v) => Atom::Float(*v),
1457 TokenKind::Str(v) => Atom::Str(v.clone()),
1458 _ => return None,
1459 };
1460 let written = text.get(t.span.start..t.span.end)?;
1461 let canonical = blue_lang_fmt::render_literal(&atom);
1462 (written != canonical).then(|| {
1463 blue_lang_check::Diagnostic::new(
1464 blue_lang_check::Code::B0033,
1465 format!("`{written}` is written `{canonical}`"),
1466 t.span,
1467 )
1468 .at_top_level(0)
1469 })
1470 })
1471 .collect()
1472}
1473
1474#[must_use]
1478pub fn syntax_diagnostic(text: &str) -> Option<blue_lang_check::Diagnostic> {
1479 let e = blue_lang_syntax::parse_program_tree(text).err()?;
1480 Some(
1481 blue_lang_check::Diagnostic::new(blue_lang_check::Code::B0006, e.message, e.span)
1482 .at_top_level(0),
1483 )
1484}
1485
1486#[must_use]
1490pub fn display_keys(message: &str) -> String {
1491 let mut out = String::with_capacity(message.len());
1492 let mut parts = message.split('`');
1493 if let Some(first) = parts.next() {
1494 out.push_str(first);
1495 }
1496 for (i, part) in parts.enumerate() {
1497 out.push('`');
1498 if i % 2 == 0 {
1500 out.push_str(&crate::observe::display_key(part));
1501 } else {
1502 out.push_str(part);
1503 }
1504 }
1505 out
1506}
1507
1508pub(crate) fn eval_prepared<H: 'static>(
1510 interp: &mut tatara_lisp_eval::Interpreter<H>,
1511 prepared: &Prepared,
1512 host: &mut H,
1513) -> Result<Value, RunError> {
1514 let Prepared {
1515 program, erased, ..
1516 } = prepared;
1517
1518 let mut value = Value::Nil;
1532 for (top_level, form) in erased.iter().enumerate() {
1533 value = interp.eval_top_form(form, host).map_err(|e| {
1534 RunError::Eval(Box::new(crate::observe::Fault::of(
1541 program,
1542 erased,
1543 top_level,
1544 &e,
1545 &interp.take_trace(),
1546 )))
1547 })?;
1548 }
1549 Ok(value)
1550}
1551
1552pub const MAIN: &str = "main";
1553
1554#[must_use]
1555pub fn entry_main(program: &crate::uses::ResolvedProgram) -> Option<(usize, tatara_lisp::Span)> {
1556 use crate::uses::ResolvedProgram;
1557 let stored = program
1558 .file(ResolvedProgram::ENTRY)
1559 .is_some_and(|f| f.path.is_some());
1560 if !stored {
1561 return None;
1562 }
1563 program.forms().iter().enumerate().find_map(|(i, f)| {
1564 if program.owner_of(i) != Some(ResolvedProgram::ENTRY) {
1565 return None;
1566 }
1567 let items = f.as_list()?;
1568 let sig = items.get(1)?.as_list()?;
1569 let named_main = matches!(
1570 items.first().and_then(tatara_lisp::Spanned::as_symbol),
1571 Some("define" | "define-typed")
1572 ) && sig.len() == 1
1573 && sig[0].as_symbol() == Some(MAIN);
1574 named_main.then_some((i, sig[0].span))
1575 })
1576}
1577
1578pub(crate) fn call_main<H: 'static>(
1579 interp: &mut tatara_lisp_eval::Interpreter<H>,
1580 prepared: &Prepared,
1581 host: &mut H,
1582) -> Result<Option<Value>, RunError> {
1583 let Some((top_level, span)) = entry_main(&prepared.program) else {
1584 return Ok(None);
1585 };
1586 let ns = namespace_of(&prepared.program, top_level);
1587 let call = tatara_lisp::Spanned::new(
1588 span,
1589 tatara_lisp::SpannedForm::List(vec![tatara_lisp::Spanned::new(
1590 span,
1591 tatara_lisp::SpannedForm::Atom(tatara_lisp::Atom::Symbol(blue_lang_check::names::key(
1592 &ns, MAIN,
1593 ))),
1594 )]),
1595 );
1596 interp.eval_top_form(&call, host).map(Some).map_err(|e| {
1597 RunError::Eval(Box::new(crate::observe::Fault::of(
1598 &prepared.program,
1599 &prepared.erased,
1600 top_level,
1601 &e,
1602 &interp.take_trace(),
1603 )))
1604 })
1605}
1606
1607#[cfg(test)]
1623mod position_tests {
1624 use super::*;
1625 use crate::uses::{Entry, Loader};
1626 use std::collections::BTreeMap;
1627 use std::path::Path;
1628
1629 struct MemLoader(BTreeMap<&'static str, &'static str>);
1630
1631 impl Loader for MemLoader {
1632 fn load(&self, name: &str) -> Result<Vec<(String, String)>, String> {
1633 self.0
1634 .get(name)
1635 .map(|s| vec![(format!("{name}.b"), (*s).to_owned())])
1636 .ok_or_else(|| format!("no bidama named \"{name}\""))
1637 }
1638 }
1639
1640 const BAKUHATSU: &str = "\
1648def tsukawanai_a()
1649 1
1650end
1651
1652def tsukawanai_b()
1653 2
1654end
1655
1656# waive B0001: the fixture needs a raise at runtime
1657def bakuhatsu()
1658 kore_wa_sonzai_shinai()
1659end
1660
1661bakuhatsu";
1662
1663 const KOTAE: &str = "def kotae()\n 42\nend";
1664
1665 const BAKUHATSU2: &str =
1668 "# waive B0001: a raise at runtime\ndef yobu()\n kore_wa_sonzai_shinai()\nend";
1669
1670 fn loader() -> MemLoader {
1671 MemLoader(BTreeMap::from([
1672 ("kotae", KOTAE),
1673 ("bakuhatsu", BAKUHATSU),
1674 ("bakuhatsu2", BAKUHATSU2),
1675 ]))
1676 }
1677
1678 fn run_named(path: &str, src: &str) -> RunError {
1679 run_in_surface(
1680 Entry {
1681 path: Some(Path::new(path)),
1682 text: src,
1683 },
1684 Inputs::new(),
1685 &loader(),
1686 None,
1687 )
1688 .expect_err("the fixture must raise")
1689 }
1690
1691 const TWO_IMPORTS: &str =
1736 "use(\"kotae\")\n# waive B0015: kotae loads first on purpose\nuse(\"bakuhatsu\")\n";
1737
1738 #[test]
1739 fn a_raise_inside_an_imported_package_names_that_package() {
1740 let err = run_named("entry.b", TWO_IMPORTS);
1741 assert_eq!(
1742 err.to_string(),
1743 "runtime error: bakuhatsu.b:11:3: unbound symbol `kore_wa_sonzai_shinai`\n \
1744 at bakuhatsu::bakuhatsu (bakuhatsu.b:11:3)\n \
1745 at <top level> (bakuhatsu.b:14:1)"
1746 );
1747 }
1748
1749 #[test]
1757 fn line_11_column_3_of_the_fixture_is_the_failing_call() {
1758 let line = BAKUHATSU.lines().nth(10).expect("line 11 exists");
1759 assert_eq!(line, " kore_wa_sonzai_shinai()");
1760 assert_eq!(
1761 &line[2..],
1762 "kore_wa_sonzai_shinai()",
1763 "column 3 (1-indexed) is where the call starts"
1764 );
1765 let offset = BAKUHATSU.find("kore_wa_sonzai_shinai").expect("in fixture");
1768 assert!(
1769 offset > KOTAE.len(),
1770 "kotae.b could answer for byte {offset}"
1771 );
1772 assert!(
1773 offset > TWO_IMPORTS.len(),
1774 "the entry file could answer for byte {offset}"
1775 );
1776 }
1777
1778 #[test]
1797 fn a_raise_in_the_entry_file_names_the_entry_file() {
1798 let err = run_named(
1799 "honmono.b",
1800 "def f()\n 1\nend\n\n# waive B0001: a raise at runtime\nnani_mo_nai()\n",
1801 );
1802 assert_eq!(
1803 err.to_string(),
1804 "runtime error: honmono.b:6:1: unbound symbol `nani_mo_nai`"
1805 );
1806 }
1807
1808 #[test]
1822 fn a_raise_inside_an_annotated_def_still_names_its_line() {
1823 let err = run_named(
1824 "chuu.b",
1825 "# waive B0001: a raise at runtime\ndef f(a: Int) -> Int\n mada_nai(a)\nend\n\nf(1)\n",
1826 );
1827 assert_eq!(
1828 err.to_string(),
1829 "runtime error: chuu.b:3:3: unbound symbol `mada_nai`\n \
1830 at f (chuu.b:3:3)\n \
1831 at <top level> (chuu.b:6:1)"
1832 );
1833 }
1834
1835 #[test]
1853 fn a_raise_in_a_callee_from_another_file_names_the_callees_file() {
1854 let err = run_named(
1859 "yobidashi.b",
1860 "use(\"bakuhatsu2\")\n# pad: 47's in range\nbakuhatsu2::yobu\n",
1861 );
1862 assert_eq!(
1863 err.to_string(),
1864 "runtime error: bakuhatsu2.b:3:3: unbound symbol `kore_wa_sonzai_shinai`\n \
1865 at bakuhatsu2::yobu (bakuhatsu2.b:3:3)\n \
1866 at <top level> (yobidashi.b:3:1)"
1867 );
1868 }
1869}
1870
1871#[cfg(test)]
1872mod tests {
1873 use super::*;
1874
1875 fn int(src: &str) -> i64 {
1876 match run(src).unwrap_or_else(|e| panic!("{src:?}: {e}")).value {
1877 Value::Int(v) => v,
1878 other => panic!("{src:?} produced {other:?}"),
1879 }
1880 }
1881
1882 #[test]
1885 fn annotating_buys_analysis_and_changes_nothing_else() {
1886 let plain = run("def add(a, b)\n a + b\nend\nadd(2, 3)").expect("plain");
1887 let typed = run("def add(a: Int, b: Int) -> Int\n a + b\nend\nadd(2, 3)").expect("typed");
1888
1889 assert!(matches!(plain.value, Value::Int(5)));
1890 assert!(
1891 matches!(typed.value, Value::Int(5)),
1892 "the annotated program must compute the same answer"
1893 );
1894 assert_eq!(plain.visited, 0, "no annotations means no analysis");
1895 assert!(
1896 typed.visited > 0,
1897 "an annotation must actually buy analysis, not just decorate"
1898 );
1899 assert_eq!(plain.typed_decls, 0);
1900 assert_eq!(typed.typed_decls, 1);
1901 }
1902
1903 #[test]
1907 fn a_type_error_is_reported_and_the_program_does_not_run() {
1908 let err = run("def add(a: Int, b: Int) -> Str\n a + b\nend\nadd(1, 2)")
1909 .expect_err("a declared Str return from an Int body must be rejected");
1910 assert!(
1911 matches!(err, RunError::Types(ref d) if !d.is_empty()),
1912 "expected type diagnostics, got {err}"
1913 );
1914 }
1915
1916 #[test]
1919 fn the_same_program_without_annotations_runs() {
1920 assert_eq!(int("def add(a, b)\n a + b\nend\nadd(1, 2)"), 3);
1921 }
1922
1923 #[test]
1924 fn a_parse_error_is_reported_as_one() {
1925 assert!(matches!(run("def (").unwrap_err(), RunError::Parse(_)));
1926 }
1927
1928 #[test]
1931 fn a_runtime_error_is_reported_as_one() {
1932 let err = run("throw(error(\"boom\"))").expect_err("a raise");
1933 assert!(matches!(err, RunError::Eval(_)), "got {err}");
1934 }
1935
1936 #[test]
1939 fn the_pipeline_reaches_both_runtime_layers() {
1940 assert_eq!(int("6 % 3"), 0);
1941 assert_eq!(int("7 % 3"), 1);
1942 assert_eq!(int("2 + 3 * 4"), 14);
1943 }
1944
1945 #[test]
1955 fn the_deleted_round_trip_agreed_with_the_direct_lowering() {
1956 let corpus = [
1957 "def add(a, b)\n a + b\nend\nadd(2, 3)",
1958 "def fact(n)\n if n < 2\n 1\n else\n n * fact(n - 1)\n end\nend\nfact(5)",
1959 "def f(a, b)\n c = a + b\n c * 2\nend\nf(1, 2)",
1960 "defmacro sq(e)\n quote\n unquote(e) * unquote(e)\n end\nend\nsq(2 + 3)",
1961 "\"a string with spaces, a ( and a )\"",
1962 "def g(a: Int) -> Int\n a + 1\nend\ng(1)",
1963 "6 % 3",
1964 "1.5 + 2.25",
1965 ];
1966 for src in corpus {
1967 let erased = crate::to_sexps(&erase_types(&parse_tree(src).expect("parse")));
1970
1971 let direct: Vec<Sexp> = crate::lower_to_spanned(&erased)
1972 .iter()
1973 .map(tatara_lisp::Spanned::to_sexp)
1974 .collect();
1975 assert_eq!(direct, erased, "the direct lowering must be the identity");
1976
1977 let text = erased
1978 .iter()
1979 .map(ToString::to_string)
1980 .collect::<Vec<_>>()
1981 .join("\n");
1982 let round_tripped: Vec<Sexp> = tatara_lisp::read_spanned(&text)
1983 .unwrap_or_else(|e| panic!("{src:?}: the old path could not read back: {e:?}"))
1984 .iter()
1985 .map(tatara_lisp::Spanned::to_sexp)
1986 .collect();
1987 assert_eq!(
1988 round_tripped, erased,
1989 "{src:?}: the old print-and-reparse path changed the tree"
1990 );
1991 }
1992 }
1993
1994 #[test]
2012 fn the_round_trip_is_not_the_identity_in_general() {
2013 let tree = Sexp::List(vec![
2014 Sexp::Atom(tatara_lisp::Atom::Symbol("f".into())),
2015 Sexp::Atom(tatara_lisp::Atom::Symbol("a b".into())),
2016 ]);
2017 let text = tree.to_string();
2018 let back: Vec<Sexp> = tatara_lisp::read_spanned(&text)
2019 .expect("it reads back cleanly — that IS the problem")
2020 .iter()
2021 .map(tatara_lisp::Spanned::to_sexp)
2022 .collect();
2023 assert_ne!(
2024 back,
2025 vec![tree.clone()],
2026 "if print-then-read became inverse over symbols, the class would be \
2027 closed upstream and this test should be deleted rather than relaxed"
2028 );
2029 let direct: Vec<Sexp> = crate::lower_to_spanned(std::slice::from_ref(&tree))
2031 .iter()
2032 .map(tatara_lisp::Spanned::to_sexp)
2033 .collect();
2034 assert_eq!(direct, vec![tree]);
2035 }
2036}
2037
2038#[cfg(test)]
2039mod macro_tests {
2040 use super::*;
2041
2042 fn int(src: &str) -> i64 {
2043 match run(src).unwrap_or_else(|e| panic!("{src:?}: {e}")).value {
2044 Value::Int(v) => v,
2045 other => panic!("{src:?} produced {other:?}"),
2046 }
2047 }
2048
2049 #[test]
2051 fn a_macro_expands_and_runs() {
2052 assert_eq!(
2053 int("defmacro double(x)\n quote\n unquote(x) + unquote(x)\n end\nend\ndouble(21)"),
2054 42
2055 );
2056 }
2057
2058 #[test]
2061 fn a_macro_operates_on_syntax_not_values() {
2062 assert_eq!(
2063 int("defmacro sq(e)\n quote\n unquote(e) * unquote(e)\n end\nend\nsq(2 + 3)"),
2064 25,
2065 "the argument form `2 + 3` must be substituted twice"
2066 );
2067 }
2068
2069 #[test]
2088 fn a_runaway_macro_fails_the_compilation_rather_than_the_process() {
2089 let err =
2090 run("defmacro forever(x)\n quote\n forever(unquote(x))\n end\nend\nforever(1)")
2091 .expect_err("a self-referential macro must be rejected");
2092 let msg = err.to_string();
2093 assert!(
2094 msg.contains("forever") && msg.contains("expansion"),
2095 "the error must name the macro and the limit it hit: {msg}"
2096 );
2097 assert!(
2098 msg.contains("<anonymous>:6:1"),
2099 "and must place the call that ran away — line 6 is `forever(1)`: {msg}"
2100 );
2101 }
2102}
2103
2104#[cfg(test)]
2105mod input_tests {
2106 use super::*;
2107 use crate::inputs::{Declaration, Inputs};
2108
2109 const SCHEMA: &[u8] = b"3";
2111
2112 fn with_schema(src: &str) -> Result<Run, RunError> {
2113 let hash = Inputs::hash_of(SCHEMA);
2114 let mut inputs = Inputs::new();
2115 inputs
2116 .bind(
2117 &Declaration {
2118 name: "schema".to_string(),
2119 hash,
2120 },
2121 SCHEMA.to_vec(),
2122 )
2123 .expect("bind");
2124 run_with_inputs(src, inputs)
2125 }
2126
2127 fn decl_line() -> String {
2128 let mut s = String::from("definput(\"schema\", \"");
2129 s.push_str(&Inputs::hash_of(SCHEMA));
2130 s.push_str("\")\n");
2131 s
2132 }
2133
2134 #[test]
2139 fn a_macro_can_read_a_declared_build_input() {
2140 let src = decl_line() + "input(\"schema\")";
2141 let out = with_schema(&src).expect("run");
2142 assert!(
2143 matches!(out.value, Value::Str(ref s) if &**s == "3"),
2144 "got {:?}",
2145 out.value
2146 );
2147 }
2148
2149 #[test]
2153 fn an_undeclared_input_is_an_error() {
2154 let err = with_schema("input(\"not_declared\")").expect_err("must fail");
2155 let msg = err.to_string();
2156 assert!(msg.contains("not_declared"), "must name it: {msg}");
2157 assert!(msg.contains("definput"), "and say how to declare it: {msg}");
2158 }
2159
2160 #[cfg(not(feature = "sys"))]
2168 #[test]
2169 fn there_is_no_ambient_file_read() {
2170 for attempt in [
2171 "read_file(\"/etc/passwd\")",
2172 "File(\"/etc/passwd\")",
2173 "slurp(\"/etc/passwd\")",
2174 "open(\"/etc/passwd\")",
2175 ] {
2176 let err = with_schema(attempt).expect_err("must not resolve");
2177 assert!(
2178 err.to_string().contains("unbound"),
2179 "{attempt} must be UNBOUND — a capability removed by absence, \
2180 not guarded by a check: {err}"
2181 );
2182 }
2183 }
2184
2185 #[cfg(feature = "sys")]
2191 #[test]
2192 fn sys_read_file_is_the_trusted_cli_only_exception() {
2193 let err = with_schema("definitely_not_a_primitive(\"x\")").expect_err("must not resolve");
2194 assert!(err.to_string().contains("unbound"), "{err}");
2195 assert!(
2196 with_schema("read_file(\"/etc/passwd\")").is_ok(),
2197 "with `sys` on, read_file is the trusted CLI surface"
2198 );
2199 let out = with_schema("input(\"schema\")").expect("run");
2200 assert!(
2201 matches!(out.value, Value::Str(ref s) if &**s == "3"),
2202 "input() still binds beside the sys surface: {:?}",
2203 out.value
2204 );
2205 }
2206
2207 #[test]
2210 fn a_declared_input_with_no_bytes_supplied_fails() {
2211 let src = decl_line() + "input(\"schema\")";
2212 assert!(run(&src).is_err(), "no bytes were supplied");
2213 }
2214}
2215
2216#[cfg(test)]
2217mod tier2_tests {
2218 use super::*;
2219 use crate::inputs::{Declaration, Inputs};
2220
2221 #[test]
2233 fn a_macro_generates_code_from_a_schema() {
2234 let schema = b"7";
2235 let mut inputs = Inputs::new();
2236 inputs
2237 .bind(
2238 &Declaration {
2239 name: "arity".to_string(),
2240 hash: Inputs::hash_of(schema),
2241 },
2242 schema.to_vec(),
2243 )
2244 .expect("bind");
2245
2246 let mut src = String::from("definput(\"arity\", \"");
2249 src.push_str(&Inputs::hash_of(schema));
2250 src.push_str("\")\n");
2251 src.push_str(
2252 "defmacro from_schema()\n quote\n unquote(to_int(input(\"arity\")))\n end\nend\n\
2253 from_schema() * 6",
2254 );
2255
2256 let out = run_with_inputs(&src, inputs).expect("run");
2257 assert!(
2258 matches!(out.value, Value::Int(42)),
2259 "the schema's 7 must reach the generated code: got {:?}",
2260 out.value
2261 );
2262 }
2263}