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 }
1323}
1324
1325#[must_use]
1326pub fn form_diagnostics(text: &str) -> Vec<blue_lang_check::Diagnostic> {
1327 match blue_lang_fmt::format_source_lossless(text) {
1328 Ok(formatted) => form_diagnostics_against(text, &formatted),
1329 Err(_) => Vec::new(),
1330 }
1331}
1332
1333#[must_use]
1334pub fn form_diagnostics_against(text: &str, formatted: &str) -> Vec<blue_lang_check::Diagnostic> {
1335 use blue_lang_check::{Code, Diagnostic};
1336 if formatted.trim_end() == text.trim_end() {
1337 return Vec::new();
1338 }
1339 let help = "`blue fmt --write FILE` writes it; no compile door writes a file (`BLUE_LAYOUT=rewrite` restores formatting in place)";
1340 let mut out: Vec<Diagnostic> = blue_lang_syntax::spelling_notes(text)
1341 .unwrap_or_default()
1342 .into_iter()
1343 .map(|n| {
1344 let message = if n.canonical.is_empty() {
1345 format!("`{}` is not written", n.written)
1346 } else {
1347 format!("`{}` is written `{}`", n.written, n.canonical)
1348 };
1349 Diagnostic::new(spelling_code(n.spelling), message, n.span)
1350 .at_top_level(0)
1351 .with_help(help)
1352 })
1353 .collect();
1354 out.extend(
1355 literal_diagnostics(text)
1356 .into_iter()
1357 .map(|d| d.with_help(help)),
1358 );
1359 if out.is_empty() {
1360 let at = text
1361 .lines()
1362 .zip(formatted.lines())
1363 .position(|(a, b)| a != b)
1364 .unwrap_or_else(|| text.lines().count().min(formatted.lines().count()));
1365 let start: usize = text.lines().take(at).map(|l| l.len() + 1).sum();
1366 let start = start.min(text.len());
1367 let end = text[start..].find('\n').map_or(text.len(), |e| start + e);
1368 out.push(
1369 Diagnostic::new(
1370 Code::B0024,
1371 format!("line {} is not laid out as blue writes it", at + 1),
1372 tatara_lisp::Span::new(start, end),
1373 )
1374 .at_top_level(0)
1375 .with_help(help),
1376 );
1377 }
1378 out.sort_by_key(|d| (d.span.start, d.code));
1379 out
1380}
1381
1382fn literal_diagnostics(text: &str) -> Vec<blue_lang_check::Diagnostic> {
1383 use blue_lang_syntax::{Atom, TokenKind};
1384 let Ok(toks) = blue_lang_syntax::lex(text) else {
1385 return Vec::new();
1386 };
1387 toks.iter()
1388 .filter_map(|t| {
1389 let atom = match &t.kind {
1390 TokenKind::Int(v) => Atom::Int(*v),
1391 TokenKind::Float(v) => Atom::Float(*v),
1392 TokenKind::Str(v) => Atom::Str(v.clone()),
1393 _ => return None,
1394 };
1395 let written = text.get(t.span.start..t.span.end)?;
1396 let canonical = blue_lang_fmt::render_literal(&atom);
1397 (written != canonical).then(|| {
1398 blue_lang_check::Diagnostic::new(
1399 blue_lang_check::Code::B0033,
1400 format!("`{written}` is written `{canonical}`"),
1401 t.span,
1402 )
1403 .at_top_level(0)
1404 })
1405 })
1406 .collect()
1407}
1408
1409#[must_use]
1413pub fn syntax_diagnostic(text: &str) -> Option<blue_lang_check::Diagnostic> {
1414 let e = blue_lang_syntax::parse_program_tree(text).err()?;
1415 Some(
1416 blue_lang_check::Diagnostic::new(blue_lang_check::Code::B0006, e.message, e.span)
1417 .at_top_level(0),
1418 )
1419}
1420
1421#[must_use]
1425pub fn display_keys(message: &str) -> String {
1426 let mut out = String::with_capacity(message.len());
1427 let mut parts = message.split('`');
1428 if let Some(first) = parts.next() {
1429 out.push_str(first);
1430 }
1431 for (i, part) in parts.enumerate() {
1432 out.push('`');
1433 if i % 2 == 0 {
1435 out.push_str(&crate::observe::display_key(part));
1436 } else {
1437 out.push_str(part);
1438 }
1439 }
1440 out
1441}
1442
1443pub(crate) fn eval_prepared<H: 'static>(
1445 interp: &mut tatara_lisp_eval::Interpreter<H>,
1446 prepared: &Prepared,
1447 host: &mut H,
1448) -> Result<Value, RunError> {
1449 let Prepared {
1450 program, erased, ..
1451 } = prepared;
1452
1453 let mut value = Value::Nil;
1467 for (top_level, form) in erased.iter().enumerate() {
1468 value = interp.eval_top_form(form, host).map_err(|e| {
1469 RunError::Eval(Box::new(crate::observe::Fault::of(
1476 program,
1477 erased,
1478 top_level,
1479 &e,
1480 &interp.take_trace(),
1481 )))
1482 })?;
1483 }
1484 Ok(value)
1485}
1486
1487pub const MAIN: &str = "main";
1488
1489#[must_use]
1490pub fn entry_main(program: &crate::uses::ResolvedProgram) -> Option<(usize, tatara_lisp::Span)> {
1491 use crate::uses::ResolvedProgram;
1492 let stored = program
1493 .file(ResolvedProgram::ENTRY)
1494 .is_some_and(|f| f.path.is_some());
1495 if !stored {
1496 return None;
1497 }
1498 program.forms().iter().enumerate().find_map(|(i, f)| {
1499 if program.owner_of(i) != Some(ResolvedProgram::ENTRY) {
1500 return None;
1501 }
1502 let items = f.as_list()?;
1503 let sig = items.get(1)?.as_list()?;
1504 let named_main = matches!(
1505 items.first().and_then(tatara_lisp::Spanned::as_symbol),
1506 Some("define" | "define-typed")
1507 ) && sig.len() == 1
1508 && sig[0].as_symbol() == Some(MAIN);
1509 named_main.then_some((i, sig[0].span))
1510 })
1511}
1512
1513pub(crate) fn call_main<H: 'static>(
1514 interp: &mut tatara_lisp_eval::Interpreter<H>,
1515 prepared: &Prepared,
1516 host: &mut H,
1517) -> Result<Option<Value>, RunError> {
1518 let Some((top_level, span)) = entry_main(&prepared.program) else {
1519 return Ok(None);
1520 };
1521 let ns = namespace_of(&prepared.program, top_level);
1522 let call = tatara_lisp::Spanned::new(
1523 span,
1524 tatara_lisp::SpannedForm::List(vec![tatara_lisp::Spanned::new(
1525 span,
1526 tatara_lisp::SpannedForm::Atom(tatara_lisp::Atom::Symbol(blue_lang_check::names::key(
1527 &ns, MAIN,
1528 ))),
1529 )]),
1530 );
1531 interp.eval_top_form(&call, host).map(Some).map_err(|e| {
1532 RunError::Eval(Box::new(crate::observe::Fault::of(
1533 &prepared.program,
1534 &prepared.erased,
1535 top_level,
1536 &e,
1537 &interp.take_trace(),
1538 )))
1539 })
1540}
1541
1542#[cfg(test)]
1558mod position_tests {
1559 use super::*;
1560 use crate::uses::{Entry, Loader};
1561 use std::collections::BTreeMap;
1562 use std::path::Path;
1563
1564 struct MemLoader(BTreeMap<&'static str, &'static str>);
1565
1566 impl Loader for MemLoader {
1567 fn load(&self, name: &str) -> Result<Vec<(String, String)>, String> {
1568 self.0
1569 .get(name)
1570 .map(|s| vec![(format!("{name}.b"), (*s).to_owned())])
1571 .ok_or_else(|| format!("no bidama named \"{name}\""))
1572 }
1573 }
1574
1575 const BAKUHATSU: &str = "\
1583def tsukawanai_a()
1584 1
1585end
1586
1587def tsukawanai_b()
1588 2
1589end
1590
1591# waive B0001: the fixture needs a raise at runtime
1592def bakuhatsu()
1593 kore_wa_sonzai_shinai()
1594end
1595
1596bakuhatsu";
1597
1598 const KOTAE: &str = "def kotae()\n 42\nend";
1599
1600 const BAKUHATSU2: &str =
1603 "# waive B0001: a raise at runtime\ndef yobu()\n kore_wa_sonzai_shinai()\nend";
1604
1605 fn loader() -> MemLoader {
1606 MemLoader(BTreeMap::from([
1607 ("kotae", KOTAE),
1608 ("bakuhatsu", BAKUHATSU),
1609 ("bakuhatsu2", BAKUHATSU2),
1610 ]))
1611 }
1612
1613 fn run_named(path: &str, src: &str) -> RunError {
1614 run_in_surface(
1615 Entry {
1616 path: Some(Path::new(path)),
1617 text: src,
1618 },
1619 Inputs::new(),
1620 &loader(),
1621 None,
1622 )
1623 .expect_err("the fixture must raise")
1624 }
1625
1626 const TWO_IMPORTS: &str =
1671 "use(\"kotae\")\n# waive B0015: kotae loads first on purpose\nuse(\"bakuhatsu\")\n";
1672
1673 #[test]
1674 fn a_raise_inside_an_imported_package_names_that_package() {
1675 let err = run_named("entry.b", TWO_IMPORTS);
1676 assert_eq!(
1677 err.to_string(),
1678 "runtime error: bakuhatsu.b:11:3: unbound symbol `kore_wa_sonzai_shinai`\n \
1679 at bakuhatsu::bakuhatsu (bakuhatsu.b:11:3)\n \
1680 at <top level> (bakuhatsu.b:14:1)"
1681 );
1682 }
1683
1684 #[test]
1692 fn line_11_column_3_of_the_fixture_is_the_failing_call() {
1693 let line = BAKUHATSU.lines().nth(10).expect("line 11 exists");
1694 assert_eq!(line, " kore_wa_sonzai_shinai()");
1695 assert_eq!(
1696 &line[2..],
1697 "kore_wa_sonzai_shinai()",
1698 "column 3 (1-indexed) is where the call starts"
1699 );
1700 let offset = BAKUHATSU.find("kore_wa_sonzai_shinai").expect("in fixture");
1703 assert!(
1704 offset > KOTAE.len(),
1705 "kotae.b could answer for byte {offset}"
1706 );
1707 assert!(
1708 offset > TWO_IMPORTS.len(),
1709 "the entry file could answer for byte {offset}"
1710 );
1711 }
1712
1713 #[test]
1732 fn a_raise_in_the_entry_file_names_the_entry_file() {
1733 let err = run_named(
1734 "honmono.b",
1735 "def f()\n 1\nend\n\n# waive B0001: a raise at runtime\nnani_mo_nai()\n",
1736 );
1737 assert_eq!(
1738 err.to_string(),
1739 "runtime error: honmono.b:6:1: unbound symbol `nani_mo_nai`"
1740 );
1741 }
1742
1743 #[test]
1757 fn a_raise_inside_an_annotated_def_still_names_its_line() {
1758 let err = run_named(
1759 "chuu.b",
1760 "# waive B0001: a raise at runtime\ndef f(a: Int) -> Int\n mada_nai(a)\nend\n\nf(1)\n",
1761 );
1762 assert_eq!(
1763 err.to_string(),
1764 "runtime error: chuu.b:3:3: unbound symbol `mada_nai`\n \
1765 at f (chuu.b:3:3)\n \
1766 at <top level> (chuu.b:6:1)"
1767 );
1768 }
1769
1770 #[test]
1788 fn a_raise_in_a_callee_from_another_file_names_the_callees_file() {
1789 let err = run_named(
1794 "yobidashi.b",
1795 "use(\"bakuhatsu2\")\n# pad: 47's in range\nbakuhatsu2::yobu\n",
1796 );
1797 assert_eq!(
1798 err.to_string(),
1799 "runtime error: bakuhatsu2.b:3:3: unbound symbol `kore_wa_sonzai_shinai`\n \
1800 at bakuhatsu2::yobu (bakuhatsu2.b:3:3)\n \
1801 at <top level> (yobidashi.b:3:1)"
1802 );
1803 }
1804}
1805
1806#[cfg(test)]
1807mod tests {
1808 use super::*;
1809
1810 fn int(src: &str) -> i64 {
1811 match run(src).unwrap_or_else(|e| panic!("{src:?}: {e}")).value {
1812 Value::Int(v) => v,
1813 other => panic!("{src:?} produced {other:?}"),
1814 }
1815 }
1816
1817 #[test]
1820 fn annotating_buys_analysis_and_changes_nothing_else() {
1821 let plain = run("def add(a, b)\n a + b\nend\nadd(2, 3)").expect("plain");
1822 let typed = run("def add(a: Int, b: Int) -> Int\n a + b\nend\nadd(2, 3)").expect("typed");
1823
1824 assert!(matches!(plain.value, Value::Int(5)));
1825 assert!(
1826 matches!(typed.value, Value::Int(5)),
1827 "the annotated program must compute the same answer"
1828 );
1829 assert_eq!(plain.visited, 0, "no annotations means no analysis");
1830 assert!(
1831 typed.visited > 0,
1832 "an annotation must actually buy analysis, not just decorate"
1833 );
1834 assert_eq!(plain.typed_decls, 0);
1835 assert_eq!(typed.typed_decls, 1);
1836 }
1837
1838 #[test]
1842 fn a_type_error_is_reported_and_the_program_does_not_run() {
1843 let err = run("def add(a: Int, b: Int) -> Str\n a + b\nend\nadd(1, 2)")
1844 .expect_err("a declared Str return from an Int body must be rejected");
1845 assert!(
1846 matches!(err, RunError::Types(ref d) if !d.is_empty()),
1847 "expected type diagnostics, got {err}"
1848 );
1849 }
1850
1851 #[test]
1854 fn the_same_program_without_annotations_runs() {
1855 assert_eq!(int("def add(a, b)\n a + b\nend\nadd(1, 2)"), 3);
1856 }
1857
1858 #[test]
1859 fn a_parse_error_is_reported_as_one() {
1860 assert!(matches!(run("def (").unwrap_err(), RunError::Parse(_)));
1861 }
1862
1863 #[test]
1866 fn a_runtime_error_is_reported_as_one() {
1867 let err = run("throw(error(\"boom\"))").expect_err("a raise");
1868 assert!(matches!(err, RunError::Eval(_)), "got {err}");
1869 }
1870
1871 #[test]
1874 fn the_pipeline_reaches_both_runtime_layers() {
1875 assert_eq!(int("6 % 3"), 0);
1876 assert_eq!(int("7 % 3"), 1);
1877 assert_eq!(int("2 + 3 * 4"), 14);
1878 }
1879
1880 #[test]
1890 fn the_deleted_round_trip_agreed_with_the_direct_lowering() {
1891 let corpus = [
1892 "def add(a, b)\n a + b\nend\nadd(2, 3)",
1893 "def fact(n)\n if n < 2\n 1\n else\n n * fact(n - 1)\n end\nend\nfact(5)",
1894 "def f(a, b)\n c = a + b\n c * 2\nend\nf(1, 2)",
1895 "defmacro sq(e)\n quote\n unquote(e) * unquote(e)\n end\nend\nsq(2 + 3)",
1896 "\"a string with spaces, a ( and a )\"",
1897 "def g(a: Int) -> Int\n a + 1\nend\ng(1)",
1898 "6 % 3",
1899 "1.5 + 2.25",
1900 ];
1901 for src in corpus {
1902 let erased = crate::to_sexps(&erase_types(&parse_tree(src).expect("parse")));
1905
1906 let direct: Vec<Sexp> = crate::lower_to_spanned(&erased)
1907 .iter()
1908 .map(tatara_lisp::Spanned::to_sexp)
1909 .collect();
1910 assert_eq!(direct, erased, "the direct lowering must be the identity");
1911
1912 let text = erased
1913 .iter()
1914 .map(ToString::to_string)
1915 .collect::<Vec<_>>()
1916 .join("\n");
1917 let round_tripped: Vec<Sexp> = tatara_lisp::read_spanned(&text)
1918 .unwrap_or_else(|e| panic!("{src:?}: the old path could not read back: {e:?}"))
1919 .iter()
1920 .map(tatara_lisp::Spanned::to_sexp)
1921 .collect();
1922 assert_eq!(
1923 round_tripped, erased,
1924 "{src:?}: the old print-and-reparse path changed the tree"
1925 );
1926 }
1927 }
1928
1929 #[test]
1947 fn the_round_trip_is_not_the_identity_in_general() {
1948 let tree = Sexp::List(vec![
1949 Sexp::Atom(tatara_lisp::Atom::Symbol("f".into())),
1950 Sexp::Atom(tatara_lisp::Atom::Symbol("a b".into())),
1951 ]);
1952 let text = tree.to_string();
1953 let back: Vec<Sexp> = tatara_lisp::read_spanned(&text)
1954 .expect("it reads back cleanly — that IS the problem")
1955 .iter()
1956 .map(tatara_lisp::Spanned::to_sexp)
1957 .collect();
1958 assert_ne!(
1959 back,
1960 vec![tree.clone()],
1961 "if print-then-read became inverse over symbols, the class would be \
1962 closed upstream and this test should be deleted rather than relaxed"
1963 );
1964 let direct: Vec<Sexp> = crate::lower_to_spanned(std::slice::from_ref(&tree))
1966 .iter()
1967 .map(tatara_lisp::Spanned::to_sexp)
1968 .collect();
1969 assert_eq!(direct, vec![tree]);
1970 }
1971}
1972
1973#[cfg(test)]
1974mod macro_tests {
1975 use super::*;
1976
1977 fn int(src: &str) -> i64 {
1978 match run(src).unwrap_or_else(|e| panic!("{src:?}: {e}")).value {
1979 Value::Int(v) => v,
1980 other => panic!("{src:?} produced {other:?}"),
1981 }
1982 }
1983
1984 #[test]
1986 fn a_macro_expands_and_runs() {
1987 assert_eq!(
1988 int("defmacro double(x)\n quote\n unquote(x) + unquote(x)\n end\nend\ndouble(21)"),
1989 42
1990 );
1991 }
1992
1993 #[test]
1996 fn a_macro_operates_on_syntax_not_values() {
1997 assert_eq!(
1998 int("defmacro sq(e)\n quote\n unquote(e) * unquote(e)\n end\nend\nsq(2 + 3)"),
1999 25,
2000 "the argument form `2 + 3` must be substituted twice"
2001 );
2002 }
2003
2004 #[test]
2023 fn a_runaway_macro_fails_the_compilation_rather_than_the_process() {
2024 let err =
2025 run("defmacro forever(x)\n quote\n forever(unquote(x))\n end\nend\nforever(1)")
2026 .expect_err("a self-referential macro must be rejected");
2027 let msg = err.to_string();
2028 assert!(
2029 msg.contains("forever") && msg.contains("expansion"),
2030 "the error must name the macro and the limit it hit: {msg}"
2031 );
2032 assert!(
2033 msg.contains("<anonymous>:6:1"),
2034 "and must place the call that ran away — line 6 is `forever(1)`: {msg}"
2035 );
2036 }
2037}
2038
2039#[cfg(test)]
2040mod input_tests {
2041 use super::*;
2042 use crate::inputs::{Declaration, Inputs};
2043
2044 const SCHEMA: &[u8] = b"3";
2046
2047 fn with_schema(src: &str) -> Result<Run, RunError> {
2048 let hash = Inputs::hash_of(SCHEMA);
2049 let mut inputs = Inputs::new();
2050 inputs
2051 .bind(
2052 &Declaration {
2053 name: "schema".to_string(),
2054 hash,
2055 },
2056 SCHEMA.to_vec(),
2057 )
2058 .expect("bind");
2059 run_with_inputs(src, inputs)
2060 }
2061
2062 fn decl_line() -> String {
2063 let mut s = String::from("definput(\"schema\", \"");
2064 s.push_str(&Inputs::hash_of(SCHEMA));
2065 s.push_str("\")\n");
2066 s
2067 }
2068
2069 #[test]
2074 fn a_macro_can_read_a_declared_build_input() {
2075 let src = decl_line() + "input(\"schema\")";
2076 let out = with_schema(&src).expect("run");
2077 assert!(
2078 matches!(out.value, Value::Str(ref s) if &**s == "3"),
2079 "got {:?}",
2080 out.value
2081 );
2082 }
2083
2084 #[test]
2088 fn an_undeclared_input_is_an_error() {
2089 let err = with_schema("input(\"not_declared\")").expect_err("must fail");
2090 let msg = err.to_string();
2091 assert!(msg.contains("not_declared"), "must name it: {msg}");
2092 assert!(msg.contains("definput"), "and say how to declare it: {msg}");
2093 }
2094
2095 #[cfg(not(feature = "sys"))]
2103 #[test]
2104 fn there_is_no_ambient_file_read() {
2105 for attempt in [
2106 "read_file(\"/etc/passwd\")",
2107 "File(\"/etc/passwd\")",
2108 "slurp(\"/etc/passwd\")",
2109 "open(\"/etc/passwd\")",
2110 ] {
2111 let err = with_schema(attempt).expect_err("must not resolve");
2112 assert!(
2113 err.to_string().contains("unbound"),
2114 "{attempt} must be UNBOUND — a capability removed by absence, \
2115 not guarded by a check: {err}"
2116 );
2117 }
2118 }
2119
2120 #[cfg(feature = "sys")]
2126 #[test]
2127 fn sys_read_file_is_the_trusted_cli_only_exception() {
2128 let err = with_schema("definitely_not_a_primitive(\"x\")").expect_err("must not resolve");
2129 assert!(err.to_string().contains("unbound"), "{err}");
2130 assert!(
2131 with_schema("read_file(\"/etc/passwd\")").is_ok(),
2132 "with `sys` on, read_file is the trusted CLI surface"
2133 );
2134 let out = with_schema("input(\"schema\")").expect("run");
2135 assert!(
2136 matches!(out.value, Value::Str(ref s) if &**s == "3"),
2137 "input() still binds beside the sys surface: {:?}",
2138 out.value
2139 );
2140 }
2141
2142 #[test]
2145 fn a_declared_input_with_no_bytes_supplied_fails() {
2146 let src = decl_line() + "input(\"schema\")";
2147 assert!(run(&src).is_err(), "no bytes were supplied");
2148 }
2149}
2150
2151#[cfg(test)]
2152mod tier2_tests {
2153 use super::*;
2154 use crate::inputs::{Declaration, Inputs};
2155
2156 #[test]
2168 fn a_macro_generates_code_from_a_schema() {
2169 let schema = b"7";
2170 let mut inputs = Inputs::new();
2171 inputs
2172 .bind(
2173 &Declaration {
2174 name: "arity".to_string(),
2175 hash: Inputs::hash_of(schema),
2176 },
2177 schema.to_vec(),
2178 )
2179 .expect("bind");
2180
2181 let mut src = String::from("definput(\"arity\", \"");
2184 src.push_str(&Inputs::hash_of(schema));
2185 src.push_str("\")\n");
2186 src.push_str(
2187 "defmacro from_schema()\n quote\n unquote(to_int(input(\"arity\")))\n end\nend\n\
2188 from_schema() * 6",
2189 );
2190
2191 let out = run_with_inputs(&src, inputs).expect("run");
2192 assert!(
2193 matches!(out.value, Value::Int(42)),
2194 "the schema's 7 must reach the generated code: got {:?}",
2195 out.value
2196 );
2197 }
2198}