1use tatara_lisp::Sexp;
17use tatara_lisp_eval::Value;
18
19use blue_lang_check::{NameTable, Namespace};
20
21use crate::erase::erase_types;
22use crate::inputs::Inputs;
23use crate::uses::Entry;
24
25#[derive(Debug, thiserror::Error)]
27pub enum RunError {
28 #[error("parse error: {0}")]
29 Parse(String),
30 #[error("{} error(s) in the check stage:\n{}", .0.len(), .0.join("\n"))]
42 Types(Vec<String>),
43 #[error("the emitted tatara-lisp could not be read back: {0}")]
55 Lower(String),
56 #[error("runtime error: {0}")]
65 Eval(Box<crate::observe::Fault>),
66 #[error("import error: {0}")]
73 Import(String),
74}
75
76impl RunError {
77 #[must_use]
80 pub fn exit_status(&self) -> u8 {
81 match self {
82 RunError::Eval(fault) if fault.breach => crate::BREACH_EXIT,
83 _ => 1,
84 }
85 }
86}
87
88#[derive(Debug)]
90pub struct Run {
91 pub value: Value,
92 pub visited: usize,
96 pub typed_decls: usize,
98 pub seams: usize,
100}
101
102pub fn parse(src: &str) -> Result<Vec<Sexp>, RunError> {
104 parse_with_depth(src, blue_lang_syntax::MAX_EXPR_DEPTH)
105}
106
107pub fn parse_with_depth(src: &str, max_depth: usize) -> Result<Vec<Sexp>, RunError> {
114 blue_lang_syntax::parse_program_with_depth(src, max_depth)
115 .map_err(|e| RunError::Parse(e.to_string()))
116}
117
118pub fn parse_tree_with_depth(
126 src: &str,
127 max_depth: usize,
128) -> Result<Vec<blue_lang_syntax::Spanned>, RunError> {
129 blue_lang_syntax::parse_program_tree_with_depth(src, max_depth)
130 .map_err(|e| RunError::Parse(e.to_string()))
131}
132
133pub fn parse_tree(src: &str) -> Result<Vec<blue_lang_syntax::Spanned>, RunError> {
140 parse_tree_with_depth(src, blue_lang_syntax::MAX_EXPR_DEPTH)
141}
142
143pub fn run(src: &str) -> Result<Run, RunError> {
145 run_with_inputs(src, Inputs::new())
146}
147
148pub fn run_with_inputs(src: &str, inputs: Inputs) -> Result<Run, RunError> {
154 run_with_loader(src, inputs, &crate::uses::NoLoader)
155}
156
157pub fn run_with_loader(
165 src: &str,
166 inputs: Inputs,
167 loader: &dyn crate::uses::Loader,
168) -> Result<Run, RunError> {
169 run_in_surface(Entry::anonymous(src), inputs, loader, None)
170}
171
172pub fn run_in_surface(
189 entry: Entry<'_>,
190 inputs: Inputs,
191 loader: &dyn crate::uses::Loader,
192 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
193) -> Result<Run, RunError> {
194 run_framed(entry, inputs, loader, surface, None)
195}
196
197pub fn run_framed(
208 entry: Entry<'_>,
209 inputs: Inputs,
210 loader: &dyn crate::uses::Loader,
211 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
212 hold: Option<&crate::Hold>,
213) -> Result<Run, RunError> {
214 run_framed_accounted(
215 entry,
216 inputs,
217 loader,
218 surface,
219 hold,
220 &crate::Account::default(),
221 )
222}
223
224pub fn run_framed_accounted(
231 entry: Entry<'_>,
232 inputs: Inputs,
233 loader: &dyn crate::uses::Loader,
234 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
235 hold: Option<&crate::Hold>,
236 account: &crate::Account,
237) -> Result<Run, RunError> {
238 let (mut interp, mut prepared) = prepared_on(entry, inputs, loader, surface)?;
243 hold_to_frame(&mut interp, &mut prepared, hold, account)?;
244 let value = evaluate_held(&mut interp, &prepared, hold, account)?;
245 Ok(Run {
246 value,
247 visited: prepared.visited,
248 typed_decls: prepared.typed_decls,
249 seams: prepared.seams,
250 })
251}
252
253fn evaluate_held(
256 interp: &mut tatara_lisp_eval::Interpreter<()>,
257 prepared: &Prepared,
258 hold: Option<&crate::Hold>,
259 account: &crate::Account,
260) -> Result<Value, RunError> {
261 let bound = crate::execution_bounds_under(hold).max_alloc_bytes;
262 let (outcome, over) = crate::memory::held_on(interp, bound, |interp| {
263 let value = crate::timings::stage(crate::timings::Stage::Eval, || {
264 eval_prepared(interp, prepared, &mut ())
265 })?;
266 Ok(crate::timings::stage(crate::timings::Stage::Main, || {
267 call_main(interp, prepared, &mut ())
268 })?
269 .unwrap_or(value))
270 });
271 if let Some(steps) = &account.steps {
272 steps.store(
273 u64::try_from(interp.steps_spent()).unwrap_or(u64::MAX),
274 std::sync::atomic::Ordering::Relaxed,
275 );
276 }
277 refused_past(outcome, over)
278}
279
280fn refused_past<T>(outcome: Result<T, RunError>, over: Option<u64>) -> Result<T, RunError> {
283 match (outcome, over) {
284 (Ok(_), Some(bound)) => Err(RunError::Eval(Box::new(crate::observe::Fault::unplaced(
285 &crate::memory::refusal(bound),
286 &[],
287 )))),
288 (outcome, _) => outcome,
289 }
290}
291
292fn hold_to_frame(
298 interp: &mut tatara_lisp_eval::Interpreter<()>,
299 prepared: &mut Prepared,
300 hold: Option<&crate::Hold>,
301 account: &crate::Account,
302) -> Result<(), RunError> {
303 if account.defer_identities(
304 &prepared.program,
305 std::sync::Arc::clone(&prepared.lowered),
306 main_key(prepared),
307 ) {
308 let places = places_of(&prepared.program, &prepared.erased);
309 account.places.defer(move || places);
310 }
311 let Some(hold) = hold else {
312 if account.identify.is_some() {
313 crate::attach_node_accounted(interp, None, account);
314 }
315 return Ok(());
316 };
317 let withheld = hold.rebind_scoped(&prepared.program, &mut prepared.erased);
318 let escapes = entry_escapes(&prepared.program, &withheld, &|cap| {
319 format!("this run does not allow; pass --allow {cap} to grant it")
320 });
321 if !escapes.is_empty() {
322 return Err(RunError::Types(escapes));
323 }
324 hold.bind(interp, "this run's frame", &account.host_io);
325 crate::attach_node_accounted(interp, Some(hold), account);
326 Ok(())
327}
328
329fn places_of(
334 program: &crate::uses::ResolvedProgram,
335 erased: &[tatara_lisp::Spanned],
336) -> std::collections::BTreeMap<String, blue_lang_proc::Source> {
337 let files: Vec<blue_lang_proc::Source> = program
338 .files()
339 .iter()
340 .map(|f| blue_lang_proc::Source {
341 file: f
342 .path
343 .as_ref()
344 .map_or_else(|| "<anonymous>".to_string(), |p| p.display().to_string()),
345 text: std::sync::Arc::from(f.text.as_str()),
346 })
347 .collect();
348 erased
349 .iter()
350 .enumerate()
351 .filter_map(|(i, form)| {
352 let key = blue_lang_proc::violation::defined_key(form)?;
353 let source = files.get(program.owner_of(i)?.index())?;
354 Some((key.to_string(), source.clone()))
355 })
356 .collect()
357}
358
359fn entry_escapes(
360 program: &crate::uses::ResolvedProgram,
361 refused: &[(&'static str, blue_lang_waku::Capability)],
362 grant: &dyn Fn(&str) -> String,
363) -> Vec<String> {
364 use crate::uses::ResolvedProgram;
365 let forms: Vec<tatara_lisp::Spanned> = program
366 .forms()
367 .iter()
368 .enumerate()
369 .filter(|(i, _)| program.owner_of(*i) == Some(ResolvedProgram::ENTRY))
370 .map(|(_, f)| f.clone())
371 .collect();
372 let label = |name: &str| {
373 refused
374 .iter()
375 .find(|(n, _)| *n == name)
376 .map(|(_, c)| c.label())
377 };
378 blue_lang_waku::free_names(&forms, &|name| label(name).is_none())
379 .into_iter()
380 .filter_map(|name| {
381 let cap = label(&name)?;
382 let at = program
383 .file(ResolvedProgram::ENTRY)
384 .and_then(|f| f.path.clone())
385 .map_or_else(|| "<entry>".to_string(), |p| p.display().to_string());
386 Some(format!(
387 "{at}: refused: `{name}` reaches the host ({cap}), which {}",
388 grant(cap)
389 ))
390 })
391 .collect()
392}
393
394fn runtime(inputs: Inputs) -> tatara_lisp_eval::Interpreter<()> {
396 let mut interp =
397 crate::timings::stage(crate::timings::Stage::Runtime, crate::interpreter_hostless);
398 crate::inputs::install_input_primitives(&mut interp, inputs);
399 interp
400}
401
402fn prepared_on(
404 entry: Entry<'_>,
405 inputs: Inputs,
406 loader: &dyn crate::uses::Loader,
407 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
408) -> Result<(tatara_lisp_eval::Interpreter<()>, Prepared), RunError> {
409 let interp = runtime(inputs);
410 let builtins = crate::timings::stage(crate::timings::Stage::Names, || builtin_names(&interp));
411 let prepared = prepare(entry, loader, surface, &builtins)?;
412 Ok((interp, prepared))
413}
414
415pub struct Compiled {
421 pub program: crate::uses::ResolvedProgram,
422 pub lowered: Vec<tatara_lisp::Spanned>,
423 pub erased: Vec<tatara_lisp::Spanned>,
424 pub main: Option<String>,
425}
426
427pub fn compile(
435 entry: Entry<'_>,
436 loader: &dyn crate::uses::Loader,
437 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
438 hold: Option<&crate::Hold>,
439) -> Result<Compiled, RunError> {
440 let (_, prepared) = prepared_on(entry, Inputs::new(), loader, surface)?;
441 if let Some(hold) = hold {
442 let escapes = entry_escapes(&prepared.program, &hold.withheld_host_names(), &|cap| {
443 format!("this bundle's policy does not allow; declare allow(%i[{cap}]) in the project's Bluefile and run `blue lock`")
444 });
445 if !escapes.is_empty() {
446 return Err(RunError::Types(escapes));
447 }
448 }
449 let main = main_key(&prepared);
450 let Prepared {
451 program,
452 lowered,
453 erased,
454 ..
455 } = prepared;
456 Ok(Compiled {
457 program,
458 lowered: std::sync::Arc::unwrap_or_clone(lowered),
459 erased,
460 main,
461 })
462}
463
464pub fn run_compiled(
474 erased: &[tatara_lisp::Spanned],
475 main: Option<&str>,
476 hold: &crate::Hold,
477 who: &'static str,
478) -> Result<Value, RunError> {
479 let mut interp = runtime(Inputs::new());
480 let account = crate::Account::default();
481 hold.bind(&mut interp, who, &account.host_io);
482 crate::attach_node_accounted(&mut interp, Some(hold), &account);
483 let fault = |e: tatara_lisp_eval::EvalError| {
484 RunError::Eval(Box::new(crate::observe::Fault::unplaced(
485 &crate::memory::named(e),
486 erased,
487 )))
488 };
489 let bound = hold.execution_bounds().max_alloc_bytes;
490 let (outcome, over) = crate::memory::held_on(&mut interp, bound, |interp| {
491 let mut value = Value::Nil;
492 for form in erased {
493 value = interp.eval_top_form(form, &mut ()).map_err(fault)?;
494 }
495 if let Some(key) = main {
496 let at = tatara_lisp::Span::synthetic();
497 let call = tatara_lisp::Spanned::new(
498 at,
499 tatara_lisp::SpannedForm::List(vec![tatara_lisp::Spanned::new(
500 at,
501 tatara_lisp::SpannedForm::Atom(tatara_lisp::Atom::Symbol(key.to_string())),
502 )]),
503 );
504 value = interp.eval_top_form(&call, &mut ()).map_err(fault)?;
505 }
506 Ok(value)
507 });
508 refused_past(outcome, over)
509}
510
511pub struct Observed {
514 pub definitions: std::sync::Arc<crate::observe::Definitions>,
515 pub outcome: Result<Run, RunError>,
517}
518
519pub struct Watched<'a> {
523 pub definitions: &'a std::sync::Arc<crate::observe::Definitions>,
524 pub program: &'a crate::uses::ResolvedProgram,
525 pub lowered: &'a [tatara_lisp::Spanned],
526 pub erased: &'a [tatara_lisp::Spanned],
527}
528
529pub fn run_observed(
540 entry: Entry<'_>,
541 inputs: Inputs,
542 loader: &dyn crate::uses::Loader,
543 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
544 watch: impl FnOnce(&Watched<'_>) -> std::sync::Arc<dyn crate::observe::CallObserver>,
545) -> Result<Observed, RunError> {
546 run_observed_framed(entry, inputs, loader, surface, None, watch)
547}
548
549pub fn run_observed_framed(
556 entry: Entry<'_>,
557 inputs: Inputs,
558 loader: &dyn crate::uses::Loader,
559 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
560 hold: Option<&crate::Hold>,
561 watch: impl FnOnce(&Watched<'_>) -> std::sync::Arc<dyn crate::observe::CallObserver>,
562) -> Result<Observed, RunError> {
563 run_observed_accounted(
564 entry,
565 inputs,
566 loader,
567 surface,
568 hold,
569 &crate::Account::default(),
570 watch,
571 )
572}
573
574pub fn run_observed_accounted(
581 entry: Entry<'_>,
582 inputs: Inputs,
583 loader: &dyn crate::uses::Loader,
584 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
585 hold: Option<&crate::Hold>,
586 account: &crate::Account,
587 watch: impl FnOnce(&Watched<'_>) -> std::sync::Arc<dyn crate::observe::CallObserver>,
588) -> Result<Observed, RunError> {
589 let (mut interp, mut prepared) = prepared_on(entry, inputs, loader, surface)?;
590 hold_to_frame(&mut interp, &mut prepared, hold, account)?;
591 let definitions = std::sync::Arc::new(crate::observe::Definitions::of(
592 &prepared.program,
593 &prepared.erased,
594 ));
595 interp.set_call_observer(Some(watch(&Watched {
596 definitions: &definitions,
597 program: &prepared.program,
598 lowered: &prepared.lowered,
599 erased: &prepared.erased,
600 })));
601 let outcome = evaluate_held(&mut interp, &prepared, hold, account).map(|value| Run {
602 value,
603 visited: prepared.visited,
604 typed_decls: prepared.typed_decls,
605 seams: prepared.seams,
606 });
607 interp.set_call_observer(None);
608 Ok(Observed {
609 definitions,
610 outcome,
611 })
612}
613
614pub(crate) struct Prepared {
619 program: crate::uses::ResolvedProgram,
620 lowered: std::sync::Arc<Vec<tatara_lisp::Spanned>>,
621 erased: Vec<tatara_lisp::Spanned>,
622 visited: usize,
623 typed_decls: usize,
624 seams: usize,
625}
626
627impl Prepared {
628 pub(crate) fn erased(&self) -> &[tatara_lisp::Spanned] {
630 &self.erased
631 }
632
633 pub(crate) fn entry_namespace(&self) -> Namespace {
635 let first = (0..self.program.forms().len())
636 .find(|i| self.program.owner_of(*i) == Some(crate::uses::ResolvedProgram::ENTRY));
637 match first {
638 Some(i) => namespace_of(&self.program, i),
639 None => match self
640 .program
641 .file(crate::uses::ResolvedProgram::ENTRY)
642 .and_then(|f| f.package.clone())
643 {
644 Some(p) => Namespace::Bidama(p),
645 None => Namespace::File(String::new()),
646 },
647 }
648 }
649}
650
651pub(crate) fn prepare(
657 entry: Entry<'_>,
658 loader: &dyn crate::uses::Loader,
659 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
660 builtins: &NameTable,
661) -> Result<Prepared, RunError> {
662 let mut program = parse_and_resolve(entry, loader, surface)?;
663
664 program.retain(|f| !crate::uses::is_test_form(f));
677
678 let (outcome, names) = crate::timings::stage(crate::timings::Stage::Check, || {
693 check_stage(&program, builtins, false)
694 });
695 if !outcome.ok() {
696 return Err(RunError::Types(
697 outcome
698 .errors()
699 .map(|d| render(&program, d))
702 .collect(),
703 ));
704 }
705
706 let (lowered, erased) = crate::timings::stage(crate::timings::Stage::Erase, || {
720 let lowered = lower(&program, &names);
721 let erased = erase_types(&lowered);
722 (lowered, erased)
723 });
724
725 Ok(Prepared {
726 program,
727 lowered: std::sync::Arc::new(lowered),
728 erased,
729 visited: outcome.stats.visited,
730 typed_decls: outcome.stats.typed_decls,
731 seams: outcome.seams.len(),
732 })
733}
734
735pub(crate) fn parse_and_resolve(
737 entry: Entry<'_>,
738 loader: &dyn crate::uses::Loader,
739 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
740) -> Result<crate::uses::ResolvedProgram, RunError> {
741 let forms = crate::timings::stage(crate::timings::Stage::Parse, || {
742 let mut forms = match surface {
743 Some(pack) => blue_lang_syntax::parse_program_tree_in(entry.text, pack)
744 .map_err(|e| RunError::Parse(e.to_string()))?,
745 None => loader
746 .parse(entry.text)
747 .map_err(|e| RunError::Parse(e.to_string()))?,
748 };
749 if surface.is_none() {
752 forms.extend(
753 blue_lang_syntax::doc::doctests(entry.text)
754 .map_err(|e| RunError::Parse(e.to_string()))?,
755 );
756 }
757 Ok::<_, RunError>(forms)
758 })?;
759
760 crate::timings::stage(crate::timings::Stage::Resolve, || {
771 crate::uses::resolve_uses(forms, entry, loader).map_err(RunError::Import)
772 })
773}
774
775pub(crate) fn check_stage(
788 program: &crate::uses::ResolvedProgram,
789 builtins: &NameTable,
790 with_tests: bool,
791) -> (blue_lang_check::Outcome, NameTable) {
792 use crate::uses::ResolvedProgram;
793 let forms = program.forms();
794 let is_entry = |i: usize| program.owner_of(i) == Some(ResolvedProgram::ENTRY);
795
796 let mut outcome = blue_lang_check::check_program_in(forms, &|a, b| {
798 program.owner_of(a) == program.owner_of(b)
799 });
800 outcome
801 .diagnostics
802 .extend(data_law_diagnostics(program, &is_entry));
803
804 let table = program_names(program, builtins);
807 let whole = |i: usize| with_tests && is_entry(i);
808 let (names, resolved) = blue_lang_check::names::check_names_whole(
809 forms,
810 &table,
811 &|i| namespace_of(program, i),
812 &is_entry,
813 &whole,
814 );
815 let stored = program
816 .file(ResolvedProgram::ENTRY)
817 .is_some_and(|f| f.path.is_some());
818 outcome.diagnostics.extend(names);
819 outcome.stats.names_resolved = resolved;
820 if stored {
821 let script = program
822 .file(ResolvedProgram::ENTRY)
823 .is_some_and(|f| f.package.is_none());
824 let found = blue_lang_check::form_rules::check(forms, &is_entry, script);
825 let silenced: Vec<tatara_lisp::Span> = found
826 .iter()
827 .filter(|d| d.code == blue_lang_check::Code::B0038)
828 .map(|d| d.span)
829 .collect();
830 outcome
831 .diagnostics
832 .retain(|d| !(d.code == blue_lang_check::Code::B0002 && silenced.contains(&d.span)));
833 outcome.diagnostics.extend(found);
834 }
835 outcome
836 .diagnostics
837 .extend(blue_lang_check::contracts::check(
838 forms,
839 &table,
840 &|i| namespace_of(program, i),
841 &is_entry,
842 &|i| {
843 program
844 .owner_of(i)
845 .and_then(|f| program.file(f))
846 .map(|f| f.text.as_str())
847 },
848 crate::docs::signatures(),
849 ));
850
851 outcome.diagnostics.extend(doc_diagnostics(program));
852
853 let mut waivers = Vec::new();
856 for file in program.files() {
857 let spans: Vec<(usize, tatara_lisp::Span)> = forms
858 .iter()
859 .enumerate()
860 .filter(|(i, _)| program.owner_of(*i) == Some(file.id))
861 .map(|(i, f)| (i, f.span))
862 .collect();
863 let (found, malformed) = blue_lang_check::waiver::collect(&file.text, &spans);
864 waivers.extend(found);
865 outcome.diagnostics.extend(malformed);
866 }
867 outcome.waived = blue_lang_check::waiver::apply(&mut outcome.diagnostics, waivers, &is_entry);
868 if !stored {
869 outcome.diagnostics.retain(|d| {
870 !matches!(
871 d.code,
872 blue_lang_check::Code::B0023
873 | blue_lang_check::Code::B0036
874 | blue_lang_check::Code::B0037
875 | blue_lang_check::Code::B0067
876 | blue_lang_check::Code::B0061
877 | blue_lang_check::Code::B0062
878 | blue_lang_check::Code::B0063
879 )
880 });
881 }
882 let judge = blue_lang_check::evolution::FormJudge::from_env().unwrap_or_default();
883 outcome
884 .diagnostics
885 .retain(|d| judge.reports(d.code, is_entry(d.top_level)));
886 let (enforced, census): (Vec<_>, Vec<_>) = std::mem::take(&mut outcome.diagnostics)
889 .into_iter()
890 .partition(|d| d.code.rule().active());
891 outcome.diagnostics = enforced;
892 outcome.census = census;
893 outcome
894 .diagnostics
895 .sort_by_key(|d| (d.top_level, d.span.start, d.code));
896 (outcome, table)
897}
898
899fn doc_diagnostics(program: &crate::uses::ResolvedProgram) -> Vec<blue_lang_check::Diagnostic> {
903 use crate::uses::ResolvedProgram;
904 let Some(file) = program.file(ResolvedProgram::ENTRY) else {
905 return Vec::new();
906 };
907 let Some(package) = file.package.as_deref() else {
908 return Vec::new();
909 };
910 let forms: Vec<(usize, &tatara_lisp::Spanned)> = program
911 .forms()
912 .iter()
913 .enumerate()
914 .filter(|(i, f)| {
915 program.owner_of(*i) == Some(ResolvedProgram::ENTRY)
916 && !blue_lang_syntax::doc::is_doctest(f)
917 })
918 .collect();
919 let graph = file.path.as_deref().map(crate::doc_sources::graph_meanings);
920 let meaning = |name: &str| {
921 graph
922 .as_ref()
923 .and_then(|g| g.get(&(package.to_string(), name.to_string())).cloned())
924 };
925 let names_line = |name: &str| crate::docs::doc_of(name).map(|d| d.doc.to_string());
926 blue_lang_check::doc_rules::check_file(
927 &file.text,
928 &forms,
929 package,
930 &blue_lang_check::doc_rules::Sources {
931 meaning: &meaning,
932 names_line: &names_line,
933 },
934 )
935}
936
937fn data_law_diagnostics(
938 program: &crate::uses::ResolvedProgram,
939 is_entry: &dyn Fn(usize) -> bool,
940) -> Vec<blue_lang_check::Diagnostic> {
941 use blue_lang_check::data_laws;
942 let forms = program.forms();
943 let fragments = data_laws::Fragments::collect(forms);
944 let frontend = program.frontend();
945 let mut out = Vec::new();
946 let schema = match frontend.and_then(|f| f.table.as_ref()) {
947 Some((label, text)) => match data_laws::Schema::read(text) {
948 Ok(s) => Some(s),
949 Err(why) => {
950 if let Some((i, first)) = forms.iter().enumerate().find(|(i, _)| is_entry(*i)) {
951 out.push(
952 blue_lang_check::Diagnostic::new(
953 blue_lang_check::Code::B0050,
954 format!("the generated schema table {label} does not read: {why}"),
955 first.span,
956 )
957 .at_top_level(i)
958 .with_help("regenerate it: `nix run .#regen`"),
959 );
960 }
961 None
962 }
963 },
964 None => None,
965 };
966 if fragments.is_empty() && schema.is_none() && !frontend.is_some_and(|f| f.view) {
967 return out;
968 }
969 out.extend(data_laws::check(
970 forms,
971 &data_laws::Context {
972 fragments: &fragments,
973 schema: schema.as_ref(),
974 is_entry,
975 view: frontend.is_some_and(|f| f.view),
976 },
977 ));
978 out
979}
980
981#[must_use]
994pub fn program_names(program: &crate::uses::ResolvedProgram, builtins: &NameTable) -> NameTable {
995 use blue_lang_check::Binding;
996 let mut table = NameTable::new();
997 table.add_program(program.forms(), |i| namespace_of(program, i));
998 let builtin_macro = |head: &str| {
999 builtins
1000 .scopes()
1001 .iter()
1002 .any(|s| s.namespace == Namespace::Macro && s.get(head).is_some())
1003 };
1004 let mut expander: Option<tatara_lisp_eval::Interpreter<()>> = None;
1005 for (i, form) in program.forms().iter().enumerate() {
1006 let Some(head) = form
1007 .as_list()
1008 .and_then(|items| items.first())
1009 .and_then(tatara_lisp::Spanned::as_symbol)
1010 else {
1011 continue;
1012 };
1013 if !builtin_macro(head) {
1014 continue;
1015 }
1016 let interp = expander.get_or_insert_with(crate::interpreter_hostless);
1017 let Ok(expanded) = interp.fully_expand(form, &mut ()) else {
1020 continue;
1021 };
1022 let nullary = blue_lang_syntax::scope::nullary_definition(&expanded);
1023 for (name, span, kind) in blue_lang_check::names::definitions_of(&expanded) {
1024 table.define(
1025 namespace_of(program, i),
1026 Binding {
1027 nullary: nullary == Some(name.as_str()),
1028 name,
1029 kind,
1030 span: Some(span),
1031 top_level: Some(i),
1032 },
1033 );
1034 }
1035 }
1036 for scope in builtins.scopes() {
1037 table.push(scope.clone());
1038 }
1039 table.spell_bare_like(builtins);
1040 let form_file: Vec<usize> = (0..program.forms().len())
1041 .map(|i| program.owner_of(i).map_or(usize::MAX, |f| f.index()))
1042 .collect();
1043 let mut imports: std::collections::BTreeMap<usize, blue_lang_check::names::FileImports> =
1044 std::collections::BTreeMap::new();
1045 for (file, import) in program.imports() {
1046 let entry = imports.entry(file.index()).or_default();
1047 entry.add(&import.package, import.names.iter().map(|(n, _)| n.clone()));
1048 let at = import.span.start;
1049 entry.use_at = Some(entry.use_at.map_or(at, |a| a.min(at)));
1050 }
1051 let facade_uses =
1056 |pkg: &str,
1057 imports: &std::collections::BTreeMap<usize, blue_lang_check::names::FileImports>| {
1058 let defines = table.bidama(pkg).is_some_and(|s| !s.is_empty());
1059 if defines {
1060 return std::collections::BTreeSet::new();
1061 }
1062 program
1063 .files()
1064 .iter()
1065 .filter(|f| f.package.as_deref() == Some(pkg))
1066 .filter_map(|f| imports.get(&f.id.index()))
1067 .flat_map(|i| i.uses.iter().cloned())
1068 .collect::<std::collections::BTreeSet<String>>()
1069 };
1070 loop {
1071 let mut grew = false;
1072 let files: Vec<usize> = imports.keys().copied().collect();
1073 for f in files {
1074 let used: Vec<String> = imports[&f].uses.iter().cloned().collect();
1075 for p in used {
1076 for member in facade_uses(&p, &imports) {
1077 if let Some(i) = imports.get_mut(&f) {
1078 if i.uses.insert(member.clone()) {
1079 i.via.insert(member, p.clone());
1080 grew = true;
1081 }
1082 }
1083 }
1084 }
1085 }
1086 if !grew {
1087 break;
1088 }
1089 }
1090 let mut pkg_uses: std::collections::BTreeMap<String, std::collections::BTreeSet<String>> =
1091 std::collections::BTreeMap::new();
1092 for (file, import) in program.imports() {
1093 if let Some(p) = program.file(*file).and_then(|f| f.package.clone()) {
1094 pkg_uses
1095 .entry(p)
1096 .or_default()
1097 .insert(import.package.clone());
1098 }
1099 }
1100 table.attach_files(form_file, imports);
1101 table.attach_package_uses(pkg_uses);
1102 table.attach_needs(program.needs().clone());
1103 let canonical = |p: &std::path::Path| {
1107 std::fs::canonicalize(p)
1108 .unwrap_or_else(|_| p.to_path_buf())
1109 .display()
1110 .to_string()
1111 };
1112 let mut file_paths = std::collections::BTreeMap::new();
1113 let mut pkg_roots = std::collections::BTreeMap::new();
1114 for f in program.files() {
1115 if let Some(path) = &f.path {
1116 file_paths.insert(f.id.index(), canonical(path));
1117 if let (Some(pkg), Some(root)) =
1118 (&f.package, path.parent().and_then(std::path::Path::parent))
1119 {
1120 pkg_roots
1121 .entry(pkg.clone())
1122 .or_insert_with(|| canonical(root));
1123 }
1124 }
1125 }
1126 table.attach_paths(file_paths, pkg_roots);
1127 let versions = program.versions().clone();
1128 table.apply_legacy(program.forms(), &|i| namespace_of(program, i), &|p| {
1129 versions.get(p).cloned()
1130 });
1131 table
1132}
1133
1134#[must_use]
1146pub fn lower(
1147 program: &crate::uses::ResolvedProgram,
1148 names: &NameTable,
1149) -> Vec<tatara_lisp::Spanned> {
1150 lower_with(program, names, true)
1151}
1152
1153#[must_use]
1154pub fn lower_with(
1155 program: &crate::uses::ResolvedProgram,
1156 names: &NameTable,
1157 simplify_chains: bool,
1158) -> Vec<tatara_lisp::Spanned> {
1159 let mut resolved = blue_lang_check::names::resolve_program(program.forms(), names, &|i| {
1160 namespace_of(program, i)
1161 });
1162 let simplified = if simplify_chains {
1163 chain_simplified(program.forms(), &resolved)
1164 } else {
1165 None
1166 };
1167 let forms: &[tatara_lisp::Spanned] = simplified.as_deref().unwrap_or(program.forms());
1168 if simplified.is_some() {
1169 resolved =
1170 blue_lang_check::names::resolve_program(forms, names, &|i| namespace_of(program, i));
1171 }
1172 resolved
1173 .resolved_tree(forms, blue_lang_check::names::Rule::Namespaced)
1174 .into_iter()
1175 .zip(forms)
1176 .enumerate()
1177 .map(|(i, (resolved, written))| {
1178 if crate::uses::use_target(written).is_some()
1179 || blue_lang_syntax::scope::legacy_target(written).is_some()
1180 {
1181 tatara_lisp::Spanned::new(written.span, tatara_lisp::SpannedForm::Nil)
1182 } else {
1183 let file = program.owner_of(i).and_then(|id| program.file(id));
1184 match file {
1185 Some(f) => locate_raises(&resolved, &raise_site(f)),
1186 None => resolved,
1187 }
1188 }
1189 })
1190 .collect()
1191}
1192
1193fn chain_simplified(
1194 forms: &[tatara_lisp::Spanned],
1195 resolved: &blue_lang_check::names::Resolved,
1196) -> Option<Vec<tatara_lisp::Spanned>> {
1197 let respell: std::collections::HashMap<usize, blue_lang_check::respell::Respelling> = resolved
1198 .respellings
1199 .iter()
1200 .filter(|(_, r)| matches!(r.respell, blue_lang_check::respell::Respell::Chain(_)))
1201 .cloned()
1202 .collect();
1203 if respell.is_empty() {
1204 return None;
1205 }
1206 let bare = std::collections::HashSet::new();
1207 let canon = Canon {
1208 bare: &bare,
1209 respell: &respell,
1210 };
1211 Some(forms.iter().map(|f| canon.form(f)).collect())
1212}
1213
1214fn raise_site(file: &crate::uses::SourceFile) -> impl Fn(tatara_lisp::Span) -> String + '_ {
1215 move |span| {
1216 let (line, _) =
1217 blue_lang_syntax::Span::line_col(&file.text, span.start.min(file.text.len()));
1218 let shown = file
1219 .path
1220 .as_ref()
1221 .map_or_else(|| "<source>".to_string(), |p| p.display().to_string());
1222 format!("{shown}:{line}")
1223 }
1224}
1225
1226fn locate_raises(
1227 form: &tatara_lisp::Spanned,
1228 site: &dyn Fn(tatara_lisp::Span) -> String,
1229) -> tatara_lisp::Spanned {
1230 use tatara_lisp::{Atom, Spanned, SpannedForm};
1231 let inner = match &form.form {
1232 SpannedForm::List(items) => {
1233 let mut out: Vec<Spanned> = items.iter().map(|i| locate_raises(i, site)).collect();
1234 let raising = items.first().and_then(Spanned::as_symbol) == Some("raise")
1235 && (3..=4).contains(&items.len());
1236 if raising {
1237 out[0] = Spanned::new(
1238 items[0].span,
1239 SpannedForm::Atom(Atom::Symbol(crate::errors::RAISE_AT.to_string())),
1240 );
1241 out.insert(
1242 1,
1243 Spanned::new(items[0].span, SpannedForm::Atom(Atom::Str(site(form.span)))),
1244 );
1245 }
1246 SpannedForm::List(out)
1247 }
1248 SpannedForm::Quote(_) | SpannedForm::Quasiquote(_) => return form.clone(),
1249 other => other.clone(),
1250 };
1251 Spanned::new(form.span, inner)
1252}
1253
1254#[must_use]
1256pub fn namespace_of(program: &crate::uses::ResolvedProgram, i: usize) -> Namespace {
1257 let file = program.owner_of(i).and_then(|id| program.file(id));
1258 match file {
1259 Some(f) => match &f.package {
1260 Some(p) => Namespace::Bidama(p.clone()),
1261 None => Namespace::File(
1262 f.path
1263 .as_ref()
1264 .map_or_else(|| "<anonymous>".to_string(), |p| p.display().to_string()),
1265 ),
1266 },
1267 None => Namespace::File("<unknown file>".to_string()),
1268 }
1269}
1270
1271pub const HARNESS_NAMES: &[&str] = &[
1285 blue_lang_syntax::LOWERED_ASSERT,
1286 blue_lang_syntax::LOWERED_ASSERT_PANICS,
1287 blue_lang_syntax::LOWERED_TEST_META,
1288];
1289
1290#[must_use]
1300pub fn builtin_names<H: 'static>(interp: &tatara_lisp_eval::Interpreter<H>) -> NameTable {
1301 builtin_names_with(interp, &[])
1302}
1303
1304#[must_use]
1305pub fn builtin_names_with<H: 'static>(
1306 interp: &tatara_lisp_eval::Interpreter<H>,
1307 host: &[crate::host_surface::HostName],
1308) -> NameTable {
1309 use blue_lang_check::{Binding, Scope, ScopeKind};
1310 use tatara_lisp_eval::HeadBinding;
1311 let mut special = Scope::new(Namespace::SpecialForm);
1312 let mut macros = Scope::new(Namespace::Macro);
1313 let mut values = Scope::new(Namespace::Builtin);
1314 let mut names: Vec<std::sync::Arc<str>> = interp.reserved_head_names().into_iter().collect();
1319 names.extend(
1320 crate::inputs::INPUT_PRIMITIVES
1321 .iter()
1322 .filter(|n| interp.resolve_head(n).is_some())
1323 .map(|n| std::sync::Arc::from(*n)),
1324 );
1325 for name in names {
1326 let (scope, kind) = match interp.resolve_head(&name) {
1327 Some(HeadBinding::SpecialForm) => (&mut special, ScopeKind::SpecialForm),
1328 Some(HeadBinding::Macro) => (&mut macros, ScopeKind::Macro),
1329 _ => (&mut values, ScopeKind::Value),
1330 };
1331 let nullary = kind == ScopeKind::Value
1332 && interp.lookup_global(&name).is_some_and(|v| {
1333 nullary_value(&v)
1334 || (crate::docs::doc_of(&name).is_some_and(|d| d.signature == &*name)
1335 && matches!(
1336 v,
1337 tatara_lisp_eval::Value::NativeFn(_)
1338 | tatara_lisp_eval::Value::Closure(_)
1339 ))
1340 });
1341 scope.bind(Binding {
1342 name: name.to_string(),
1343 kind,
1344 span: None,
1345 top_level: None,
1346 nullary,
1347 });
1348 }
1349 for h in host {
1350 values.bind(Binding {
1351 name: h.name.clone(),
1352 kind: ScopeKind::Value,
1353 span: None,
1354 top_level: None,
1355 nullary: h.nullary(),
1356 });
1357 }
1358 let mut table = NameTable::new();
1359 let mut harness = Scope::new(Namespace::Harness);
1360 for n in HARNESS_NAMES {
1361 harness.bind(Binding {
1362 name: (*n).to_string(),
1363 kind: ScopeKind::Value,
1364 span: None,
1365 top_level: None,
1366 nullary: false,
1367 });
1368 }
1369 table.push(harness);
1370 table.push(special);
1371 table.push(macros);
1372 table.push(values);
1373 table.spell_bare_only(
1374 crate::docs::NAMES
1375 .iter()
1376 .filter(|d| d.signature == d.name)
1377 .map(|d| d.name.to_string())
1378 .chain(host.iter().filter(|h| h.nullary()).map(|h| h.name.clone()))
1379 .collect(),
1380 );
1381 table
1382}
1383
1384#[must_use]
1385pub fn nullary_value(value: &tatara_lisp_eval::Value) -> bool {
1386 match value {
1387 tatara_lisp_eval::Value::NativeFn(f) => {
1388 matches!(f.arity, tatara_lisp_eval::ffi::Arity::Exact(0))
1389 }
1390 tatara_lisp_eval::Value::Closure(c) => c.params.is_empty() && c.rest.is_none(),
1391 _ => false,
1392 }
1393}
1394
1395#[must_use]
1399pub fn render(program: &crate::uses::ResolvedProgram, d: &blue_lang_check::Diagnostic) -> String {
1400 let head = d.to_string();
1401 let mut out = program.locate(d.top_level, d.span, &head).to_string();
1402 if let Some(help) = &d.help {
1403 out.push_str("\n help: ");
1404 out.push_str(help);
1405 }
1406 let file = program
1407 .owner_of(d.top_level)
1408 .and_then(|id| program.file(id))
1409 .and_then(|f| f.path.as_ref());
1410 if let Some(cmd) =
1411 file.and_then(|p| blue_lang_check::evolution::fix_command(d, &p.display().to_string()))
1412 {
1413 out.push_str("\n fix: ");
1414 out.push_str(&cmd);
1415 }
1416 out
1417}
1418
1419#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1421pub enum Checking {
1422 Program,
1424 WithTests,
1427}
1428
1429pub struct Checked {
1431 pub program: crate::uses::ResolvedProgram,
1432 pub outcome: blue_lang_check::Outcome,
1433 pub names: NameTable,
1436}
1437
1438impl Checked {
1439 #[must_use]
1442 pub fn erased(&self) -> Vec<tatara_lisp::Spanned> {
1443 erase_types(&self.lowered())
1444 }
1445
1446 #[must_use]
1449 pub fn lowered(&self) -> Vec<tatara_lisp::Spanned> {
1450 lower(&self.program, &self.names)
1451 }
1452
1453 #[must_use]
1456 pub fn evaluable(&self) -> Vec<tatara_lisp::Sexp> {
1457 lower(&self.program, &self.names)
1458 .iter()
1459 .map(tatara_lisp::Spanned::to_sexp)
1460 .collect()
1461 }
1462
1463 #[must_use]
1467 pub fn resolve(&self) -> blue_lang_check::names::Resolved {
1468 blue_lang_check::names::resolve_program(self.program.forms(), &self.names, &|i| {
1469 namespace_of(&self.program, i)
1470 })
1471 }
1472}
1473
1474impl Checked {
1475 #[must_use]
1476 pub fn call_explicit_forms(&self) -> Vec<tatara_lisp::Spanned> {
1477 self.resolve().call_explicit_tree(self.program.forms())
1478 }
1479
1480 #[must_use]
1481 pub fn canonical_entry(&self, src: &str) -> Option<Vec<tatara_lisp::Spanned>> {
1482 use crate::uses::ResolvedProgram;
1483 let program = &self.program;
1484 let entry: Vec<&tatara_lisp::Spanned> = program
1485 .forms()
1486 .iter()
1487 .enumerate()
1488 .filter(|(i, _)| program.owner_of(*i) == Some(ResolvedProgram::ENTRY))
1489 .map(|(_, f)| f)
1490 .filter(|f| !blue_lang_syntax::doc::is_doctest(f))
1491 .collect();
1492 let written = blue_lang_syntax::parse_program_tree(src).ok()?;
1493 let same = written.len() == entry.len()
1494 && written
1495 .iter()
1496 .zip(&entry)
1497 .all(|(w, e)| w.to_sexp() == e.to_sexp() && w.span == e.span);
1498 if !same {
1499 return None;
1500 }
1501 let resolved = self.resolve();
1502 let bare: std::collections::HashSet<usize> = resolved
1503 .parenthesized_nullary_calls()
1504 .into_iter()
1505 .filter(|r| program.owner_of(r.top_level) == Some(ResolvedProgram::ENTRY))
1506 .map(blue_lang_check::names::Reference::node_address)
1507 .collect();
1508 let waived: Vec<tatara_lisp::Span> = self
1509 .outcome
1510 .waived
1511 .iter()
1512 .map(|w| w.diagnostic.span)
1513 .collect();
1514 let respell: std::collections::HashMap<usize, blue_lang_check::respell::Respelling> =
1515 resolved
1516 .respellings
1517 .iter()
1518 .filter(|(_, r)| !waived.contains(&r.span))
1519 .cloned()
1520 .collect();
1521 let canon = Canon {
1522 bare: &bare,
1523 respell: &respell,
1524 };
1525 let forms: Vec<tatara_lisp::Spanned> = entry.into_iter().map(|f| canon.form(f)).collect();
1526 let script = program
1527 .file(ResolvedProgram::ENTRY)
1528 .is_some_and(|f| f.package.is_none());
1529 let forms = if script {
1530 blue_lang_check::form_rules::without_prefix(&forms).unwrap_or(forms)
1531 } else {
1532 forms
1533 };
1534 let forms = blue_lang_check::form_rules::without_main_guard(&forms).unwrap_or(forms);
1535 Some(blue_lang_check::form_rules::eta_reduced(&forms).unwrap_or(forms))
1536 }
1537}
1538
1539struct Canon<'a> {
1540 bare: &'a std::collections::HashSet<usize>,
1541 respell: &'a std::collections::HashMap<usize, blue_lang_check::respell::Respelling>,
1542}
1543
1544impl Canon<'_> {
1545 fn form(&self, form: &tatara_lisp::Spanned) -> tatara_lisp::Spanned {
1546 use tatara_lisp::{Spanned, SpannedForm};
1547 let inner = match &form.form {
1548 SpannedForm::List(items) => match items.as_slice() {
1549 [head] if self.bare.contains(&(std::ptr::from_ref(head) as usize)) => {
1550 return Spanned::new(form.span, head.form.clone());
1551 }
1552 [head, ..]
1553 if self
1554 .respell
1555 .contains_key(&(std::ptr::from_ref(head) as usize)) =>
1556 {
1557 let r = &self.respell[&(std::ptr::from_ref(head) as usize)];
1558 let walked: Vec<Spanned> = items.iter().map(|i| self.form(i)).collect();
1559 return blue_lang_check::respell::apply(form, &walked, r);
1560 }
1561 _ => SpannedForm::List(items.iter().map(|i| self.form(i)).collect()),
1562 },
1563 SpannedForm::Quote(i) => SpannedForm::Quote(Box::new(self.form(i))),
1564 SpannedForm::Quasiquote(i) => SpannedForm::Quasiquote(Box::new(self.form(i))),
1565 SpannedForm::Unquote(i) => SpannedForm::Unquote(Box::new(self.form(i))),
1566 SpannedForm::UnquoteSplice(i) => SpannedForm::UnquoteSplice(Box::new(self.form(i))),
1567 other => other.clone(),
1568 };
1569 Spanned::new(form.span, inner)
1570 }
1571}
1572
1573pub fn check_entry(
1589 entry: Entry<'_>,
1590 loader: &dyn crate::uses::Loader,
1591 surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
1592 checking: Checking,
1593) -> Result<Checked, RunError> {
1594 let mut program = parse_and_resolve(entry, loader, surface)?;
1595 if checking == Checking::Program {
1596 program.retain(|f| !crate::uses::is_test_form(f));
1597 }
1598 let host = match entry.path {
1599 Some(path) => crate::host_surface::near(path)
1600 .map_err(|e| RunError::Import(e.to_string()))?
1601 .map(|s| s.functions)
1602 .unwrap_or_default(),
1603 None => Vec::new(),
1604 };
1605 let interp = runtime(Inputs::new());
1606 let builtins = crate::timings::stage(crate::timings::Stage::Names, || {
1607 builtin_names_with(&interp, &host)
1608 });
1609 let (outcome, names) = crate::timings::stage(crate::timings::Stage::Check, || {
1610 check_stage(&program, &builtins, checking == Checking::WithTests)
1611 });
1612 Ok(Checked {
1613 program,
1614 outcome,
1615 names,
1616 })
1617}
1618
1619#[must_use]
1620pub fn spelling_code(spelling: blue_lang_syntax::Spelling) -> blue_lang_check::Code {
1621 use blue_lang_check::Code;
1622 use blue_lang_syntax::Spelling;
1623 match spelling {
1624 Spelling::LogicalSymbol => Code::B0025,
1625 Spelling::Unless => Code::B0026,
1626 Spelling::ElseIf => Code::B0027,
1627 Spelling::CallForm => Code::B0028,
1628 Spelling::KeywordRocket => Code::B0029,
1629 Spelling::ParameterList => Code::B0030,
1630 Spelling::OperatorSpelling => Code::B0031,
1631 Spelling::TrailingComma => Code::B0032,
1632 Spelling::NegatedComparison => Code::B0034,
1633 Spelling::ElseNil => Code::B0035,
1634 Spelling::TryCatch => Code::B0037,
1635 Spelling::Apply => Code::B0042,
1636 Spelling::MapPun => Code::B0046,
1637 Spelling::NilTest => Code::B0047,
1638 Spelling::NewlineEscape => Code::B0065,
1639 Spelling::Index => Code::B0066,
1640 Spelling::TestDeclaration => Code::B0068,
1641 }
1642}
1643
1644#[must_use]
1645pub fn form_diagnostics(text: &str) -> Vec<blue_lang_check::Diagnostic> {
1646 match blue_lang_fmt::format_source_lossless(text) {
1647 Ok(formatted) => form_diagnostics_against(text, &formatted),
1648 Err(_) => Vec::new(),
1649 }
1650}
1651
1652#[must_use]
1653pub fn form_diagnostics_against(text: &str, formatted: &str) -> Vec<blue_lang_check::Diagnostic> {
1654 use blue_lang_check::{Code, Diagnostic};
1655 if formatted.trim_end() == text.trim_end() {
1656 return Vec::new();
1657 }
1658 let help = "`blue fmt --write FILE` writes it; no compile door writes a file (`BLUE_LAYOUT=rewrite` restores formatting in place)";
1659 let mut out: Vec<Diagnostic> = blue_lang_syntax::spelling_notes(text)
1660 .unwrap_or_default()
1661 .into_iter()
1662 .map(|n| {
1663 let message = if n.canonical.is_empty() {
1664 format!("`{}` is not written", n.written)
1665 } else {
1666 format!("`{}` is written `{}`", n.written, n.canonical)
1667 };
1668 Diagnostic::new(spelling_code(n.spelling), message, n.span)
1669 .at_top_level(0)
1670 .with_help(help)
1671 })
1672 .collect();
1673 out.extend(
1674 literal_diagnostics(text)
1675 .into_iter()
1676 .map(|d| d.with_help(help)),
1677 );
1678 if out.is_empty() {
1679 let at = text
1680 .lines()
1681 .zip(formatted.lines())
1682 .position(|(a, b)| a != b)
1683 .unwrap_or_else(|| text.lines().count().min(formatted.lines().count()));
1684 let start: usize = text.lines().take(at).map(|l| l.len() + 1).sum();
1685 let start = start.min(text.len());
1686 let end = text[start..].find('\n').map_or(text.len(), |e| start + e);
1687 out.push(
1688 Diagnostic::new(
1689 Code::B0024,
1690 format!("line {} is not laid out as blue writes it", at + 1),
1691 tatara_lisp::Span::new(start, end),
1692 )
1693 .at_top_level(0)
1694 .with_help(help),
1695 );
1696 }
1697 out.sort_by_key(|d| (d.span.start, d.code));
1698 out
1699}
1700
1701fn literal_diagnostics(text: &str) -> Vec<blue_lang_check::Diagnostic> {
1702 use blue_lang_syntax::{Atom, TokenKind};
1703 let Ok(toks) = blue_lang_syntax::lex(text) else {
1704 return Vec::new();
1705 };
1706 toks.iter()
1707 .filter_map(|t| {
1708 let atom = match &t.kind {
1709 TokenKind::Int(v) => Atom::Int(*v),
1710 TokenKind::Float(v) => Atom::Float(*v),
1711 TokenKind::Str(v) => Atom::Str(v.clone()),
1712 _ => return None,
1713 };
1714 let written = text.get(t.span.start..t.span.end)?;
1715 let canonical = blue_lang_fmt::render_literal(&atom);
1716 (written != canonical).then(|| {
1717 blue_lang_check::Diagnostic::new(
1718 blue_lang_check::Code::B0033,
1719 format!("`{written}` is written `{canonical}`"),
1720 t.span,
1721 )
1722 .at_top_level(0)
1723 })
1724 })
1725 .collect()
1726}
1727
1728#[must_use]
1732pub fn syntax_diagnostic(text: &str) -> Option<blue_lang_check::Diagnostic> {
1733 let e = blue_lang_syntax::parse_program_tree(text).err()?;
1734 Some(
1735 blue_lang_check::Diagnostic::new(blue_lang_check::Code::B0006, e.message, e.span)
1736 .at_top_level(0),
1737 )
1738}
1739
1740#[must_use]
1744pub fn display_keys(message: &str) -> String {
1745 let mut out = String::with_capacity(message.len());
1746 let mut parts = message.split('`');
1747 if let Some(first) = parts.next() {
1748 out.push_str(first);
1749 }
1750 for (i, part) in parts.enumerate() {
1751 out.push('`');
1752 if i % 2 == 0 {
1754 out.push_str(&crate::observe::display_key(part));
1755 } else {
1756 out.push_str(part);
1757 }
1758 }
1759 out
1760}
1761
1762pub(crate) fn eval_prepared<H: 'static>(
1764 interp: &mut tatara_lisp_eval::Interpreter<H>,
1765 prepared: &Prepared,
1766 host: &mut H,
1767) -> Result<Value, RunError> {
1768 let Prepared {
1769 program, erased, ..
1770 } = prepared;
1771
1772 let mut value = Value::Nil;
1786 for (top_level, form) in erased.iter().enumerate() {
1787 value = interp.eval_top_form(form, host).map_err(|e| {
1788 RunError::Eval(Box::new(crate::observe::Fault::of(
1795 program,
1796 erased,
1797 top_level,
1798 &crate::memory::named(e),
1799 &interp.take_trace(),
1800 )))
1801 })?;
1802 }
1803 Ok(value)
1804}
1805
1806pub const MAIN: &str = "main";
1807
1808#[must_use]
1809pub fn entry_main(program: &crate::uses::ResolvedProgram) -> Option<(usize, tatara_lisp::Span)> {
1810 use crate::uses::ResolvedProgram;
1811 let stored = program
1812 .file(ResolvedProgram::ENTRY)
1813 .is_some_and(|f| f.path.is_some());
1814 if !stored {
1815 return None;
1816 }
1817 program.forms().iter().enumerate().find_map(|(i, f)| {
1818 if program.owner_of(i) != Some(ResolvedProgram::ENTRY) {
1819 return None;
1820 }
1821 let items = f.as_list()?;
1822 let sig = items.get(1)?.as_list()?;
1823 let named_main = matches!(
1824 items.first().and_then(tatara_lisp::Spanned::as_symbol),
1825 Some("define" | "define-typed")
1826 ) && sig.len() == 1
1827 && sig[0].as_symbol() == Some(MAIN);
1828 named_main.then_some((i, sig[0].span))
1829 })
1830}
1831
1832fn main_call(prepared: &Prepared) -> Option<(usize, tatara_lisp::Span, String)> {
1835 let (top_level, span) = entry_main(&prepared.program)?;
1836 let ns = namespace_of(&prepared.program, top_level);
1837 Some((top_level, span, blue_lang_check::names::key(&ns, MAIN)))
1838}
1839
1840fn main_key(prepared: &Prepared) -> Option<String> {
1841 main_call(prepared).map(|(_, _, key)| key)
1842}
1843
1844pub(crate) fn call_main<H: 'static>(
1845 interp: &mut tatara_lisp_eval::Interpreter<H>,
1846 prepared: &Prepared,
1847 host: &mut H,
1848) -> Result<Option<Value>, RunError> {
1849 let Some((top_level, span, key)) = main_call(prepared) else {
1850 return Ok(None);
1851 };
1852 let call = tatara_lisp::Spanned::new(
1853 span,
1854 tatara_lisp::SpannedForm::List(vec![tatara_lisp::Spanned::new(
1855 span,
1856 tatara_lisp::SpannedForm::Atom(tatara_lisp::Atom::Symbol(key)),
1857 )]),
1858 );
1859 interp.eval_top_form(&call, host).map(Some).map_err(|e| {
1860 RunError::Eval(Box::new(crate::observe::Fault::of(
1861 &prepared.program,
1862 &prepared.erased,
1863 top_level,
1864 &crate::memory::named(e),
1865 &interp.take_trace(),
1866 )))
1867 })
1868}
1869
1870#[cfg(test)]
1886mod position_tests {
1887 use super::*;
1888 use crate::uses::{Entry, Loader};
1889 use std::collections::BTreeMap;
1890 use std::path::Path;
1891
1892 struct MemLoader(BTreeMap<&'static str, &'static str>);
1893
1894 impl Loader for MemLoader {
1895 fn load(&self, name: &str) -> Result<Vec<(String, String)>, String> {
1896 self.0
1897 .get(name)
1898 .map(|s| vec![(format!("{name}.b"), (*s).to_owned())])
1899 .ok_or_else(|| format!("no bidama named \"{name}\""))
1900 }
1901 }
1902
1903 const BAKUHATSU: &str = "\
1911def tsukawanai_a()
1912 1
1913end
1914
1915def tsukawanai_b()
1916 2
1917end
1918
1919# waive B0001: the fixture needs a raise at runtime
1920def bakuhatsu()
1921 kore_wa_sonzai_shinai()
1922end
1923
1924bakuhatsu";
1925
1926 const KOTAE: &str = "def kotae()\n 42\nend";
1927
1928 const BAKUHATSU2: &str =
1931 "# waive B0001: a raise at runtime\ndef yobu()\n kore_wa_sonzai_shinai()\nend";
1932
1933 fn loader() -> MemLoader {
1934 MemLoader(BTreeMap::from([
1935 ("kotae", KOTAE),
1936 ("bakuhatsu", BAKUHATSU),
1937 ("bakuhatsu2", BAKUHATSU2),
1938 ]))
1939 }
1940
1941 fn run_named(path: &str, src: &str) -> RunError {
1942 run_in_surface(
1943 Entry {
1944 path: Some(Path::new(path)),
1945 text: src,
1946 },
1947 Inputs::new(),
1948 &loader(),
1949 None,
1950 )
1951 .expect_err("the fixture must raise")
1952 }
1953
1954 const TWO_IMPORTS: &str =
1999 "use(\"kotae\")\n# waive B0015: kotae loads first on purpose\nuse(\"bakuhatsu\")\n";
2000
2001 #[test]
2002 fn a_raise_inside_an_imported_package_names_that_package() {
2003 let err = run_named("entry.b", TWO_IMPORTS);
2004 assert_eq!(
2005 err.to_string(),
2006 "runtime error: bakuhatsu.b:11:3: unbound symbol `kore_wa_sonzai_shinai`\n \
2007 at bakuhatsu::bakuhatsu (bakuhatsu.b:11:3)\n \
2008 at <top level> (bakuhatsu.b:14:1)"
2009 );
2010 }
2011
2012 #[test]
2020 fn line_11_column_3_of_the_fixture_is_the_failing_call() {
2021 let line = BAKUHATSU.lines().nth(10).expect("line 11 exists");
2022 assert_eq!(line, " kore_wa_sonzai_shinai()");
2023 assert_eq!(
2024 &line[2..],
2025 "kore_wa_sonzai_shinai()",
2026 "column 3 (1-indexed) is where the call starts"
2027 );
2028 let offset = BAKUHATSU.find("kore_wa_sonzai_shinai").expect("in fixture");
2031 assert!(
2032 offset > KOTAE.len(),
2033 "kotae.b could answer for byte {offset}"
2034 );
2035 assert!(
2036 offset > TWO_IMPORTS.len(),
2037 "the entry file could answer for byte {offset}"
2038 );
2039 }
2040
2041 #[test]
2060 fn a_raise_in_the_entry_file_names_the_entry_file() {
2061 let err = run_named(
2062 "honmono.b",
2063 "def f()\n 1\nend\n\n# waive B0001: a raise at runtime\nnani_mo_nai()\n",
2064 );
2065 assert_eq!(
2066 err.to_string(),
2067 "runtime error: honmono.b:6:1: unbound symbol `nani_mo_nai`"
2068 );
2069 }
2070
2071 #[test]
2085 fn a_raise_inside_an_annotated_def_still_names_its_line() {
2086 let err = run_named(
2087 "chuu.b",
2088 "# waive B0001: a raise at runtime\ndef f(a: Int) -> Int\n mada_nai(a)\nend\n\nf(1)\n",
2089 );
2090 assert_eq!(
2091 err.to_string(),
2092 "runtime error: chuu.b:3:3: unbound symbol `mada_nai`\n \
2093 at f (chuu.b:3:3)\n \
2094 at <top level> (chuu.b:6:1)"
2095 );
2096 }
2097
2098 #[test]
2116 fn a_raise_in_a_callee_from_another_file_names_the_callees_file() {
2117 let err = run_named(
2122 "yobidashi.b",
2123 "use(\"bakuhatsu2\")\n# pad: 47's in range\nbakuhatsu2::yobu\n",
2124 );
2125 assert_eq!(
2126 err.to_string(),
2127 "runtime error: bakuhatsu2.b:3:3: unbound symbol `kore_wa_sonzai_shinai`\n \
2128 at bakuhatsu2::yobu (bakuhatsu2.b:3:3)\n \
2129 at <top level> (yobidashi.b:3:1)"
2130 );
2131 }
2132}
2133
2134#[cfg(test)]
2135mod tests {
2136 use super::*;
2137
2138 fn int(src: &str) -> i64 {
2139 match run(src).unwrap_or_else(|e| panic!("{src:?}: {e}")).value {
2140 Value::Int(v) => v,
2141 other => panic!("{src:?} produced {other:?}"),
2142 }
2143 }
2144
2145 #[test]
2148 fn annotating_buys_analysis_and_changes_nothing_else() {
2149 let plain = run("def add(a, b)\n a + b\nend\nadd(2, 3)").expect("plain");
2150 let typed = run("def add(a: Int, b: Int) -> Int\n a + b\nend\nadd(2, 3)").expect("typed");
2151
2152 assert!(matches!(plain.value, Value::Int(5)));
2153 assert!(
2154 matches!(typed.value, Value::Int(5)),
2155 "the annotated program must compute the same answer"
2156 );
2157 assert_eq!(plain.visited, 0, "no annotations means no analysis");
2158 assert!(
2159 typed.visited > 0,
2160 "an annotation must actually buy analysis, not just decorate"
2161 );
2162 assert_eq!(plain.typed_decls, 0);
2163 assert_eq!(typed.typed_decls, 1);
2164 }
2165
2166 #[test]
2170 fn a_type_error_is_reported_and_the_program_does_not_run() {
2171 let err = run("def add(a: Int, b: Int) -> Str\n a + b\nend\nadd(1, 2)")
2172 .expect_err("a declared Str return from an Int body must be rejected");
2173 assert!(
2174 matches!(err, RunError::Types(ref d) if !d.is_empty()),
2175 "expected type diagnostics, got {err}"
2176 );
2177 }
2178
2179 #[test]
2182 fn the_same_program_without_annotations_runs() {
2183 assert_eq!(int("def add(a, b)\n a + b\nend\nadd(1, 2)"), 3);
2184 }
2185
2186 #[test]
2187 fn a_parse_error_is_reported_as_one() {
2188 assert!(matches!(run("def (").unwrap_err(), RunError::Parse(_)));
2189 }
2190
2191 #[test]
2194 fn a_runtime_error_is_reported_as_one() {
2195 let err = run("throw(error(\"boom\"))").expect_err("a raise");
2196 assert!(matches!(err, RunError::Eval(_)), "got {err}");
2197 }
2198
2199 #[test]
2202 fn the_pipeline_reaches_both_runtime_layers() {
2203 assert_eq!(int("6 % 3"), 0);
2204 assert_eq!(int("7 % 3"), 1);
2205 assert_eq!(int("2 + 3 * 4"), 14);
2206 }
2207
2208 #[test]
2218 fn the_deleted_round_trip_agreed_with_the_direct_lowering() {
2219 let corpus = [
2220 "def add(a, b)\n a + b\nend\nadd(2, 3)",
2221 "def fact(n)\n if n < 2\n 1\n else\n n * fact(n - 1)\n end\nend\nfact(5)",
2222 "def f(a, b)\n c = a + b\n c * 2\nend\nf(1, 2)",
2223 "defmacro sq(e)\n quote\n unquote(e) * unquote(e)\n end\nend\nsq(2 + 3)",
2224 "\"a string with spaces, a ( and a )\"",
2225 "def g(a: Int) -> Int\n a + 1\nend\ng(1)",
2226 "6 % 3",
2227 "1.5 + 2.25",
2228 ];
2229 for src in corpus {
2230 let erased = crate::to_sexps(&erase_types(&parse_tree(src).expect("parse")));
2233
2234 let direct: Vec<Sexp> = crate::lower_to_spanned(&erased)
2235 .iter()
2236 .map(tatara_lisp::Spanned::to_sexp)
2237 .collect();
2238 assert_eq!(direct, erased, "the direct lowering must be the identity");
2239
2240 let text = erased
2241 .iter()
2242 .map(ToString::to_string)
2243 .collect::<Vec<_>>()
2244 .join("\n");
2245 let round_tripped: Vec<Sexp> = tatara_lisp::read_spanned(&text)
2246 .unwrap_or_else(|e| panic!("{src:?}: the old path could not read back: {e:?}"))
2247 .iter()
2248 .map(tatara_lisp::Spanned::to_sexp)
2249 .collect();
2250 assert_eq!(
2251 round_tripped, erased,
2252 "{src:?}: the old print-and-reparse path changed the tree"
2253 );
2254 }
2255 }
2256
2257 #[test]
2275 fn the_round_trip_is_not_the_identity_in_general() {
2276 let tree = Sexp::List(vec![
2277 Sexp::Atom(tatara_lisp::Atom::Symbol("f".into())),
2278 Sexp::Atom(tatara_lisp::Atom::Symbol("a b".into())),
2279 ]);
2280 let text = tree.to_string();
2281 let back: Vec<Sexp> = tatara_lisp::read_spanned(&text)
2282 .expect("it reads back cleanly — that IS the problem")
2283 .iter()
2284 .map(tatara_lisp::Spanned::to_sexp)
2285 .collect();
2286 assert_ne!(
2287 back,
2288 vec![tree.clone()],
2289 "if print-then-read became inverse over symbols, the class would be \
2290 closed upstream and this test should be deleted rather than relaxed"
2291 );
2292 let direct: Vec<Sexp> = crate::lower_to_spanned(std::slice::from_ref(&tree))
2294 .iter()
2295 .map(tatara_lisp::Spanned::to_sexp)
2296 .collect();
2297 assert_eq!(direct, vec![tree]);
2298 }
2299}
2300
2301#[cfg(test)]
2302mod macro_tests {
2303 use super::*;
2304
2305 fn int(src: &str) -> i64 {
2306 match run(src).unwrap_or_else(|e| panic!("{src:?}: {e}")).value {
2307 Value::Int(v) => v,
2308 other => panic!("{src:?} produced {other:?}"),
2309 }
2310 }
2311
2312 #[test]
2314 fn a_macro_expands_and_runs() {
2315 assert_eq!(
2316 int("defmacro double(x)\n quote\n unquote(x) + unquote(x)\n end\nend\ndouble(21)"),
2317 42
2318 );
2319 }
2320
2321 #[test]
2324 fn a_macro_operates_on_syntax_not_values() {
2325 assert_eq!(
2326 int("defmacro sq(e)\n quote\n unquote(e) * unquote(e)\n end\nend\nsq(2 + 3)"),
2327 25,
2328 "the argument form `2 + 3` must be substituted twice"
2329 );
2330 }
2331
2332 #[test]
2351 fn a_runaway_macro_fails_the_compilation_rather_than_the_process() {
2352 let err =
2353 run("defmacro forever(x)\n quote\n forever(unquote(x))\n end\nend\nforever(1)")
2354 .expect_err("a self-referential macro must be rejected");
2355 let msg = err.to_string();
2356 assert!(
2357 msg.contains("forever") && msg.contains("expansion"),
2358 "the error must name the macro and the limit it hit: {msg}"
2359 );
2360 assert!(
2361 msg.contains("<anonymous>:6:1"),
2362 "and must place the call that ran away — line 6 is `forever(1)`: {msg}"
2363 );
2364 }
2365}
2366
2367#[cfg(test)]
2368mod input_tests {
2369 use super::*;
2370 use crate::inputs::{Declaration, Inputs};
2371
2372 const SCHEMA: &[u8] = b"3";
2374
2375 fn with_schema(src: &str) -> Result<Run, RunError> {
2376 let hash = Inputs::hash_of(SCHEMA);
2377 let mut inputs = Inputs::new();
2378 inputs
2379 .bind(
2380 &Declaration {
2381 name: "schema".to_string(),
2382 hash,
2383 },
2384 SCHEMA.to_vec(),
2385 )
2386 .expect("bind");
2387 run_with_inputs(src, inputs)
2388 }
2389
2390 fn decl_line() -> String {
2391 let mut s = String::from("definput(\"schema\", \"");
2392 s.push_str(&Inputs::hash_of(SCHEMA));
2393 s.push_str("\")\n");
2394 s
2395 }
2396
2397 #[test]
2402 fn a_macro_can_read_a_declared_build_input() {
2403 let src = decl_line() + "input(\"schema\")";
2404 let out = with_schema(&src).expect("run");
2405 assert!(
2406 matches!(out.value, Value::Str(ref s) if &**s == "3"),
2407 "got {:?}",
2408 out.value
2409 );
2410 }
2411
2412 #[test]
2416 fn an_undeclared_input_is_an_error() {
2417 let err = with_schema("input(\"not_declared\")").expect_err("must fail");
2418 let msg = err.to_string();
2419 assert!(msg.contains("not_declared"), "must name it: {msg}");
2420 assert!(msg.contains("definput"), "and say how to declare it: {msg}");
2421 }
2422
2423 #[cfg(not(feature = "sys"))]
2431 #[test]
2432 fn there_is_no_ambient_file_read() {
2433 for attempt in [
2434 "read_file(\"/etc/passwd\")",
2435 "File(\"/etc/passwd\")",
2436 "slurp(\"/etc/passwd\")",
2437 "open(\"/etc/passwd\")",
2438 ] {
2439 let err = with_schema(attempt).expect_err("must not resolve");
2440 assert!(
2441 err.to_string().contains("unbound"),
2442 "{attempt} must be UNBOUND — a capability removed by absence, \
2443 not guarded by a check: {err}"
2444 );
2445 }
2446 }
2447
2448 #[cfg(feature = "sys")]
2454 #[test]
2455 fn sys_read_file_is_the_trusted_cli_only_exception() {
2456 let err = with_schema("definitely_not_a_primitive(\"x\")").expect_err("must not resolve");
2457 assert!(err.to_string().contains("unbound"), "{err}");
2458 assert!(
2459 with_schema("read_file(\"/etc/passwd\")").is_ok(),
2460 "with `sys` on, read_file is the trusted CLI surface"
2461 );
2462 let out = with_schema("input(\"schema\")").expect("run");
2463 assert!(
2464 matches!(out.value, Value::Str(ref s) if &**s == "3"),
2465 "input() still binds beside the sys surface: {:?}",
2466 out.value
2467 );
2468 }
2469
2470 #[test]
2473 fn a_declared_input_with_no_bytes_supplied_fails() {
2474 let src = decl_line() + "input(\"schema\")";
2475 assert!(run(&src).is_err(), "no bytes were supplied");
2476 }
2477}
2478
2479#[cfg(test)]
2480mod tier2_tests {
2481 use super::*;
2482 use crate::inputs::{Declaration, Inputs};
2483
2484 #[test]
2496 fn a_macro_generates_code_from_a_schema() {
2497 let schema = b"7";
2498 let mut inputs = Inputs::new();
2499 inputs
2500 .bind(
2501 &Declaration {
2502 name: "arity".to_string(),
2503 hash: Inputs::hash_of(schema),
2504 },
2505 schema.to_vec(),
2506 )
2507 .expect("bind");
2508
2509 let mut src = String::from("definput(\"arity\", \"");
2512 src.push_str(&Inputs::hash_of(schema));
2513 src.push_str("\")\n");
2514 src.push_str(
2515 "defmacro from_schema()\n quote\n unquote(to_int(input(\"arity\")))\n end\nend\n\
2516 from_schema() * 6",
2517 );
2518
2519 let out = run_with_inputs(&src, inputs).expect("run");
2520 assert!(
2521 matches!(out.value, Value::Int(42)),
2522 "the schema's 7 must reach the generated code: got {:?}",
2523 out.value
2524 );
2525 }
2526}