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 }
278 let Some(hold) = hold else {
279 if account.identify.is_some() {
280 crate::attach_node_accounted(interp, None, account);
281 }
282 return Ok(());
283 };
284 let withheld = hold.rebind_scoped(&prepared.program, &mut prepared.erased);
285 let escapes = entry_escapes(&prepared.program, &withheld, &|cap| {
286 format!("this run does not allow; pass --allow {cap} to grant it")
287 });
288 if !escapes.is_empty() {
289 return Err(RunError::Types(escapes));
290 }
291 hold.bind(interp, "this run's frame", &account.host_io);
292 crate::attach_node_accounted(interp, Some(hold), account);
293 Ok(())
294}
295
296fn entry_escapes(
297 program: &crate::uses::ResolvedProgram,
298 refused: &[(&'static str, blue_lang_waku::Capability)],
299 grant: &dyn Fn(&str) -> String,
300) -> Vec<String> {
301 use crate::uses::ResolvedProgram;
302 let forms: Vec<tatara_lisp::Spanned> = program
303 .forms()
304 .iter()
305 .enumerate()
306 .filter(|(i, _)| program.owner_of(*i) == Some(ResolvedProgram::ENTRY))
307 .map(|(_, f)| f.clone())
308 .collect();
309 let label = |name: &str| {
310 refused
311 .iter()
312 .find(|(n, _)| *n == name)
313 .map(|(_, c)| c.label())
314 };
315 blue_lang_waku::free_names(&forms, &|name| label(name).is_none())
316 .into_iter()
317 .filter_map(|name| {
318 let cap = label(&name)?;
319 let at = program
320 .file(ResolvedProgram::ENTRY)
321 .and_then(|f| f.path.clone())
322 .map_or_else(|| "<entry>".to_string(), |p| p.display().to_string());
323 Some(format!(
324 "{at}: refused: `{name}` reaches the host ({cap}), which {}",
325 grant(cap)
326 ))
327 })
328 .collect()
329}
330
331fn runtime(inputs: Inputs) -> tatara_lisp_eval::Interpreter<()> {
333 let mut interp =
334 crate::timings::stage(crate::timings::Stage::Runtime, crate::interpreter_hostless);
335 crate::inputs::install_input_primitives(&mut interp, inputs);
336 interp
337}
338
339fn prepared_on(
341 entry: Entry<'_>,
342 inputs: Inputs,
343 loader: &dyn crate::uses::Loader,
344 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
345) -> Result<(tatara_lisp_eval::Interpreter<()>, Prepared), RunError> {
346 let interp = runtime(inputs);
347 let builtins = crate::timings::stage(crate::timings::Stage::Names, || builtin_names(&interp));
348 let prepared = prepare(entry, loader, surface, &builtins)?;
349 Ok((interp, prepared))
350}
351
352pub struct Compiled {
358 pub program: crate::uses::ResolvedProgram,
359 pub lowered: Vec<tatara_lisp::Spanned>,
360 pub erased: Vec<tatara_lisp::Spanned>,
361 pub main: Option<String>,
362}
363
364pub fn compile(
372 entry: Entry<'_>,
373 loader: &dyn crate::uses::Loader,
374 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
375 hold: Option<&crate::Hold>,
376) -> Result<Compiled, RunError> {
377 let (_, prepared) = prepared_on(entry, Inputs::new(), loader, surface)?;
378 if let Some(hold) = hold {
379 let escapes = entry_escapes(&prepared.program, &hold.withheld_host_names(), &|cap| {
380 format!("this bundle's policy does not allow; declare allow(%i[{cap}]) in the project's Bluefile and run `blue lock`")
381 });
382 if !escapes.is_empty() {
383 return Err(RunError::Types(escapes));
384 }
385 }
386 let main = main_key(&prepared);
387 let Prepared {
388 program,
389 lowered,
390 erased,
391 ..
392 } = prepared;
393 Ok(Compiled {
394 program,
395 lowered: std::sync::Arc::unwrap_or_clone(lowered),
396 erased,
397 main,
398 })
399}
400
401pub fn run_compiled(
411 erased: &[tatara_lisp::Spanned],
412 main: Option<&str>,
413 hold: &crate::Hold,
414 who: &'static str,
415) -> Result<Value, RunError> {
416 let mut interp = runtime(Inputs::new());
417 let account = crate::Account::default();
418 hold.bind(&mut interp, who, &account.host_io);
419 crate::attach_node_accounted(&mut interp, Some(hold), &account);
420 let fault = |e: &tatara_lisp_eval::EvalError| {
421 RunError::Eval(Box::new(crate::observe::Fault::unplaced(e, erased)))
422 };
423 let mut value = Value::Nil;
424 for form in erased {
425 value = interp.eval_top_form(form, &mut ()).map_err(|e| fault(&e))?;
426 }
427 if let Some(key) = main {
428 let at = tatara_lisp::Span::synthetic();
429 let call = tatara_lisp::Spanned::new(
430 at,
431 tatara_lisp::SpannedForm::List(vec![tatara_lisp::Spanned::new(
432 at,
433 tatara_lisp::SpannedForm::Atom(tatara_lisp::Atom::Symbol(key.to_string())),
434 )]),
435 );
436 value = interp
437 .eval_top_form(&call, &mut ())
438 .map_err(|e| fault(&e))?;
439 }
440 Ok(value)
441}
442
443pub struct Observed {
446 pub definitions: std::sync::Arc<crate::observe::Definitions>,
447 pub outcome: Result<Run, RunError>,
449}
450
451pub struct Watched<'a> {
455 pub definitions: &'a std::sync::Arc<crate::observe::Definitions>,
456 pub program: &'a crate::uses::ResolvedProgram,
457 pub lowered: &'a [tatara_lisp::Spanned],
458 pub erased: &'a [tatara_lisp::Spanned],
459}
460
461pub fn run_observed(
472 entry: Entry<'_>,
473 inputs: Inputs,
474 loader: &dyn crate::uses::Loader,
475 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
476 watch: impl FnOnce(&Watched<'_>) -> std::sync::Arc<dyn crate::observe::CallObserver>,
477) -> Result<Observed, RunError> {
478 run_observed_framed(entry, inputs, loader, surface, None, watch)
479}
480
481pub fn run_observed_framed(
488 entry: Entry<'_>,
489 inputs: Inputs,
490 loader: &dyn crate::uses::Loader,
491 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
492 hold: Option<&crate::Hold>,
493 watch: impl FnOnce(&Watched<'_>) -> std::sync::Arc<dyn crate::observe::CallObserver>,
494) -> Result<Observed, RunError> {
495 run_observed_accounted(
496 entry,
497 inputs,
498 loader,
499 surface,
500 hold,
501 &crate::Account::default(),
502 watch,
503 )
504}
505
506pub fn run_observed_accounted(
513 entry: Entry<'_>,
514 inputs: Inputs,
515 loader: &dyn crate::uses::Loader,
516 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
517 hold: Option<&crate::Hold>,
518 account: &crate::Account,
519 watch: impl FnOnce(&Watched<'_>) -> std::sync::Arc<dyn crate::observe::CallObserver>,
520) -> Result<Observed, RunError> {
521 let (mut interp, mut prepared) = prepared_on(entry, inputs, loader, surface)?;
522 hold_to_frame(&mut interp, &mut prepared, hold, account)?;
523 let definitions = std::sync::Arc::new(crate::observe::Definitions::of(
524 &prepared.program,
525 &prepared.erased,
526 ));
527 interp.set_call_observer(Some(watch(&Watched {
528 definitions: &definitions,
529 program: &prepared.program,
530 lowered: &prepared.lowered,
531 erased: &prepared.erased,
532 })));
533 let outcome = crate::timings::stage(crate::timings::Stage::Eval, || {
534 eval_prepared(&mut interp, &prepared, &mut ())
535 })
536 .and_then(|value| {
537 Ok(crate::timings::stage(crate::timings::Stage::Main, || {
538 call_main(&mut interp, &prepared, &mut ())
539 })?
540 .unwrap_or(value))
541 })
542 .map(|value| Run {
543 value,
544 visited: prepared.visited,
545 typed_decls: prepared.typed_decls,
546 seams: prepared.seams,
547 });
548 interp.set_call_observer(None);
549 Ok(Observed {
550 definitions,
551 outcome,
552 })
553}
554
555pub(crate) struct Prepared {
560 program: crate::uses::ResolvedProgram,
561 lowered: std::sync::Arc<Vec<tatara_lisp::Spanned>>,
562 erased: Vec<tatara_lisp::Spanned>,
563 visited: usize,
564 typed_decls: usize,
565 seams: usize,
566}
567
568impl Prepared {
569 pub(crate) fn erased(&self) -> &[tatara_lisp::Spanned] {
571 &self.erased
572 }
573
574 pub(crate) fn entry_namespace(&self) -> Namespace {
576 let first = (0..self.program.forms().len())
577 .find(|i| self.program.owner_of(*i) == Some(crate::uses::ResolvedProgram::ENTRY));
578 match first {
579 Some(i) => namespace_of(&self.program, i),
580 None => match self
581 .program
582 .file(crate::uses::ResolvedProgram::ENTRY)
583 .and_then(|f| f.package.clone())
584 {
585 Some(p) => Namespace::Bidama(p),
586 None => Namespace::File(String::new()),
587 },
588 }
589 }
590}
591
592pub(crate) fn prepare(
598 entry: Entry<'_>,
599 loader: &dyn crate::uses::Loader,
600 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
601 builtins: &NameTable,
602) -> Result<Prepared, RunError> {
603 let mut program = parse_and_resolve(entry, loader, surface)?;
604
605 program.retain(|f| !crate::uses::is_test_form(f));
618
619 let (outcome, names) = crate::timings::stage(crate::timings::Stage::Check, || {
634 check_stage(&program, builtins, false)
635 });
636 if !outcome.ok() {
637 return Err(RunError::Types(
638 outcome
639 .errors()
640 .map(|d| render(&program, d))
643 .collect(),
644 ));
645 }
646
647 let (lowered, erased) = crate::timings::stage(crate::timings::Stage::Erase, || {
661 let lowered = lower(&program, &names);
662 let erased = erase_types(&lowered);
663 (lowered, erased)
664 });
665
666 Ok(Prepared {
667 program,
668 lowered: std::sync::Arc::new(lowered),
669 erased,
670 visited: outcome.stats.visited,
671 typed_decls: outcome.stats.typed_decls,
672 seams: outcome.seams.len(),
673 })
674}
675
676pub(crate) fn parse_and_resolve(
678 entry: Entry<'_>,
679 loader: &dyn crate::uses::Loader,
680 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
681) -> Result<crate::uses::ResolvedProgram, RunError> {
682 let forms = crate::timings::stage(crate::timings::Stage::Parse, || {
683 let mut forms = match surface {
684 Some(pack) => blue_lang_syntax::parse_program_tree_in(entry.text, pack)
685 .map_err(|e| RunError::Parse(e.to_string()))?,
686 None => loader
687 .parse(entry.text)
688 .map_err(|e| RunError::Parse(e.to_string()))?,
689 };
690 if surface.is_none() {
693 forms.extend(
694 blue_lang_syntax::doc::doctests(entry.text)
695 .map_err(|e| RunError::Parse(e.to_string()))?,
696 );
697 }
698 Ok::<_, RunError>(forms)
699 })?;
700
701 crate::timings::stage(crate::timings::Stage::Resolve, || {
712 crate::uses::resolve_uses(forms, entry, loader).map_err(RunError::Import)
713 })
714}
715
716pub(crate) fn check_stage(
729 program: &crate::uses::ResolvedProgram,
730 builtins: &NameTable,
731 with_tests: bool,
732) -> (blue_lang_check::Outcome, NameTable) {
733 use crate::uses::ResolvedProgram;
734 let forms = program.forms();
735 let is_entry = |i: usize| program.owner_of(i) == Some(ResolvedProgram::ENTRY);
736
737 let mut outcome = blue_lang_check::check_program_in(forms, &|a, b| {
739 program.owner_of(a) == program.owner_of(b)
740 });
741 outcome
742 .diagnostics
743 .extend(data_law_diagnostics(program, &is_entry));
744
745 let table = program_names(program, builtins);
748 let whole = |i: usize| with_tests && is_entry(i);
749 let (names, resolved) = blue_lang_check::names::check_names_whole(
750 forms,
751 &table,
752 &|i| namespace_of(program, i),
753 &is_entry,
754 &whole,
755 );
756 let stored = program
757 .file(ResolvedProgram::ENTRY)
758 .is_some_and(|f| f.path.is_some());
759 outcome.diagnostics.extend(names);
760 outcome.stats.names_resolved = resolved;
761 if stored {
762 let script = program
763 .file(ResolvedProgram::ENTRY)
764 .is_some_and(|f| f.package.is_none());
765 let found = blue_lang_check::form_rules::check(forms, &is_entry, script);
766 let silenced: Vec<tatara_lisp::Span> = found
767 .iter()
768 .filter(|d| d.code == blue_lang_check::Code::B0038)
769 .map(|d| d.span)
770 .collect();
771 outcome
772 .diagnostics
773 .retain(|d| !(d.code == blue_lang_check::Code::B0002 && silenced.contains(&d.span)));
774 outcome.diagnostics.extend(found);
775 }
776 outcome
777 .diagnostics
778 .extend(blue_lang_check::contracts::check(
779 forms,
780 &table,
781 &|i| namespace_of(program, i),
782 &is_entry,
783 &|i| {
784 program
785 .owner_of(i)
786 .and_then(|f| program.file(f))
787 .map(|f| f.text.as_str())
788 },
789 crate::docs::signatures(),
790 ));
791
792 outcome.diagnostics.extend(doc_diagnostics(program));
793
794 let mut waivers = Vec::new();
797 for file in program.files() {
798 let spans: Vec<(usize, tatara_lisp::Span)> = forms
799 .iter()
800 .enumerate()
801 .filter(|(i, _)| program.owner_of(*i) == Some(file.id))
802 .map(|(i, f)| (i, f.span))
803 .collect();
804 let (found, malformed) = blue_lang_check::waiver::collect(&file.text, &spans);
805 waivers.extend(found);
806 outcome.diagnostics.extend(malformed);
807 }
808 outcome.waived = blue_lang_check::waiver::apply(&mut outcome.diagnostics, waivers, &is_entry);
809 if !stored {
810 outcome.diagnostics.retain(|d| {
811 !matches!(
812 d.code,
813 blue_lang_check::Code::B0023
814 | blue_lang_check::Code::B0036
815 | blue_lang_check::Code::B0037
816 | blue_lang_check::Code::B0067
817 | blue_lang_check::Code::B0061
818 | blue_lang_check::Code::B0062
819 | blue_lang_check::Code::B0063
820 )
821 });
822 }
823 let judge = blue_lang_check::evolution::FormJudge::from_env().unwrap_or_default();
824 outcome
825 .diagnostics
826 .retain(|d| judge.reports(d.code, is_entry(d.top_level)));
827 let (enforced, census): (Vec<_>, Vec<_>) = std::mem::take(&mut outcome.diagnostics)
830 .into_iter()
831 .partition(|d| d.code.rule().active());
832 outcome.diagnostics = enforced;
833 outcome.census = census;
834 outcome
835 .diagnostics
836 .sort_by_key(|d| (d.top_level, d.span.start, d.code));
837 (outcome, table)
838}
839
840fn doc_diagnostics(program: &crate::uses::ResolvedProgram) -> Vec<blue_lang_check::Diagnostic> {
844 use crate::uses::ResolvedProgram;
845 let Some(file) = program.file(ResolvedProgram::ENTRY) else {
846 return Vec::new();
847 };
848 let Some(package) = file.package.as_deref() else {
849 return Vec::new();
850 };
851 let forms: Vec<(usize, &tatara_lisp::Spanned)> = program
852 .forms()
853 .iter()
854 .enumerate()
855 .filter(|(i, f)| {
856 program.owner_of(*i) == Some(ResolvedProgram::ENTRY)
857 && !blue_lang_syntax::doc::is_doctest(f)
858 })
859 .collect();
860 let graph = file.path.as_deref().map(crate::doc_sources::graph_meanings);
861 let meaning = |name: &str| {
862 graph
863 .as_ref()
864 .and_then(|g| g.get(&(package.to_string(), name.to_string())).cloned())
865 };
866 let names_line = |name: &str| crate::docs::doc_of(name).map(|d| d.doc.to_string());
867 blue_lang_check::doc_rules::check_file(
868 &file.text,
869 &forms,
870 package,
871 &blue_lang_check::doc_rules::Sources {
872 meaning: &meaning,
873 names_line: &names_line,
874 },
875 )
876}
877
878fn data_law_diagnostics(
879 program: &crate::uses::ResolvedProgram,
880 is_entry: &dyn Fn(usize) -> bool,
881) -> Vec<blue_lang_check::Diagnostic> {
882 use blue_lang_check::data_laws;
883 let forms = program.forms();
884 let fragments = data_laws::Fragments::collect(forms);
885 let frontend = program.frontend();
886 let mut out = Vec::new();
887 let schema = match frontend.and_then(|f| f.table.as_ref()) {
888 Some((label, text)) => match data_laws::Schema::read(text) {
889 Ok(s) => Some(s),
890 Err(why) => {
891 if let Some((i, first)) = forms.iter().enumerate().find(|(i, _)| is_entry(*i)) {
892 out.push(
893 blue_lang_check::Diagnostic::new(
894 blue_lang_check::Code::B0050,
895 format!("the generated schema table {label} does not read: {why}"),
896 first.span,
897 )
898 .at_top_level(i)
899 .with_help("regenerate it: `nix run .#regen`"),
900 );
901 }
902 None
903 }
904 },
905 None => None,
906 };
907 if fragments.is_empty() && schema.is_none() && !frontend.is_some_and(|f| f.view) {
908 return out;
909 }
910 out.extend(data_laws::check(
911 forms,
912 &data_laws::Context {
913 fragments: &fragments,
914 schema: schema.as_ref(),
915 is_entry,
916 view: frontend.is_some_and(|f| f.view),
917 },
918 ));
919 out
920}
921
922#[must_use]
935pub fn program_names(program: &crate::uses::ResolvedProgram, builtins: &NameTable) -> NameTable {
936 use blue_lang_check::Binding;
937 let mut table = NameTable::new();
938 table.add_program(program.forms(), |i| namespace_of(program, i));
939 let builtin_macro = |head: &str| {
940 builtins
941 .scopes()
942 .iter()
943 .any(|s| s.namespace == Namespace::Macro && s.get(head).is_some())
944 };
945 let mut expander: Option<tatara_lisp_eval::Interpreter<()>> = None;
946 for (i, form) in program.forms().iter().enumerate() {
947 let Some(head) = form
948 .as_list()
949 .and_then(|items| items.first())
950 .and_then(tatara_lisp::Spanned::as_symbol)
951 else {
952 continue;
953 };
954 if !builtin_macro(head) {
955 continue;
956 }
957 let interp = expander.get_or_insert_with(crate::interpreter_hostless);
958 let Ok(expanded) = interp.fully_expand(form, &mut ()) else {
961 continue;
962 };
963 let nullary = blue_lang_syntax::scope::nullary_definition(&expanded);
964 for (name, span, kind) in blue_lang_check::names::definitions_of(&expanded) {
965 table.define(
966 namespace_of(program, i),
967 Binding {
968 nullary: nullary == Some(name.as_str()),
969 name,
970 kind,
971 span: Some(span),
972 top_level: Some(i),
973 },
974 );
975 }
976 }
977 for scope in builtins.scopes() {
978 table.push(scope.clone());
979 }
980 table.spell_bare_like(builtins);
981 let form_file: Vec<usize> = (0..program.forms().len())
982 .map(|i| program.owner_of(i).map_or(usize::MAX, |f| f.index()))
983 .collect();
984 let mut imports: std::collections::BTreeMap<usize, blue_lang_check::names::FileImports> =
985 std::collections::BTreeMap::new();
986 for (file, import) in program.imports() {
987 let entry = imports.entry(file.index()).or_default();
988 entry.add(&import.package, import.names.iter().map(|(n, _)| n.clone()));
989 let at = import.span.start;
990 entry.use_at = Some(entry.use_at.map_or(at, |a| a.min(at)));
991 }
992 let facade_uses =
997 |pkg: &str,
998 imports: &std::collections::BTreeMap<usize, blue_lang_check::names::FileImports>| {
999 let defines = table.bidama(pkg).is_some_and(|s| !s.is_empty());
1000 if defines {
1001 return std::collections::BTreeSet::new();
1002 }
1003 program
1004 .files()
1005 .iter()
1006 .filter(|f| f.package.as_deref() == Some(pkg))
1007 .filter_map(|f| imports.get(&f.id.index()))
1008 .flat_map(|i| i.uses.iter().cloned())
1009 .collect::<std::collections::BTreeSet<String>>()
1010 };
1011 loop {
1012 let mut grew = false;
1013 let files: Vec<usize> = imports.keys().copied().collect();
1014 for f in files {
1015 let used: Vec<String> = imports[&f].uses.iter().cloned().collect();
1016 for p in used {
1017 for member in facade_uses(&p, &imports) {
1018 if let Some(i) = imports.get_mut(&f) {
1019 if i.uses.insert(member.clone()) {
1020 i.via.insert(member, p.clone());
1021 grew = true;
1022 }
1023 }
1024 }
1025 }
1026 }
1027 if !grew {
1028 break;
1029 }
1030 }
1031 let mut pkg_uses: std::collections::BTreeMap<String, std::collections::BTreeSet<String>> =
1032 std::collections::BTreeMap::new();
1033 for (file, import) in program.imports() {
1034 if let Some(p) = program.file(*file).and_then(|f| f.package.clone()) {
1035 pkg_uses
1036 .entry(p)
1037 .or_default()
1038 .insert(import.package.clone());
1039 }
1040 }
1041 table.attach_files(form_file, imports);
1042 table.attach_package_uses(pkg_uses);
1043 table.attach_needs(program.needs().clone());
1044 let canonical = |p: &std::path::Path| {
1048 std::fs::canonicalize(p)
1049 .unwrap_or_else(|_| p.to_path_buf())
1050 .display()
1051 .to_string()
1052 };
1053 let mut file_paths = std::collections::BTreeMap::new();
1054 let mut pkg_roots = std::collections::BTreeMap::new();
1055 for f in program.files() {
1056 if let Some(path) = &f.path {
1057 file_paths.insert(f.id.index(), canonical(path));
1058 if let (Some(pkg), Some(root)) =
1059 (&f.package, path.parent().and_then(std::path::Path::parent))
1060 {
1061 pkg_roots
1062 .entry(pkg.clone())
1063 .or_insert_with(|| canonical(root));
1064 }
1065 }
1066 }
1067 table.attach_paths(file_paths, pkg_roots);
1068 let versions = program.versions().clone();
1069 table.apply_legacy(program.forms(), &|i| namespace_of(program, i), &|p| {
1070 versions.get(p).cloned()
1071 });
1072 table
1073}
1074
1075#[must_use]
1087pub fn lower(
1088 program: &crate::uses::ResolvedProgram,
1089 names: &NameTable,
1090) -> Vec<tatara_lisp::Spanned> {
1091 lower_with(program, names, true)
1092}
1093
1094#[must_use]
1095pub fn lower_with(
1096 program: &crate::uses::ResolvedProgram,
1097 names: &NameTable,
1098 simplify_chains: bool,
1099) -> Vec<tatara_lisp::Spanned> {
1100 let mut resolved = blue_lang_check::names::resolve_program(program.forms(), names, &|i| {
1101 namespace_of(program, i)
1102 });
1103 let simplified = if simplify_chains {
1104 chain_simplified(program.forms(), &resolved)
1105 } else {
1106 None
1107 };
1108 let forms: &[tatara_lisp::Spanned] = simplified.as_deref().unwrap_or(program.forms());
1109 if simplified.is_some() {
1110 resolved =
1111 blue_lang_check::names::resolve_program(forms, names, &|i| namespace_of(program, i));
1112 }
1113 resolved
1114 .resolved_tree(forms, blue_lang_check::names::Rule::Namespaced)
1115 .into_iter()
1116 .zip(forms)
1117 .enumerate()
1118 .map(|(i, (resolved, written))| {
1119 if crate::uses::use_target(written).is_some()
1120 || blue_lang_syntax::scope::legacy_target(written).is_some()
1121 {
1122 tatara_lisp::Spanned::new(written.span, tatara_lisp::SpannedForm::Nil)
1123 } else {
1124 let file = program.owner_of(i).and_then(|id| program.file(id));
1125 match file {
1126 Some(f) => locate_raises(&resolved, &raise_site(f)),
1127 None => resolved,
1128 }
1129 }
1130 })
1131 .collect()
1132}
1133
1134fn chain_simplified(
1135 forms: &[tatara_lisp::Spanned],
1136 resolved: &blue_lang_check::names::Resolved,
1137) -> Option<Vec<tatara_lisp::Spanned>> {
1138 let respell: std::collections::HashMap<usize, blue_lang_check::respell::Respelling> = resolved
1139 .respellings
1140 .iter()
1141 .filter(|(_, r)| matches!(r.respell, blue_lang_check::respell::Respell::Chain(_)))
1142 .cloned()
1143 .collect();
1144 if respell.is_empty() {
1145 return None;
1146 }
1147 let bare = std::collections::HashSet::new();
1148 let canon = Canon {
1149 bare: &bare,
1150 respell: &respell,
1151 };
1152 Some(forms.iter().map(|f| canon.form(f)).collect())
1153}
1154
1155fn raise_site(file: &crate::uses::SourceFile) -> impl Fn(tatara_lisp::Span) -> String + '_ {
1156 move |span| {
1157 let (line, _) =
1158 blue_lang_syntax::Span::line_col(&file.text, span.start.min(file.text.len()));
1159 let shown = file
1160 .path
1161 .as_ref()
1162 .map_or_else(|| "<source>".to_string(), |p| p.display().to_string());
1163 format!("{shown}:{line}")
1164 }
1165}
1166
1167fn locate_raises(
1168 form: &tatara_lisp::Spanned,
1169 site: &dyn Fn(tatara_lisp::Span) -> String,
1170) -> tatara_lisp::Spanned {
1171 use tatara_lisp::{Atom, Spanned, SpannedForm};
1172 let inner = match &form.form {
1173 SpannedForm::List(items) => {
1174 let mut out: Vec<Spanned> = items.iter().map(|i| locate_raises(i, site)).collect();
1175 let raising = items.first().and_then(Spanned::as_symbol) == Some("raise")
1176 && (3..=4).contains(&items.len());
1177 if raising {
1178 out[0] = Spanned::new(
1179 items[0].span,
1180 SpannedForm::Atom(Atom::Symbol(crate::errors::RAISE_AT.to_string())),
1181 );
1182 out.insert(
1183 1,
1184 Spanned::new(items[0].span, SpannedForm::Atom(Atom::Str(site(form.span)))),
1185 );
1186 }
1187 SpannedForm::List(out)
1188 }
1189 SpannedForm::Quote(_) | SpannedForm::Quasiquote(_) => return form.clone(),
1190 other => other.clone(),
1191 };
1192 Spanned::new(form.span, inner)
1193}
1194
1195#[must_use]
1197pub fn namespace_of(program: &crate::uses::ResolvedProgram, i: usize) -> Namespace {
1198 let file = program.owner_of(i).and_then(|id| program.file(id));
1199 match file {
1200 Some(f) => match &f.package {
1201 Some(p) => Namespace::Bidama(p.clone()),
1202 None => Namespace::File(
1203 f.path
1204 .as_ref()
1205 .map_or_else(|| "<anonymous>".to_string(), |p| p.display().to_string()),
1206 ),
1207 },
1208 None => Namespace::File("<unknown file>".to_string()),
1209 }
1210}
1211
1212pub const HARNESS_NAMES: &[&str] = &[
1226 blue_lang_syntax::LOWERED_ASSERT,
1227 blue_lang_syntax::LOWERED_ASSERT_PANICS,
1228 blue_lang_syntax::LOWERED_TEST_META,
1229];
1230
1231#[must_use]
1241pub fn builtin_names<H: 'static>(interp: &tatara_lisp_eval::Interpreter<H>) -> NameTable {
1242 builtin_names_with(interp, &[])
1243}
1244
1245#[must_use]
1246pub fn builtin_names_with<H: 'static>(
1247 interp: &tatara_lisp_eval::Interpreter<H>,
1248 host: &[crate::host_surface::HostName],
1249) -> NameTable {
1250 use blue_lang_check::{Binding, Scope, ScopeKind};
1251 use tatara_lisp_eval::HeadBinding;
1252 let mut special = Scope::new(Namespace::SpecialForm);
1253 let mut macros = Scope::new(Namespace::Macro);
1254 let mut values = Scope::new(Namespace::Builtin);
1255 let mut names: Vec<std::sync::Arc<str>> = interp.reserved_head_names().into_iter().collect();
1260 names.extend(
1261 crate::inputs::INPUT_PRIMITIVES
1262 .iter()
1263 .filter(|n| interp.resolve_head(n).is_some())
1264 .map(|n| std::sync::Arc::from(*n)),
1265 );
1266 for name in names {
1267 let (scope, kind) = match interp.resolve_head(&name) {
1268 Some(HeadBinding::SpecialForm) => (&mut special, ScopeKind::SpecialForm),
1269 Some(HeadBinding::Macro) => (&mut macros, ScopeKind::Macro),
1270 _ => (&mut values, ScopeKind::Value),
1271 };
1272 let nullary = kind == ScopeKind::Value
1273 && interp.lookup_global(&name).is_some_and(|v| {
1274 nullary_value(&v)
1275 || (crate::docs::doc_of(&name).is_some_and(|d| d.signature == &*name)
1276 && matches!(
1277 v,
1278 tatara_lisp_eval::Value::NativeFn(_)
1279 | tatara_lisp_eval::Value::Closure(_)
1280 ))
1281 });
1282 scope.bind(Binding {
1283 name: name.to_string(),
1284 kind,
1285 span: None,
1286 top_level: None,
1287 nullary,
1288 });
1289 }
1290 for h in host {
1291 values.bind(Binding {
1292 name: h.name.clone(),
1293 kind: ScopeKind::Value,
1294 span: None,
1295 top_level: None,
1296 nullary: h.nullary(),
1297 });
1298 }
1299 let mut table = NameTable::new();
1300 let mut harness = Scope::new(Namespace::Harness);
1301 for n in HARNESS_NAMES {
1302 harness.bind(Binding {
1303 name: (*n).to_string(),
1304 kind: ScopeKind::Value,
1305 span: None,
1306 top_level: None,
1307 nullary: false,
1308 });
1309 }
1310 table.push(harness);
1311 table.push(special);
1312 table.push(macros);
1313 table.push(values);
1314 table.spell_bare_only(
1315 crate::docs::NAMES
1316 .iter()
1317 .filter(|d| d.signature == d.name)
1318 .map(|d| d.name.to_string())
1319 .chain(host.iter().filter(|h| h.nullary()).map(|h| h.name.clone()))
1320 .collect(),
1321 );
1322 table
1323}
1324
1325#[must_use]
1326pub fn nullary_value(value: &tatara_lisp_eval::Value) -> bool {
1327 match value {
1328 tatara_lisp_eval::Value::NativeFn(f) => {
1329 matches!(f.arity, tatara_lisp_eval::ffi::Arity::Exact(0))
1330 }
1331 tatara_lisp_eval::Value::Closure(c) => c.params.is_empty() && c.rest.is_none(),
1332 _ => false,
1333 }
1334}
1335
1336#[must_use]
1340pub fn render(program: &crate::uses::ResolvedProgram, d: &blue_lang_check::Diagnostic) -> String {
1341 let head = d.to_string();
1342 let mut out = program.locate(d.top_level, d.span, &head).to_string();
1343 if let Some(help) = &d.help {
1344 out.push_str("\n help: ");
1345 out.push_str(help);
1346 }
1347 let file = program
1348 .owner_of(d.top_level)
1349 .and_then(|id| program.file(id))
1350 .and_then(|f| f.path.as_ref());
1351 if let Some(cmd) =
1352 file.and_then(|p| blue_lang_check::evolution::fix_command(d, &p.display().to_string()))
1353 {
1354 out.push_str("\n fix: ");
1355 out.push_str(&cmd);
1356 }
1357 out
1358}
1359
1360#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1362pub enum Checking {
1363 Program,
1365 WithTests,
1368}
1369
1370pub struct Checked {
1372 pub program: crate::uses::ResolvedProgram,
1373 pub outcome: blue_lang_check::Outcome,
1374 pub names: NameTable,
1377}
1378
1379impl Checked {
1380 #[must_use]
1383 pub fn erased(&self) -> Vec<tatara_lisp::Spanned> {
1384 erase_types(&lower(&self.program, &self.names))
1385 }
1386
1387 #[must_use]
1390 pub fn evaluable(&self) -> Vec<tatara_lisp::Sexp> {
1391 lower(&self.program, &self.names)
1392 .iter()
1393 .map(tatara_lisp::Spanned::to_sexp)
1394 .collect()
1395 }
1396
1397 #[must_use]
1401 pub fn resolve(&self) -> blue_lang_check::names::Resolved {
1402 blue_lang_check::names::resolve_program(self.program.forms(), &self.names, &|i| {
1403 namespace_of(&self.program, i)
1404 })
1405 }
1406}
1407
1408impl Checked {
1409 #[must_use]
1410 pub fn call_explicit_forms(&self) -> Vec<tatara_lisp::Spanned> {
1411 self.resolve().call_explicit_tree(self.program.forms())
1412 }
1413
1414 #[must_use]
1415 pub fn canonical_entry(&self, src: &str) -> Option<Vec<tatara_lisp::Spanned>> {
1416 use crate::uses::ResolvedProgram;
1417 let program = &self.program;
1418 let entry: Vec<&tatara_lisp::Spanned> = program
1419 .forms()
1420 .iter()
1421 .enumerate()
1422 .filter(|(i, _)| program.owner_of(*i) == Some(ResolvedProgram::ENTRY))
1423 .map(|(_, f)| f)
1424 .filter(|f| !blue_lang_syntax::doc::is_doctest(f))
1425 .collect();
1426 let written = blue_lang_syntax::parse_program_tree(src).ok()?;
1427 let same = written.len() == entry.len()
1428 && written
1429 .iter()
1430 .zip(&entry)
1431 .all(|(w, e)| w.to_sexp() == e.to_sexp() && w.span == e.span);
1432 if !same {
1433 return None;
1434 }
1435 let resolved = self.resolve();
1436 let bare: std::collections::HashSet<usize> = resolved
1437 .parenthesized_nullary_calls()
1438 .into_iter()
1439 .filter(|r| program.owner_of(r.top_level) == Some(ResolvedProgram::ENTRY))
1440 .map(blue_lang_check::names::Reference::node_address)
1441 .collect();
1442 let waived: Vec<tatara_lisp::Span> = self
1443 .outcome
1444 .waived
1445 .iter()
1446 .map(|w| w.diagnostic.span)
1447 .collect();
1448 let respell: std::collections::HashMap<usize, blue_lang_check::respell::Respelling> =
1449 resolved
1450 .respellings
1451 .iter()
1452 .filter(|(_, r)| !waived.contains(&r.span))
1453 .cloned()
1454 .collect();
1455 let canon = Canon {
1456 bare: &bare,
1457 respell: &respell,
1458 };
1459 let forms: Vec<tatara_lisp::Spanned> = entry.into_iter().map(|f| canon.form(f)).collect();
1460 let script = program
1461 .file(ResolvedProgram::ENTRY)
1462 .is_some_and(|f| f.package.is_none());
1463 let forms = if script {
1464 blue_lang_check::form_rules::without_prefix(&forms).unwrap_or(forms)
1465 } else {
1466 forms
1467 };
1468 let forms = blue_lang_check::form_rules::without_main_guard(&forms).unwrap_or(forms);
1469 Some(blue_lang_check::form_rules::eta_reduced(&forms).unwrap_or(forms))
1470 }
1471}
1472
1473struct Canon<'a> {
1474 bare: &'a std::collections::HashSet<usize>,
1475 respell: &'a std::collections::HashMap<usize, blue_lang_check::respell::Respelling>,
1476}
1477
1478impl Canon<'_> {
1479 fn form(&self, form: &tatara_lisp::Spanned) -> tatara_lisp::Spanned {
1480 use tatara_lisp::{Spanned, SpannedForm};
1481 let inner = match &form.form {
1482 SpannedForm::List(items) => match items.as_slice() {
1483 [head] if self.bare.contains(&(std::ptr::from_ref(head) as usize)) => {
1484 return Spanned::new(form.span, head.form.clone());
1485 }
1486 [head, ..]
1487 if self
1488 .respell
1489 .contains_key(&(std::ptr::from_ref(head) as usize)) =>
1490 {
1491 let r = &self.respell[&(std::ptr::from_ref(head) as usize)];
1492 let walked: Vec<Spanned> = items.iter().map(|i| self.form(i)).collect();
1493 return blue_lang_check::respell::apply(form, &walked, r);
1494 }
1495 _ => SpannedForm::List(items.iter().map(|i| self.form(i)).collect()),
1496 },
1497 SpannedForm::Quote(i) => SpannedForm::Quote(Box::new(self.form(i))),
1498 SpannedForm::Quasiquote(i) => SpannedForm::Quasiquote(Box::new(self.form(i))),
1499 SpannedForm::Unquote(i) => SpannedForm::Unquote(Box::new(self.form(i))),
1500 SpannedForm::UnquoteSplice(i) => SpannedForm::UnquoteSplice(Box::new(self.form(i))),
1501 other => other.clone(),
1502 };
1503 Spanned::new(form.span, inner)
1504 }
1505}
1506
1507pub fn check_entry(
1523 entry: Entry<'_>,
1524 loader: &dyn crate::uses::Loader,
1525 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
1526 checking: Checking,
1527) -> Result<Checked, RunError> {
1528 let mut program = parse_and_resolve(entry, loader, surface)?;
1529 if checking == Checking::Program {
1530 program.retain(|f| !crate::uses::is_test_form(f));
1531 }
1532 let host = match entry.path {
1533 Some(path) => crate::host_surface::near(path)
1534 .map_err(|e| RunError::Import(e.to_string()))?
1535 .map(|s| s.functions)
1536 .unwrap_or_default(),
1537 None => Vec::new(),
1538 };
1539 let interp = runtime(Inputs::new());
1540 let builtins = crate::timings::stage(crate::timings::Stage::Names, || {
1541 builtin_names_with(&interp, &host)
1542 });
1543 let (outcome, names) = crate::timings::stage(crate::timings::Stage::Check, || {
1544 check_stage(&program, &builtins, checking == Checking::WithTests)
1545 });
1546 Ok(Checked {
1547 program,
1548 outcome,
1549 names,
1550 })
1551}
1552
1553#[must_use]
1554pub fn spelling_code(spelling: blue_lang_syntax::Spelling) -> blue_lang_check::Code {
1555 use blue_lang_check::Code;
1556 use blue_lang_syntax::Spelling;
1557 match spelling {
1558 Spelling::LogicalSymbol => Code::B0025,
1559 Spelling::Unless => Code::B0026,
1560 Spelling::ElseIf => Code::B0027,
1561 Spelling::CallForm => Code::B0028,
1562 Spelling::KeywordRocket => Code::B0029,
1563 Spelling::ParameterList => Code::B0030,
1564 Spelling::OperatorSpelling => Code::B0031,
1565 Spelling::TrailingComma => Code::B0032,
1566 Spelling::NegatedComparison => Code::B0034,
1567 Spelling::ElseNil => Code::B0035,
1568 Spelling::TryCatch => Code::B0037,
1569 Spelling::Apply => Code::B0042,
1570 Spelling::MapPun => Code::B0046,
1571 Spelling::NilTest => Code::B0047,
1572 Spelling::NewlineEscape => Code::B0065,
1573 Spelling::Index => Code::B0066,
1574 Spelling::TestDeclaration => Code::B0068,
1575 }
1576}
1577
1578#[must_use]
1579pub fn form_diagnostics(text: &str) -> Vec<blue_lang_check::Diagnostic> {
1580 match blue_lang_fmt::format_source_lossless(text) {
1581 Ok(formatted) => form_diagnostics_against(text, &formatted),
1582 Err(_) => Vec::new(),
1583 }
1584}
1585
1586#[must_use]
1587pub fn form_diagnostics_against(text: &str, formatted: &str) -> Vec<blue_lang_check::Diagnostic> {
1588 use blue_lang_check::{Code, Diagnostic};
1589 if formatted.trim_end() == text.trim_end() {
1590 return Vec::new();
1591 }
1592 let help = "`blue fmt --write FILE` writes it; no compile door writes a file (`BLUE_LAYOUT=rewrite` restores formatting in place)";
1593 let mut out: Vec<Diagnostic> = blue_lang_syntax::spelling_notes(text)
1594 .unwrap_or_default()
1595 .into_iter()
1596 .map(|n| {
1597 let message = if n.canonical.is_empty() {
1598 format!("`{}` is not written", n.written)
1599 } else {
1600 format!("`{}` is written `{}`", n.written, n.canonical)
1601 };
1602 Diagnostic::new(spelling_code(n.spelling), message, n.span)
1603 .at_top_level(0)
1604 .with_help(help)
1605 })
1606 .collect();
1607 out.extend(
1608 literal_diagnostics(text)
1609 .into_iter()
1610 .map(|d| d.with_help(help)),
1611 );
1612 if out.is_empty() {
1613 let at = text
1614 .lines()
1615 .zip(formatted.lines())
1616 .position(|(a, b)| a != b)
1617 .unwrap_or_else(|| text.lines().count().min(formatted.lines().count()));
1618 let start: usize = text.lines().take(at).map(|l| l.len() + 1).sum();
1619 let start = start.min(text.len());
1620 let end = text[start..].find('\n').map_or(text.len(), |e| start + e);
1621 out.push(
1622 Diagnostic::new(
1623 Code::B0024,
1624 format!("line {} is not laid out as blue writes it", at + 1),
1625 tatara_lisp::Span::new(start, end),
1626 )
1627 .at_top_level(0)
1628 .with_help(help),
1629 );
1630 }
1631 out.sort_by_key(|d| (d.span.start, d.code));
1632 out
1633}
1634
1635fn literal_diagnostics(text: &str) -> Vec<blue_lang_check::Diagnostic> {
1636 use blue_lang_syntax::{Atom, TokenKind};
1637 let Ok(toks) = blue_lang_syntax::lex(text) else {
1638 return Vec::new();
1639 };
1640 toks.iter()
1641 .filter_map(|t| {
1642 let atom = match &t.kind {
1643 TokenKind::Int(v) => Atom::Int(*v),
1644 TokenKind::Float(v) => Atom::Float(*v),
1645 TokenKind::Str(v) => Atom::Str(v.clone()),
1646 _ => return None,
1647 };
1648 let written = text.get(t.span.start..t.span.end)?;
1649 let canonical = blue_lang_fmt::render_literal(&atom);
1650 (written != canonical).then(|| {
1651 blue_lang_check::Diagnostic::new(
1652 blue_lang_check::Code::B0033,
1653 format!("`{written}` is written `{canonical}`"),
1654 t.span,
1655 )
1656 .at_top_level(0)
1657 })
1658 })
1659 .collect()
1660}
1661
1662#[must_use]
1666pub fn syntax_diagnostic(text: &str) -> Option<blue_lang_check::Diagnostic> {
1667 let e = blue_lang_syntax::parse_program_tree(text).err()?;
1668 Some(
1669 blue_lang_check::Diagnostic::new(blue_lang_check::Code::B0006, e.message, e.span)
1670 .at_top_level(0),
1671 )
1672}
1673
1674#[must_use]
1678pub fn display_keys(message: &str) -> String {
1679 let mut out = String::with_capacity(message.len());
1680 let mut parts = message.split('`');
1681 if let Some(first) = parts.next() {
1682 out.push_str(first);
1683 }
1684 for (i, part) in parts.enumerate() {
1685 out.push('`');
1686 if i % 2 == 0 {
1688 out.push_str(&crate::observe::display_key(part));
1689 } else {
1690 out.push_str(part);
1691 }
1692 }
1693 out
1694}
1695
1696pub(crate) fn eval_prepared<H: 'static>(
1698 interp: &mut tatara_lisp_eval::Interpreter<H>,
1699 prepared: &Prepared,
1700 host: &mut H,
1701) -> Result<Value, RunError> {
1702 let Prepared {
1703 program, erased, ..
1704 } = prepared;
1705
1706 let mut value = Value::Nil;
1720 for (top_level, form) in erased.iter().enumerate() {
1721 value = interp.eval_top_form(form, host).map_err(|e| {
1722 RunError::Eval(Box::new(crate::observe::Fault::of(
1729 program,
1730 erased,
1731 top_level,
1732 &e,
1733 &interp.take_trace(),
1734 )))
1735 })?;
1736 }
1737 Ok(value)
1738}
1739
1740pub const MAIN: &str = "main";
1741
1742#[must_use]
1743pub fn entry_main(program: &crate::uses::ResolvedProgram) -> Option<(usize, tatara_lisp::Span)> {
1744 use crate::uses::ResolvedProgram;
1745 let stored = program
1746 .file(ResolvedProgram::ENTRY)
1747 .is_some_and(|f| f.path.is_some());
1748 if !stored {
1749 return None;
1750 }
1751 program.forms().iter().enumerate().find_map(|(i, f)| {
1752 if program.owner_of(i) != Some(ResolvedProgram::ENTRY) {
1753 return None;
1754 }
1755 let items = f.as_list()?;
1756 let sig = items.get(1)?.as_list()?;
1757 let named_main = matches!(
1758 items.first().and_then(tatara_lisp::Spanned::as_symbol),
1759 Some("define" | "define-typed")
1760 ) && sig.len() == 1
1761 && sig[0].as_symbol() == Some(MAIN);
1762 named_main.then_some((i, sig[0].span))
1763 })
1764}
1765
1766fn main_call(prepared: &Prepared) -> Option<(usize, tatara_lisp::Span, String)> {
1769 let (top_level, span) = entry_main(&prepared.program)?;
1770 let ns = namespace_of(&prepared.program, top_level);
1771 Some((top_level, span, blue_lang_check::names::key(&ns, MAIN)))
1772}
1773
1774fn main_key(prepared: &Prepared) -> Option<String> {
1775 main_call(prepared).map(|(_, _, key)| key)
1776}
1777
1778pub(crate) fn call_main<H: 'static>(
1779 interp: &mut tatara_lisp_eval::Interpreter<H>,
1780 prepared: &Prepared,
1781 host: &mut H,
1782) -> Result<Option<Value>, RunError> {
1783 let Some((top_level, span, key)) = main_call(prepared) else {
1784 return Ok(None);
1785 };
1786 let call = tatara_lisp::Spanned::new(
1787 span,
1788 tatara_lisp::SpannedForm::List(vec![tatara_lisp::Spanned::new(
1789 span,
1790 tatara_lisp::SpannedForm::Atom(tatara_lisp::Atom::Symbol(key)),
1791 )]),
1792 );
1793 interp.eval_top_form(&call, host).map(Some).map_err(|e| {
1794 RunError::Eval(Box::new(crate::observe::Fault::of(
1795 &prepared.program,
1796 &prepared.erased,
1797 top_level,
1798 &e,
1799 &interp.take_trace(),
1800 )))
1801 })
1802}
1803
1804#[cfg(test)]
1820mod position_tests {
1821 use super::*;
1822 use crate::uses::{Entry, Loader};
1823 use std::collections::BTreeMap;
1824 use std::path::Path;
1825
1826 struct MemLoader(BTreeMap<&'static str, &'static str>);
1827
1828 impl Loader for MemLoader {
1829 fn load(&self, name: &str) -> Result<Vec<(String, String)>, String> {
1830 self.0
1831 .get(name)
1832 .map(|s| vec![(format!("{name}.b"), (*s).to_owned())])
1833 .ok_or_else(|| format!("no bidama named \"{name}\""))
1834 }
1835 }
1836
1837 const BAKUHATSU: &str = "\
1845def tsukawanai_a()
1846 1
1847end
1848
1849def tsukawanai_b()
1850 2
1851end
1852
1853# waive B0001: the fixture needs a raise at runtime
1854def bakuhatsu()
1855 kore_wa_sonzai_shinai()
1856end
1857
1858bakuhatsu";
1859
1860 const KOTAE: &str = "def kotae()\n 42\nend";
1861
1862 const BAKUHATSU2: &str =
1865 "# waive B0001: a raise at runtime\ndef yobu()\n kore_wa_sonzai_shinai()\nend";
1866
1867 fn loader() -> MemLoader {
1868 MemLoader(BTreeMap::from([
1869 ("kotae", KOTAE),
1870 ("bakuhatsu", BAKUHATSU),
1871 ("bakuhatsu2", BAKUHATSU2),
1872 ]))
1873 }
1874
1875 fn run_named(path: &str, src: &str) -> RunError {
1876 run_in_surface(
1877 Entry {
1878 path: Some(Path::new(path)),
1879 text: src,
1880 },
1881 Inputs::new(),
1882 &loader(),
1883 None,
1884 )
1885 .expect_err("the fixture must raise")
1886 }
1887
1888 const TWO_IMPORTS: &str =
1933 "use(\"kotae\")\n# waive B0015: kotae loads first on purpose\nuse(\"bakuhatsu\")\n";
1934
1935 #[test]
1936 fn a_raise_inside_an_imported_package_names_that_package() {
1937 let err = run_named("entry.b", TWO_IMPORTS);
1938 assert_eq!(
1939 err.to_string(),
1940 "runtime error: bakuhatsu.b:11:3: unbound symbol `kore_wa_sonzai_shinai`\n \
1941 at bakuhatsu::bakuhatsu (bakuhatsu.b:11:3)\n \
1942 at <top level> (bakuhatsu.b:14:1)"
1943 );
1944 }
1945
1946 #[test]
1954 fn line_11_column_3_of_the_fixture_is_the_failing_call() {
1955 let line = BAKUHATSU.lines().nth(10).expect("line 11 exists");
1956 assert_eq!(line, " kore_wa_sonzai_shinai()");
1957 assert_eq!(
1958 &line[2..],
1959 "kore_wa_sonzai_shinai()",
1960 "column 3 (1-indexed) is where the call starts"
1961 );
1962 let offset = BAKUHATSU.find("kore_wa_sonzai_shinai").expect("in fixture");
1965 assert!(
1966 offset > KOTAE.len(),
1967 "kotae.b could answer for byte {offset}"
1968 );
1969 assert!(
1970 offset > TWO_IMPORTS.len(),
1971 "the entry file could answer for byte {offset}"
1972 );
1973 }
1974
1975 #[test]
1994 fn a_raise_in_the_entry_file_names_the_entry_file() {
1995 let err = run_named(
1996 "honmono.b",
1997 "def f()\n 1\nend\n\n# waive B0001: a raise at runtime\nnani_mo_nai()\n",
1998 );
1999 assert_eq!(
2000 err.to_string(),
2001 "runtime error: honmono.b:6:1: unbound symbol `nani_mo_nai`"
2002 );
2003 }
2004
2005 #[test]
2019 fn a_raise_inside_an_annotated_def_still_names_its_line() {
2020 let err = run_named(
2021 "chuu.b",
2022 "# waive B0001: a raise at runtime\ndef f(a: Int) -> Int\n mada_nai(a)\nend\n\nf(1)\n",
2023 );
2024 assert_eq!(
2025 err.to_string(),
2026 "runtime error: chuu.b:3:3: unbound symbol `mada_nai`\n \
2027 at f (chuu.b:3:3)\n \
2028 at <top level> (chuu.b:6:1)"
2029 );
2030 }
2031
2032 #[test]
2050 fn a_raise_in_a_callee_from_another_file_names_the_callees_file() {
2051 let err = run_named(
2056 "yobidashi.b",
2057 "use(\"bakuhatsu2\")\n# pad: 47's in range\nbakuhatsu2::yobu\n",
2058 );
2059 assert_eq!(
2060 err.to_string(),
2061 "runtime error: bakuhatsu2.b:3:3: unbound symbol `kore_wa_sonzai_shinai`\n \
2062 at bakuhatsu2::yobu (bakuhatsu2.b:3:3)\n \
2063 at <top level> (yobidashi.b:3:1)"
2064 );
2065 }
2066}
2067
2068#[cfg(test)]
2069mod tests {
2070 use super::*;
2071
2072 fn int(src: &str) -> i64 {
2073 match run(src).unwrap_or_else(|e| panic!("{src:?}: {e}")).value {
2074 Value::Int(v) => v,
2075 other => panic!("{src:?} produced {other:?}"),
2076 }
2077 }
2078
2079 #[test]
2082 fn annotating_buys_analysis_and_changes_nothing_else() {
2083 let plain = run("def add(a, b)\n a + b\nend\nadd(2, 3)").expect("plain");
2084 let typed = run("def add(a: Int, b: Int) -> Int\n a + b\nend\nadd(2, 3)").expect("typed");
2085
2086 assert!(matches!(plain.value, Value::Int(5)));
2087 assert!(
2088 matches!(typed.value, Value::Int(5)),
2089 "the annotated program must compute the same answer"
2090 );
2091 assert_eq!(plain.visited, 0, "no annotations means no analysis");
2092 assert!(
2093 typed.visited > 0,
2094 "an annotation must actually buy analysis, not just decorate"
2095 );
2096 assert_eq!(plain.typed_decls, 0);
2097 assert_eq!(typed.typed_decls, 1);
2098 }
2099
2100 #[test]
2104 fn a_type_error_is_reported_and_the_program_does_not_run() {
2105 let err = run("def add(a: Int, b: Int) -> Str\n a + b\nend\nadd(1, 2)")
2106 .expect_err("a declared Str return from an Int body must be rejected");
2107 assert!(
2108 matches!(err, RunError::Types(ref d) if !d.is_empty()),
2109 "expected type diagnostics, got {err}"
2110 );
2111 }
2112
2113 #[test]
2116 fn the_same_program_without_annotations_runs() {
2117 assert_eq!(int("def add(a, b)\n a + b\nend\nadd(1, 2)"), 3);
2118 }
2119
2120 #[test]
2121 fn a_parse_error_is_reported_as_one() {
2122 assert!(matches!(run("def (").unwrap_err(), RunError::Parse(_)));
2123 }
2124
2125 #[test]
2128 fn a_runtime_error_is_reported_as_one() {
2129 let err = run("throw(error(\"boom\"))").expect_err("a raise");
2130 assert!(matches!(err, RunError::Eval(_)), "got {err}");
2131 }
2132
2133 #[test]
2136 fn the_pipeline_reaches_both_runtime_layers() {
2137 assert_eq!(int("6 % 3"), 0);
2138 assert_eq!(int("7 % 3"), 1);
2139 assert_eq!(int("2 + 3 * 4"), 14);
2140 }
2141
2142 #[test]
2152 fn the_deleted_round_trip_agreed_with_the_direct_lowering() {
2153 let corpus = [
2154 "def add(a, b)\n a + b\nend\nadd(2, 3)",
2155 "def fact(n)\n if n < 2\n 1\n else\n n * fact(n - 1)\n end\nend\nfact(5)",
2156 "def f(a, b)\n c = a + b\n c * 2\nend\nf(1, 2)",
2157 "defmacro sq(e)\n quote\n unquote(e) * unquote(e)\n end\nend\nsq(2 + 3)",
2158 "\"a string with spaces, a ( and a )\"",
2159 "def g(a: Int) -> Int\n a + 1\nend\ng(1)",
2160 "6 % 3",
2161 "1.5 + 2.25",
2162 ];
2163 for src in corpus {
2164 let erased = crate::to_sexps(&erase_types(&parse_tree(src).expect("parse")));
2167
2168 let direct: Vec<Sexp> = crate::lower_to_spanned(&erased)
2169 .iter()
2170 .map(tatara_lisp::Spanned::to_sexp)
2171 .collect();
2172 assert_eq!(direct, erased, "the direct lowering must be the identity");
2173
2174 let text = erased
2175 .iter()
2176 .map(ToString::to_string)
2177 .collect::<Vec<_>>()
2178 .join("\n");
2179 let round_tripped: Vec<Sexp> = tatara_lisp::read_spanned(&text)
2180 .unwrap_or_else(|e| panic!("{src:?}: the old path could not read back: {e:?}"))
2181 .iter()
2182 .map(tatara_lisp::Spanned::to_sexp)
2183 .collect();
2184 assert_eq!(
2185 round_tripped, erased,
2186 "{src:?}: the old print-and-reparse path changed the tree"
2187 );
2188 }
2189 }
2190
2191 #[test]
2209 fn the_round_trip_is_not_the_identity_in_general() {
2210 let tree = Sexp::List(vec![
2211 Sexp::Atom(tatara_lisp::Atom::Symbol("f".into())),
2212 Sexp::Atom(tatara_lisp::Atom::Symbol("a b".into())),
2213 ]);
2214 let text = tree.to_string();
2215 let back: Vec<Sexp> = tatara_lisp::read_spanned(&text)
2216 .expect("it reads back cleanly — that IS the problem")
2217 .iter()
2218 .map(tatara_lisp::Spanned::to_sexp)
2219 .collect();
2220 assert_ne!(
2221 back,
2222 vec![tree.clone()],
2223 "if print-then-read became inverse over symbols, the class would be \
2224 closed upstream and this test should be deleted rather than relaxed"
2225 );
2226 let direct: Vec<Sexp> = crate::lower_to_spanned(std::slice::from_ref(&tree))
2228 .iter()
2229 .map(tatara_lisp::Spanned::to_sexp)
2230 .collect();
2231 assert_eq!(direct, vec![tree]);
2232 }
2233}
2234
2235#[cfg(test)]
2236mod macro_tests {
2237 use super::*;
2238
2239 fn int(src: &str) -> i64 {
2240 match run(src).unwrap_or_else(|e| panic!("{src:?}: {e}")).value {
2241 Value::Int(v) => v,
2242 other => panic!("{src:?} produced {other:?}"),
2243 }
2244 }
2245
2246 #[test]
2248 fn a_macro_expands_and_runs() {
2249 assert_eq!(
2250 int("defmacro double(x)\n quote\n unquote(x) + unquote(x)\n end\nend\ndouble(21)"),
2251 42
2252 );
2253 }
2254
2255 #[test]
2258 fn a_macro_operates_on_syntax_not_values() {
2259 assert_eq!(
2260 int("defmacro sq(e)\n quote\n unquote(e) * unquote(e)\n end\nend\nsq(2 + 3)"),
2261 25,
2262 "the argument form `2 + 3` must be substituted twice"
2263 );
2264 }
2265
2266 #[test]
2285 fn a_runaway_macro_fails_the_compilation_rather_than_the_process() {
2286 let err =
2287 run("defmacro forever(x)\n quote\n forever(unquote(x))\n end\nend\nforever(1)")
2288 .expect_err("a self-referential macro must be rejected");
2289 let msg = err.to_string();
2290 assert!(
2291 msg.contains("forever") && msg.contains("expansion"),
2292 "the error must name the macro and the limit it hit: {msg}"
2293 );
2294 assert!(
2295 msg.contains("<anonymous>:6:1"),
2296 "and must place the call that ran away — line 6 is `forever(1)`: {msg}"
2297 );
2298 }
2299}
2300
2301#[cfg(test)]
2302mod input_tests {
2303 use super::*;
2304 use crate::inputs::{Declaration, Inputs};
2305
2306 const SCHEMA: &[u8] = b"3";
2308
2309 fn with_schema(src: &str) -> Result<Run, RunError> {
2310 let hash = Inputs::hash_of(SCHEMA);
2311 let mut inputs = Inputs::new();
2312 inputs
2313 .bind(
2314 &Declaration {
2315 name: "schema".to_string(),
2316 hash,
2317 },
2318 SCHEMA.to_vec(),
2319 )
2320 .expect("bind");
2321 run_with_inputs(src, inputs)
2322 }
2323
2324 fn decl_line() -> String {
2325 let mut s = String::from("definput(\"schema\", \"");
2326 s.push_str(&Inputs::hash_of(SCHEMA));
2327 s.push_str("\")\n");
2328 s
2329 }
2330
2331 #[test]
2336 fn a_macro_can_read_a_declared_build_input() {
2337 let src = decl_line() + "input(\"schema\")";
2338 let out = with_schema(&src).expect("run");
2339 assert!(
2340 matches!(out.value, Value::Str(ref s) if &**s == "3"),
2341 "got {:?}",
2342 out.value
2343 );
2344 }
2345
2346 #[test]
2350 fn an_undeclared_input_is_an_error() {
2351 let err = with_schema("input(\"not_declared\")").expect_err("must fail");
2352 let msg = err.to_string();
2353 assert!(msg.contains("not_declared"), "must name it: {msg}");
2354 assert!(msg.contains("definput"), "and say how to declare it: {msg}");
2355 }
2356
2357 #[cfg(not(feature = "sys"))]
2365 #[test]
2366 fn there_is_no_ambient_file_read() {
2367 for attempt in [
2368 "read_file(\"/etc/passwd\")",
2369 "File(\"/etc/passwd\")",
2370 "slurp(\"/etc/passwd\")",
2371 "open(\"/etc/passwd\")",
2372 ] {
2373 let err = with_schema(attempt).expect_err("must not resolve");
2374 assert!(
2375 err.to_string().contains("unbound"),
2376 "{attempt} must be UNBOUND — a capability removed by absence, \
2377 not guarded by a check: {err}"
2378 );
2379 }
2380 }
2381
2382 #[cfg(feature = "sys")]
2388 #[test]
2389 fn sys_read_file_is_the_trusted_cli_only_exception() {
2390 let err = with_schema("definitely_not_a_primitive(\"x\")").expect_err("must not resolve");
2391 assert!(err.to_string().contains("unbound"), "{err}");
2392 assert!(
2393 with_schema("read_file(\"/etc/passwd\")").is_ok(),
2394 "with `sys` on, read_file is the trusted CLI surface"
2395 );
2396 let out = with_schema("input(\"schema\")").expect("run");
2397 assert!(
2398 matches!(out.value, Value::Str(ref s) if &**s == "3"),
2399 "input() still binds beside the sys surface: {:?}",
2400 out.value
2401 );
2402 }
2403
2404 #[test]
2407 fn a_declared_input_with_no_bytes_supplied_fails() {
2408 let src = decl_line() + "input(\"schema\")";
2409 assert!(run(&src).is_err(), "no bytes were supplied");
2410 }
2411}
2412
2413#[cfg(test)]
2414mod tier2_tests {
2415 use super::*;
2416 use crate::inputs::{Declaration, Inputs};
2417
2418 #[test]
2430 fn a_macro_generates_code_from_a_schema() {
2431 let schema = b"7";
2432 let mut inputs = Inputs::new();
2433 inputs
2434 .bind(
2435 &Declaration {
2436 name: "arity".to_string(),
2437 hash: Inputs::hash_of(schema),
2438 },
2439 schema.to_vec(),
2440 )
2441 .expect("bind");
2442
2443 let mut src = String::from("definput(\"arity\", \"");
2446 src.push_str(&Inputs::hash_of(schema));
2447 src.push_str("\")\n");
2448 src.push_str(
2449 "defmacro from_schema()\n quote\n unquote(to_int(input(\"arity\")))\n end\nend\n\
2450 from_schema() * 6",
2451 );
2452
2453 let out = run_with_inputs(&src, inputs).expect("run");
2454 assert!(
2455 matches!(out.value, Value::Int(42)),
2456 "the schema's 7 must reach the generated code: got {:?}",
2457 out.value
2458 );
2459 }
2460}