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
76impl RunError {
77 #[must_use]
80 pub fn exit_status(&self) -> u8 {
81 match self {
82 RunError::Eval(fault) if fault.breach => crate::BREACH_EXIT,
83 _ => 1,
84 }
85 }
86}
87
88#[derive(Debug)]
90pub struct Run {
91 pub value: Value,
92 pub visited: usize,
96 pub typed_decls: usize,
98 pub seams: usize,
100}
101
102pub fn parse(src: &str) -> Result<Vec<Sexp>, RunError> {
104 parse_with_depth(src, blue_lang_syntax::MAX_EXPR_DEPTH)
105}
106
107pub fn parse_with_depth(src: &str, max_depth: usize) -> Result<Vec<Sexp>, RunError> {
114 blue_lang_syntax::parse_program_with_depth(src, max_depth)
115 .map_err(|e| RunError::Parse(e.to_string()))
116}
117
118pub fn parse_tree_with_depth(
126 src: &str,
127 max_depth: usize,
128) -> Result<Vec<blue_lang_syntax::Spanned>, RunError> {
129 blue_lang_syntax::parse_program_tree_with_depth(src, max_depth)
130 .map_err(|e| RunError::Parse(e.to_string()))
131}
132
133pub fn parse_tree(src: &str) -> Result<Vec<blue_lang_syntax::Spanned>, RunError> {
140 parse_tree_with_depth(src, blue_lang_syntax::MAX_EXPR_DEPTH)
141}
142
143pub fn run(src: &str) -> Result<Run, RunError> {
145 run_with_inputs(src, Inputs::new())
146}
147
148pub fn run_with_inputs(src: &str, inputs: Inputs) -> Result<Run, RunError> {
154 run_with_loader(src, inputs, &crate::uses::NoLoader)
155}
156
157pub fn run_with_loader(
165 src: &str,
166 inputs: Inputs,
167 loader: &dyn crate::uses::Loader,
168) -> Result<Run, RunError> {
169 run_in_surface(Entry::anonymous(src), inputs, loader, None)
170}
171
172pub fn run_in_surface(
189 entry: Entry<'_>,
190 inputs: Inputs,
191 loader: &dyn crate::uses::Loader,
192 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
193) -> Result<Run, RunError> {
194 run_framed(entry, inputs, loader, surface, None)
195}
196
197pub fn run_framed(
208 entry: Entry<'_>,
209 inputs: Inputs,
210 loader: &dyn crate::uses::Loader,
211 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
212 hold: Option<&crate::Hold>,
213) -> Result<Run, RunError> {
214 run_framed_accounted(
215 entry,
216 inputs,
217 loader,
218 surface,
219 hold,
220 &crate::Account::default(),
221 )
222}
223
224pub fn run_framed_accounted(
231 entry: Entry<'_>,
232 inputs: Inputs,
233 loader: &dyn crate::uses::Loader,
234 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
235 hold: Option<&crate::Hold>,
236 account: &crate::Account,
237) -> Result<Run, RunError> {
238 let (mut interp, mut prepared) = prepared_on(entry, inputs, loader, surface)?;
243 hold_to_frame(&mut interp, &mut prepared, hold, account)?;
244 let value = evaluate_held(&mut interp, &prepared, hold)?;
245 Ok(Run {
246 value,
247 visited: prepared.visited,
248 typed_decls: prepared.typed_decls,
249 seams: prepared.seams,
250 })
251}
252
253fn evaluate_held(
256 interp: &mut tatara_lisp_eval::Interpreter<()>,
257 prepared: &Prepared,
258 hold: Option<&crate::Hold>,
259) -> Result<Value, RunError> {
260 let bound = crate::execution_bounds_under(hold).max_alloc_bytes;
261 let (outcome, over) = crate::memory::held_on(interp, bound, |interp| {
262 let value = crate::timings::stage(crate::timings::Stage::Eval, || {
263 eval_prepared(interp, prepared, &mut ())
264 })?;
265 Ok(crate::timings::stage(crate::timings::Stage::Main, || {
266 call_main(interp, prepared, &mut ())
267 })?
268 .unwrap_or(value))
269 });
270 refused_past(outcome, over)
271}
272
273fn refused_past<T>(outcome: Result<T, RunError>, over: Option<u64>) -> Result<T, RunError> {
276 match (outcome, over) {
277 (Ok(_), Some(bound)) => Err(RunError::Eval(Box::new(crate::observe::Fault::unplaced(
278 &crate::memory::refusal(bound),
279 &[],
280 )))),
281 (outcome, _) => outcome,
282 }
283}
284
285fn hold_to_frame(
291 interp: &mut tatara_lisp_eval::Interpreter<()>,
292 prepared: &mut Prepared,
293 hold: Option<&crate::Hold>,
294 account: &crate::Account,
295) -> Result<(), RunError> {
296 if account.defer_identities(
297 &prepared.program,
298 std::sync::Arc::clone(&prepared.lowered),
299 main_key(prepared),
300 ) {
301 let places = places_of(&prepared.program, &prepared.erased);
302 account.places.defer(move || places);
303 }
304 let Some(hold) = hold else {
305 if account.identify.is_some() {
306 crate::attach_node_accounted(interp, None, account);
307 }
308 return Ok(());
309 };
310 let withheld = hold.rebind_scoped(&prepared.program, &mut prepared.erased);
311 let escapes = entry_escapes(&prepared.program, &withheld, &|cap| {
312 format!("this run does not allow; pass --allow {cap} to grant it")
313 });
314 if !escapes.is_empty() {
315 return Err(RunError::Types(escapes));
316 }
317 hold.bind(interp, "this run's frame", &account.host_io);
318 crate::attach_node_accounted(interp, Some(hold), account);
319 Ok(())
320}
321
322fn places_of(
327 program: &crate::uses::ResolvedProgram,
328 erased: &[tatara_lisp::Spanned],
329) -> std::collections::BTreeMap<String, blue_lang_proc::Source> {
330 let files: Vec<blue_lang_proc::Source> = program
331 .files()
332 .iter()
333 .map(|f| blue_lang_proc::Source {
334 file: f
335 .path
336 .as_ref()
337 .map_or_else(|| "<anonymous>".to_string(), |p| p.display().to_string()),
338 text: std::sync::Arc::from(f.text.as_str()),
339 })
340 .collect();
341 erased
342 .iter()
343 .enumerate()
344 .filter_map(|(i, form)| {
345 let key = blue_lang_proc::violation::defined_key(form)?;
346 let source = files.get(program.owner_of(i)?.index())?;
347 Some((key.to_string(), source.clone()))
348 })
349 .collect()
350}
351
352fn entry_escapes(
353 program: &crate::uses::ResolvedProgram,
354 refused: &[(&'static str, blue_lang_waku::Capability)],
355 grant: &dyn Fn(&str) -> String,
356) -> Vec<String> {
357 use crate::uses::ResolvedProgram;
358 let forms: Vec<tatara_lisp::Spanned> = program
359 .forms()
360 .iter()
361 .enumerate()
362 .filter(|(i, _)| program.owner_of(*i) == Some(ResolvedProgram::ENTRY))
363 .map(|(_, f)| f.clone())
364 .collect();
365 let label = |name: &str| {
366 refused
367 .iter()
368 .find(|(n, _)| *n == name)
369 .map(|(_, c)| c.label())
370 };
371 blue_lang_waku::free_names(&forms, &|name| label(name).is_none())
372 .into_iter()
373 .filter_map(|name| {
374 let cap = label(&name)?;
375 let at = program
376 .file(ResolvedProgram::ENTRY)
377 .and_then(|f| f.path.clone())
378 .map_or_else(|| "<entry>".to_string(), |p| p.display().to_string());
379 Some(format!(
380 "{at}: refused: `{name}` reaches the host ({cap}), which {}",
381 grant(cap)
382 ))
383 })
384 .collect()
385}
386
387fn runtime(inputs: Inputs) -> tatara_lisp_eval::Interpreter<()> {
389 let mut interp =
390 crate::timings::stage(crate::timings::Stage::Runtime, crate::interpreter_hostless);
391 crate::inputs::install_input_primitives(&mut interp, inputs);
392 interp
393}
394
395fn prepared_on(
397 entry: Entry<'_>,
398 inputs: Inputs,
399 loader: &dyn crate::uses::Loader,
400 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
401) -> Result<(tatara_lisp_eval::Interpreter<()>, Prepared), RunError> {
402 let interp = runtime(inputs);
403 let builtins = crate::timings::stage(crate::timings::Stage::Names, || builtin_names(&interp));
404 let prepared = prepare(entry, loader, surface, &builtins)?;
405 Ok((interp, prepared))
406}
407
408pub struct Compiled {
414 pub program: crate::uses::ResolvedProgram,
415 pub lowered: Vec<tatara_lisp::Spanned>,
416 pub erased: Vec<tatara_lisp::Spanned>,
417 pub main: Option<String>,
418}
419
420pub fn compile(
428 entry: Entry<'_>,
429 loader: &dyn crate::uses::Loader,
430 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
431 hold: Option<&crate::Hold>,
432) -> Result<Compiled, RunError> {
433 let (_, prepared) = prepared_on(entry, Inputs::new(), loader, surface)?;
434 if let Some(hold) = hold {
435 let escapes = entry_escapes(&prepared.program, &hold.withheld_host_names(), &|cap| {
436 format!("this bundle's policy does not allow; declare allow(%i[{cap}]) in the project's Bluefile and run `blue lock`")
437 });
438 if !escapes.is_empty() {
439 return Err(RunError::Types(escapes));
440 }
441 }
442 let main = main_key(&prepared);
443 let Prepared {
444 program,
445 lowered,
446 erased,
447 ..
448 } = prepared;
449 Ok(Compiled {
450 program,
451 lowered: std::sync::Arc::unwrap_or_clone(lowered),
452 erased,
453 main,
454 })
455}
456
457pub fn run_compiled(
467 erased: &[tatara_lisp::Spanned],
468 main: Option<&str>,
469 hold: &crate::Hold,
470 who: &'static str,
471) -> Result<Value, RunError> {
472 let mut interp = runtime(Inputs::new());
473 let account = crate::Account::default();
474 hold.bind(&mut interp, who, &account.host_io);
475 crate::attach_node_accounted(&mut interp, Some(hold), &account);
476 let fault = |e: tatara_lisp_eval::EvalError| {
477 RunError::Eval(Box::new(crate::observe::Fault::unplaced(
478 &crate::memory::named(e),
479 erased,
480 )))
481 };
482 let bound = hold.execution_bounds().max_alloc_bytes;
483 let (outcome, over) = crate::memory::held_on(&mut interp, bound, |interp| {
484 let mut value = Value::Nil;
485 for form in erased {
486 value = interp.eval_top_form(form, &mut ()).map_err(fault)?;
487 }
488 if let Some(key) = main {
489 let at = tatara_lisp::Span::synthetic();
490 let call = tatara_lisp::Spanned::new(
491 at,
492 tatara_lisp::SpannedForm::List(vec![tatara_lisp::Spanned::new(
493 at,
494 tatara_lisp::SpannedForm::Atom(tatara_lisp::Atom::Symbol(key.to_string())),
495 )]),
496 );
497 value = interp.eval_top_form(&call, &mut ()).map_err(fault)?;
498 }
499 Ok(value)
500 });
501 refused_past(outcome, over)
502}
503
504pub struct Observed {
507 pub definitions: std::sync::Arc<crate::observe::Definitions>,
508 pub outcome: Result<Run, RunError>,
510}
511
512pub struct Watched<'a> {
516 pub definitions: &'a std::sync::Arc<crate::observe::Definitions>,
517 pub program: &'a crate::uses::ResolvedProgram,
518 pub lowered: &'a [tatara_lisp::Spanned],
519 pub erased: &'a [tatara_lisp::Spanned],
520}
521
522pub fn run_observed(
533 entry: Entry<'_>,
534 inputs: Inputs,
535 loader: &dyn crate::uses::Loader,
536 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
537 watch: impl FnOnce(&Watched<'_>) -> std::sync::Arc<dyn crate::observe::CallObserver>,
538) -> Result<Observed, RunError> {
539 run_observed_framed(entry, inputs, loader, surface, None, watch)
540}
541
542pub fn run_observed_framed(
549 entry: Entry<'_>,
550 inputs: Inputs,
551 loader: &dyn crate::uses::Loader,
552 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
553 hold: Option<&crate::Hold>,
554 watch: impl FnOnce(&Watched<'_>) -> std::sync::Arc<dyn crate::observe::CallObserver>,
555) -> Result<Observed, RunError> {
556 run_observed_accounted(
557 entry,
558 inputs,
559 loader,
560 surface,
561 hold,
562 &crate::Account::default(),
563 watch,
564 )
565}
566
567pub fn run_observed_accounted(
574 entry: Entry<'_>,
575 inputs: Inputs,
576 loader: &dyn crate::uses::Loader,
577 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
578 hold: Option<&crate::Hold>,
579 account: &crate::Account,
580 watch: impl FnOnce(&Watched<'_>) -> std::sync::Arc<dyn crate::observe::CallObserver>,
581) -> Result<Observed, RunError> {
582 let (mut interp, mut prepared) = prepared_on(entry, inputs, loader, surface)?;
583 hold_to_frame(&mut interp, &mut prepared, hold, account)?;
584 let definitions = std::sync::Arc::new(crate::observe::Definitions::of(
585 &prepared.program,
586 &prepared.erased,
587 ));
588 interp.set_call_observer(Some(watch(&Watched {
589 definitions: &definitions,
590 program: &prepared.program,
591 lowered: &prepared.lowered,
592 erased: &prepared.erased,
593 })));
594 let outcome = evaluate_held(&mut interp, &prepared, hold).map(|value| Run {
595 value,
596 visited: prepared.visited,
597 typed_decls: prepared.typed_decls,
598 seams: prepared.seams,
599 });
600 interp.set_call_observer(None);
601 Ok(Observed {
602 definitions,
603 outcome,
604 })
605}
606
607pub(crate) struct Prepared {
612 program: crate::uses::ResolvedProgram,
613 lowered: std::sync::Arc<Vec<tatara_lisp::Spanned>>,
614 erased: Vec<tatara_lisp::Spanned>,
615 visited: usize,
616 typed_decls: usize,
617 seams: usize,
618}
619
620impl Prepared {
621 pub(crate) fn erased(&self) -> &[tatara_lisp::Spanned] {
623 &self.erased
624 }
625
626 pub(crate) fn entry_namespace(&self) -> Namespace {
628 let first = (0..self.program.forms().len())
629 .find(|i| self.program.owner_of(*i) == Some(crate::uses::ResolvedProgram::ENTRY));
630 match first {
631 Some(i) => namespace_of(&self.program, i),
632 None => match self
633 .program
634 .file(crate::uses::ResolvedProgram::ENTRY)
635 .and_then(|f| f.package.clone())
636 {
637 Some(p) => Namespace::Bidama(p),
638 None => Namespace::File(String::new()),
639 },
640 }
641 }
642}
643
644pub(crate) fn prepare(
650 entry: Entry<'_>,
651 loader: &dyn crate::uses::Loader,
652 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
653 builtins: &NameTable,
654) -> Result<Prepared, RunError> {
655 let mut program = parse_and_resolve(entry, loader, surface)?;
656
657 program.retain(|f| !crate::uses::is_test_form(f));
670
671 let (outcome, names) = crate::timings::stage(crate::timings::Stage::Check, || {
686 check_stage(&program, builtins, false)
687 });
688 if !outcome.ok() {
689 return Err(RunError::Types(
690 outcome
691 .errors()
692 .map(|d| render(&program, d))
695 .collect(),
696 ));
697 }
698
699 let (lowered, erased) = crate::timings::stage(crate::timings::Stage::Erase, || {
713 let lowered = lower(&program, &names);
714 let erased = erase_types(&lowered);
715 (lowered, erased)
716 });
717
718 Ok(Prepared {
719 program,
720 lowered: std::sync::Arc::new(lowered),
721 erased,
722 visited: outcome.stats.visited,
723 typed_decls: outcome.stats.typed_decls,
724 seams: outcome.seams.len(),
725 })
726}
727
728pub(crate) fn parse_and_resolve(
730 entry: Entry<'_>,
731 loader: &dyn crate::uses::Loader,
732 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
733) -> Result<crate::uses::ResolvedProgram, RunError> {
734 let forms = crate::timings::stage(crate::timings::Stage::Parse, || {
735 let mut forms = match surface {
736 Some(pack) => blue_lang_syntax::parse_program_tree_in(entry.text, pack)
737 .map_err(|e| RunError::Parse(e.to_string()))?,
738 None => loader
739 .parse(entry.text)
740 .map_err(|e| RunError::Parse(e.to_string()))?,
741 };
742 if surface.is_none() {
745 forms.extend(
746 blue_lang_syntax::doc::doctests(entry.text)
747 .map_err(|e| RunError::Parse(e.to_string()))?,
748 );
749 }
750 Ok::<_, RunError>(forms)
751 })?;
752
753 crate::timings::stage(crate::timings::Stage::Resolve, || {
764 crate::uses::resolve_uses(forms, entry, loader).map_err(RunError::Import)
765 })
766}
767
768pub(crate) fn check_stage(
781 program: &crate::uses::ResolvedProgram,
782 builtins: &NameTable,
783 with_tests: bool,
784) -> (blue_lang_check::Outcome, NameTable) {
785 use crate::uses::ResolvedProgram;
786 let forms = program.forms();
787 let is_entry = |i: usize| program.owner_of(i) == Some(ResolvedProgram::ENTRY);
788
789 let mut outcome = blue_lang_check::check_program_in(forms, &|a, b| {
791 program.owner_of(a) == program.owner_of(b)
792 });
793 outcome
794 .diagnostics
795 .extend(data_law_diagnostics(program, &is_entry));
796
797 let table = program_names(program, builtins);
800 let whole = |i: usize| with_tests && is_entry(i);
801 let (names, resolved) = blue_lang_check::names::check_names_whole(
802 forms,
803 &table,
804 &|i| namespace_of(program, i),
805 &is_entry,
806 &whole,
807 );
808 let stored = program
809 .file(ResolvedProgram::ENTRY)
810 .is_some_and(|f| f.path.is_some());
811 outcome.diagnostics.extend(names);
812 outcome.stats.names_resolved = resolved;
813 if stored {
814 let script = program
815 .file(ResolvedProgram::ENTRY)
816 .is_some_and(|f| f.package.is_none());
817 let found = blue_lang_check::form_rules::check(forms, &is_entry, script);
818 let silenced: Vec<tatara_lisp::Span> = found
819 .iter()
820 .filter(|d| d.code == blue_lang_check::Code::B0038)
821 .map(|d| d.span)
822 .collect();
823 outcome
824 .diagnostics
825 .retain(|d| !(d.code == blue_lang_check::Code::B0002 && silenced.contains(&d.span)));
826 outcome.diagnostics.extend(found);
827 }
828 outcome
829 .diagnostics
830 .extend(blue_lang_check::contracts::check(
831 forms,
832 &table,
833 &|i| namespace_of(program, i),
834 &is_entry,
835 &|i| {
836 program
837 .owner_of(i)
838 .and_then(|f| program.file(f))
839 .map(|f| f.text.as_str())
840 },
841 crate::docs::signatures(),
842 ));
843
844 outcome.diagnostics.extend(doc_diagnostics(program));
845
846 let mut waivers = Vec::new();
849 for file in program.files() {
850 let spans: Vec<(usize, tatara_lisp::Span)> = forms
851 .iter()
852 .enumerate()
853 .filter(|(i, _)| program.owner_of(*i) == Some(file.id))
854 .map(|(i, f)| (i, f.span))
855 .collect();
856 let (found, malformed) = blue_lang_check::waiver::collect(&file.text, &spans);
857 waivers.extend(found);
858 outcome.diagnostics.extend(malformed);
859 }
860 outcome.waived = blue_lang_check::waiver::apply(&mut outcome.diagnostics, waivers, &is_entry);
861 if !stored {
862 outcome.diagnostics.retain(|d| {
863 !matches!(
864 d.code,
865 blue_lang_check::Code::B0023
866 | blue_lang_check::Code::B0036
867 | blue_lang_check::Code::B0037
868 | blue_lang_check::Code::B0067
869 | blue_lang_check::Code::B0061
870 | blue_lang_check::Code::B0062
871 | blue_lang_check::Code::B0063
872 )
873 });
874 }
875 let judge = blue_lang_check::evolution::FormJudge::from_env().unwrap_or_default();
876 outcome
877 .diagnostics
878 .retain(|d| judge.reports(d.code, is_entry(d.top_level)));
879 let (enforced, census): (Vec<_>, Vec<_>) = std::mem::take(&mut outcome.diagnostics)
882 .into_iter()
883 .partition(|d| d.code.rule().active());
884 outcome.diagnostics = enforced;
885 outcome.census = census;
886 outcome
887 .diagnostics
888 .sort_by_key(|d| (d.top_level, d.span.start, d.code));
889 (outcome, table)
890}
891
892fn doc_diagnostics(program: &crate::uses::ResolvedProgram) -> Vec<blue_lang_check::Diagnostic> {
896 use crate::uses::ResolvedProgram;
897 let Some(file) = program.file(ResolvedProgram::ENTRY) else {
898 return Vec::new();
899 };
900 let Some(package) = file.package.as_deref() else {
901 return Vec::new();
902 };
903 let forms: Vec<(usize, &tatara_lisp::Spanned)> = program
904 .forms()
905 .iter()
906 .enumerate()
907 .filter(|(i, f)| {
908 program.owner_of(*i) == Some(ResolvedProgram::ENTRY)
909 && !blue_lang_syntax::doc::is_doctest(f)
910 })
911 .collect();
912 let graph = file.path.as_deref().map(crate::doc_sources::graph_meanings);
913 let meaning = |name: &str| {
914 graph
915 .as_ref()
916 .and_then(|g| g.get(&(package.to_string(), name.to_string())).cloned())
917 };
918 let names_line = |name: &str| crate::docs::doc_of(name).map(|d| d.doc.to_string());
919 blue_lang_check::doc_rules::check_file(
920 &file.text,
921 &forms,
922 package,
923 &blue_lang_check::doc_rules::Sources {
924 meaning: &meaning,
925 names_line: &names_line,
926 },
927 )
928}
929
930fn data_law_diagnostics(
931 program: &crate::uses::ResolvedProgram,
932 is_entry: &dyn Fn(usize) -> bool,
933) -> Vec<blue_lang_check::Diagnostic> {
934 use blue_lang_check::data_laws;
935 let forms = program.forms();
936 let fragments = data_laws::Fragments::collect(forms);
937 let frontend = program.frontend();
938 let mut out = Vec::new();
939 let schema = match frontend.and_then(|f| f.table.as_ref()) {
940 Some((label, text)) => match data_laws::Schema::read(text) {
941 Ok(s) => Some(s),
942 Err(why) => {
943 if let Some((i, first)) = forms.iter().enumerate().find(|(i, _)| is_entry(*i)) {
944 out.push(
945 blue_lang_check::Diagnostic::new(
946 blue_lang_check::Code::B0050,
947 format!("the generated schema table {label} does not read: {why}"),
948 first.span,
949 )
950 .at_top_level(i)
951 .with_help("regenerate it: `nix run .#regen`"),
952 );
953 }
954 None
955 }
956 },
957 None => None,
958 };
959 if fragments.is_empty() && schema.is_none() && !frontend.is_some_and(|f| f.view) {
960 return out;
961 }
962 out.extend(data_laws::check(
963 forms,
964 &data_laws::Context {
965 fragments: &fragments,
966 schema: schema.as_ref(),
967 is_entry,
968 view: frontend.is_some_and(|f| f.view),
969 },
970 ));
971 out
972}
973
974#[must_use]
987pub fn program_names(program: &crate::uses::ResolvedProgram, builtins: &NameTable) -> NameTable {
988 use blue_lang_check::Binding;
989 let mut table = NameTable::new();
990 table.add_program(program.forms(), |i| namespace_of(program, i));
991 let builtin_macro = |head: &str| {
992 builtins
993 .scopes()
994 .iter()
995 .any(|s| s.namespace == Namespace::Macro && s.get(head).is_some())
996 };
997 let mut expander: Option<tatara_lisp_eval::Interpreter<()>> = None;
998 for (i, form) in program.forms().iter().enumerate() {
999 let Some(head) = form
1000 .as_list()
1001 .and_then(|items| items.first())
1002 .and_then(tatara_lisp::Spanned::as_symbol)
1003 else {
1004 continue;
1005 };
1006 if !builtin_macro(head) {
1007 continue;
1008 }
1009 let interp = expander.get_or_insert_with(crate::interpreter_hostless);
1010 let Ok(expanded) = interp.fully_expand(form, &mut ()) else {
1013 continue;
1014 };
1015 let nullary = blue_lang_syntax::scope::nullary_definition(&expanded);
1016 for (name, span, kind) in blue_lang_check::names::definitions_of(&expanded) {
1017 table.define(
1018 namespace_of(program, i),
1019 Binding {
1020 nullary: nullary == Some(name.as_str()),
1021 name,
1022 kind,
1023 span: Some(span),
1024 top_level: Some(i),
1025 },
1026 );
1027 }
1028 }
1029 for scope in builtins.scopes() {
1030 table.push(scope.clone());
1031 }
1032 table.spell_bare_like(builtins);
1033 let form_file: Vec<usize> = (0..program.forms().len())
1034 .map(|i| program.owner_of(i).map_or(usize::MAX, |f| f.index()))
1035 .collect();
1036 let mut imports: std::collections::BTreeMap<usize, blue_lang_check::names::FileImports> =
1037 std::collections::BTreeMap::new();
1038 for (file, import) in program.imports() {
1039 let entry = imports.entry(file.index()).or_default();
1040 entry.add(&import.package, import.names.iter().map(|(n, _)| n.clone()));
1041 let at = import.span.start;
1042 entry.use_at = Some(entry.use_at.map_or(at, |a| a.min(at)));
1043 }
1044 let facade_uses =
1049 |pkg: &str,
1050 imports: &std::collections::BTreeMap<usize, blue_lang_check::names::FileImports>| {
1051 let defines = table.bidama(pkg).is_some_and(|s| !s.is_empty());
1052 if defines {
1053 return std::collections::BTreeSet::new();
1054 }
1055 program
1056 .files()
1057 .iter()
1058 .filter(|f| f.package.as_deref() == Some(pkg))
1059 .filter_map(|f| imports.get(&f.id.index()))
1060 .flat_map(|i| i.uses.iter().cloned())
1061 .collect::<std::collections::BTreeSet<String>>()
1062 };
1063 loop {
1064 let mut grew = false;
1065 let files: Vec<usize> = imports.keys().copied().collect();
1066 for f in files {
1067 let used: Vec<String> = imports[&f].uses.iter().cloned().collect();
1068 for p in used {
1069 for member in facade_uses(&p, &imports) {
1070 if let Some(i) = imports.get_mut(&f) {
1071 if i.uses.insert(member.clone()) {
1072 i.via.insert(member, p.clone());
1073 grew = true;
1074 }
1075 }
1076 }
1077 }
1078 }
1079 if !grew {
1080 break;
1081 }
1082 }
1083 let mut pkg_uses: std::collections::BTreeMap<String, std::collections::BTreeSet<String>> =
1084 std::collections::BTreeMap::new();
1085 for (file, import) in program.imports() {
1086 if let Some(p) = program.file(*file).and_then(|f| f.package.clone()) {
1087 pkg_uses
1088 .entry(p)
1089 .or_default()
1090 .insert(import.package.clone());
1091 }
1092 }
1093 table.attach_files(form_file, imports);
1094 table.attach_package_uses(pkg_uses);
1095 table.attach_needs(program.needs().clone());
1096 let canonical = |p: &std::path::Path| {
1100 std::fs::canonicalize(p)
1101 .unwrap_or_else(|_| p.to_path_buf())
1102 .display()
1103 .to_string()
1104 };
1105 let mut file_paths = std::collections::BTreeMap::new();
1106 let mut pkg_roots = std::collections::BTreeMap::new();
1107 for f in program.files() {
1108 if let Some(path) = &f.path {
1109 file_paths.insert(f.id.index(), canonical(path));
1110 if let (Some(pkg), Some(root)) =
1111 (&f.package, path.parent().and_then(std::path::Path::parent))
1112 {
1113 pkg_roots
1114 .entry(pkg.clone())
1115 .or_insert_with(|| canonical(root));
1116 }
1117 }
1118 }
1119 table.attach_paths(file_paths, pkg_roots);
1120 let versions = program.versions().clone();
1121 table.apply_legacy(program.forms(), &|i| namespace_of(program, i), &|p| {
1122 versions.get(p).cloned()
1123 });
1124 table
1125}
1126
1127#[must_use]
1139pub fn lower(
1140 program: &crate::uses::ResolvedProgram,
1141 names: &NameTable,
1142) -> Vec<tatara_lisp::Spanned> {
1143 lower_with(program, names, true)
1144}
1145
1146#[must_use]
1147pub fn lower_with(
1148 program: &crate::uses::ResolvedProgram,
1149 names: &NameTable,
1150 simplify_chains: bool,
1151) -> Vec<tatara_lisp::Spanned> {
1152 let mut resolved = blue_lang_check::names::resolve_program(program.forms(), names, &|i| {
1153 namespace_of(program, i)
1154 });
1155 let simplified = if simplify_chains {
1156 chain_simplified(program.forms(), &resolved)
1157 } else {
1158 None
1159 };
1160 let forms: &[tatara_lisp::Spanned] = simplified.as_deref().unwrap_or(program.forms());
1161 if simplified.is_some() {
1162 resolved =
1163 blue_lang_check::names::resolve_program(forms, names, &|i| namespace_of(program, i));
1164 }
1165 resolved
1166 .resolved_tree(forms, blue_lang_check::names::Rule::Namespaced)
1167 .into_iter()
1168 .zip(forms)
1169 .enumerate()
1170 .map(|(i, (resolved, written))| {
1171 if crate::uses::use_target(written).is_some()
1172 || blue_lang_syntax::scope::legacy_target(written).is_some()
1173 {
1174 tatara_lisp::Spanned::new(written.span, tatara_lisp::SpannedForm::Nil)
1175 } else {
1176 let file = program.owner_of(i).and_then(|id| program.file(id));
1177 match file {
1178 Some(f) => locate_raises(&resolved, &raise_site(f)),
1179 None => resolved,
1180 }
1181 }
1182 })
1183 .collect()
1184}
1185
1186fn chain_simplified(
1187 forms: &[tatara_lisp::Spanned],
1188 resolved: &blue_lang_check::names::Resolved,
1189) -> Option<Vec<tatara_lisp::Spanned>> {
1190 let respell: std::collections::HashMap<usize, blue_lang_check::respell::Respelling> = resolved
1191 .respellings
1192 .iter()
1193 .filter(|(_, r)| matches!(r.respell, blue_lang_check::respell::Respell::Chain(_)))
1194 .cloned()
1195 .collect();
1196 if respell.is_empty() {
1197 return None;
1198 }
1199 let bare = std::collections::HashSet::new();
1200 let canon = Canon {
1201 bare: &bare,
1202 respell: &respell,
1203 };
1204 Some(forms.iter().map(|f| canon.form(f)).collect())
1205}
1206
1207fn raise_site(file: &crate::uses::SourceFile) -> impl Fn(tatara_lisp::Span) -> String + '_ {
1208 move |span| {
1209 let (line, _) =
1210 blue_lang_syntax::Span::line_col(&file.text, span.start.min(file.text.len()));
1211 let shown = file
1212 .path
1213 .as_ref()
1214 .map_or_else(|| "<source>".to_string(), |p| p.display().to_string());
1215 format!("{shown}:{line}")
1216 }
1217}
1218
1219fn locate_raises(
1220 form: &tatara_lisp::Spanned,
1221 site: &dyn Fn(tatara_lisp::Span) -> String,
1222) -> tatara_lisp::Spanned {
1223 use tatara_lisp::{Atom, Spanned, SpannedForm};
1224 let inner = match &form.form {
1225 SpannedForm::List(items) => {
1226 let mut out: Vec<Spanned> = items.iter().map(|i| locate_raises(i, site)).collect();
1227 let raising = items.first().and_then(Spanned::as_symbol) == Some("raise")
1228 && (3..=4).contains(&items.len());
1229 if raising {
1230 out[0] = Spanned::new(
1231 items[0].span,
1232 SpannedForm::Atom(Atom::Symbol(crate::errors::RAISE_AT.to_string())),
1233 );
1234 out.insert(
1235 1,
1236 Spanned::new(items[0].span, SpannedForm::Atom(Atom::Str(site(form.span)))),
1237 );
1238 }
1239 SpannedForm::List(out)
1240 }
1241 SpannedForm::Quote(_) | SpannedForm::Quasiquote(_) => return form.clone(),
1242 other => other.clone(),
1243 };
1244 Spanned::new(form.span, inner)
1245}
1246
1247#[must_use]
1249pub fn namespace_of(program: &crate::uses::ResolvedProgram, i: usize) -> Namespace {
1250 let file = program.owner_of(i).and_then(|id| program.file(id));
1251 match file {
1252 Some(f) => match &f.package {
1253 Some(p) => Namespace::Bidama(p.clone()),
1254 None => Namespace::File(
1255 f.path
1256 .as_ref()
1257 .map_or_else(|| "<anonymous>".to_string(), |p| p.display().to_string()),
1258 ),
1259 },
1260 None => Namespace::File("<unknown file>".to_string()),
1261 }
1262}
1263
1264pub const HARNESS_NAMES: &[&str] = &[
1278 blue_lang_syntax::LOWERED_ASSERT,
1279 blue_lang_syntax::LOWERED_ASSERT_PANICS,
1280 blue_lang_syntax::LOWERED_TEST_META,
1281];
1282
1283#[must_use]
1293pub fn builtin_names<H: 'static>(interp: &tatara_lisp_eval::Interpreter<H>) -> NameTable {
1294 builtin_names_with(interp, &[])
1295}
1296
1297#[must_use]
1298pub fn builtin_names_with<H: 'static>(
1299 interp: &tatara_lisp_eval::Interpreter<H>,
1300 host: &[crate::host_surface::HostName],
1301) -> NameTable {
1302 use blue_lang_check::{Binding, Scope, ScopeKind};
1303 use tatara_lisp_eval::HeadBinding;
1304 let mut special = Scope::new(Namespace::SpecialForm);
1305 let mut macros = Scope::new(Namespace::Macro);
1306 let mut values = Scope::new(Namespace::Builtin);
1307 let mut names: Vec<std::sync::Arc<str>> = interp.reserved_head_names().into_iter().collect();
1312 names.extend(
1313 crate::inputs::INPUT_PRIMITIVES
1314 .iter()
1315 .filter(|n| interp.resolve_head(n).is_some())
1316 .map(|n| std::sync::Arc::from(*n)),
1317 );
1318 for name in names {
1319 let (scope, kind) = match interp.resolve_head(&name) {
1320 Some(HeadBinding::SpecialForm) => (&mut special, ScopeKind::SpecialForm),
1321 Some(HeadBinding::Macro) => (&mut macros, ScopeKind::Macro),
1322 _ => (&mut values, ScopeKind::Value),
1323 };
1324 let nullary = kind == ScopeKind::Value
1325 && interp.lookup_global(&name).is_some_and(|v| {
1326 nullary_value(&v)
1327 || (crate::docs::doc_of(&name).is_some_and(|d| d.signature == &*name)
1328 && matches!(
1329 v,
1330 tatara_lisp_eval::Value::NativeFn(_)
1331 | tatara_lisp_eval::Value::Closure(_)
1332 ))
1333 });
1334 scope.bind(Binding {
1335 name: name.to_string(),
1336 kind,
1337 span: None,
1338 top_level: None,
1339 nullary,
1340 });
1341 }
1342 for h in host {
1343 values.bind(Binding {
1344 name: h.name.clone(),
1345 kind: ScopeKind::Value,
1346 span: None,
1347 top_level: None,
1348 nullary: h.nullary(),
1349 });
1350 }
1351 let mut table = NameTable::new();
1352 let mut harness = Scope::new(Namespace::Harness);
1353 for n in HARNESS_NAMES {
1354 harness.bind(Binding {
1355 name: (*n).to_string(),
1356 kind: ScopeKind::Value,
1357 span: None,
1358 top_level: None,
1359 nullary: false,
1360 });
1361 }
1362 table.push(harness);
1363 table.push(special);
1364 table.push(macros);
1365 table.push(values);
1366 table.spell_bare_only(
1367 crate::docs::NAMES
1368 .iter()
1369 .filter(|d| d.signature == d.name)
1370 .map(|d| d.name.to_string())
1371 .chain(host.iter().filter(|h| h.nullary()).map(|h| h.name.clone()))
1372 .collect(),
1373 );
1374 table
1375}
1376
1377#[must_use]
1378pub fn nullary_value(value: &tatara_lisp_eval::Value) -> bool {
1379 match value {
1380 tatara_lisp_eval::Value::NativeFn(f) => {
1381 matches!(f.arity, tatara_lisp_eval::ffi::Arity::Exact(0))
1382 }
1383 tatara_lisp_eval::Value::Closure(c) => c.params.is_empty() && c.rest.is_none(),
1384 _ => false,
1385 }
1386}
1387
1388#[must_use]
1392pub fn render(program: &crate::uses::ResolvedProgram, d: &blue_lang_check::Diagnostic) -> String {
1393 let head = d.to_string();
1394 let mut out = program.locate(d.top_level, d.span, &head).to_string();
1395 if let Some(help) = &d.help {
1396 out.push_str("\n help: ");
1397 out.push_str(help);
1398 }
1399 let file = program
1400 .owner_of(d.top_level)
1401 .and_then(|id| program.file(id))
1402 .and_then(|f| f.path.as_ref());
1403 if let Some(cmd) =
1404 file.and_then(|p| blue_lang_check::evolution::fix_command(d, &p.display().to_string()))
1405 {
1406 out.push_str("\n fix: ");
1407 out.push_str(&cmd);
1408 }
1409 out
1410}
1411
1412#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1414pub enum Checking {
1415 Program,
1417 WithTests,
1420}
1421
1422pub struct Checked {
1424 pub program: crate::uses::ResolvedProgram,
1425 pub outcome: blue_lang_check::Outcome,
1426 pub names: NameTable,
1429}
1430
1431impl Checked {
1432 #[must_use]
1435 pub fn erased(&self) -> Vec<tatara_lisp::Spanned> {
1436 erase_types(&self.lowered())
1437 }
1438
1439 #[must_use]
1442 pub fn lowered(&self) -> Vec<tatara_lisp::Spanned> {
1443 lower(&self.program, &self.names)
1444 }
1445
1446 #[must_use]
1449 pub fn evaluable(&self) -> Vec<tatara_lisp::Sexp> {
1450 lower(&self.program, &self.names)
1451 .iter()
1452 .map(tatara_lisp::Spanned::to_sexp)
1453 .collect()
1454 }
1455
1456 #[must_use]
1460 pub fn resolve(&self) -> blue_lang_check::names::Resolved {
1461 blue_lang_check::names::resolve_program(self.program.forms(), &self.names, &|i| {
1462 namespace_of(&self.program, i)
1463 })
1464 }
1465}
1466
1467impl Checked {
1468 #[must_use]
1469 pub fn call_explicit_forms(&self) -> Vec<tatara_lisp::Spanned> {
1470 self.resolve().call_explicit_tree(self.program.forms())
1471 }
1472
1473 #[must_use]
1474 pub fn canonical_entry(&self, src: &str) -> Option<Vec<tatara_lisp::Spanned>> {
1475 use crate::uses::ResolvedProgram;
1476 let program = &self.program;
1477 let entry: Vec<&tatara_lisp::Spanned> = program
1478 .forms()
1479 .iter()
1480 .enumerate()
1481 .filter(|(i, _)| program.owner_of(*i) == Some(ResolvedProgram::ENTRY))
1482 .map(|(_, f)| f)
1483 .filter(|f| !blue_lang_syntax::doc::is_doctest(f))
1484 .collect();
1485 let written = blue_lang_syntax::parse_program_tree(src).ok()?;
1486 let same = written.len() == entry.len()
1487 && written
1488 .iter()
1489 .zip(&entry)
1490 .all(|(w, e)| w.to_sexp() == e.to_sexp() && w.span == e.span);
1491 if !same {
1492 return None;
1493 }
1494 let resolved = self.resolve();
1495 let bare: std::collections::HashSet<usize> = resolved
1496 .parenthesized_nullary_calls()
1497 .into_iter()
1498 .filter(|r| program.owner_of(r.top_level) == Some(ResolvedProgram::ENTRY))
1499 .map(blue_lang_check::names::Reference::node_address)
1500 .collect();
1501 let waived: Vec<tatara_lisp::Span> = self
1502 .outcome
1503 .waived
1504 .iter()
1505 .map(|w| w.diagnostic.span)
1506 .collect();
1507 let respell: std::collections::HashMap<usize, blue_lang_check::respell::Respelling> =
1508 resolved
1509 .respellings
1510 .iter()
1511 .filter(|(_, r)| !waived.contains(&r.span))
1512 .cloned()
1513 .collect();
1514 let canon = Canon {
1515 bare: &bare,
1516 respell: &respell,
1517 };
1518 let forms: Vec<tatara_lisp::Spanned> = entry.into_iter().map(|f| canon.form(f)).collect();
1519 let script = program
1520 .file(ResolvedProgram::ENTRY)
1521 .is_some_and(|f| f.package.is_none());
1522 let forms = if script {
1523 blue_lang_check::form_rules::without_prefix(&forms).unwrap_or(forms)
1524 } else {
1525 forms
1526 };
1527 let forms = blue_lang_check::form_rules::without_main_guard(&forms).unwrap_or(forms);
1528 Some(blue_lang_check::form_rules::eta_reduced(&forms).unwrap_or(forms))
1529 }
1530}
1531
1532struct Canon<'a> {
1533 bare: &'a std::collections::HashSet<usize>,
1534 respell: &'a std::collections::HashMap<usize, blue_lang_check::respell::Respelling>,
1535}
1536
1537impl Canon<'_> {
1538 fn form(&self, form: &tatara_lisp::Spanned) -> tatara_lisp::Spanned {
1539 use tatara_lisp::{Spanned, SpannedForm};
1540 let inner = match &form.form {
1541 SpannedForm::List(items) => match items.as_slice() {
1542 [head] if self.bare.contains(&(std::ptr::from_ref(head) as usize)) => {
1543 return Spanned::new(form.span, head.form.clone());
1544 }
1545 [head, ..]
1546 if self
1547 .respell
1548 .contains_key(&(std::ptr::from_ref(head) as usize)) =>
1549 {
1550 let r = &self.respell[&(std::ptr::from_ref(head) as usize)];
1551 let walked: Vec<Spanned> = items.iter().map(|i| self.form(i)).collect();
1552 return blue_lang_check::respell::apply(form, &walked, r);
1553 }
1554 _ => SpannedForm::List(items.iter().map(|i| self.form(i)).collect()),
1555 },
1556 SpannedForm::Quote(i) => SpannedForm::Quote(Box::new(self.form(i))),
1557 SpannedForm::Quasiquote(i) => SpannedForm::Quasiquote(Box::new(self.form(i))),
1558 SpannedForm::Unquote(i) => SpannedForm::Unquote(Box::new(self.form(i))),
1559 SpannedForm::UnquoteSplice(i) => SpannedForm::UnquoteSplice(Box::new(self.form(i))),
1560 other => other.clone(),
1561 };
1562 Spanned::new(form.span, inner)
1563 }
1564}
1565
1566pub fn check_entry(
1582 entry: Entry<'_>,
1583 loader: &dyn crate::uses::Loader,
1584 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
1585 checking: Checking,
1586) -> Result<Checked, RunError> {
1587 let mut program = parse_and_resolve(entry, loader, surface)?;
1588 if checking == Checking::Program {
1589 program.retain(|f| !crate::uses::is_test_form(f));
1590 }
1591 let host = match entry.path {
1592 Some(path) => crate::host_surface::near(path)
1593 .map_err(|e| RunError::Import(e.to_string()))?
1594 .map(|s| s.functions)
1595 .unwrap_or_default(),
1596 None => Vec::new(),
1597 };
1598 let interp = runtime(Inputs::new());
1599 let builtins = crate::timings::stage(crate::timings::Stage::Names, || {
1600 builtin_names_with(&interp, &host)
1601 });
1602 let (outcome, names) = crate::timings::stage(crate::timings::Stage::Check, || {
1603 check_stage(&program, &builtins, checking == Checking::WithTests)
1604 });
1605 Ok(Checked {
1606 program,
1607 outcome,
1608 names,
1609 })
1610}
1611
1612#[must_use]
1613pub fn spelling_code(spelling: blue_lang_syntax::Spelling) -> blue_lang_check::Code {
1614 use blue_lang_check::Code;
1615 use blue_lang_syntax::Spelling;
1616 match spelling {
1617 Spelling::LogicalSymbol => Code::B0025,
1618 Spelling::Unless => Code::B0026,
1619 Spelling::ElseIf => Code::B0027,
1620 Spelling::CallForm => Code::B0028,
1621 Spelling::KeywordRocket => Code::B0029,
1622 Spelling::ParameterList => Code::B0030,
1623 Spelling::OperatorSpelling => Code::B0031,
1624 Spelling::TrailingComma => Code::B0032,
1625 Spelling::NegatedComparison => Code::B0034,
1626 Spelling::ElseNil => Code::B0035,
1627 Spelling::TryCatch => Code::B0037,
1628 Spelling::Apply => Code::B0042,
1629 Spelling::MapPun => Code::B0046,
1630 Spelling::NilTest => Code::B0047,
1631 Spelling::NewlineEscape => Code::B0065,
1632 Spelling::Index => Code::B0066,
1633 Spelling::TestDeclaration => Code::B0068,
1634 }
1635}
1636
1637#[must_use]
1638pub fn form_diagnostics(text: &str) -> Vec<blue_lang_check::Diagnostic> {
1639 match blue_lang_fmt::format_source_lossless(text) {
1640 Ok(formatted) => form_diagnostics_against(text, &formatted),
1641 Err(_) => Vec::new(),
1642 }
1643}
1644
1645#[must_use]
1646pub fn form_diagnostics_against(text: &str, formatted: &str) -> Vec<blue_lang_check::Diagnostic> {
1647 use blue_lang_check::{Code, Diagnostic};
1648 if formatted.trim_end() == text.trim_end() {
1649 return Vec::new();
1650 }
1651 let help = "`blue fmt --write FILE` writes it; no compile door writes a file (`BLUE_LAYOUT=rewrite` restores formatting in place)";
1652 let mut out: Vec<Diagnostic> = blue_lang_syntax::spelling_notes(text)
1653 .unwrap_or_default()
1654 .into_iter()
1655 .map(|n| {
1656 let message = if n.canonical.is_empty() {
1657 format!("`{}` is not written", n.written)
1658 } else {
1659 format!("`{}` is written `{}`", n.written, n.canonical)
1660 };
1661 Diagnostic::new(spelling_code(n.spelling), message, n.span)
1662 .at_top_level(0)
1663 .with_help(help)
1664 })
1665 .collect();
1666 out.extend(
1667 literal_diagnostics(text)
1668 .into_iter()
1669 .map(|d| d.with_help(help)),
1670 );
1671 if out.is_empty() {
1672 let at = text
1673 .lines()
1674 .zip(formatted.lines())
1675 .position(|(a, b)| a != b)
1676 .unwrap_or_else(|| text.lines().count().min(formatted.lines().count()));
1677 let start: usize = text.lines().take(at).map(|l| l.len() + 1).sum();
1678 let start = start.min(text.len());
1679 let end = text[start..].find('\n').map_or(text.len(), |e| start + e);
1680 out.push(
1681 Diagnostic::new(
1682 Code::B0024,
1683 format!("line {} is not laid out as blue writes it", at + 1),
1684 tatara_lisp::Span::new(start, end),
1685 )
1686 .at_top_level(0)
1687 .with_help(help),
1688 );
1689 }
1690 out.sort_by_key(|d| (d.span.start, d.code));
1691 out
1692}
1693
1694fn literal_diagnostics(text: &str) -> Vec<blue_lang_check::Diagnostic> {
1695 use blue_lang_syntax::{Atom, TokenKind};
1696 let Ok(toks) = blue_lang_syntax::lex(text) else {
1697 return Vec::new();
1698 };
1699 toks.iter()
1700 .filter_map(|t| {
1701 let atom = match &t.kind {
1702 TokenKind::Int(v) => Atom::Int(*v),
1703 TokenKind::Float(v) => Atom::Float(*v),
1704 TokenKind::Str(v) => Atom::Str(v.clone()),
1705 _ => return None,
1706 };
1707 let written = text.get(t.span.start..t.span.end)?;
1708 let canonical = blue_lang_fmt::render_literal(&atom);
1709 (written != canonical).then(|| {
1710 blue_lang_check::Diagnostic::new(
1711 blue_lang_check::Code::B0033,
1712 format!("`{written}` is written `{canonical}`"),
1713 t.span,
1714 )
1715 .at_top_level(0)
1716 })
1717 })
1718 .collect()
1719}
1720
1721#[must_use]
1725pub fn syntax_diagnostic(text: &str) -> Option<blue_lang_check::Diagnostic> {
1726 let e = blue_lang_syntax::parse_program_tree(text).err()?;
1727 Some(
1728 blue_lang_check::Diagnostic::new(blue_lang_check::Code::B0006, e.message, e.span)
1729 .at_top_level(0),
1730 )
1731}
1732
1733#[must_use]
1737pub fn display_keys(message: &str) -> String {
1738 let mut out = String::with_capacity(message.len());
1739 let mut parts = message.split('`');
1740 if let Some(first) = parts.next() {
1741 out.push_str(first);
1742 }
1743 for (i, part) in parts.enumerate() {
1744 out.push('`');
1745 if i % 2 == 0 {
1747 out.push_str(&crate::observe::display_key(part));
1748 } else {
1749 out.push_str(part);
1750 }
1751 }
1752 out
1753}
1754
1755pub(crate) fn eval_prepared<H: 'static>(
1757 interp: &mut tatara_lisp_eval::Interpreter<H>,
1758 prepared: &Prepared,
1759 host: &mut H,
1760) -> Result<Value, RunError> {
1761 let Prepared {
1762 program, erased, ..
1763 } = prepared;
1764
1765 let mut value = Value::Nil;
1779 for (top_level, form) in erased.iter().enumerate() {
1780 value = interp.eval_top_form(form, host).map_err(|e| {
1781 RunError::Eval(Box::new(crate::observe::Fault::of(
1788 program,
1789 erased,
1790 top_level,
1791 &crate::memory::named(e),
1792 &interp.take_trace(),
1793 )))
1794 })?;
1795 }
1796 Ok(value)
1797}
1798
1799pub const MAIN: &str = "main";
1800
1801#[must_use]
1802pub fn entry_main(program: &crate::uses::ResolvedProgram) -> Option<(usize, tatara_lisp::Span)> {
1803 use crate::uses::ResolvedProgram;
1804 let stored = program
1805 .file(ResolvedProgram::ENTRY)
1806 .is_some_and(|f| f.path.is_some());
1807 if !stored {
1808 return None;
1809 }
1810 program.forms().iter().enumerate().find_map(|(i, f)| {
1811 if program.owner_of(i) != Some(ResolvedProgram::ENTRY) {
1812 return None;
1813 }
1814 let items = f.as_list()?;
1815 let sig = items.get(1)?.as_list()?;
1816 let named_main = matches!(
1817 items.first().and_then(tatara_lisp::Spanned::as_symbol),
1818 Some("define" | "define-typed")
1819 ) && sig.len() == 1
1820 && sig[0].as_symbol() == Some(MAIN);
1821 named_main.then_some((i, sig[0].span))
1822 })
1823}
1824
1825fn main_call(prepared: &Prepared) -> Option<(usize, tatara_lisp::Span, String)> {
1828 let (top_level, span) = entry_main(&prepared.program)?;
1829 let ns = namespace_of(&prepared.program, top_level);
1830 Some((top_level, span, blue_lang_check::names::key(&ns, MAIN)))
1831}
1832
1833fn main_key(prepared: &Prepared) -> Option<String> {
1834 main_call(prepared).map(|(_, _, key)| key)
1835}
1836
1837pub(crate) fn call_main<H: 'static>(
1838 interp: &mut tatara_lisp_eval::Interpreter<H>,
1839 prepared: &Prepared,
1840 host: &mut H,
1841) -> Result<Option<Value>, RunError> {
1842 let Some((top_level, span, key)) = main_call(prepared) else {
1843 return Ok(None);
1844 };
1845 let call = tatara_lisp::Spanned::new(
1846 span,
1847 tatara_lisp::SpannedForm::List(vec![tatara_lisp::Spanned::new(
1848 span,
1849 tatara_lisp::SpannedForm::Atom(tatara_lisp::Atom::Symbol(key)),
1850 )]),
1851 );
1852 interp.eval_top_form(&call, host).map(Some).map_err(|e| {
1853 RunError::Eval(Box::new(crate::observe::Fault::of(
1854 &prepared.program,
1855 &prepared.erased,
1856 top_level,
1857 &crate::memory::named(e),
1858 &interp.take_trace(),
1859 )))
1860 })
1861}
1862
1863#[cfg(test)]
1879mod position_tests {
1880 use super::*;
1881 use crate::uses::{Entry, Loader};
1882 use std::collections::BTreeMap;
1883 use std::path::Path;
1884
1885 struct MemLoader(BTreeMap<&'static str, &'static str>);
1886
1887 impl Loader for MemLoader {
1888 fn load(&self, name: &str) -> Result<Vec<(String, String)>, String> {
1889 self.0
1890 .get(name)
1891 .map(|s| vec![(format!("{name}.b"), (*s).to_owned())])
1892 .ok_or_else(|| format!("no bidama named \"{name}\""))
1893 }
1894 }
1895
1896 const BAKUHATSU: &str = "\
1904def tsukawanai_a()
1905 1
1906end
1907
1908def tsukawanai_b()
1909 2
1910end
1911
1912# waive B0001: the fixture needs a raise at runtime
1913def bakuhatsu()
1914 kore_wa_sonzai_shinai()
1915end
1916
1917bakuhatsu";
1918
1919 const KOTAE: &str = "def kotae()\n 42\nend";
1920
1921 const BAKUHATSU2: &str =
1924 "# waive B0001: a raise at runtime\ndef yobu()\n kore_wa_sonzai_shinai()\nend";
1925
1926 fn loader() -> MemLoader {
1927 MemLoader(BTreeMap::from([
1928 ("kotae", KOTAE),
1929 ("bakuhatsu", BAKUHATSU),
1930 ("bakuhatsu2", BAKUHATSU2),
1931 ]))
1932 }
1933
1934 fn run_named(path: &str, src: &str) -> RunError {
1935 run_in_surface(
1936 Entry {
1937 path: Some(Path::new(path)),
1938 text: src,
1939 },
1940 Inputs::new(),
1941 &loader(),
1942 None,
1943 )
1944 .expect_err("the fixture must raise")
1945 }
1946
1947 const TWO_IMPORTS: &str =
1992 "use(\"kotae\")\n# waive B0015: kotae loads first on purpose\nuse(\"bakuhatsu\")\n";
1993
1994 #[test]
1995 fn a_raise_inside_an_imported_package_names_that_package() {
1996 let err = run_named("entry.b", TWO_IMPORTS);
1997 assert_eq!(
1998 err.to_string(),
1999 "runtime error: bakuhatsu.b:11:3: unbound symbol `kore_wa_sonzai_shinai`\n \
2000 at bakuhatsu::bakuhatsu (bakuhatsu.b:11:3)\n \
2001 at <top level> (bakuhatsu.b:14:1)"
2002 );
2003 }
2004
2005 #[test]
2013 fn line_11_column_3_of_the_fixture_is_the_failing_call() {
2014 let line = BAKUHATSU.lines().nth(10).expect("line 11 exists");
2015 assert_eq!(line, " kore_wa_sonzai_shinai()");
2016 assert_eq!(
2017 &line[2..],
2018 "kore_wa_sonzai_shinai()",
2019 "column 3 (1-indexed) is where the call starts"
2020 );
2021 let offset = BAKUHATSU.find("kore_wa_sonzai_shinai").expect("in fixture");
2024 assert!(
2025 offset > KOTAE.len(),
2026 "kotae.b could answer for byte {offset}"
2027 );
2028 assert!(
2029 offset > TWO_IMPORTS.len(),
2030 "the entry file could answer for byte {offset}"
2031 );
2032 }
2033
2034 #[test]
2053 fn a_raise_in_the_entry_file_names_the_entry_file() {
2054 let err = run_named(
2055 "honmono.b",
2056 "def f()\n 1\nend\n\n# waive B0001: a raise at runtime\nnani_mo_nai()\n",
2057 );
2058 assert_eq!(
2059 err.to_string(),
2060 "runtime error: honmono.b:6:1: unbound symbol `nani_mo_nai`"
2061 );
2062 }
2063
2064 #[test]
2078 fn a_raise_inside_an_annotated_def_still_names_its_line() {
2079 let err = run_named(
2080 "chuu.b",
2081 "# waive B0001: a raise at runtime\ndef f(a: Int) -> Int\n mada_nai(a)\nend\n\nf(1)\n",
2082 );
2083 assert_eq!(
2084 err.to_string(),
2085 "runtime error: chuu.b:3:3: unbound symbol `mada_nai`\n \
2086 at f (chuu.b:3:3)\n \
2087 at <top level> (chuu.b:6:1)"
2088 );
2089 }
2090
2091 #[test]
2109 fn a_raise_in_a_callee_from_another_file_names_the_callees_file() {
2110 let err = run_named(
2115 "yobidashi.b",
2116 "use(\"bakuhatsu2\")\n# pad: 47's in range\nbakuhatsu2::yobu\n",
2117 );
2118 assert_eq!(
2119 err.to_string(),
2120 "runtime error: bakuhatsu2.b:3:3: unbound symbol `kore_wa_sonzai_shinai`\n \
2121 at bakuhatsu2::yobu (bakuhatsu2.b:3:3)\n \
2122 at <top level> (yobidashi.b:3:1)"
2123 );
2124 }
2125}
2126
2127#[cfg(test)]
2128mod tests {
2129 use super::*;
2130
2131 fn int(src: &str) -> i64 {
2132 match run(src).unwrap_or_else(|e| panic!("{src:?}: {e}")).value {
2133 Value::Int(v) => v,
2134 other => panic!("{src:?} produced {other:?}"),
2135 }
2136 }
2137
2138 #[test]
2141 fn annotating_buys_analysis_and_changes_nothing_else() {
2142 let plain = run("def add(a, b)\n a + b\nend\nadd(2, 3)").expect("plain");
2143 let typed = run("def add(a: Int, b: Int) -> Int\n a + b\nend\nadd(2, 3)").expect("typed");
2144
2145 assert!(matches!(plain.value, Value::Int(5)));
2146 assert!(
2147 matches!(typed.value, Value::Int(5)),
2148 "the annotated program must compute the same answer"
2149 );
2150 assert_eq!(plain.visited, 0, "no annotations means no analysis");
2151 assert!(
2152 typed.visited > 0,
2153 "an annotation must actually buy analysis, not just decorate"
2154 );
2155 assert_eq!(plain.typed_decls, 0);
2156 assert_eq!(typed.typed_decls, 1);
2157 }
2158
2159 #[test]
2163 fn a_type_error_is_reported_and_the_program_does_not_run() {
2164 let err = run("def add(a: Int, b: Int) -> Str\n a + b\nend\nadd(1, 2)")
2165 .expect_err("a declared Str return from an Int body must be rejected");
2166 assert!(
2167 matches!(err, RunError::Types(ref d) if !d.is_empty()),
2168 "expected type diagnostics, got {err}"
2169 );
2170 }
2171
2172 #[test]
2175 fn the_same_program_without_annotations_runs() {
2176 assert_eq!(int("def add(a, b)\n a + b\nend\nadd(1, 2)"), 3);
2177 }
2178
2179 #[test]
2180 fn a_parse_error_is_reported_as_one() {
2181 assert!(matches!(run("def (").unwrap_err(), RunError::Parse(_)));
2182 }
2183
2184 #[test]
2187 fn a_runtime_error_is_reported_as_one() {
2188 let err = run("throw(error(\"boom\"))").expect_err("a raise");
2189 assert!(matches!(err, RunError::Eval(_)), "got {err}");
2190 }
2191
2192 #[test]
2195 fn the_pipeline_reaches_both_runtime_layers() {
2196 assert_eq!(int("6 % 3"), 0);
2197 assert_eq!(int("7 % 3"), 1);
2198 assert_eq!(int("2 + 3 * 4"), 14);
2199 }
2200
2201 #[test]
2211 fn the_deleted_round_trip_agreed_with_the_direct_lowering() {
2212 let corpus = [
2213 "def add(a, b)\n a + b\nend\nadd(2, 3)",
2214 "def fact(n)\n if n < 2\n 1\n else\n n * fact(n - 1)\n end\nend\nfact(5)",
2215 "def f(a, b)\n c = a + b\n c * 2\nend\nf(1, 2)",
2216 "defmacro sq(e)\n quote\n unquote(e) * unquote(e)\n end\nend\nsq(2 + 3)",
2217 "\"a string with spaces, a ( and a )\"",
2218 "def g(a: Int) -> Int\n a + 1\nend\ng(1)",
2219 "6 % 3",
2220 "1.5 + 2.25",
2221 ];
2222 for src in corpus {
2223 let erased = crate::to_sexps(&erase_types(&parse_tree(src).expect("parse")));
2226
2227 let direct: Vec<Sexp> = crate::lower_to_spanned(&erased)
2228 .iter()
2229 .map(tatara_lisp::Spanned::to_sexp)
2230 .collect();
2231 assert_eq!(direct, erased, "the direct lowering must be the identity");
2232
2233 let text = erased
2234 .iter()
2235 .map(ToString::to_string)
2236 .collect::<Vec<_>>()
2237 .join("\n");
2238 let round_tripped: Vec<Sexp> = tatara_lisp::read_spanned(&text)
2239 .unwrap_or_else(|e| panic!("{src:?}: the old path could not read back: {e:?}"))
2240 .iter()
2241 .map(tatara_lisp::Spanned::to_sexp)
2242 .collect();
2243 assert_eq!(
2244 round_tripped, erased,
2245 "{src:?}: the old print-and-reparse path changed the tree"
2246 );
2247 }
2248 }
2249
2250 #[test]
2268 fn the_round_trip_is_not_the_identity_in_general() {
2269 let tree = Sexp::List(vec![
2270 Sexp::Atom(tatara_lisp::Atom::Symbol("f".into())),
2271 Sexp::Atom(tatara_lisp::Atom::Symbol("a b".into())),
2272 ]);
2273 let text = tree.to_string();
2274 let back: Vec<Sexp> = tatara_lisp::read_spanned(&text)
2275 .expect("it reads back cleanly — that IS the problem")
2276 .iter()
2277 .map(tatara_lisp::Spanned::to_sexp)
2278 .collect();
2279 assert_ne!(
2280 back,
2281 vec![tree.clone()],
2282 "if print-then-read became inverse over symbols, the class would be \
2283 closed upstream and this test should be deleted rather than relaxed"
2284 );
2285 let direct: Vec<Sexp> = crate::lower_to_spanned(std::slice::from_ref(&tree))
2287 .iter()
2288 .map(tatara_lisp::Spanned::to_sexp)
2289 .collect();
2290 assert_eq!(direct, vec![tree]);
2291 }
2292}
2293
2294#[cfg(test)]
2295mod macro_tests {
2296 use super::*;
2297
2298 fn int(src: &str) -> i64 {
2299 match run(src).unwrap_or_else(|e| panic!("{src:?}: {e}")).value {
2300 Value::Int(v) => v,
2301 other => panic!("{src:?} produced {other:?}"),
2302 }
2303 }
2304
2305 #[test]
2307 fn a_macro_expands_and_runs() {
2308 assert_eq!(
2309 int("defmacro double(x)\n quote\n unquote(x) + unquote(x)\n end\nend\ndouble(21)"),
2310 42
2311 );
2312 }
2313
2314 #[test]
2317 fn a_macro_operates_on_syntax_not_values() {
2318 assert_eq!(
2319 int("defmacro sq(e)\n quote\n unquote(e) * unquote(e)\n end\nend\nsq(2 + 3)"),
2320 25,
2321 "the argument form `2 + 3` must be substituted twice"
2322 );
2323 }
2324
2325 #[test]
2344 fn a_runaway_macro_fails_the_compilation_rather_than_the_process() {
2345 let err =
2346 run("defmacro forever(x)\n quote\n forever(unquote(x))\n end\nend\nforever(1)")
2347 .expect_err("a self-referential macro must be rejected");
2348 let msg = err.to_string();
2349 assert!(
2350 msg.contains("forever") && msg.contains("expansion"),
2351 "the error must name the macro and the limit it hit: {msg}"
2352 );
2353 assert!(
2354 msg.contains("<anonymous>:6:1"),
2355 "and must place the call that ran away — line 6 is `forever(1)`: {msg}"
2356 );
2357 }
2358}
2359
2360#[cfg(test)]
2361mod input_tests {
2362 use super::*;
2363 use crate::inputs::{Declaration, Inputs};
2364
2365 const SCHEMA: &[u8] = b"3";
2367
2368 fn with_schema(src: &str) -> Result<Run, RunError> {
2369 let hash = Inputs::hash_of(SCHEMA);
2370 let mut inputs = Inputs::new();
2371 inputs
2372 .bind(
2373 &Declaration {
2374 name: "schema".to_string(),
2375 hash,
2376 },
2377 SCHEMA.to_vec(),
2378 )
2379 .expect("bind");
2380 run_with_inputs(src, inputs)
2381 }
2382
2383 fn decl_line() -> String {
2384 let mut s = String::from("definput(\"schema\", \"");
2385 s.push_str(&Inputs::hash_of(SCHEMA));
2386 s.push_str("\")\n");
2387 s
2388 }
2389
2390 #[test]
2395 fn a_macro_can_read_a_declared_build_input() {
2396 let src = decl_line() + "input(\"schema\")";
2397 let out = with_schema(&src).expect("run");
2398 assert!(
2399 matches!(out.value, Value::Str(ref s) if &**s == "3"),
2400 "got {:?}",
2401 out.value
2402 );
2403 }
2404
2405 #[test]
2409 fn an_undeclared_input_is_an_error() {
2410 let err = with_schema("input(\"not_declared\")").expect_err("must fail");
2411 let msg = err.to_string();
2412 assert!(msg.contains("not_declared"), "must name it: {msg}");
2413 assert!(msg.contains("definput"), "and say how to declare it: {msg}");
2414 }
2415
2416 #[cfg(not(feature = "sys"))]
2424 #[test]
2425 fn there_is_no_ambient_file_read() {
2426 for attempt in [
2427 "read_file(\"/etc/passwd\")",
2428 "File(\"/etc/passwd\")",
2429 "slurp(\"/etc/passwd\")",
2430 "open(\"/etc/passwd\")",
2431 ] {
2432 let err = with_schema(attempt).expect_err("must not resolve");
2433 assert!(
2434 err.to_string().contains("unbound"),
2435 "{attempt} must be UNBOUND — a capability removed by absence, \
2436 not guarded by a check: {err}"
2437 );
2438 }
2439 }
2440
2441 #[cfg(feature = "sys")]
2447 #[test]
2448 fn sys_read_file_is_the_trusted_cli_only_exception() {
2449 let err = with_schema("definitely_not_a_primitive(\"x\")").expect_err("must not resolve");
2450 assert!(err.to_string().contains("unbound"), "{err}");
2451 assert!(
2452 with_schema("read_file(\"/etc/passwd\")").is_ok(),
2453 "with `sys` on, read_file is the trusted CLI surface"
2454 );
2455 let out = with_schema("input(\"schema\")").expect("run");
2456 assert!(
2457 matches!(out.value, Value::Str(ref s) if &**s == "3"),
2458 "input() still binds beside the sys surface: {:?}",
2459 out.value
2460 );
2461 }
2462
2463 #[test]
2466 fn a_declared_input_with_no_bytes_supplied_fails() {
2467 let src = decl_line() + "input(\"schema\")";
2468 assert!(run(&src).is_err(), "no bytes were supplied");
2469 }
2470}
2471
2472#[cfg(test)]
2473mod tier2_tests {
2474 use super::*;
2475 use crate::inputs::{Declaration, Inputs};
2476
2477 #[test]
2489 fn a_macro_generates_code_from_a_schema() {
2490 let schema = b"7";
2491 let mut inputs = Inputs::new();
2492 inputs
2493 .bind(
2494 &Declaration {
2495 name: "arity".to_string(),
2496 hash: Inputs::hash_of(schema),
2497 },
2498 schema.to_vec(),
2499 )
2500 .expect("bind");
2501
2502 let mut src = String::from("definput(\"arity\", \"");
2505 src.push_str(&Inputs::hash_of(schema));
2506 src.push_str("\")\n");
2507 src.push_str(
2508 "defmacro from_schema()\n quote\n unquote(to_int(input(\"arity\")))\n end\nend\n\
2509 from_schema() * 6",
2510 );
2511
2512 let out = run_with_inputs(&src, inputs).expect("run");
2513 assert!(
2514 matches!(out.value, Value::Int(42)),
2515 "the schema's 7 must reach the generated code: got {:?}",
2516 out.value
2517 );
2518 }
2519}