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