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