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