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(
195 entry: Entry<'_>,
196 inputs: Inputs,
197 loader: &dyn crate::uses::Loader,
198 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
199 frame: Option<&blue_lang_waku::Waku>,
200) -> Result<Run, RunError> {
201 let mut interp =
206 crate::timings::stage(crate::timings::Stage::Runtime, crate::interpreter_hostless);
207 crate::inputs::install_input_primitives(&mut interp, inputs);
208 let builtins = crate::timings::stage(crate::timings::Stage::Names, || builtin_names(&interp));
209 let prepared = prepare(entry, loader, surface, &builtins)?;
210 if let Some(frame) = frame {
211 let refused = crate::ungranted_host_names(frame);
212 let escapes = entry_escapes(&prepared.program, &refused);
213 if !escapes.is_empty() {
214 return Err(RunError::Types(escapes));
215 }
216 crate::refuse_host_names(&mut interp, &refused, "this run's --allow");
217 crate::attach_node_in_frame(&mut interp, frame);
218 }
219 let mut value = crate::timings::stage(crate::timings::Stage::Eval, || {
220 eval_prepared(&mut interp, &prepared, &mut ())
221 })?;
222 if let Some(v) = crate::timings::stage(crate::timings::Stage::Main, || {
223 call_main(&mut interp, &prepared, &mut ())
224 })? {
225 value = v;
226 }
227 Ok(Run {
228 value,
229 visited: prepared.visited,
230 typed_decls: prepared.typed_decls,
231 seams: prepared.seams,
232 })
233}
234
235fn entry_escapes(
236 program: &crate::uses::ResolvedProgram,
237 refused: &[(&'static str, &'static str)],
238) -> Vec<String> {
239 use crate::uses::ResolvedProgram;
240 let forms: Vec<tatara_lisp::Spanned> = program
241 .forms()
242 .iter()
243 .enumerate()
244 .filter(|(i, _)| program.owner_of(*i) == Some(ResolvedProgram::ENTRY))
245 .map(|(_, f)| f.clone())
246 .collect();
247 let label = |name: &str| refused.iter().find(|(n, _)| *n == name).map(|(_, l)| *l);
248 blue_lang_waku::free_names(&forms, &|name| label(name).is_none())
249 .into_iter()
250 .filter_map(|name| {
251 let cap = label(&name)?;
252 let at = program
253 .file(ResolvedProgram::ENTRY)
254 .and_then(|f| f.path.clone())
255 .map_or_else(|| "<entry>".to_string(), |p| p.display().to_string());
256 Some(format!(
257 "{at}: refused: `{name}` reaches the host ({cap}), which this run does not allow; pass --allow {cap} to grant it"
258 ))
259 })
260 .collect()
261}
262
263pub struct Observed {
266 pub definitions: std::sync::Arc<crate::observe::Definitions>,
267 pub outcome: Result<Run, RunError>,
269}
270
271pub fn run_observed(
281 entry: Entry<'_>,
282 inputs: Inputs,
283 loader: &dyn crate::uses::Loader,
284 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
285 watch: impl FnOnce(
286 &std::sync::Arc<crate::observe::Definitions>,
287 ) -> std::sync::Arc<dyn crate::observe::CallObserver>,
288) -> Result<Observed, RunError> {
289 let mut interp = crate::interpreter_hostless();
290 crate::inputs::install_input_primitives(&mut interp, inputs);
291 let prepared = prepare(entry, loader, surface, &builtin_names(&interp))?;
292 let definitions = std::sync::Arc::new(crate::observe::Definitions::of(
293 &prepared.program,
294 &prepared.erased,
295 ));
296 interp.set_call_observer(Some(watch(&definitions)));
297 let outcome = eval_prepared(&mut interp, &prepared, &mut ())
298 .and_then(|value| Ok(call_main(&mut interp, &prepared, &mut ())?.unwrap_or(value)))
299 .map(|value| Run {
300 value,
301 visited: prepared.visited,
302 typed_decls: prepared.typed_decls,
303 seams: prepared.seams,
304 });
305 interp.set_call_observer(None);
306 Ok(Observed {
307 definitions,
308 outcome,
309 })
310}
311
312pub(crate) struct Prepared {
317 program: crate::uses::ResolvedProgram,
318 erased: Vec<tatara_lisp::Spanned>,
319 visited: usize,
320 typed_decls: usize,
321 seams: usize,
322}
323
324impl Prepared {
325 pub(crate) fn erased(&self) -> &[tatara_lisp::Spanned] {
327 &self.erased
328 }
329
330 pub(crate) fn entry_namespace(&self) -> Namespace {
332 let first = (0..self.program.forms().len())
333 .find(|i| self.program.owner_of(*i) == Some(crate::uses::ResolvedProgram::ENTRY));
334 match first {
335 Some(i) => namespace_of(&self.program, i),
336 None => match self
337 .program
338 .file(crate::uses::ResolvedProgram::ENTRY)
339 .and_then(|f| f.package.clone())
340 {
341 Some(p) => Namespace::Bidama(p),
342 None => Namespace::File(String::new()),
343 },
344 }
345 }
346}
347
348pub(crate) fn prepare(
354 entry: Entry<'_>,
355 loader: &dyn crate::uses::Loader,
356 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
357 builtins: &NameTable,
358) -> Result<Prepared, RunError> {
359 let mut program = parse_and_resolve(entry, loader, surface)?;
360
361 program.retain(|f| !crate::uses::is_test_form(f));
374
375 let (outcome, names) = crate::timings::stage(crate::timings::Stage::Check, || {
390 check_stage(&program, builtins, false)
391 });
392 if !outcome.ok() {
393 return Err(RunError::Types(
394 outcome
395 .errors()
396 .map(|d| render(&program, d))
399 .collect(),
400 ));
401 }
402
403 let erased = crate::timings::stage(crate::timings::Stage::Erase, || {
417 erase_types(&lower(&program, &names))
418 });
419
420 Ok(Prepared {
421 program,
422 erased,
423 visited: outcome.stats.visited,
424 typed_decls: outcome.stats.typed_decls,
425 seams: outcome.seams.len(),
426 })
427}
428
429pub(crate) fn parse_and_resolve(
431 entry: Entry<'_>,
432 loader: &dyn crate::uses::Loader,
433 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
434) -> Result<crate::uses::ResolvedProgram, RunError> {
435 let forms = crate::timings::stage(crate::timings::Stage::Parse, || {
436 let mut forms = match surface {
437 Some(pack) => blue_lang_syntax::parse_program_tree_in(entry.text, pack)
438 .map_err(|e| RunError::Parse(e.to_string()))?,
439 None => loader
440 .parse(entry.text)
441 .map_err(|e| RunError::Parse(e.to_string()))?,
442 };
443 if surface.is_none() {
446 forms.extend(
447 blue_lang_syntax::doc::doctests(entry.text)
448 .map_err(|e| RunError::Parse(e.to_string()))?,
449 );
450 }
451 Ok::<_, RunError>(forms)
452 })?;
453
454 crate::timings::stage(crate::timings::Stage::Resolve, || {
465 crate::uses::resolve_uses(forms, entry, loader).map_err(RunError::Import)
466 })
467}
468
469pub(crate) fn check_stage(
482 program: &crate::uses::ResolvedProgram,
483 builtins: &NameTable,
484 with_tests: bool,
485) -> (blue_lang_check::Outcome, NameTable) {
486 use crate::uses::ResolvedProgram;
487 let forms = program.forms();
488 let is_entry = |i: usize| program.owner_of(i) == Some(ResolvedProgram::ENTRY);
489
490 let mut outcome = blue_lang_check::check_program_in(forms, &|a, b| {
492 program.owner_of(a) == program.owner_of(b)
493 });
494 outcome
495 .diagnostics
496 .extend(data_law_diagnostics(program, &is_entry));
497
498 let table = program_names(program, builtins);
501 let whole = |i: usize| with_tests && is_entry(i);
502 let (names, resolved) = blue_lang_check::names::check_names_whole(
503 forms,
504 &table,
505 &|i| namespace_of(program, i),
506 &is_entry,
507 &whole,
508 );
509 let stored = program
510 .file(ResolvedProgram::ENTRY)
511 .is_some_and(|f| f.path.is_some());
512 outcome.diagnostics.extend(names);
513 outcome.stats.names_resolved = resolved;
514 if stored {
515 let script = program
516 .file(ResolvedProgram::ENTRY)
517 .is_some_and(|f| f.package.is_none());
518 let found = blue_lang_check::form_rules::check(forms, &is_entry, script);
519 let silenced: Vec<tatara_lisp::Span> = found
520 .iter()
521 .filter(|d| d.code == blue_lang_check::Code::B0038)
522 .map(|d| d.span)
523 .collect();
524 outcome
525 .diagnostics
526 .retain(|d| !(d.code == blue_lang_check::Code::B0002 && silenced.contains(&d.span)));
527 outcome.diagnostics.extend(found);
528 }
529 outcome
530 .diagnostics
531 .extend(blue_lang_check::contracts::check(
532 forms,
533 &table,
534 &|i| namespace_of(program, i),
535 &is_entry,
536 &|i| {
537 program
538 .owner_of(i)
539 .and_then(|f| program.file(f))
540 .map(|f| f.text.as_str())
541 },
542 crate::docs::signatures(),
543 ));
544
545 outcome.diagnostics.extend(doc_diagnostics(program));
546
547 let mut waivers = Vec::new();
550 for file in program.files() {
551 let spans: Vec<(usize, tatara_lisp::Span)> = forms
552 .iter()
553 .enumerate()
554 .filter(|(i, _)| program.owner_of(*i) == Some(file.id))
555 .map(|(i, f)| (i, f.span))
556 .collect();
557 let (found, malformed) = blue_lang_check::waiver::collect(&file.text, &spans);
558 waivers.extend(found);
559 outcome.diagnostics.extend(malformed);
560 }
561 outcome.waived = blue_lang_check::waiver::apply(&mut outcome.diagnostics, waivers, &is_entry);
562 if !stored {
563 outcome.diagnostics.retain(|d| {
564 !matches!(
565 d.code,
566 blue_lang_check::Code::B0023
567 | blue_lang_check::Code::B0036
568 | blue_lang_check::Code::B0037
569 | blue_lang_check::Code::B0061
570 | blue_lang_check::Code::B0062
571 | blue_lang_check::Code::B0063
572 )
573 });
574 }
575 let judge = blue_lang_check::evolution::FormJudge::from_env().unwrap_or_default();
576 outcome
577 .diagnostics
578 .retain(|d| judge.reports(d.code, is_entry(d.top_level)));
579 let (enforced, census): (Vec<_>, Vec<_>) = std::mem::take(&mut outcome.diagnostics)
582 .into_iter()
583 .partition(|d| d.code.rule().active());
584 outcome.diagnostics = enforced;
585 outcome.census = census;
586 outcome
587 .diagnostics
588 .sort_by_key(|d| (d.top_level, d.span.start, d.code));
589 (outcome, table)
590}
591
592fn doc_diagnostics(program: &crate::uses::ResolvedProgram) -> Vec<blue_lang_check::Diagnostic> {
596 use crate::uses::ResolvedProgram;
597 let Some(file) = program.file(ResolvedProgram::ENTRY) else {
598 return Vec::new();
599 };
600 let Some(package) = file.package.as_deref() else {
601 return Vec::new();
602 };
603 let forms: Vec<(usize, &tatara_lisp::Spanned)> = program
604 .forms()
605 .iter()
606 .enumerate()
607 .filter(|(i, f)| {
608 program.owner_of(*i) == Some(ResolvedProgram::ENTRY)
609 && !blue_lang_syntax::doc::is_doctest(f)
610 })
611 .collect();
612 let graph = file.path.as_deref().map(crate::doc_sources::graph_meanings);
613 let meaning = |name: &str| {
614 graph
615 .as_ref()
616 .and_then(|g| g.get(&(package.to_string(), name.to_string())).cloned())
617 };
618 let names_line = |name: &str| crate::docs::doc_of(name).map(|d| d.doc.to_string());
619 blue_lang_check::doc_rules::check_file(
620 &file.text,
621 &forms,
622 package,
623 &blue_lang_check::doc_rules::Sources {
624 meaning: &meaning,
625 names_line: &names_line,
626 },
627 )
628}
629
630fn data_law_diagnostics(
631 program: &crate::uses::ResolvedProgram,
632 is_entry: &dyn Fn(usize) -> bool,
633) -> Vec<blue_lang_check::Diagnostic> {
634 use blue_lang_check::data_laws;
635 let forms = program.forms();
636 let fragments = data_laws::Fragments::collect(forms);
637 let frontend = program.frontend();
638 let mut out = Vec::new();
639 let schema = match frontend.and_then(|f| f.table.as_ref()) {
640 Some((label, text)) => match data_laws::Schema::read(text) {
641 Ok(s) => Some(s),
642 Err(why) => {
643 if let Some((i, first)) = forms.iter().enumerate().find(|(i, _)| is_entry(*i)) {
644 out.push(
645 blue_lang_check::Diagnostic::new(
646 blue_lang_check::Code::B0050,
647 format!("the generated schema table {label} does not read: {why}"),
648 first.span,
649 )
650 .at_top_level(i)
651 .with_help("regenerate it: `nix run .#regen`"),
652 );
653 }
654 None
655 }
656 },
657 None => None,
658 };
659 if fragments.is_empty() && schema.is_none() && !frontend.is_some_and(|f| f.view) {
660 return out;
661 }
662 out.extend(data_laws::check(
663 forms,
664 &data_laws::Context {
665 fragments: &fragments,
666 schema: schema.as_ref(),
667 is_entry,
668 view: frontend.is_some_and(|f| f.view),
669 },
670 ));
671 out
672}
673
674#[must_use]
687pub fn program_names(program: &crate::uses::ResolvedProgram, builtins: &NameTable) -> NameTable {
688 use blue_lang_check::Binding;
689 let mut table = NameTable::new();
690 table.add_program(program.forms(), |i| namespace_of(program, i));
691 let builtin_macro = |head: &str| {
692 builtins
693 .scopes()
694 .iter()
695 .any(|s| s.namespace == Namespace::Macro && s.get(head).is_some())
696 };
697 let mut expander: Option<tatara_lisp_eval::Interpreter<()>> = None;
698 for (i, form) in program.forms().iter().enumerate() {
699 let Some(head) = form
700 .as_list()
701 .and_then(|items| items.first())
702 .and_then(tatara_lisp::Spanned::as_symbol)
703 else {
704 continue;
705 };
706 if !builtin_macro(head) {
707 continue;
708 }
709 let interp = expander.get_or_insert_with(crate::interpreter_hostless);
710 let Ok(expanded) = interp.fully_expand(form, &mut ()) else {
713 continue;
714 };
715 let nullary = blue_lang_syntax::scope::nullary_definition(&expanded);
716 for (name, span, kind) in blue_lang_check::names::definitions_of(&expanded) {
717 table.define(
718 namespace_of(program, i),
719 Binding {
720 nullary: nullary == Some(name.as_str()),
721 name,
722 kind,
723 span: Some(span),
724 top_level: Some(i),
725 },
726 );
727 }
728 }
729 for scope in builtins.scopes() {
730 table.push(scope.clone());
731 }
732 table.spell_bare_like(builtins);
733 let form_file: Vec<usize> = (0..program.forms().len())
734 .map(|i| program.owner_of(i).map_or(usize::MAX, |f| f.index()))
735 .collect();
736 let mut imports: std::collections::BTreeMap<usize, blue_lang_check::names::FileImports> =
737 std::collections::BTreeMap::new();
738 for (file, import) in program.imports() {
739 let entry = imports.entry(file.index()).or_default();
740 entry.add(&import.package, import.names.iter().map(|(n, _)| n.clone()));
741 let at = import.span.start;
742 entry.use_at = Some(entry.use_at.map_or(at, |a| a.min(at)));
743 }
744 let facade_uses =
749 |pkg: &str,
750 imports: &std::collections::BTreeMap<usize, blue_lang_check::names::FileImports>| {
751 let defines = table.bidama(pkg).is_some_and(|s| !s.is_empty());
752 if defines {
753 return std::collections::BTreeSet::new();
754 }
755 program
756 .files()
757 .iter()
758 .filter(|f| f.package.as_deref() == Some(pkg))
759 .filter_map(|f| imports.get(&f.id.index()))
760 .flat_map(|i| i.uses.iter().cloned())
761 .collect::<std::collections::BTreeSet<String>>()
762 };
763 loop {
764 let mut grew = false;
765 let files: Vec<usize> = imports.keys().copied().collect();
766 for f in files {
767 let used: Vec<String> = imports[&f].uses.iter().cloned().collect();
768 for p in used {
769 for member in facade_uses(&p, &imports) {
770 if let Some(i) = imports.get_mut(&f) {
771 if i.uses.insert(member.clone()) {
772 i.via.insert(member, p.clone());
773 grew = true;
774 }
775 }
776 }
777 }
778 }
779 if !grew {
780 break;
781 }
782 }
783 let mut pkg_uses: std::collections::BTreeMap<String, std::collections::BTreeSet<String>> =
784 std::collections::BTreeMap::new();
785 for (file, import) in program.imports() {
786 if let Some(p) = program.file(*file).and_then(|f| f.package.clone()) {
787 pkg_uses
788 .entry(p)
789 .or_default()
790 .insert(import.package.clone());
791 }
792 }
793 table.attach_files(form_file, imports);
794 table.attach_package_uses(pkg_uses);
795 table.attach_needs(program.needs().clone());
796 let canonical = |p: &std::path::Path| {
800 std::fs::canonicalize(p)
801 .unwrap_or_else(|_| p.to_path_buf())
802 .display()
803 .to_string()
804 };
805 let mut file_paths = std::collections::BTreeMap::new();
806 let mut pkg_roots = std::collections::BTreeMap::new();
807 for f in program.files() {
808 if let Some(path) = &f.path {
809 file_paths.insert(f.id.index(), canonical(path));
810 if let (Some(pkg), Some(root)) =
811 (&f.package, path.parent().and_then(std::path::Path::parent))
812 {
813 pkg_roots
814 .entry(pkg.clone())
815 .or_insert_with(|| canonical(root));
816 }
817 }
818 }
819 table.attach_paths(file_paths, pkg_roots);
820 let versions = program.versions().clone();
821 table.apply_legacy(program.forms(), &|i| namespace_of(program, i), &|p| {
822 versions.get(p).cloned()
823 });
824 table
825}
826
827#[must_use]
839pub fn lower(
840 program: &crate::uses::ResolvedProgram,
841 names: &NameTable,
842) -> Vec<tatara_lisp::Spanned> {
843 lower_with(program, names, true)
844}
845
846#[must_use]
847pub fn lower_with(
848 program: &crate::uses::ResolvedProgram,
849 names: &NameTable,
850 simplify_chains: bool,
851) -> Vec<tatara_lisp::Spanned> {
852 let mut resolved = blue_lang_check::names::resolve_program(program.forms(), names, &|i| {
853 namespace_of(program, i)
854 });
855 let simplified = if simplify_chains {
856 chain_simplified(program.forms(), &resolved)
857 } else {
858 None
859 };
860 let forms: &[tatara_lisp::Spanned] = simplified.as_deref().unwrap_or(program.forms());
861 if simplified.is_some() {
862 resolved =
863 blue_lang_check::names::resolve_program(forms, names, &|i| namespace_of(program, i));
864 }
865 resolved
866 .resolved_tree(forms, blue_lang_check::names::Rule::Namespaced)
867 .into_iter()
868 .zip(forms)
869 .enumerate()
870 .map(|(i, (resolved, written))| {
871 if crate::uses::use_target(written).is_some()
872 || blue_lang_syntax::scope::legacy_target(written).is_some()
873 {
874 tatara_lisp::Spanned::new(written.span, tatara_lisp::SpannedForm::Nil)
875 } else {
876 let file = program.owner_of(i).and_then(|id| program.file(id));
877 match file {
878 Some(f) => locate_raises(&resolved, &raise_site(f)),
879 None => resolved,
880 }
881 }
882 })
883 .collect()
884}
885
886fn chain_simplified(
887 forms: &[tatara_lisp::Spanned],
888 resolved: &blue_lang_check::names::Resolved,
889) -> Option<Vec<tatara_lisp::Spanned>> {
890 let respell: std::collections::HashMap<usize, blue_lang_check::respell::Respelling> = resolved
891 .respellings
892 .iter()
893 .filter(|(_, r)| matches!(r.respell, blue_lang_check::respell::Respell::Chain(_)))
894 .cloned()
895 .collect();
896 if respell.is_empty() {
897 return None;
898 }
899 let bare = std::collections::HashSet::new();
900 let canon = Canon {
901 bare: &bare,
902 respell: &respell,
903 };
904 Some(forms.iter().map(|f| canon.form(f)).collect())
905}
906
907fn raise_site(file: &crate::uses::SourceFile) -> impl Fn(tatara_lisp::Span) -> String + '_ {
908 move |span| {
909 let (line, _) =
910 blue_lang_syntax::Span::line_col(&file.text, span.start.min(file.text.len()));
911 let shown = file
912 .path
913 .as_ref()
914 .map_or_else(|| "<source>".to_string(), |p| p.display().to_string());
915 format!("{shown}:{line}")
916 }
917}
918
919fn locate_raises(
920 form: &tatara_lisp::Spanned,
921 site: &dyn Fn(tatara_lisp::Span) -> String,
922) -> tatara_lisp::Spanned {
923 use tatara_lisp::{Atom, Spanned, SpannedForm};
924 let inner = match &form.form {
925 SpannedForm::List(items) => {
926 let mut out: Vec<Spanned> = items.iter().map(|i| locate_raises(i, site)).collect();
927 let raising = items.first().and_then(Spanned::as_symbol) == Some("raise")
928 && (3..=4).contains(&items.len());
929 if raising {
930 out[0] = Spanned::new(
931 items[0].span,
932 SpannedForm::Atom(Atom::Symbol(crate::errors::RAISE_AT.to_string())),
933 );
934 out.insert(
935 1,
936 Spanned::new(items[0].span, SpannedForm::Atom(Atom::Str(site(form.span)))),
937 );
938 }
939 SpannedForm::List(out)
940 }
941 SpannedForm::Quote(_) | SpannedForm::Quasiquote(_) => return form.clone(),
942 other => other.clone(),
943 };
944 Spanned::new(form.span, inner)
945}
946
947#[must_use]
949pub fn namespace_of(program: &crate::uses::ResolvedProgram, i: usize) -> Namespace {
950 let file = program.owner_of(i).and_then(|id| program.file(id));
951 match file {
952 Some(f) => match &f.package {
953 Some(p) => Namespace::Bidama(p.clone()),
954 None => Namespace::File(
955 f.path
956 .as_ref()
957 .map_or_else(|| "<anonymous>".to_string(), |p| p.display().to_string()),
958 ),
959 },
960 None => Namespace::File("<unknown file>".to_string()),
961 }
962}
963
964pub const HARNESS_NAMES: &[&str] = &[blue_lang_syntax::LOWERED_ASSERT];
978
979#[must_use]
989pub fn builtin_names<H: 'static>(interp: &tatara_lisp_eval::Interpreter<H>) -> NameTable {
990 builtin_names_with(interp, &[])
991}
992
993#[must_use]
994pub fn builtin_names_with<H: 'static>(
995 interp: &tatara_lisp_eval::Interpreter<H>,
996 host: &[crate::host_surface::HostName],
997) -> NameTable {
998 use blue_lang_check::{Binding, Scope, ScopeKind};
999 use tatara_lisp_eval::HeadBinding;
1000 let mut special = Scope::new(Namespace::SpecialForm);
1001 let mut macros = Scope::new(Namespace::Macro);
1002 let mut values = Scope::new(Namespace::Builtin);
1003 let mut names: Vec<std::sync::Arc<str>> = interp.reserved_head_names().into_iter().collect();
1008 names.extend(
1009 crate::inputs::INPUT_PRIMITIVES
1010 .iter()
1011 .filter(|n| interp.resolve_head(n).is_some())
1012 .map(|n| std::sync::Arc::from(*n)),
1013 );
1014 for name in names {
1015 let (scope, kind) = match interp.resolve_head(&name) {
1016 Some(HeadBinding::SpecialForm) => (&mut special, ScopeKind::SpecialForm),
1017 Some(HeadBinding::Macro) => (&mut macros, ScopeKind::Macro),
1018 _ => (&mut values, ScopeKind::Value),
1019 };
1020 let nullary = kind == ScopeKind::Value
1021 && interp.lookup_global(&name).is_some_and(|v| {
1022 nullary_value(&v)
1023 || (crate::docs::doc_of(&name).is_some_and(|d| d.signature == &*name)
1024 && matches!(
1025 v,
1026 tatara_lisp_eval::Value::NativeFn(_)
1027 | tatara_lisp_eval::Value::Closure(_)
1028 ))
1029 });
1030 scope.bind(Binding {
1031 name: name.to_string(),
1032 kind,
1033 span: None,
1034 top_level: None,
1035 nullary,
1036 });
1037 }
1038 for h in host {
1039 values.bind(Binding {
1040 name: h.name.clone(),
1041 kind: ScopeKind::Value,
1042 span: None,
1043 top_level: None,
1044 nullary: h.nullary(),
1045 });
1046 }
1047 let mut table = NameTable::new();
1048 let mut harness = Scope::new(Namespace::Harness);
1049 for n in HARNESS_NAMES {
1050 harness.bind(Binding {
1051 name: (*n).to_string(),
1052 kind: ScopeKind::Value,
1053 span: None,
1054 top_level: None,
1055 nullary: false,
1056 });
1057 }
1058 table.push(harness);
1059 table.push(special);
1060 table.push(macros);
1061 table.push(values);
1062 table.spell_bare_only(
1063 crate::docs::NAMES
1064 .iter()
1065 .filter(|d| d.signature == d.name)
1066 .map(|d| d.name.to_string())
1067 .chain(host.iter().filter(|h| h.nullary()).map(|h| h.name.clone()))
1068 .collect(),
1069 );
1070 table
1071}
1072
1073#[must_use]
1074pub fn nullary_value(value: &tatara_lisp_eval::Value) -> bool {
1075 match value {
1076 tatara_lisp_eval::Value::NativeFn(f) => {
1077 matches!(f.arity, tatara_lisp_eval::ffi::Arity::Exact(0))
1078 }
1079 tatara_lisp_eval::Value::Closure(c) => c.params.is_empty() && c.rest.is_none(),
1080 _ => false,
1081 }
1082}
1083
1084#[must_use]
1088pub fn render(program: &crate::uses::ResolvedProgram, d: &blue_lang_check::Diagnostic) -> String {
1089 let head = d.to_string();
1090 let mut out = program.locate(d.top_level, d.span, &head).to_string();
1091 if let Some(help) = &d.help {
1092 out.push_str("\n help: ");
1093 out.push_str(help);
1094 }
1095 let file = program
1096 .owner_of(d.top_level)
1097 .and_then(|id| program.file(id))
1098 .and_then(|f| f.path.as_ref());
1099 if let Some(cmd) =
1100 file.and_then(|p| blue_lang_check::evolution::fix_command(d, &p.display().to_string()))
1101 {
1102 out.push_str("\n fix: ");
1103 out.push_str(&cmd);
1104 }
1105 out
1106}
1107
1108#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1110pub enum Checking {
1111 Program,
1113 WithTests,
1116}
1117
1118pub struct Checked {
1120 pub program: crate::uses::ResolvedProgram,
1121 pub outcome: blue_lang_check::Outcome,
1122 pub names: NameTable,
1125}
1126
1127impl Checked {
1128 #[must_use]
1131 pub fn erased(&self) -> Vec<tatara_lisp::Spanned> {
1132 erase_types(&lower(&self.program, &self.names))
1133 }
1134
1135 #[must_use]
1138 pub fn evaluable(&self) -> Vec<tatara_lisp::Sexp> {
1139 lower(&self.program, &self.names)
1140 .iter()
1141 .map(tatara_lisp::Spanned::to_sexp)
1142 .collect()
1143 }
1144
1145 #[must_use]
1149 pub fn resolve(&self) -> blue_lang_check::names::Resolved {
1150 blue_lang_check::names::resolve_program(self.program.forms(), &self.names, &|i| {
1151 namespace_of(&self.program, i)
1152 })
1153 }
1154}
1155
1156impl Checked {
1157 #[must_use]
1158 pub fn call_explicit_forms(&self) -> Vec<tatara_lisp::Spanned> {
1159 self.resolve().call_explicit_tree(self.program.forms())
1160 }
1161
1162 #[must_use]
1163 pub fn canonical_entry(&self, src: &str) -> Option<Vec<tatara_lisp::Spanned>> {
1164 use crate::uses::ResolvedProgram;
1165 let program = &self.program;
1166 let entry: Vec<&tatara_lisp::Spanned> = program
1167 .forms()
1168 .iter()
1169 .enumerate()
1170 .filter(|(i, _)| program.owner_of(*i) == Some(ResolvedProgram::ENTRY))
1171 .map(|(_, f)| f)
1172 .filter(|f| !blue_lang_syntax::doc::is_doctest(f))
1173 .collect();
1174 let written = blue_lang_syntax::parse_program_tree(src).ok()?;
1175 let same = written.len() == entry.len()
1176 && written
1177 .iter()
1178 .zip(&entry)
1179 .all(|(w, e)| w.to_sexp() == e.to_sexp() && w.span == e.span);
1180 if !same {
1181 return None;
1182 }
1183 let resolved = self.resolve();
1184 let bare: std::collections::HashSet<usize> = resolved
1185 .parenthesized_nullary_calls()
1186 .into_iter()
1187 .filter(|r| program.owner_of(r.top_level) == Some(ResolvedProgram::ENTRY))
1188 .map(blue_lang_check::names::Reference::node_address)
1189 .collect();
1190 let waived: Vec<tatara_lisp::Span> = self
1191 .outcome
1192 .waived
1193 .iter()
1194 .map(|w| w.diagnostic.span)
1195 .collect();
1196 let respell: std::collections::HashMap<usize, blue_lang_check::respell::Respelling> =
1197 resolved
1198 .respellings
1199 .iter()
1200 .filter(|(_, r)| !waived.contains(&r.span))
1201 .cloned()
1202 .collect();
1203 let canon = Canon {
1204 bare: &bare,
1205 respell: &respell,
1206 };
1207 let forms: Vec<tatara_lisp::Spanned> = entry.into_iter().map(|f| canon.form(f)).collect();
1208 let script = program
1209 .file(ResolvedProgram::ENTRY)
1210 .is_some_and(|f| f.package.is_none());
1211 let forms = if script {
1212 blue_lang_check::form_rules::without_prefix(&forms).unwrap_or(forms)
1213 } else {
1214 forms
1215 };
1216 let forms = blue_lang_check::form_rules::without_main_guard(&forms).unwrap_or(forms);
1217 Some(blue_lang_check::form_rules::eta_reduced(&forms).unwrap_or(forms))
1218 }
1219}
1220
1221struct Canon<'a> {
1222 bare: &'a std::collections::HashSet<usize>,
1223 respell: &'a std::collections::HashMap<usize, blue_lang_check::respell::Respelling>,
1224}
1225
1226impl Canon<'_> {
1227 fn form(&self, form: &tatara_lisp::Spanned) -> tatara_lisp::Spanned {
1228 use tatara_lisp::{Spanned, SpannedForm};
1229 let inner = match &form.form {
1230 SpannedForm::List(items) => match items.as_slice() {
1231 [head] if self.bare.contains(&(std::ptr::from_ref(head) as usize)) => {
1232 return Spanned::new(form.span, head.form.clone());
1233 }
1234 [head, ..]
1235 if self
1236 .respell
1237 .contains_key(&(std::ptr::from_ref(head) as usize)) =>
1238 {
1239 let r = &self.respell[&(std::ptr::from_ref(head) as usize)];
1240 let walked: Vec<Spanned> = items.iter().map(|i| self.form(i)).collect();
1241 return blue_lang_check::respell::apply(form, &walked, r);
1242 }
1243 _ => SpannedForm::List(items.iter().map(|i| self.form(i)).collect()),
1244 },
1245 SpannedForm::Quote(i) => SpannedForm::Quote(Box::new(self.form(i))),
1246 SpannedForm::Quasiquote(i) => SpannedForm::Quasiquote(Box::new(self.form(i))),
1247 SpannedForm::Unquote(i) => SpannedForm::Unquote(Box::new(self.form(i))),
1248 SpannedForm::UnquoteSplice(i) => SpannedForm::UnquoteSplice(Box::new(self.form(i))),
1249 other => other.clone(),
1250 };
1251 Spanned::new(form.span, inner)
1252 }
1253}
1254
1255pub fn check_entry(
1271 entry: Entry<'_>,
1272 loader: &dyn crate::uses::Loader,
1273 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
1274 checking: Checking,
1275) -> Result<Checked, RunError> {
1276 let mut program = parse_and_resolve(entry, loader, surface)?;
1277 if checking == Checking::Program {
1278 program.retain(|f| !crate::uses::is_test_form(f));
1279 }
1280 let host = match entry.path {
1281 Some(path) => crate::host_surface::near(path)
1282 .map_err(|e| RunError::Import(e.to_string()))?
1283 .map(|s| s.functions)
1284 .unwrap_or_default(),
1285 None => Vec::new(),
1286 };
1287 let mut interp =
1288 crate::timings::stage(crate::timings::Stage::Runtime, crate::interpreter_hostless);
1289 crate::inputs::install_input_primitives(&mut interp, Inputs::new());
1290 let builtins = crate::timings::stage(crate::timings::Stage::Names, || {
1291 builtin_names_with(&interp, &host)
1292 });
1293 let (outcome, names) = crate::timings::stage(crate::timings::Stage::Check, || {
1294 check_stage(&program, &builtins, checking == Checking::WithTests)
1295 });
1296 Ok(Checked {
1297 program,
1298 outcome,
1299 names,
1300 })
1301}
1302
1303#[must_use]
1304pub fn spelling_code(spelling: blue_lang_syntax::Spelling) -> blue_lang_check::Code {
1305 use blue_lang_check::Code;
1306 use blue_lang_syntax::Spelling;
1307 match spelling {
1308 Spelling::LogicalSymbol => Code::B0025,
1309 Spelling::Unless => Code::B0026,
1310 Spelling::ElseIf => Code::B0027,
1311 Spelling::CallForm => Code::B0028,
1312 Spelling::KeywordRocket => Code::B0029,
1313 Spelling::ParameterList => Code::B0030,
1314 Spelling::OperatorSpelling => Code::B0031,
1315 Spelling::TrailingComma => Code::B0032,
1316 Spelling::NegatedComparison => Code::B0034,
1317 Spelling::ElseNil => Code::B0035,
1318 Spelling::TryCatch => Code::B0037,
1319 Spelling::Apply => Code::B0042,
1320 Spelling::MapPun => Code::B0046,
1321 Spelling::NilTest => Code::B0047,
1322 Spelling::NewlineEscape => Code::B0065,
1323 }
1324}
1325
1326#[must_use]
1327pub fn form_diagnostics(text: &str) -> Vec<blue_lang_check::Diagnostic> {
1328 match blue_lang_fmt::format_source_lossless(text) {
1329 Ok(formatted) => form_diagnostics_against(text, &formatted),
1330 Err(_) => Vec::new(),
1331 }
1332}
1333
1334#[must_use]
1335pub fn form_diagnostics_against(text: &str, formatted: &str) -> Vec<blue_lang_check::Diagnostic> {
1336 use blue_lang_check::{Code, Diagnostic};
1337 if formatted.trim_end() == text.trim_end() {
1338 return Vec::new();
1339 }
1340 let help = "`blue fmt --write FILE` writes it; no compile door writes a file (`BLUE_LAYOUT=rewrite` restores formatting in place)";
1341 let mut out: Vec<Diagnostic> = blue_lang_syntax::spelling_notes(text)
1342 .unwrap_or_default()
1343 .into_iter()
1344 .map(|n| {
1345 let message = if n.canonical.is_empty() {
1346 format!("`{}` is not written", n.written)
1347 } else {
1348 format!("`{}` is written `{}`", n.written, n.canonical)
1349 };
1350 Diagnostic::new(spelling_code(n.spelling), message, n.span)
1351 .at_top_level(0)
1352 .with_help(help)
1353 })
1354 .collect();
1355 out.extend(
1356 literal_diagnostics(text)
1357 .into_iter()
1358 .map(|d| d.with_help(help)),
1359 );
1360 if out.is_empty() {
1361 let at = text
1362 .lines()
1363 .zip(formatted.lines())
1364 .position(|(a, b)| a != b)
1365 .unwrap_or_else(|| text.lines().count().min(formatted.lines().count()));
1366 let start: usize = text.lines().take(at).map(|l| l.len() + 1).sum();
1367 let start = start.min(text.len());
1368 let end = text[start..].find('\n').map_or(text.len(), |e| start + e);
1369 out.push(
1370 Diagnostic::new(
1371 Code::B0024,
1372 format!("line {} is not laid out as blue writes it", at + 1),
1373 tatara_lisp::Span::new(start, end),
1374 )
1375 .at_top_level(0)
1376 .with_help(help),
1377 );
1378 }
1379 out.sort_by_key(|d| (d.span.start, d.code));
1380 out
1381}
1382
1383fn literal_diagnostics(text: &str) -> Vec<blue_lang_check::Diagnostic> {
1384 use blue_lang_syntax::{Atom, TokenKind};
1385 let Ok(toks) = blue_lang_syntax::lex(text) else {
1386 return Vec::new();
1387 };
1388 toks.iter()
1389 .filter_map(|t| {
1390 let atom = match &t.kind {
1391 TokenKind::Int(v) => Atom::Int(*v),
1392 TokenKind::Float(v) => Atom::Float(*v),
1393 TokenKind::Str(v) => Atom::Str(v.clone()),
1394 _ => return None,
1395 };
1396 let written = text.get(t.span.start..t.span.end)?;
1397 let canonical = blue_lang_fmt::render_literal(&atom);
1398 (written != canonical).then(|| {
1399 blue_lang_check::Diagnostic::new(
1400 blue_lang_check::Code::B0033,
1401 format!("`{written}` is written `{canonical}`"),
1402 t.span,
1403 )
1404 .at_top_level(0)
1405 })
1406 })
1407 .collect()
1408}
1409
1410#[must_use]
1414pub fn syntax_diagnostic(text: &str) -> Option<blue_lang_check::Diagnostic> {
1415 let e = blue_lang_syntax::parse_program_tree(text).err()?;
1416 Some(
1417 blue_lang_check::Diagnostic::new(blue_lang_check::Code::B0006, e.message, e.span)
1418 .at_top_level(0),
1419 )
1420}
1421
1422#[must_use]
1426pub fn display_keys(message: &str) -> String {
1427 let mut out = String::with_capacity(message.len());
1428 let mut parts = message.split('`');
1429 if let Some(first) = parts.next() {
1430 out.push_str(first);
1431 }
1432 for (i, part) in parts.enumerate() {
1433 out.push('`');
1434 if i % 2 == 0 {
1436 out.push_str(&crate::observe::display_key(part));
1437 } else {
1438 out.push_str(part);
1439 }
1440 }
1441 out
1442}
1443
1444pub(crate) fn eval_prepared<H: 'static>(
1446 interp: &mut tatara_lisp_eval::Interpreter<H>,
1447 prepared: &Prepared,
1448 host: &mut H,
1449) -> Result<Value, RunError> {
1450 let Prepared {
1451 program, erased, ..
1452 } = prepared;
1453
1454 let mut value = Value::Nil;
1468 for (top_level, form) in erased.iter().enumerate() {
1469 value = interp.eval_top_form(form, host).map_err(|e| {
1470 RunError::Eval(Box::new(crate::observe::Fault::of(
1477 program,
1478 erased,
1479 top_level,
1480 &e,
1481 &interp.take_trace(),
1482 )))
1483 })?;
1484 }
1485 Ok(value)
1486}
1487
1488pub const MAIN: &str = "main";
1489
1490#[must_use]
1491pub fn entry_main(program: &crate::uses::ResolvedProgram) -> Option<(usize, tatara_lisp::Span)> {
1492 use crate::uses::ResolvedProgram;
1493 let stored = program
1494 .file(ResolvedProgram::ENTRY)
1495 .is_some_and(|f| f.path.is_some());
1496 if !stored {
1497 return None;
1498 }
1499 program.forms().iter().enumerate().find_map(|(i, f)| {
1500 if program.owner_of(i) != Some(ResolvedProgram::ENTRY) {
1501 return None;
1502 }
1503 let items = f.as_list()?;
1504 let sig = items.get(1)?.as_list()?;
1505 let named_main = matches!(
1506 items.first().and_then(tatara_lisp::Spanned::as_symbol),
1507 Some("define" | "define-typed")
1508 ) && sig.len() == 1
1509 && sig[0].as_symbol() == Some(MAIN);
1510 named_main.then_some((i, sig[0].span))
1511 })
1512}
1513
1514pub(crate) fn call_main<H: 'static>(
1515 interp: &mut tatara_lisp_eval::Interpreter<H>,
1516 prepared: &Prepared,
1517 host: &mut H,
1518) -> Result<Option<Value>, RunError> {
1519 let Some((top_level, span)) = entry_main(&prepared.program) else {
1520 return Ok(None);
1521 };
1522 let ns = namespace_of(&prepared.program, top_level);
1523 let call = tatara_lisp::Spanned::new(
1524 span,
1525 tatara_lisp::SpannedForm::List(vec![tatara_lisp::Spanned::new(
1526 span,
1527 tatara_lisp::SpannedForm::Atom(tatara_lisp::Atom::Symbol(blue_lang_check::names::key(
1528 &ns, MAIN,
1529 ))),
1530 )]),
1531 );
1532 interp.eval_top_form(&call, host).map(Some).map_err(|e| {
1533 RunError::Eval(Box::new(crate::observe::Fault::of(
1534 &prepared.program,
1535 &prepared.erased,
1536 top_level,
1537 &e,
1538 &interp.take_trace(),
1539 )))
1540 })
1541}
1542
1543#[cfg(test)]
1559mod position_tests {
1560 use super::*;
1561 use crate::uses::{Entry, Loader};
1562 use std::collections::BTreeMap;
1563 use std::path::Path;
1564
1565 struct MemLoader(BTreeMap<&'static str, &'static str>);
1566
1567 impl Loader for MemLoader {
1568 fn load(&self, name: &str) -> Result<Vec<(String, String)>, String> {
1569 self.0
1570 .get(name)
1571 .map(|s| vec![(format!("{name}.b"), (*s).to_owned())])
1572 .ok_or_else(|| format!("no bidama named \"{name}\""))
1573 }
1574 }
1575
1576 const BAKUHATSU: &str = "\
1584def tsukawanai_a()
1585 1
1586end
1587
1588def tsukawanai_b()
1589 2
1590end
1591
1592# waive B0001: the fixture needs a raise at runtime
1593def bakuhatsu()
1594 kore_wa_sonzai_shinai()
1595end
1596
1597bakuhatsu";
1598
1599 const KOTAE: &str = "def kotae()\n 42\nend";
1600
1601 const BAKUHATSU2: &str =
1604 "# waive B0001: a raise at runtime\ndef yobu()\n kore_wa_sonzai_shinai()\nend";
1605
1606 fn loader() -> MemLoader {
1607 MemLoader(BTreeMap::from([
1608 ("kotae", KOTAE),
1609 ("bakuhatsu", BAKUHATSU),
1610 ("bakuhatsu2", BAKUHATSU2),
1611 ]))
1612 }
1613
1614 fn run_named(path: &str, src: &str) -> RunError {
1615 run_in_surface(
1616 Entry {
1617 path: Some(Path::new(path)),
1618 text: src,
1619 },
1620 Inputs::new(),
1621 &loader(),
1622 None,
1623 )
1624 .expect_err("the fixture must raise")
1625 }
1626
1627 const TWO_IMPORTS: &str =
1672 "use(\"kotae\")\n# waive B0015: kotae loads first on purpose\nuse(\"bakuhatsu\")\n";
1673
1674 #[test]
1675 fn a_raise_inside_an_imported_package_names_that_package() {
1676 let err = run_named("entry.b", TWO_IMPORTS);
1677 assert_eq!(
1678 err.to_string(),
1679 "runtime error: bakuhatsu.b:11:3: unbound symbol `kore_wa_sonzai_shinai`\n \
1680 at bakuhatsu::bakuhatsu (bakuhatsu.b:11:3)\n \
1681 at <top level> (bakuhatsu.b:14:1)"
1682 );
1683 }
1684
1685 #[test]
1693 fn line_11_column_3_of_the_fixture_is_the_failing_call() {
1694 let line = BAKUHATSU.lines().nth(10).expect("line 11 exists");
1695 assert_eq!(line, " kore_wa_sonzai_shinai()");
1696 assert_eq!(
1697 &line[2..],
1698 "kore_wa_sonzai_shinai()",
1699 "column 3 (1-indexed) is where the call starts"
1700 );
1701 let offset = BAKUHATSU.find("kore_wa_sonzai_shinai").expect("in fixture");
1704 assert!(
1705 offset > KOTAE.len(),
1706 "kotae.b could answer for byte {offset}"
1707 );
1708 assert!(
1709 offset > TWO_IMPORTS.len(),
1710 "the entry file could answer for byte {offset}"
1711 );
1712 }
1713
1714 #[test]
1733 fn a_raise_in_the_entry_file_names_the_entry_file() {
1734 let err = run_named(
1735 "honmono.b",
1736 "def f()\n 1\nend\n\n# waive B0001: a raise at runtime\nnani_mo_nai()\n",
1737 );
1738 assert_eq!(
1739 err.to_string(),
1740 "runtime error: honmono.b:6:1: unbound symbol `nani_mo_nai`"
1741 );
1742 }
1743
1744 #[test]
1758 fn a_raise_inside_an_annotated_def_still_names_its_line() {
1759 let err = run_named(
1760 "chuu.b",
1761 "# waive B0001: a raise at runtime\ndef f(a: Int) -> Int\n mada_nai(a)\nend\n\nf(1)\n",
1762 );
1763 assert_eq!(
1764 err.to_string(),
1765 "runtime error: chuu.b:3:3: unbound symbol `mada_nai`\n \
1766 at f (chuu.b:3:3)\n \
1767 at <top level> (chuu.b:6:1)"
1768 );
1769 }
1770
1771 #[test]
1789 fn a_raise_in_a_callee_from_another_file_names_the_callees_file() {
1790 let err = run_named(
1795 "yobidashi.b",
1796 "use(\"bakuhatsu2\")\n# pad: 47's in range\nbakuhatsu2::yobu\n",
1797 );
1798 assert_eq!(
1799 err.to_string(),
1800 "runtime error: bakuhatsu2.b:3:3: unbound symbol `kore_wa_sonzai_shinai`\n \
1801 at bakuhatsu2::yobu (bakuhatsu2.b:3:3)\n \
1802 at <top level> (yobidashi.b:3:1)"
1803 );
1804 }
1805}
1806
1807#[cfg(test)]
1808mod tests {
1809 use super::*;
1810
1811 fn int(src: &str) -> i64 {
1812 match run(src).unwrap_or_else(|e| panic!("{src:?}: {e}")).value {
1813 Value::Int(v) => v,
1814 other => panic!("{src:?} produced {other:?}"),
1815 }
1816 }
1817
1818 #[test]
1821 fn annotating_buys_analysis_and_changes_nothing_else() {
1822 let plain = run("def add(a, b)\n a + b\nend\nadd(2, 3)").expect("plain");
1823 let typed = run("def add(a: Int, b: Int) -> Int\n a + b\nend\nadd(2, 3)").expect("typed");
1824
1825 assert!(matches!(plain.value, Value::Int(5)));
1826 assert!(
1827 matches!(typed.value, Value::Int(5)),
1828 "the annotated program must compute the same answer"
1829 );
1830 assert_eq!(plain.visited, 0, "no annotations means no analysis");
1831 assert!(
1832 typed.visited > 0,
1833 "an annotation must actually buy analysis, not just decorate"
1834 );
1835 assert_eq!(plain.typed_decls, 0);
1836 assert_eq!(typed.typed_decls, 1);
1837 }
1838
1839 #[test]
1843 fn a_type_error_is_reported_and_the_program_does_not_run() {
1844 let err = run("def add(a: Int, b: Int) -> Str\n a + b\nend\nadd(1, 2)")
1845 .expect_err("a declared Str return from an Int body must be rejected");
1846 assert!(
1847 matches!(err, RunError::Types(ref d) if !d.is_empty()),
1848 "expected type diagnostics, got {err}"
1849 );
1850 }
1851
1852 #[test]
1855 fn the_same_program_without_annotations_runs() {
1856 assert_eq!(int("def add(a, b)\n a + b\nend\nadd(1, 2)"), 3);
1857 }
1858
1859 #[test]
1860 fn a_parse_error_is_reported_as_one() {
1861 assert!(matches!(run("def (").unwrap_err(), RunError::Parse(_)));
1862 }
1863
1864 #[test]
1867 fn a_runtime_error_is_reported_as_one() {
1868 let err = run("throw(error(\"boom\"))").expect_err("a raise");
1869 assert!(matches!(err, RunError::Eval(_)), "got {err}");
1870 }
1871
1872 #[test]
1875 fn the_pipeline_reaches_both_runtime_layers() {
1876 assert_eq!(int("6 % 3"), 0);
1877 assert_eq!(int("7 % 3"), 1);
1878 assert_eq!(int("2 + 3 * 4"), 14);
1879 }
1880
1881 #[test]
1891 fn the_deleted_round_trip_agreed_with_the_direct_lowering() {
1892 let corpus = [
1893 "def add(a, b)\n a + b\nend\nadd(2, 3)",
1894 "def fact(n)\n if n < 2\n 1\n else\n n * fact(n - 1)\n end\nend\nfact(5)",
1895 "def f(a, b)\n c = a + b\n c * 2\nend\nf(1, 2)",
1896 "defmacro sq(e)\n quote\n unquote(e) * unquote(e)\n end\nend\nsq(2 + 3)",
1897 "\"a string with spaces, a ( and a )\"",
1898 "def g(a: Int) -> Int\n a + 1\nend\ng(1)",
1899 "6 % 3",
1900 "1.5 + 2.25",
1901 ];
1902 for src in corpus {
1903 let erased = crate::to_sexps(&erase_types(&parse_tree(src).expect("parse")));
1906
1907 let direct: Vec<Sexp> = crate::lower_to_spanned(&erased)
1908 .iter()
1909 .map(tatara_lisp::Spanned::to_sexp)
1910 .collect();
1911 assert_eq!(direct, erased, "the direct lowering must be the identity");
1912
1913 let text = erased
1914 .iter()
1915 .map(ToString::to_string)
1916 .collect::<Vec<_>>()
1917 .join("\n");
1918 let round_tripped: Vec<Sexp> = tatara_lisp::read_spanned(&text)
1919 .unwrap_or_else(|e| panic!("{src:?}: the old path could not read back: {e:?}"))
1920 .iter()
1921 .map(tatara_lisp::Spanned::to_sexp)
1922 .collect();
1923 assert_eq!(
1924 round_tripped, erased,
1925 "{src:?}: the old print-and-reparse path changed the tree"
1926 );
1927 }
1928 }
1929
1930 #[test]
1948 fn the_round_trip_is_not_the_identity_in_general() {
1949 let tree = Sexp::List(vec![
1950 Sexp::Atom(tatara_lisp::Atom::Symbol("f".into())),
1951 Sexp::Atom(tatara_lisp::Atom::Symbol("a b".into())),
1952 ]);
1953 let text = tree.to_string();
1954 let back: Vec<Sexp> = tatara_lisp::read_spanned(&text)
1955 .expect("it reads back cleanly — that IS the problem")
1956 .iter()
1957 .map(tatara_lisp::Spanned::to_sexp)
1958 .collect();
1959 assert_ne!(
1960 back,
1961 vec![tree.clone()],
1962 "if print-then-read became inverse over symbols, the class would be \
1963 closed upstream and this test should be deleted rather than relaxed"
1964 );
1965 let direct: Vec<Sexp> = crate::lower_to_spanned(std::slice::from_ref(&tree))
1967 .iter()
1968 .map(tatara_lisp::Spanned::to_sexp)
1969 .collect();
1970 assert_eq!(direct, vec![tree]);
1971 }
1972}
1973
1974#[cfg(test)]
1975mod macro_tests {
1976 use super::*;
1977
1978 fn int(src: &str) -> i64 {
1979 match run(src).unwrap_or_else(|e| panic!("{src:?}: {e}")).value {
1980 Value::Int(v) => v,
1981 other => panic!("{src:?} produced {other:?}"),
1982 }
1983 }
1984
1985 #[test]
1987 fn a_macro_expands_and_runs() {
1988 assert_eq!(
1989 int("defmacro double(x)\n quote\n unquote(x) + unquote(x)\n end\nend\ndouble(21)"),
1990 42
1991 );
1992 }
1993
1994 #[test]
1997 fn a_macro_operates_on_syntax_not_values() {
1998 assert_eq!(
1999 int("defmacro sq(e)\n quote\n unquote(e) * unquote(e)\n end\nend\nsq(2 + 3)"),
2000 25,
2001 "the argument form `2 + 3` must be substituted twice"
2002 );
2003 }
2004
2005 #[test]
2024 fn a_runaway_macro_fails_the_compilation_rather_than_the_process() {
2025 let err =
2026 run("defmacro forever(x)\n quote\n forever(unquote(x))\n end\nend\nforever(1)")
2027 .expect_err("a self-referential macro must be rejected");
2028 let msg = err.to_string();
2029 assert!(
2030 msg.contains("forever") && msg.contains("expansion"),
2031 "the error must name the macro and the limit it hit: {msg}"
2032 );
2033 assert!(
2034 msg.contains("<anonymous>:6:1"),
2035 "and must place the call that ran away — line 6 is `forever(1)`: {msg}"
2036 );
2037 }
2038}
2039
2040#[cfg(test)]
2041mod input_tests {
2042 use super::*;
2043 use crate::inputs::{Declaration, Inputs};
2044
2045 const SCHEMA: &[u8] = b"3";
2047
2048 fn with_schema(src: &str) -> Result<Run, RunError> {
2049 let hash = Inputs::hash_of(SCHEMA);
2050 let mut inputs = Inputs::new();
2051 inputs
2052 .bind(
2053 &Declaration {
2054 name: "schema".to_string(),
2055 hash,
2056 },
2057 SCHEMA.to_vec(),
2058 )
2059 .expect("bind");
2060 run_with_inputs(src, inputs)
2061 }
2062
2063 fn decl_line() -> String {
2064 let mut s = String::from("definput(\"schema\", \"");
2065 s.push_str(&Inputs::hash_of(SCHEMA));
2066 s.push_str("\")\n");
2067 s
2068 }
2069
2070 #[test]
2075 fn a_macro_can_read_a_declared_build_input() {
2076 let src = decl_line() + "input(\"schema\")";
2077 let out = with_schema(&src).expect("run");
2078 assert!(
2079 matches!(out.value, Value::Str(ref s) if &**s == "3"),
2080 "got {:?}",
2081 out.value
2082 );
2083 }
2084
2085 #[test]
2089 fn an_undeclared_input_is_an_error() {
2090 let err = with_schema("input(\"not_declared\")").expect_err("must fail");
2091 let msg = err.to_string();
2092 assert!(msg.contains("not_declared"), "must name it: {msg}");
2093 assert!(msg.contains("definput"), "and say how to declare it: {msg}");
2094 }
2095
2096 #[cfg(not(feature = "sys"))]
2104 #[test]
2105 fn there_is_no_ambient_file_read() {
2106 for attempt in [
2107 "read_file(\"/etc/passwd\")",
2108 "File(\"/etc/passwd\")",
2109 "slurp(\"/etc/passwd\")",
2110 "open(\"/etc/passwd\")",
2111 ] {
2112 let err = with_schema(attempt).expect_err("must not resolve");
2113 assert!(
2114 err.to_string().contains("unbound"),
2115 "{attempt} must be UNBOUND — a capability removed by absence, \
2116 not guarded by a check: {err}"
2117 );
2118 }
2119 }
2120
2121 #[cfg(feature = "sys")]
2127 #[test]
2128 fn sys_read_file_is_the_trusted_cli_only_exception() {
2129 let err = with_schema("definitely_not_a_primitive(\"x\")").expect_err("must not resolve");
2130 assert!(err.to_string().contains("unbound"), "{err}");
2131 assert!(
2132 with_schema("read_file(\"/etc/passwd\")").is_ok(),
2133 "with `sys` on, read_file is the trusted CLI surface"
2134 );
2135 let out = with_schema("input(\"schema\")").expect("run");
2136 assert!(
2137 matches!(out.value, Value::Str(ref s) if &**s == "3"),
2138 "input() still binds beside the sys surface: {:?}",
2139 out.value
2140 );
2141 }
2142
2143 #[test]
2146 fn a_declared_input_with_no_bytes_supplied_fails() {
2147 let src = decl_line() + "input(\"schema\")";
2148 assert!(run(&src).is_err(), "no bytes were supplied");
2149 }
2150}
2151
2152#[cfg(test)]
2153mod tier2_tests {
2154 use super::*;
2155 use crate::inputs::{Declaration, Inputs};
2156
2157 #[test]
2169 fn a_macro_generates_code_from_a_schema() {
2170 let schema = b"7";
2171 let mut inputs = Inputs::new();
2172 inputs
2173 .bind(
2174 &Declaration {
2175 name: "arity".to_string(),
2176 hash: Inputs::hash_of(schema),
2177 },
2178 schema.to_vec(),
2179 )
2180 .expect("bind");
2181
2182 let mut src = String::from("definput(\"arity\", \"");
2185 src.push_str(&Inputs::hash_of(schema));
2186 src.push_str("\")\n");
2187 src.push_str(
2188 "defmacro from_schema()\n quote\n unquote(to_int(input(\"arity\")))\n end\nend\n\
2189 from_schema() * 6",
2190 );
2191
2192 let out = run_with_inputs(&src, inputs).expect("run");
2193 assert!(
2194 matches!(out.value, Value::Int(42)),
2195 "the schema's 7 must reach the generated code: got {:?}",
2196 out.value
2197 );
2198 }
2199}