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 mut value = crate::timings::stage(crate::timings::Stage::Eval, || {
245 eval_prepared(&mut interp, &prepared, &mut ())
246 })?;
247 if let Some(v) = crate::timings::stage(crate::timings::Stage::Main, || {
248 call_main(&mut interp, &prepared, &mut ())
249 })? {
250 value = v;
251 }
252 Ok(Run {
253 value,
254 visited: prepared.visited,
255 typed_decls: prepared.typed_decls,
256 seams: prepared.seams,
257 })
258}
259
260fn hold_to_frame(
266 interp: &mut tatara_lisp_eval::Interpreter<()>,
267 prepared: &mut Prepared,
268 hold: Option<&crate::Hold>,
269 account: &crate::Account,
270) -> Result<(), RunError> {
271 if let Some(identify) = account.identify {
272 account.identities.defer(identify(
273 &prepared.program,
274 std::sync::Arc::clone(&prepared.lowered),
275 main_key(prepared),
276 ));
277 let places = places_of(&prepared.program, &prepared.erased);
278 account.places.defer(move || places);
279 }
280 let Some(hold) = hold else {
281 if account.identify.is_some() {
282 crate::attach_node_accounted(interp, None, account);
283 }
284 return Ok(());
285 };
286 let withheld = hold.rebind_scoped(&prepared.program, &mut prepared.erased);
287 let escapes = entry_escapes(&prepared.program, &withheld, &|cap| {
288 format!("this run does not allow; pass --allow {cap} to grant it")
289 });
290 if !escapes.is_empty() {
291 return Err(RunError::Types(escapes));
292 }
293 hold.bind(interp, "this run's frame", &account.host_io);
294 crate::attach_node_accounted(interp, Some(hold), account);
295 Ok(())
296}
297
298fn places_of(
303 program: &crate::uses::ResolvedProgram,
304 erased: &[tatara_lisp::Spanned],
305) -> std::collections::BTreeMap<String, blue_lang_proc::Source> {
306 let files: Vec<blue_lang_proc::Source> = program
307 .files()
308 .iter()
309 .map(|f| blue_lang_proc::Source {
310 file: f
311 .path
312 .as_ref()
313 .map_or_else(|| "<anonymous>".to_string(), |p| p.display().to_string()),
314 text: std::sync::Arc::from(f.text.as_str()),
315 })
316 .collect();
317 erased
318 .iter()
319 .enumerate()
320 .filter_map(|(i, form)| {
321 let key = blue_lang_proc::violation::defined_key(form)?;
322 let source = files.get(program.owner_of(i)?.index())?;
323 Some((key.to_string(), source.clone()))
324 })
325 .collect()
326}
327
328fn entry_escapes(
329 program: &crate::uses::ResolvedProgram,
330 refused: &[(&'static str, blue_lang_waku::Capability)],
331 grant: &dyn Fn(&str) -> String,
332) -> Vec<String> {
333 use crate::uses::ResolvedProgram;
334 let forms: Vec<tatara_lisp::Spanned> = program
335 .forms()
336 .iter()
337 .enumerate()
338 .filter(|(i, _)| program.owner_of(*i) == Some(ResolvedProgram::ENTRY))
339 .map(|(_, f)| f.clone())
340 .collect();
341 let label = |name: &str| {
342 refused
343 .iter()
344 .find(|(n, _)| *n == name)
345 .map(|(_, c)| c.label())
346 };
347 blue_lang_waku::free_names(&forms, &|name| label(name).is_none())
348 .into_iter()
349 .filter_map(|name| {
350 let cap = label(&name)?;
351 let at = program
352 .file(ResolvedProgram::ENTRY)
353 .and_then(|f| f.path.clone())
354 .map_or_else(|| "<entry>".to_string(), |p| p.display().to_string());
355 Some(format!(
356 "{at}: refused: `{name}` reaches the host ({cap}), which {}",
357 grant(cap)
358 ))
359 })
360 .collect()
361}
362
363fn runtime(inputs: Inputs) -> tatara_lisp_eval::Interpreter<()> {
365 let mut interp =
366 crate::timings::stage(crate::timings::Stage::Runtime, crate::interpreter_hostless);
367 crate::inputs::install_input_primitives(&mut interp, inputs);
368 interp
369}
370
371fn prepared_on(
373 entry: Entry<'_>,
374 inputs: Inputs,
375 loader: &dyn crate::uses::Loader,
376 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
377) -> Result<(tatara_lisp_eval::Interpreter<()>, Prepared), RunError> {
378 let interp = runtime(inputs);
379 let builtins = crate::timings::stage(crate::timings::Stage::Names, || builtin_names(&interp));
380 let prepared = prepare(entry, loader, surface, &builtins)?;
381 Ok((interp, prepared))
382}
383
384pub struct Compiled {
390 pub program: crate::uses::ResolvedProgram,
391 pub lowered: Vec<tatara_lisp::Spanned>,
392 pub erased: Vec<tatara_lisp::Spanned>,
393 pub main: Option<String>,
394}
395
396pub fn compile(
404 entry: Entry<'_>,
405 loader: &dyn crate::uses::Loader,
406 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
407 hold: Option<&crate::Hold>,
408) -> Result<Compiled, RunError> {
409 let (_, prepared) = prepared_on(entry, Inputs::new(), loader, surface)?;
410 if let Some(hold) = hold {
411 let escapes = entry_escapes(&prepared.program, &hold.withheld_host_names(), &|cap| {
412 format!("this bundle's policy does not allow; declare allow(%i[{cap}]) in the project's Bluefile and run `blue lock`")
413 });
414 if !escapes.is_empty() {
415 return Err(RunError::Types(escapes));
416 }
417 }
418 let main = main_key(&prepared);
419 let Prepared {
420 program,
421 lowered,
422 erased,
423 ..
424 } = prepared;
425 Ok(Compiled {
426 program,
427 lowered: std::sync::Arc::unwrap_or_clone(lowered),
428 erased,
429 main,
430 })
431}
432
433pub fn run_compiled(
443 erased: &[tatara_lisp::Spanned],
444 main: Option<&str>,
445 hold: &crate::Hold,
446 who: &'static str,
447) -> Result<Value, RunError> {
448 let mut interp = runtime(Inputs::new());
449 let account = crate::Account::default();
450 hold.bind(&mut interp, who, &account.host_io);
451 crate::attach_node_accounted(&mut interp, Some(hold), &account);
452 let fault = |e: &tatara_lisp_eval::EvalError| {
453 RunError::Eval(Box::new(crate::observe::Fault::unplaced(e, erased)))
454 };
455 let mut value = Value::Nil;
456 for form in erased {
457 value = interp.eval_top_form(form, &mut ()).map_err(|e| fault(&e))?;
458 }
459 if let Some(key) = main {
460 let at = tatara_lisp::Span::synthetic();
461 let call = tatara_lisp::Spanned::new(
462 at,
463 tatara_lisp::SpannedForm::List(vec![tatara_lisp::Spanned::new(
464 at,
465 tatara_lisp::SpannedForm::Atom(tatara_lisp::Atom::Symbol(key.to_string())),
466 )]),
467 );
468 value = interp
469 .eval_top_form(&call, &mut ())
470 .map_err(|e| fault(&e))?;
471 }
472 Ok(value)
473}
474
475pub struct Observed {
478 pub definitions: std::sync::Arc<crate::observe::Definitions>,
479 pub outcome: Result<Run, RunError>,
481}
482
483pub struct Watched<'a> {
487 pub definitions: &'a std::sync::Arc<crate::observe::Definitions>,
488 pub program: &'a crate::uses::ResolvedProgram,
489 pub lowered: &'a [tatara_lisp::Spanned],
490 pub erased: &'a [tatara_lisp::Spanned],
491}
492
493pub fn run_observed(
504 entry: Entry<'_>,
505 inputs: Inputs,
506 loader: &dyn crate::uses::Loader,
507 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
508 watch: impl FnOnce(&Watched<'_>) -> std::sync::Arc<dyn crate::observe::CallObserver>,
509) -> Result<Observed, RunError> {
510 run_observed_framed(entry, inputs, loader, surface, None, watch)
511}
512
513pub fn run_observed_framed(
520 entry: Entry<'_>,
521 inputs: Inputs,
522 loader: &dyn crate::uses::Loader,
523 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
524 hold: Option<&crate::Hold>,
525 watch: impl FnOnce(&Watched<'_>) -> std::sync::Arc<dyn crate::observe::CallObserver>,
526) -> Result<Observed, RunError> {
527 run_observed_accounted(
528 entry,
529 inputs,
530 loader,
531 surface,
532 hold,
533 &crate::Account::default(),
534 watch,
535 )
536}
537
538pub fn run_observed_accounted(
545 entry: Entry<'_>,
546 inputs: Inputs,
547 loader: &dyn crate::uses::Loader,
548 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
549 hold: Option<&crate::Hold>,
550 account: &crate::Account,
551 watch: impl FnOnce(&Watched<'_>) -> std::sync::Arc<dyn crate::observe::CallObserver>,
552) -> Result<Observed, RunError> {
553 let (mut interp, mut prepared) = prepared_on(entry, inputs, loader, surface)?;
554 hold_to_frame(&mut interp, &mut prepared, hold, account)?;
555 let definitions = std::sync::Arc::new(crate::observe::Definitions::of(
556 &prepared.program,
557 &prepared.erased,
558 ));
559 interp.set_call_observer(Some(watch(&Watched {
560 definitions: &definitions,
561 program: &prepared.program,
562 lowered: &prepared.lowered,
563 erased: &prepared.erased,
564 })));
565 let outcome = crate::timings::stage(crate::timings::Stage::Eval, || {
566 eval_prepared(&mut interp, &prepared, &mut ())
567 })
568 .and_then(|value| {
569 Ok(crate::timings::stage(crate::timings::Stage::Main, || {
570 call_main(&mut interp, &prepared, &mut ())
571 })?
572 .unwrap_or(value))
573 })
574 .map(|value| Run {
575 value,
576 visited: prepared.visited,
577 typed_decls: prepared.typed_decls,
578 seams: prepared.seams,
579 });
580 interp.set_call_observer(None);
581 Ok(Observed {
582 definitions,
583 outcome,
584 })
585}
586
587pub(crate) struct Prepared {
592 program: crate::uses::ResolvedProgram,
593 lowered: std::sync::Arc<Vec<tatara_lisp::Spanned>>,
594 erased: Vec<tatara_lisp::Spanned>,
595 visited: usize,
596 typed_decls: usize,
597 seams: usize,
598}
599
600impl Prepared {
601 pub(crate) fn erased(&self) -> &[tatara_lisp::Spanned] {
603 &self.erased
604 }
605
606 pub(crate) fn entry_namespace(&self) -> Namespace {
608 let first = (0..self.program.forms().len())
609 .find(|i| self.program.owner_of(*i) == Some(crate::uses::ResolvedProgram::ENTRY));
610 match first {
611 Some(i) => namespace_of(&self.program, i),
612 None => match self
613 .program
614 .file(crate::uses::ResolvedProgram::ENTRY)
615 .and_then(|f| f.package.clone())
616 {
617 Some(p) => Namespace::Bidama(p),
618 None => Namespace::File(String::new()),
619 },
620 }
621 }
622}
623
624pub(crate) fn prepare(
630 entry: Entry<'_>,
631 loader: &dyn crate::uses::Loader,
632 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
633 builtins: &NameTable,
634) -> Result<Prepared, RunError> {
635 let mut program = parse_and_resolve(entry, loader, surface)?;
636
637 program.retain(|f| !crate::uses::is_test_form(f));
650
651 let (outcome, names) = crate::timings::stage(crate::timings::Stage::Check, || {
666 check_stage(&program, builtins, false)
667 });
668 if !outcome.ok() {
669 return Err(RunError::Types(
670 outcome
671 .errors()
672 .map(|d| render(&program, d))
675 .collect(),
676 ));
677 }
678
679 let (lowered, erased) = crate::timings::stage(crate::timings::Stage::Erase, || {
693 let lowered = lower(&program, &names);
694 let erased = erase_types(&lowered);
695 (lowered, erased)
696 });
697
698 Ok(Prepared {
699 program,
700 lowered: std::sync::Arc::new(lowered),
701 erased,
702 visited: outcome.stats.visited,
703 typed_decls: outcome.stats.typed_decls,
704 seams: outcome.seams.len(),
705 })
706}
707
708pub(crate) fn parse_and_resolve(
710 entry: Entry<'_>,
711 loader: &dyn crate::uses::Loader,
712 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
713) -> Result<crate::uses::ResolvedProgram, RunError> {
714 let forms = crate::timings::stage(crate::timings::Stage::Parse, || {
715 let mut forms = match surface {
716 Some(pack) => blue_lang_syntax::parse_program_tree_in(entry.text, pack)
717 .map_err(|e| RunError::Parse(e.to_string()))?,
718 None => loader
719 .parse(entry.text)
720 .map_err(|e| RunError::Parse(e.to_string()))?,
721 };
722 if surface.is_none() {
725 forms.extend(
726 blue_lang_syntax::doc::doctests(entry.text)
727 .map_err(|e| RunError::Parse(e.to_string()))?,
728 );
729 }
730 Ok::<_, RunError>(forms)
731 })?;
732
733 crate::timings::stage(crate::timings::Stage::Resolve, || {
744 crate::uses::resolve_uses(forms, entry, loader).map_err(RunError::Import)
745 })
746}
747
748pub(crate) fn check_stage(
761 program: &crate::uses::ResolvedProgram,
762 builtins: &NameTable,
763 with_tests: bool,
764) -> (blue_lang_check::Outcome, NameTable) {
765 use crate::uses::ResolvedProgram;
766 let forms = program.forms();
767 let is_entry = |i: usize| program.owner_of(i) == Some(ResolvedProgram::ENTRY);
768
769 let mut outcome = blue_lang_check::check_program_in(forms, &|a, b| {
771 program.owner_of(a) == program.owner_of(b)
772 });
773 outcome
774 .diagnostics
775 .extend(data_law_diagnostics(program, &is_entry));
776
777 let table = program_names(program, builtins);
780 let whole = |i: usize| with_tests && is_entry(i);
781 let (names, resolved) = blue_lang_check::names::check_names_whole(
782 forms,
783 &table,
784 &|i| namespace_of(program, i),
785 &is_entry,
786 &whole,
787 );
788 let stored = program
789 .file(ResolvedProgram::ENTRY)
790 .is_some_and(|f| f.path.is_some());
791 outcome.diagnostics.extend(names);
792 outcome.stats.names_resolved = resolved;
793 if stored {
794 let script = program
795 .file(ResolvedProgram::ENTRY)
796 .is_some_and(|f| f.package.is_none());
797 let found = blue_lang_check::form_rules::check(forms, &is_entry, script);
798 let silenced: Vec<tatara_lisp::Span> = found
799 .iter()
800 .filter(|d| d.code == blue_lang_check::Code::B0038)
801 .map(|d| d.span)
802 .collect();
803 outcome
804 .diagnostics
805 .retain(|d| !(d.code == blue_lang_check::Code::B0002 && silenced.contains(&d.span)));
806 outcome.diagnostics.extend(found);
807 }
808 outcome
809 .diagnostics
810 .extend(blue_lang_check::contracts::check(
811 forms,
812 &table,
813 &|i| namespace_of(program, i),
814 &is_entry,
815 &|i| {
816 program
817 .owner_of(i)
818 .and_then(|f| program.file(f))
819 .map(|f| f.text.as_str())
820 },
821 crate::docs::signatures(),
822 ));
823
824 outcome.diagnostics.extend(doc_diagnostics(program));
825
826 let mut waivers = Vec::new();
829 for file in program.files() {
830 let spans: Vec<(usize, tatara_lisp::Span)> = forms
831 .iter()
832 .enumerate()
833 .filter(|(i, _)| program.owner_of(*i) == Some(file.id))
834 .map(|(i, f)| (i, f.span))
835 .collect();
836 let (found, malformed) = blue_lang_check::waiver::collect(&file.text, &spans);
837 waivers.extend(found);
838 outcome.diagnostics.extend(malformed);
839 }
840 outcome.waived = blue_lang_check::waiver::apply(&mut outcome.diagnostics, waivers, &is_entry);
841 if !stored {
842 outcome.diagnostics.retain(|d| {
843 !matches!(
844 d.code,
845 blue_lang_check::Code::B0023
846 | blue_lang_check::Code::B0036
847 | blue_lang_check::Code::B0037
848 | blue_lang_check::Code::B0067
849 | blue_lang_check::Code::B0061
850 | blue_lang_check::Code::B0062
851 | blue_lang_check::Code::B0063
852 )
853 });
854 }
855 let judge = blue_lang_check::evolution::FormJudge::from_env().unwrap_or_default();
856 outcome
857 .diagnostics
858 .retain(|d| judge.reports(d.code, is_entry(d.top_level)));
859 let (enforced, census): (Vec<_>, Vec<_>) = std::mem::take(&mut outcome.diagnostics)
862 .into_iter()
863 .partition(|d| d.code.rule().active());
864 outcome.diagnostics = enforced;
865 outcome.census = census;
866 outcome
867 .diagnostics
868 .sort_by_key(|d| (d.top_level, d.span.start, d.code));
869 (outcome, table)
870}
871
872fn doc_diagnostics(program: &crate::uses::ResolvedProgram) -> Vec<blue_lang_check::Diagnostic> {
876 use crate::uses::ResolvedProgram;
877 let Some(file) = program.file(ResolvedProgram::ENTRY) else {
878 return Vec::new();
879 };
880 let Some(package) = file.package.as_deref() else {
881 return Vec::new();
882 };
883 let forms: Vec<(usize, &tatara_lisp::Spanned)> = program
884 .forms()
885 .iter()
886 .enumerate()
887 .filter(|(i, f)| {
888 program.owner_of(*i) == Some(ResolvedProgram::ENTRY)
889 && !blue_lang_syntax::doc::is_doctest(f)
890 })
891 .collect();
892 let graph = file.path.as_deref().map(crate::doc_sources::graph_meanings);
893 let meaning = |name: &str| {
894 graph
895 .as_ref()
896 .and_then(|g| g.get(&(package.to_string(), name.to_string())).cloned())
897 };
898 let names_line = |name: &str| crate::docs::doc_of(name).map(|d| d.doc.to_string());
899 blue_lang_check::doc_rules::check_file(
900 &file.text,
901 &forms,
902 package,
903 &blue_lang_check::doc_rules::Sources {
904 meaning: &meaning,
905 names_line: &names_line,
906 },
907 )
908}
909
910fn data_law_diagnostics(
911 program: &crate::uses::ResolvedProgram,
912 is_entry: &dyn Fn(usize) -> bool,
913) -> Vec<blue_lang_check::Diagnostic> {
914 use blue_lang_check::data_laws;
915 let forms = program.forms();
916 let fragments = data_laws::Fragments::collect(forms);
917 let frontend = program.frontend();
918 let mut out = Vec::new();
919 let schema = match frontend.and_then(|f| f.table.as_ref()) {
920 Some((label, text)) => match data_laws::Schema::read(text) {
921 Ok(s) => Some(s),
922 Err(why) => {
923 if let Some((i, first)) = forms.iter().enumerate().find(|(i, _)| is_entry(*i)) {
924 out.push(
925 blue_lang_check::Diagnostic::new(
926 blue_lang_check::Code::B0050,
927 format!("the generated schema table {label} does not read: {why}"),
928 first.span,
929 )
930 .at_top_level(i)
931 .with_help("regenerate it: `nix run .#regen`"),
932 );
933 }
934 None
935 }
936 },
937 None => None,
938 };
939 if fragments.is_empty() && schema.is_none() && !frontend.is_some_and(|f| f.view) {
940 return out;
941 }
942 out.extend(data_laws::check(
943 forms,
944 &data_laws::Context {
945 fragments: &fragments,
946 schema: schema.as_ref(),
947 is_entry,
948 view: frontend.is_some_and(|f| f.view),
949 },
950 ));
951 out
952}
953
954#[must_use]
967pub fn program_names(program: &crate::uses::ResolvedProgram, builtins: &NameTable) -> NameTable {
968 use blue_lang_check::Binding;
969 let mut table = NameTable::new();
970 table.add_program(program.forms(), |i| namespace_of(program, i));
971 let builtin_macro = |head: &str| {
972 builtins
973 .scopes()
974 .iter()
975 .any(|s| s.namespace == Namespace::Macro && s.get(head).is_some())
976 };
977 let mut expander: Option<tatara_lisp_eval::Interpreter<()>> = None;
978 for (i, form) in program.forms().iter().enumerate() {
979 let Some(head) = form
980 .as_list()
981 .and_then(|items| items.first())
982 .and_then(tatara_lisp::Spanned::as_symbol)
983 else {
984 continue;
985 };
986 if !builtin_macro(head) {
987 continue;
988 }
989 let interp = expander.get_or_insert_with(crate::interpreter_hostless);
990 let Ok(expanded) = interp.fully_expand(form, &mut ()) else {
993 continue;
994 };
995 let nullary = blue_lang_syntax::scope::nullary_definition(&expanded);
996 for (name, span, kind) in blue_lang_check::names::definitions_of(&expanded) {
997 table.define(
998 namespace_of(program, i),
999 Binding {
1000 nullary: nullary == Some(name.as_str()),
1001 name,
1002 kind,
1003 span: Some(span),
1004 top_level: Some(i),
1005 },
1006 );
1007 }
1008 }
1009 for scope in builtins.scopes() {
1010 table.push(scope.clone());
1011 }
1012 table.spell_bare_like(builtins);
1013 let form_file: Vec<usize> = (0..program.forms().len())
1014 .map(|i| program.owner_of(i).map_or(usize::MAX, |f| f.index()))
1015 .collect();
1016 let mut imports: std::collections::BTreeMap<usize, blue_lang_check::names::FileImports> =
1017 std::collections::BTreeMap::new();
1018 for (file, import) in program.imports() {
1019 let entry = imports.entry(file.index()).or_default();
1020 entry.add(&import.package, import.names.iter().map(|(n, _)| n.clone()));
1021 let at = import.span.start;
1022 entry.use_at = Some(entry.use_at.map_or(at, |a| a.min(at)));
1023 }
1024 let facade_uses =
1029 |pkg: &str,
1030 imports: &std::collections::BTreeMap<usize, blue_lang_check::names::FileImports>| {
1031 let defines = table.bidama(pkg).is_some_and(|s| !s.is_empty());
1032 if defines {
1033 return std::collections::BTreeSet::new();
1034 }
1035 program
1036 .files()
1037 .iter()
1038 .filter(|f| f.package.as_deref() == Some(pkg))
1039 .filter_map(|f| imports.get(&f.id.index()))
1040 .flat_map(|i| i.uses.iter().cloned())
1041 .collect::<std::collections::BTreeSet<String>>()
1042 };
1043 loop {
1044 let mut grew = false;
1045 let files: Vec<usize> = imports.keys().copied().collect();
1046 for f in files {
1047 let used: Vec<String> = imports[&f].uses.iter().cloned().collect();
1048 for p in used {
1049 for member in facade_uses(&p, &imports) {
1050 if let Some(i) = imports.get_mut(&f) {
1051 if i.uses.insert(member.clone()) {
1052 i.via.insert(member, p.clone());
1053 grew = true;
1054 }
1055 }
1056 }
1057 }
1058 }
1059 if !grew {
1060 break;
1061 }
1062 }
1063 let mut pkg_uses: std::collections::BTreeMap<String, std::collections::BTreeSet<String>> =
1064 std::collections::BTreeMap::new();
1065 for (file, import) in program.imports() {
1066 if let Some(p) = program.file(*file).and_then(|f| f.package.clone()) {
1067 pkg_uses
1068 .entry(p)
1069 .or_default()
1070 .insert(import.package.clone());
1071 }
1072 }
1073 table.attach_files(form_file, imports);
1074 table.attach_package_uses(pkg_uses);
1075 table.attach_needs(program.needs().clone());
1076 let canonical = |p: &std::path::Path| {
1080 std::fs::canonicalize(p)
1081 .unwrap_or_else(|_| p.to_path_buf())
1082 .display()
1083 .to_string()
1084 };
1085 let mut file_paths = std::collections::BTreeMap::new();
1086 let mut pkg_roots = std::collections::BTreeMap::new();
1087 for f in program.files() {
1088 if let Some(path) = &f.path {
1089 file_paths.insert(f.id.index(), canonical(path));
1090 if let (Some(pkg), Some(root)) =
1091 (&f.package, path.parent().and_then(std::path::Path::parent))
1092 {
1093 pkg_roots
1094 .entry(pkg.clone())
1095 .or_insert_with(|| canonical(root));
1096 }
1097 }
1098 }
1099 table.attach_paths(file_paths, pkg_roots);
1100 let versions = program.versions().clone();
1101 table.apply_legacy(program.forms(), &|i| namespace_of(program, i), &|p| {
1102 versions.get(p).cloned()
1103 });
1104 table
1105}
1106
1107#[must_use]
1119pub fn lower(
1120 program: &crate::uses::ResolvedProgram,
1121 names: &NameTable,
1122) -> Vec<tatara_lisp::Spanned> {
1123 lower_with(program, names, true)
1124}
1125
1126#[must_use]
1127pub fn lower_with(
1128 program: &crate::uses::ResolvedProgram,
1129 names: &NameTable,
1130 simplify_chains: bool,
1131) -> Vec<tatara_lisp::Spanned> {
1132 let mut resolved = blue_lang_check::names::resolve_program(program.forms(), names, &|i| {
1133 namespace_of(program, i)
1134 });
1135 let simplified = if simplify_chains {
1136 chain_simplified(program.forms(), &resolved)
1137 } else {
1138 None
1139 };
1140 let forms: &[tatara_lisp::Spanned] = simplified.as_deref().unwrap_or(program.forms());
1141 if simplified.is_some() {
1142 resolved =
1143 blue_lang_check::names::resolve_program(forms, names, &|i| namespace_of(program, i));
1144 }
1145 resolved
1146 .resolved_tree(forms, blue_lang_check::names::Rule::Namespaced)
1147 .into_iter()
1148 .zip(forms)
1149 .enumerate()
1150 .map(|(i, (resolved, written))| {
1151 if crate::uses::use_target(written).is_some()
1152 || blue_lang_syntax::scope::legacy_target(written).is_some()
1153 {
1154 tatara_lisp::Spanned::new(written.span, tatara_lisp::SpannedForm::Nil)
1155 } else {
1156 let file = program.owner_of(i).and_then(|id| program.file(id));
1157 match file {
1158 Some(f) => locate_raises(&resolved, &raise_site(f)),
1159 None => resolved,
1160 }
1161 }
1162 })
1163 .collect()
1164}
1165
1166fn chain_simplified(
1167 forms: &[tatara_lisp::Spanned],
1168 resolved: &blue_lang_check::names::Resolved,
1169) -> Option<Vec<tatara_lisp::Spanned>> {
1170 let respell: std::collections::HashMap<usize, blue_lang_check::respell::Respelling> = resolved
1171 .respellings
1172 .iter()
1173 .filter(|(_, r)| matches!(r.respell, blue_lang_check::respell::Respell::Chain(_)))
1174 .cloned()
1175 .collect();
1176 if respell.is_empty() {
1177 return None;
1178 }
1179 let bare = std::collections::HashSet::new();
1180 let canon = Canon {
1181 bare: &bare,
1182 respell: &respell,
1183 };
1184 Some(forms.iter().map(|f| canon.form(f)).collect())
1185}
1186
1187fn raise_site(file: &crate::uses::SourceFile) -> impl Fn(tatara_lisp::Span) -> String + '_ {
1188 move |span| {
1189 let (line, _) =
1190 blue_lang_syntax::Span::line_col(&file.text, span.start.min(file.text.len()));
1191 let shown = file
1192 .path
1193 .as_ref()
1194 .map_or_else(|| "<source>".to_string(), |p| p.display().to_string());
1195 format!("{shown}:{line}")
1196 }
1197}
1198
1199fn locate_raises(
1200 form: &tatara_lisp::Spanned,
1201 site: &dyn Fn(tatara_lisp::Span) -> String,
1202) -> tatara_lisp::Spanned {
1203 use tatara_lisp::{Atom, Spanned, SpannedForm};
1204 let inner = match &form.form {
1205 SpannedForm::List(items) => {
1206 let mut out: Vec<Spanned> = items.iter().map(|i| locate_raises(i, site)).collect();
1207 let raising = items.first().and_then(Spanned::as_symbol) == Some("raise")
1208 && (3..=4).contains(&items.len());
1209 if raising {
1210 out[0] = Spanned::new(
1211 items[0].span,
1212 SpannedForm::Atom(Atom::Symbol(crate::errors::RAISE_AT.to_string())),
1213 );
1214 out.insert(
1215 1,
1216 Spanned::new(items[0].span, SpannedForm::Atom(Atom::Str(site(form.span)))),
1217 );
1218 }
1219 SpannedForm::List(out)
1220 }
1221 SpannedForm::Quote(_) | SpannedForm::Quasiquote(_) => return form.clone(),
1222 other => other.clone(),
1223 };
1224 Spanned::new(form.span, inner)
1225}
1226
1227#[must_use]
1229pub fn namespace_of(program: &crate::uses::ResolvedProgram, i: usize) -> Namespace {
1230 let file = program.owner_of(i).and_then(|id| program.file(id));
1231 match file {
1232 Some(f) => match &f.package {
1233 Some(p) => Namespace::Bidama(p.clone()),
1234 None => Namespace::File(
1235 f.path
1236 .as_ref()
1237 .map_or_else(|| "<anonymous>".to_string(), |p| p.display().to_string()),
1238 ),
1239 },
1240 None => Namespace::File("<unknown file>".to_string()),
1241 }
1242}
1243
1244pub const HARNESS_NAMES: &[&str] = &[
1258 blue_lang_syntax::LOWERED_ASSERT,
1259 blue_lang_syntax::LOWERED_ASSERT_PANICS,
1260 blue_lang_syntax::LOWERED_TEST_META,
1261];
1262
1263#[must_use]
1273pub fn builtin_names<H: 'static>(interp: &tatara_lisp_eval::Interpreter<H>) -> NameTable {
1274 builtin_names_with(interp, &[])
1275}
1276
1277#[must_use]
1278pub fn builtin_names_with<H: 'static>(
1279 interp: &tatara_lisp_eval::Interpreter<H>,
1280 host: &[crate::host_surface::HostName],
1281) -> NameTable {
1282 use blue_lang_check::{Binding, Scope, ScopeKind};
1283 use tatara_lisp_eval::HeadBinding;
1284 let mut special = Scope::new(Namespace::SpecialForm);
1285 let mut macros = Scope::new(Namespace::Macro);
1286 let mut values = Scope::new(Namespace::Builtin);
1287 let mut names: Vec<std::sync::Arc<str>> = interp.reserved_head_names().into_iter().collect();
1292 names.extend(
1293 crate::inputs::INPUT_PRIMITIVES
1294 .iter()
1295 .filter(|n| interp.resolve_head(n).is_some())
1296 .map(|n| std::sync::Arc::from(*n)),
1297 );
1298 for name in names {
1299 let (scope, kind) = match interp.resolve_head(&name) {
1300 Some(HeadBinding::SpecialForm) => (&mut special, ScopeKind::SpecialForm),
1301 Some(HeadBinding::Macro) => (&mut macros, ScopeKind::Macro),
1302 _ => (&mut values, ScopeKind::Value),
1303 };
1304 let nullary = kind == ScopeKind::Value
1305 && interp.lookup_global(&name).is_some_and(|v| {
1306 nullary_value(&v)
1307 || (crate::docs::doc_of(&name).is_some_and(|d| d.signature == &*name)
1308 && matches!(
1309 v,
1310 tatara_lisp_eval::Value::NativeFn(_)
1311 | tatara_lisp_eval::Value::Closure(_)
1312 ))
1313 });
1314 scope.bind(Binding {
1315 name: name.to_string(),
1316 kind,
1317 span: None,
1318 top_level: None,
1319 nullary,
1320 });
1321 }
1322 for h in host {
1323 values.bind(Binding {
1324 name: h.name.clone(),
1325 kind: ScopeKind::Value,
1326 span: None,
1327 top_level: None,
1328 nullary: h.nullary(),
1329 });
1330 }
1331 let mut table = NameTable::new();
1332 let mut harness = Scope::new(Namespace::Harness);
1333 for n in HARNESS_NAMES {
1334 harness.bind(Binding {
1335 name: (*n).to_string(),
1336 kind: ScopeKind::Value,
1337 span: None,
1338 top_level: None,
1339 nullary: false,
1340 });
1341 }
1342 table.push(harness);
1343 table.push(special);
1344 table.push(macros);
1345 table.push(values);
1346 table.spell_bare_only(
1347 crate::docs::NAMES
1348 .iter()
1349 .filter(|d| d.signature == d.name)
1350 .map(|d| d.name.to_string())
1351 .chain(host.iter().filter(|h| h.nullary()).map(|h| h.name.clone()))
1352 .collect(),
1353 );
1354 table
1355}
1356
1357#[must_use]
1358pub fn nullary_value(value: &tatara_lisp_eval::Value) -> bool {
1359 match value {
1360 tatara_lisp_eval::Value::NativeFn(f) => {
1361 matches!(f.arity, tatara_lisp_eval::ffi::Arity::Exact(0))
1362 }
1363 tatara_lisp_eval::Value::Closure(c) => c.params.is_empty() && c.rest.is_none(),
1364 _ => false,
1365 }
1366}
1367
1368#[must_use]
1372pub fn render(program: &crate::uses::ResolvedProgram, d: &blue_lang_check::Diagnostic) -> String {
1373 let head = d.to_string();
1374 let mut out = program.locate(d.top_level, d.span, &head).to_string();
1375 if let Some(help) = &d.help {
1376 out.push_str("\n help: ");
1377 out.push_str(help);
1378 }
1379 let file = program
1380 .owner_of(d.top_level)
1381 .and_then(|id| program.file(id))
1382 .and_then(|f| f.path.as_ref());
1383 if let Some(cmd) =
1384 file.and_then(|p| blue_lang_check::evolution::fix_command(d, &p.display().to_string()))
1385 {
1386 out.push_str("\n fix: ");
1387 out.push_str(&cmd);
1388 }
1389 out
1390}
1391
1392#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1394pub enum Checking {
1395 Program,
1397 WithTests,
1400}
1401
1402pub struct Checked {
1404 pub program: crate::uses::ResolvedProgram,
1405 pub outcome: blue_lang_check::Outcome,
1406 pub names: NameTable,
1409}
1410
1411impl Checked {
1412 #[must_use]
1415 pub fn erased(&self) -> Vec<tatara_lisp::Spanned> {
1416 erase_types(&lower(&self.program, &self.names))
1417 }
1418
1419 #[must_use]
1422 pub fn evaluable(&self) -> Vec<tatara_lisp::Sexp> {
1423 lower(&self.program, &self.names)
1424 .iter()
1425 .map(tatara_lisp::Spanned::to_sexp)
1426 .collect()
1427 }
1428
1429 #[must_use]
1433 pub fn resolve(&self) -> blue_lang_check::names::Resolved {
1434 blue_lang_check::names::resolve_program(self.program.forms(), &self.names, &|i| {
1435 namespace_of(&self.program, i)
1436 })
1437 }
1438}
1439
1440impl Checked {
1441 #[must_use]
1442 pub fn call_explicit_forms(&self) -> Vec<tatara_lisp::Spanned> {
1443 self.resolve().call_explicit_tree(self.program.forms())
1444 }
1445
1446 #[must_use]
1447 pub fn canonical_entry(&self, src: &str) -> Option<Vec<tatara_lisp::Spanned>> {
1448 use crate::uses::ResolvedProgram;
1449 let program = &self.program;
1450 let entry: Vec<&tatara_lisp::Spanned> = program
1451 .forms()
1452 .iter()
1453 .enumerate()
1454 .filter(|(i, _)| program.owner_of(*i) == Some(ResolvedProgram::ENTRY))
1455 .map(|(_, f)| f)
1456 .filter(|f| !blue_lang_syntax::doc::is_doctest(f))
1457 .collect();
1458 let written = blue_lang_syntax::parse_program_tree(src).ok()?;
1459 let same = written.len() == entry.len()
1460 && written
1461 .iter()
1462 .zip(&entry)
1463 .all(|(w, e)| w.to_sexp() == e.to_sexp() && w.span == e.span);
1464 if !same {
1465 return None;
1466 }
1467 let resolved = self.resolve();
1468 let bare: std::collections::HashSet<usize> = resolved
1469 .parenthesized_nullary_calls()
1470 .into_iter()
1471 .filter(|r| program.owner_of(r.top_level) == Some(ResolvedProgram::ENTRY))
1472 .map(blue_lang_check::names::Reference::node_address)
1473 .collect();
1474 let waived: Vec<tatara_lisp::Span> = self
1475 .outcome
1476 .waived
1477 .iter()
1478 .map(|w| w.diagnostic.span)
1479 .collect();
1480 let respell: std::collections::HashMap<usize, blue_lang_check::respell::Respelling> =
1481 resolved
1482 .respellings
1483 .iter()
1484 .filter(|(_, r)| !waived.contains(&r.span))
1485 .cloned()
1486 .collect();
1487 let canon = Canon {
1488 bare: &bare,
1489 respell: &respell,
1490 };
1491 let forms: Vec<tatara_lisp::Spanned> = entry.into_iter().map(|f| canon.form(f)).collect();
1492 let script = program
1493 .file(ResolvedProgram::ENTRY)
1494 .is_some_and(|f| f.package.is_none());
1495 let forms = if script {
1496 blue_lang_check::form_rules::without_prefix(&forms).unwrap_or(forms)
1497 } else {
1498 forms
1499 };
1500 let forms = blue_lang_check::form_rules::without_main_guard(&forms).unwrap_or(forms);
1501 Some(blue_lang_check::form_rules::eta_reduced(&forms).unwrap_or(forms))
1502 }
1503}
1504
1505struct Canon<'a> {
1506 bare: &'a std::collections::HashSet<usize>,
1507 respell: &'a std::collections::HashMap<usize, blue_lang_check::respell::Respelling>,
1508}
1509
1510impl Canon<'_> {
1511 fn form(&self, form: &tatara_lisp::Spanned) -> tatara_lisp::Spanned {
1512 use tatara_lisp::{Spanned, SpannedForm};
1513 let inner = match &form.form {
1514 SpannedForm::List(items) => match items.as_slice() {
1515 [head] if self.bare.contains(&(std::ptr::from_ref(head) as usize)) => {
1516 return Spanned::new(form.span, head.form.clone());
1517 }
1518 [head, ..]
1519 if self
1520 .respell
1521 .contains_key(&(std::ptr::from_ref(head) as usize)) =>
1522 {
1523 let r = &self.respell[&(std::ptr::from_ref(head) as usize)];
1524 let walked: Vec<Spanned> = items.iter().map(|i| self.form(i)).collect();
1525 return blue_lang_check::respell::apply(form, &walked, r);
1526 }
1527 _ => SpannedForm::List(items.iter().map(|i| self.form(i)).collect()),
1528 },
1529 SpannedForm::Quote(i) => SpannedForm::Quote(Box::new(self.form(i))),
1530 SpannedForm::Quasiquote(i) => SpannedForm::Quasiquote(Box::new(self.form(i))),
1531 SpannedForm::Unquote(i) => SpannedForm::Unquote(Box::new(self.form(i))),
1532 SpannedForm::UnquoteSplice(i) => SpannedForm::UnquoteSplice(Box::new(self.form(i))),
1533 other => other.clone(),
1534 };
1535 Spanned::new(form.span, inner)
1536 }
1537}
1538
1539pub fn check_entry(
1555 entry: Entry<'_>,
1556 loader: &dyn crate::uses::Loader,
1557 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
1558 checking: Checking,
1559) -> Result<Checked, RunError> {
1560 let mut program = parse_and_resolve(entry, loader, surface)?;
1561 if checking == Checking::Program {
1562 program.retain(|f| !crate::uses::is_test_form(f));
1563 }
1564 let host = match entry.path {
1565 Some(path) => crate::host_surface::near(path)
1566 .map_err(|e| RunError::Import(e.to_string()))?
1567 .map(|s| s.functions)
1568 .unwrap_or_default(),
1569 None => Vec::new(),
1570 };
1571 let interp = runtime(Inputs::new());
1572 let builtins = crate::timings::stage(crate::timings::Stage::Names, || {
1573 builtin_names_with(&interp, &host)
1574 });
1575 let (outcome, names) = crate::timings::stage(crate::timings::Stage::Check, || {
1576 check_stage(&program, &builtins, checking == Checking::WithTests)
1577 });
1578 Ok(Checked {
1579 program,
1580 outcome,
1581 names,
1582 })
1583}
1584
1585#[must_use]
1586pub fn spelling_code(spelling: blue_lang_syntax::Spelling) -> blue_lang_check::Code {
1587 use blue_lang_check::Code;
1588 use blue_lang_syntax::Spelling;
1589 match spelling {
1590 Spelling::LogicalSymbol => Code::B0025,
1591 Spelling::Unless => Code::B0026,
1592 Spelling::ElseIf => Code::B0027,
1593 Spelling::CallForm => Code::B0028,
1594 Spelling::KeywordRocket => Code::B0029,
1595 Spelling::ParameterList => Code::B0030,
1596 Spelling::OperatorSpelling => Code::B0031,
1597 Spelling::TrailingComma => Code::B0032,
1598 Spelling::NegatedComparison => Code::B0034,
1599 Spelling::ElseNil => Code::B0035,
1600 Spelling::TryCatch => Code::B0037,
1601 Spelling::Apply => Code::B0042,
1602 Spelling::MapPun => Code::B0046,
1603 Spelling::NilTest => Code::B0047,
1604 Spelling::NewlineEscape => Code::B0065,
1605 Spelling::Index => Code::B0066,
1606 Spelling::TestDeclaration => Code::B0068,
1607 }
1608}
1609
1610#[must_use]
1611pub fn form_diagnostics(text: &str) -> Vec<blue_lang_check::Diagnostic> {
1612 match blue_lang_fmt::format_source_lossless(text) {
1613 Ok(formatted) => form_diagnostics_against(text, &formatted),
1614 Err(_) => Vec::new(),
1615 }
1616}
1617
1618#[must_use]
1619pub fn form_diagnostics_against(text: &str, formatted: &str) -> Vec<blue_lang_check::Diagnostic> {
1620 use blue_lang_check::{Code, Diagnostic};
1621 if formatted.trim_end() == text.trim_end() {
1622 return Vec::new();
1623 }
1624 let help = "`blue fmt --write FILE` writes it; no compile door writes a file (`BLUE_LAYOUT=rewrite` restores formatting in place)";
1625 let mut out: Vec<Diagnostic> = blue_lang_syntax::spelling_notes(text)
1626 .unwrap_or_default()
1627 .into_iter()
1628 .map(|n| {
1629 let message = if n.canonical.is_empty() {
1630 format!("`{}` is not written", n.written)
1631 } else {
1632 format!("`{}` is written `{}`", n.written, n.canonical)
1633 };
1634 Diagnostic::new(spelling_code(n.spelling), message, n.span)
1635 .at_top_level(0)
1636 .with_help(help)
1637 })
1638 .collect();
1639 out.extend(
1640 literal_diagnostics(text)
1641 .into_iter()
1642 .map(|d| d.with_help(help)),
1643 );
1644 if out.is_empty() {
1645 let at = text
1646 .lines()
1647 .zip(formatted.lines())
1648 .position(|(a, b)| a != b)
1649 .unwrap_or_else(|| text.lines().count().min(formatted.lines().count()));
1650 let start: usize = text.lines().take(at).map(|l| l.len() + 1).sum();
1651 let start = start.min(text.len());
1652 let end = text[start..].find('\n').map_or(text.len(), |e| start + e);
1653 out.push(
1654 Diagnostic::new(
1655 Code::B0024,
1656 format!("line {} is not laid out as blue writes it", at + 1),
1657 tatara_lisp::Span::new(start, end),
1658 )
1659 .at_top_level(0)
1660 .with_help(help),
1661 );
1662 }
1663 out.sort_by_key(|d| (d.span.start, d.code));
1664 out
1665}
1666
1667fn literal_diagnostics(text: &str) -> Vec<blue_lang_check::Diagnostic> {
1668 use blue_lang_syntax::{Atom, TokenKind};
1669 let Ok(toks) = blue_lang_syntax::lex(text) else {
1670 return Vec::new();
1671 };
1672 toks.iter()
1673 .filter_map(|t| {
1674 let atom = match &t.kind {
1675 TokenKind::Int(v) => Atom::Int(*v),
1676 TokenKind::Float(v) => Atom::Float(*v),
1677 TokenKind::Str(v) => Atom::Str(v.clone()),
1678 _ => return None,
1679 };
1680 let written = text.get(t.span.start..t.span.end)?;
1681 let canonical = blue_lang_fmt::render_literal(&atom);
1682 (written != canonical).then(|| {
1683 blue_lang_check::Diagnostic::new(
1684 blue_lang_check::Code::B0033,
1685 format!("`{written}` is written `{canonical}`"),
1686 t.span,
1687 )
1688 .at_top_level(0)
1689 })
1690 })
1691 .collect()
1692}
1693
1694#[must_use]
1698pub fn syntax_diagnostic(text: &str) -> Option<blue_lang_check::Diagnostic> {
1699 let e = blue_lang_syntax::parse_program_tree(text).err()?;
1700 Some(
1701 blue_lang_check::Diagnostic::new(blue_lang_check::Code::B0006, e.message, e.span)
1702 .at_top_level(0),
1703 )
1704}
1705
1706#[must_use]
1710pub fn display_keys(message: &str) -> String {
1711 let mut out = String::with_capacity(message.len());
1712 let mut parts = message.split('`');
1713 if let Some(first) = parts.next() {
1714 out.push_str(first);
1715 }
1716 for (i, part) in parts.enumerate() {
1717 out.push('`');
1718 if i % 2 == 0 {
1720 out.push_str(&crate::observe::display_key(part));
1721 } else {
1722 out.push_str(part);
1723 }
1724 }
1725 out
1726}
1727
1728pub(crate) fn eval_prepared<H: 'static>(
1730 interp: &mut tatara_lisp_eval::Interpreter<H>,
1731 prepared: &Prepared,
1732 host: &mut H,
1733) -> Result<Value, RunError> {
1734 let Prepared {
1735 program, erased, ..
1736 } = prepared;
1737
1738 let mut value = Value::Nil;
1752 for (top_level, form) in erased.iter().enumerate() {
1753 value = interp.eval_top_form(form, host).map_err(|e| {
1754 RunError::Eval(Box::new(crate::observe::Fault::of(
1761 program,
1762 erased,
1763 top_level,
1764 &e,
1765 &interp.take_trace(),
1766 )))
1767 })?;
1768 }
1769 Ok(value)
1770}
1771
1772pub const MAIN: &str = "main";
1773
1774#[must_use]
1775pub fn entry_main(program: &crate::uses::ResolvedProgram) -> Option<(usize, tatara_lisp::Span)> {
1776 use crate::uses::ResolvedProgram;
1777 let stored = program
1778 .file(ResolvedProgram::ENTRY)
1779 .is_some_and(|f| f.path.is_some());
1780 if !stored {
1781 return None;
1782 }
1783 program.forms().iter().enumerate().find_map(|(i, f)| {
1784 if program.owner_of(i) != Some(ResolvedProgram::ENTRY) {
1785 return None;
1786 }
1787 let items = f.as_list()?;
1788 let sig = items.get(1)?.as_list()?;
1789 let named_main = matches!(
1790 items.first().and_then(tatara_lisp::Spanned::as_symbol),
1791 Some("define" | "define-typed")
1792 ) && sig.len() == 1
1793 && sig[0].as_symbol() == Some(MAIN);
1794 named_main.then_some((i, sig[0].span))
1795 })
1796}
1797
1798fn main_call(prepared: &Prepared) -> Option<(usize, tatara_lisp::Span, String)> {
1801 let (top_level, span) = entry_main(&prepared.program)?;
1802 let ns = namespace_of(&prepared.program, top_level);
1803 Some((top_level, span, blue_lang_check::names::key(&ns, MAIN)))
1804}
1805
1806fn main_key(prepared: &Prepared) -> Option<String> {
1807 main_call(prepared).map(|(_, _, key)| key)
1808}
1809
1810pub(crate) fn call_main<H: 'static>(
1811 interp: &mut tatara_lisp_eval::Interpreter<H>,
1812 prepared: &Prepared,
1813 host: &mut H,
1814) -> Result<Option<Value>, RunError> {
1815 let Some((top_level, span, key)) = main_call(prepared) else {
1816 return Ok(None);
1817 };
1818 let call = tatara_lisp::Spanned::new(
1819 span,
1820 tatara_lisp::SpannedForm::List(vec![tatara_lisp::Spanned::new(
1821 span,
1822 tatara_lisp::SpannedForm::Atom(tatara_lisp::Atom::Symbol(key)),
1823 )]),
1824 );
1825 interp.eval_top_form(&call, host).map(Some).map_err(|e| {
1826 RunError::Eval(Box::new(crate::observe::Fault::of(
1827 &prepared.program,
1828 &prepared.erased,
1829 top_level,
1830 &e,
1831 &interp.take_trace(),
1832 )))
1833 })
1834}
1835
1836#[cfg(test)]
1852mod position_tests {
1853 use super::*;
1854 use crate::uses::{Entry, Loader};
1855 use std::collections::BTreeMap;
1856 use std::path::Path;
1857
1858 struct MemLoader(BTreeMap<&'static str, &'static str>);
1859
1860 impl Loader for MemLoader {
1861 fn load(&self, name: &str) -> Result<Vec<(String, String)>, String> {
1862 self.0
1863 .get(name)
1864 .map(|s| vec![(format!("{name}.b"), (*s).to_owned())])
1865 .ok_or_else(|| format!("no bidama named \"{name}\""))
1866 }
1867 }
1868
1869 const BAKUHATSU: &str = "\
1877def tsukawanai_a()
1878 1
1879end
1880
1881def tsukawanai_b()
1882 2
1883end
1884
1885# waive B0001: the fixture needs a raise at runtime
1886def bakuhatsu()
1887 kore_wa_sonzai_shinai()
1888end
1889
1890bakuhatsu";
1891
1892 const KOTAE: &str = "def kotae()\n 42\nend";
1893
1894 const BAKUHATSU2: &str =
1897 "# waive B0001: a raise at runtime\ndef yobu()\n kore_wa_sonzai_shinai()\nend";
1898
1899 fn loader() -> MemLoader {
1900 MemLoader(BTreeMap::from([
1901 ("kotae", KOTAE),
1902 ("bakuhatsu", BAKUHATSU),
1903 ("bakuhatsu2", BAKUHATSU2),
1904 ]))
1905 }
1906
1907 fn run_named(path: &str, src: &str) -> RunError {
1908 run_in_surface(
1909 Entry {
1910 path: Some(Path::new(path)),
1911 text: src,
1912 },
1913 Inputs::new(),
1914 &loader(),
1915 None,
1916 )
1917 .expect_err("the fixture must raise")
1918 }
1919
1920 const TWO_IMPORTS: &str =
1965 "use(\"kotae\")\n# waive B0015: kotae loads first on purpose\nuse(\"bakuhatsu\")\n";
1966
1967 #[test]
1968 fn a_raise_inside_an_imported_package_names_that_package() {
1969 let err = run_named("entry.b", TWO_IMPORTS);
1970 assert_eq!(
1971 err.to_string(),
1972 "runtime error: bakuhatsu.b:11:3: unbound symbol `kore_wa_sonzai_shinai`\n \
1973 at bakuhatsu::bakuhatsu (bakuhatsu.b:11:3)\n \
1974 at <top level> (bakuhatsu.b:14:1)"
1975 );
1976 }
1977
1978 #[test]
1986 fn line_11_column_3_of_the_fixture_is_the_failing_call() {
1987 let line = BAKUHATSU.lines().nth(10).expect("line 11 exists");
1988 assert_eq!(line, " kore_wa_sonzai_shinai()");
1989 assert_eq!(
1990 &line[2..],
1991 "kore_wa_sonzai_shinai()",
1992 "column 3 (1-indexed) is where the call starts"
1993 );
1994 let offset = BAKUHATSU.find("kore_wa_sonzai_shinai").expect("in fixture");
1997 assert!(
1998 offset > KOTAE.len(),
1999 "kotae.b could answer for byte {offset}"
2000 );
2001 assert!(
2002 offset > TWO_IMPORTS.len(),
2003 "the entry file could answer for byte {offset}"
2004 );
2005 }
2006
2007 #[test]
2026 fn a_raise_in_the_entry_file_names_the_entry_file() {
2027 let err = run_named(
2028 "honmono.b",
2029 "def f()\n 1\nend\n\n# waive B0001: a raise at runtime\nnani_mo_nai()\n",
2030 );
2031 assert_eq!(
2032 err.to_string(),
2033 "runtime error: honmono.b:6:1: unbound symbol `nani_mo_nai`"
2034 );
2035 }
2036
2037 #[test]
2051 fn a_raise_inside_an_annotated_def_still_names_its_line() {
2052 let err = run_named(
2053 "chuu.b",
2054 "# waive B0001: a raise at runtime\ndef f(a: Int) -> Int\n mada_nai(a)\nend\n\nf(1)\n",
2055 );
2056 assert_eq!(
2057 err.to_string(),
2058 "runtime error: chuu.b:3:3: unbound symbol `mada_nai`\n \
2059 at f (chuu.b:3:3)\n \
2060 at <top level> (chuu.b:6:1)"
2061 );
2062 }
2063
2064 #[test]
2082 fn a_raise_in_a_callee_from_another_file_names_the_callees_file() {
2083 let err = run_named(
2088 "yobidashi.b",
2089 "use(\"bakuhatsu2\")\n# pad: 47's in range\nbakuhatsu2::yobu\n",
2090 );
2091 assert_eq!(
2092 err.to_string(),
2093 "runtime error: bakuhatsu2.b:3:3: unbound symbol `kore_wa_sonzai_shinai`\n \
2094 at bakuhatsu2::yobu (bakuhatsu2.b:3:3)\n \
2095 at <top level> (yobidashi.b:3:1)"
2096 );
2097 }
2098}
2099
2100#[cfg(test)]
2101mod tests {
2102 use super::*;
2103
2104 fn int(src: &str) -> i64 {
2105 match run(src).unwrap_or_else(|e| panic!("{src:?}: {e}")).value {
2106 Value::Int(v) => v,
2107 other => panic!("{src:?} produced {other:?}"),
2108 }
2109 }
2110
2111 #[test]
2114 fn annotating_buys_analysis_and_changes_nothing_else() {
2115 let plain = run("def add(a, b)\n a + b\nend\nadd(2, 3)").expect("plain");
2116 let typed = run("def add(a: Int, b: Int) -> Int\n a + b\nend\nadd(2, 3)").expect("typed");
2117
2118 assert!(matches!(plain.value, Value::Int(5)));
2119 assert!(
2120 matches!(typed.value, Value::Int(5)),
2121 "the annotated program must compute the same answer"
2122 );
2123 assert_eq!(plain.visited, 0, "no annotations means no analysis");
2124 assert!(
2125 typed.visited > 0,
2126 "an annotation must actually buy analysis, not just decorate"
2127 );
2128 assert_eq!(plain.typed_decls, 0);
2129 assert_eq!(typed.typed_decls, 1);
2130 }
2131
2132 #[test]
2136 fn a_type_error_is_reported_and_the_program_does_not_run() {
2137 let err = run("def add(a: Int, b: Int) -> Str\n a + b\nend\nadd(1, 2)")
2138 .expect_err("a declared Str return from an Int body must be rejected");
2139 assert!(
2140 matches!(err, RunError::Types(ref d) if !d.is_empty()),
2141 "expected type diagnostics, got {err}"
2142 );
2143 }
2144
2145 #[test]
2148 fn the_same_program_without_annotations_runs() {
2149 assert_eq!(int("def add(a, b)\n a + b\nend\nadd(1, 2)"), 3);
2150 }
2151
2152 #[test]
2153 fn a_parse_error_is_reported_as_one() {
2154 assert!(matches!(run("def (").unwrap_err(), RunError::Parse(_)));
2155 }
2156
2157 #[test]
2160 fn a_runtime_error_is_reported_as_one() {
2161 let err = run("throw(error(\"boom\"))").expect_err("a raise");
2162 assert!(matches!(err, RunError::Eval(_)), "got {err}");
2163 }
2164
2165 #[test]
2168 fn the_pipeline_reaches_both_runtime_layers() {
2169 assert_eq!(int("6 % 3"), 0);
2170 assert_eq!(int("7 % 3"), 1);
2171 assert_eq!(int("2 + 3 * 4"), 14);
2172 }
2173
2174 #[test]
2184 fn the_deleted_round_trip_agreed_with_the_direct_lowering() {
2185 let corpus = [
2186 "def add(a, b)\n a + b\nend\nadd(2, 3)",
2187 "def fact(n)\n if n < 2\n 1\n else\n n * fact(n - 1)\n end\nend\nfact(5)",
2188 "def f(a, b)\n c = a + b\n c * 2\nend\nf(1, 2)",
2189 "defmacro sq(e)\n quote\n unquote(e) * unquote(e)\n end\nend\nsq(2 + 3)",
2190 "\"a string with spaces, a ( and a )\"",
2191 "def g(a: Int) -> Int\n a + 1\nend\ng(1)",
2192 "6 % 3",
2193 "1.5 + 2.25",
2194 ];
2195 for src in corpus {
2196 let erased = crate::to_sexps(&erase_types(&parse_tree(src).expect("parse")));
2199
2200 let direct: Vec<Sexp> = crate::lower_to_spanned(&erased)
2201 .iter()
2202 .map(tatara_lisp::Spanned::to_sexp)
2203 .collect();
2204 assert_eq!(direct, erased, "the direct lowering must be the identity");
2205
2206 let text = erased
2207 .iter()
2208 .map(ToString::to_string)
2209 .collect::<Vec<_>>()
2210 .join("\n");
2211 let round_tripped: Vec<Sexp> = tatara_lisp::read_spanned(&text)
2212 .unwrap_or_else(|e| panic!("{src:?}: the old path could not read back: {e:?}"))
2213 .iter()
2214 .map(tatara_lisp::Spanned::to_sexp)
2215 .collect();
2216 assert_eq!(
2217 round_tripped, erased,
2218 "{src:?}: the old print-and-reparse path changed the tree"
2219 );
2220 }
2221 }
2222
2223 #[test]
2241 fn the_round_trip_is_not_the_identity_in_general() {
2242 let tree = Sexp::List(vec![
2243 Sexp::Atom(tatara_lisp::Atom::Symbol("f".into())),
2244 Sexp::Atom(tatara_lisp::Atom::Symbol("a b".into())),
2245 ]);
2246 let text = tree.to_string();
2247 let back: Vec<Sexp> = tatara_lisp::read_spanned(&text)
2248 .expect("it reads back cleanly — that IS the problem")
2249 .iter()
2250 .map(tatara_lisp::Spanned::to_sexp)
2251 .collect();
2252 assert_ne!(
2253 back,
2254 vec![tree.clone()],
2255 "if print-then-read became inverse over symbols, the class would be \
2256 closed upstream and this test should be deleted rather than relaxed"
2257 );
2258 let direct: Vec<Sexp> = crate::lower_to_spanned(std::slice::from_ref(&tree))
2260 .iter()
2261 .map(tatara_lisp::Spanned::to_sexp)
2262 .collect();
2263 assert_eq!(direct, vec![tree]);
2264 }
2265}
2266
2267#[cfg(test)]
2268mod macro_tests {
2269 use super::*;
2270
2271 fn int(src: &str) -> i64 {
2272 match run(src).unwrap_or_else(|e| panic!("{src:?}: {e}")).value {
2273 Value::Int(v) => v,
2274 other => panic!("{src:?} produced {other:?}"),
2275 }
2276 }
2277
2278 #[test]
2280 fn a_macro_expands_and_runs() {
2281 assert_eq!(
2282 int("defmacro double(x)\n quote\n unquote(x) + unquote(x)\n end\nend\ndouble(21)"),
2283 42
2284 );
2285 }
2286
2287 #[test]
2290 fn a_macro_operates_on_syntax_not_values() {
2291 assert_eq!(
2292 int("defmacro sq(e)\n quote\n unquote(e) * unquote(e)\n end\nend\nsq(2 + 3)"),
2293 25,
2294 "the argument form `2 + 3` must be substituted twice"
2295 );
2296 }
2297
2298 #[test]
2317 fn a_runaway_macro_fails_the_compilation_rather_than_the_process() {
2318 let err =
2319 run("defmacro forever(x)\n quote\n forever(unquote(x))\n end\nend\nforever(1)")
2320 .expect_err("a self-referential macro must be rejected");
2321 let msg = err.to_string();
2322 assert!(
2323 msg.contains("forever") && msg.contains("expansion"),
2324 "the error must name the macro and the limit it hit: {msg}"
2325 );
2326 assert!(
2327 msg.contains("<anonymous>:6:1"),
2328 "and must place the call that ran away — line 6 is `forever(1)`: {msg}"
2329 );
2330 }
2331}
2332
2333#[cfg(test)]
2334mod input_tests {
2335 use super::*;
2336 use crate::inputs::{Declaration, Inputs};
2337
2338 const SCHEMA: &[u8] = b"3";
2340
2341 fn with_schema(src: &str) -> Result<Run, RunError> {
2342 let hash = Inputs::hash_of(SCHEMA);
2343 let mut inputs = Inputs::new();
2344 inputs
2345 .bind(
2346 &Declaration {
2347 name: "schema".to_string(),
2348 hash,
2349 },
2350 SCHEMA.to_vec(),
2351 )
2352 .expect("bind");
2353 run_with_inputs(src, inputs)
2354 }
2355
2356 fn decl_line() -> String {
2357 let mut s = String::from("definput(\"schema\", \"");
2358 s.push_str(&Inputs::hash_of(SCHEMA));
2359 s.push_str("\")\n");
2360 s
2361 }
2362
2363 #[test]
2368 fn a_macro_can_read_a_declared_build_input() {
2369 let src = decl_line() + "input(\"schema\")";
2370 let out = with_schema(&src).expect("run");
2371 assert!(
2372 matches!(out.value, Value::Str(ref s) if &**s == "3"),
2373 "got {:?}",
2374 out.value
2375 );
2376 }
2377
2378 #[test]
2382 fn an_undeclared_input_is_an_error() {
2383 let err = with_schema("input(\"not_declared\")").expect_err("must fail");
2384 let msg = err.to_string();
2385 assert!(msg.contains("not_declared"), "must name it: {msg}");
2386 assert!(msg.contains("definput"), "and say how to declare it: {msg}");
2387 }
2388
2389 #[cfg(not(feature = "sys"))]
2397 #[test]
2398 fn there_is_no_ambient_file_read() {
2399 for attempt in [
2400 "read_file(\"/etc/passwd\")",
2401 "File(\"/etc/passwd\")",
2402 "slurp(\"/etc/passwd\")",
2403 "open(\"/etc/passwd\")",
2404 ] {
2405 let err = with_schema(attempt).expect_err("must not resolve");
2406 assert!(
2407 err.to_string().contains("unbound"),
2408 "{attempt} must be UNBOUND — a capability removed by absence, \
2409 not guarded by a check: {err}"
2410 );
2411 }
2412 }
2413
2414 #[cfg(feature = "sys")]
2420 #[test]
2421 fn sys_read_file_is_the_trusted_cli_only_exception() {
2422 let err = with_schema("definitely_not_a_primitive(\"x\")").expect_err("must not resolve");
2423 assert!(err.to_string().contains("unbound"), "{err}");
2424 assert!(
2425 with_schema("read_file(\"/etc/passwd\")").is_ok(),
2426 "with `sys` on, read_file is the trusted CLI surface"
2427 );
2428 let out = with_schema("input(\"schema\")").expect("run");
2429 assert!(
2430 matches!(out.value, Value::Str(ref s) if &**s == "3"),
2431 "input() still binds beside the sys surface: {:?}",
2432 out.value
2433 );
2434 }
2435
2436 #[test]
2439 fn a_declared_input_with_no_bytes_supplied_fails() {
2440 let src = decl_line() + "input(\"schema\")";
2441 assert!(run(&src).is_err(), "no bytes were supplied");
2442 }
2443}
2444
2445#[cfg(test)]
2446mod tier2_tests {
2447 use super::*;
2448 use crate::inputs::{Declaration, Inputs};
2449
2450 #[test]
2462 fn a_macro_generates_code_from_a_schema() {
2463 let schema = b"7";
2464 let mut inputs = Inputs::new();
2465 inputs
2466 .bind(
2467 &Declaration {
2468 name: "arity".to_string(),
2469 hash: Inputs::hash_of(schema),
2470 },
2471 schema.to_vec(),
2472 )
2473 .expect("bind");
2474
2475 let mut src = String::from("definput(\"arity\", \"");
2478 src.push_str(&Inputs::hash_of(schema));
2479 src.push_str("\")\n");
2480 src.push_str(
2481 "defmacro from_schema()\n quote\n unquote(to_int(input(\"arity\")))\n end\nend\n\
2482 from_schema() * 6",
2483 );
2484
2485 let out = run_with_inputs(&src, inputs).expect("run");
2486 assert!(
2487 matches!(out.value, Value::Int(42)),
2488 "the schema's 7 must reach the generated code: got {:?}",
2489 out.value
2490 );
2491 }
2492}