Skip to main content

blue_lang_runtime/
pipeline.rs

1//! The blue pipeline: **parse → check → erase → run**, in that order, once.
2//!
3//! The order is the whole reason this module exists. Each stage is available
4//! separately for tools that want one, but the *default* path is a single
5//! function, because two of the four orderings are silently wrong:
6//!
7//! - **Erase before check** discards every annotation, so a program with type
8//!   errors passes. The checker sees `(define …)` and has nothing to check.
9//! - **Run before check** reports a type error after the side effects.
10//!
11//! Neither fails loudly. Both produce a green run on a program that should
12//! have been rejected. Leaving the order to each caller means every caller
13//! is one reordering away from turning the type checker off — so the order
14//! lives here, and callers ask for a *result*, not a sequence of steps.
15
16use 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/// Why a run stopped short.
26#[derive(Debug, thiserror::Error)]
27pub enum RunError {
28    #[error("parse error: {0}")]
29    Parse(String),
30    /// The check stage rejected the program — a type error, an unbound name,
31    /// or any other error-severity rule in `blue_lang_check::RULES`. Carries
32    /// every error, not just the first: a caller fixing one wants to see the
33    /// rest. (The variant keeps its first name because it is public API on a
34    /// released crate.)
35    ///
36    /// Each one is already rendered `file:line:col: message` against the file
37    /// the offending form came from — which, in a program with imports, is
38    /// frequently not the file the user named. The rendering happens here
39    /// rather than at the consumer because here is where the file table exists;
40    /// see `uses::ResolvedProgram::locate`.
41    #[error("{} error(s) in the check stage:\n{}", .0.len(), .0.join("\n"))]
42    Types(Vec<String>),
43    /// **No longer reachable, and that is the point.** This reported "blue
44    /// emitted a tree the reader could not read back" — a failure only a
45    /// print-then-reparse hop could have. [`crate::lower_to_spanned`] deleted
46    /// the hop, so there is nothing left to fail: the tree the evaluator gets
47    /// IS the tree erasure produced, not a re-reading of its text.
48    ///
49    /// Kept rather than removed because it is public API on a released crate
50    /// and a consumer may still match on it, per ★★ MODULARIZE, DON'T DELETE.
51    /// It is retired, not orphaned — if a future stage ever serialises again
52    /// it has a typed home. **Nothing constructs it today**; do not read its
53    /// presence as evidence the pipeline can still fail this way.
54    #[error("the emitted tatara-lisp could not be read back: {0}")]
55    Lower(String),
56    /// The program ran and raised.
57    ///
58    /// Rendered `file:line:col: message`, then the blue call stack the error
59    /// unwound through, innermost first (`crate::observe::Fault`). The
60    /// position is resolved against the file of the innermost frame's
61    /// definition, so a raise inside a callee from another file names the
62    /// callee's file and line; before the stack existed that case could only
63    /// name the caller's file, with no position.
64    #[error("runtime error: {0}")]
65    Eval(Box<crate::observe::Fault>),
66    /// A `use("name")` could not be resolved.
67    ///
68    /// Its own variant rather than folded into `Parse`, because the reader's
69    /// next action is different: a parse error is in the source in front of
70    /// them, an import error is in their packaging — a missing bidama, a
71    /// BLUE_PATH that does not contain it, or no loader at all.
72    #[error("import error: {0}")]
73    Import(String),
74}
75
76impl RunError {
77    /// The exit status a program that stopped here returns:
78    /// [`crate::BREACH_EXIT`] for a capability breach, 1 for anything else.
79    #[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/// What a run produced, plus what the checker did on the way.
89#[derive(Debug)]
90pub struct Run {
91    pub value: Value,
92    /// Nodes the type walk visited. Zero for a fully untyped program — this
93    /// is what makes "no annotations, no analysis" a *measurement* rather
94    /// than a claim.
95    pub visited: usize,
96    /// Declarations that carried an annotation.
97    pub typed_decls: usize,
98    /// Boundaries where typed code meets untyped code.
99    pub seams: usize,
100}
101
102/// Parse blue source to tatara-lisp forms.
103pub fn parse(src: &str) -> Result<Vec<Sexp>, RunError> {
104    parse_with_depth(src, blue_lang_syntax::MAX_EXPR_DEPTH)
105}
106
107/// [`parse`] with the parser's nesting bound supplied by the caller.
108///
109/// The bound exists so a stack overflow — which `catch_unwind` cannot catch —
110/// arrives as a typed `Err` instead. It is a *limit*, not a dialect: raising
111/// it changes no program's meaning, which is exactly why it is safe to expose
112/// as configuration (`blue-lang-cli`'s `config` module holds the rule).
113pub 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
118/// [`parse_with_depth`] keeping **every node's** source span.
119///
120/// The door for anything that will report a position to a human. It exists here,
121/// beside the spanless one, so a caller that wants spans still parses under the
122/// CONFIGURED nesting bound — a separate `blue_lang_syntax` call would be the
123/// second door `parse_with_depth`'s own docs exist to prevent, with
124/// `max_expr_depth` true of some subcommands and not others.
125pub 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
133/// [`parse`] keeping **every node's** source span.
134///
135/// The spanned twin of [`parse`], at the same default bound — the door for
136/// anything downstream of a parse that will report a position, which since
137/// `use` learned to carry file identity is every path through
138/// [`run_in_surface`].
139pub 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
143/// Run blue source with no build inputs.
144pub fn run(src: &str) -> Result<Run, RunError> {
145    run_with_inputs(src, Inputs::new())
146}
147
148/// Run blue source, giving the macro phase access to verified build inputs.
149///
150/// `inputs` is already verified — [`Inputs`] cannot hold bytes that do not match
151/// their declared hash — so nothing here re-checks. The capability a macro gains
152/// is exactly "these hashed bytes", never a path.
153pub fn run_with_inputs(src: &str, inputs: Inputs) -> Result<Run, RunError> {
154    run_with_loader(src, inputs, &crate::uses::NoLoader)
155}
156
157/// Run blue source with a loader, so `use("name")` can resolve.
158///
159/// Split from [`run_with_inputs`] rather than folded into it because loading a
160/// package reads a filesystem, and this crate has a `wasm32-unknown-unknown`
161/// consumer with zero host imports. The capability is injected by callers that
162/// have it — `blue_lang_pkg::LoadPath` is the real one — and absent by default,
163/// where a `use` is a typed error naming the package.
164pub 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
172/// Run blue source written in a `yakugo` surface.
173///
174/// The pack applies at PARSE time and nowhere else — by the time the checker
175/// sees the program it is canonical, so every stage below is identical whatever
176/// surface the author wrote in. That is what makes a surface a surface: it
177/// changes how a program is spelled and nothing about how it runs.
178///
179/// `entry` carries the source AND the file it was read from, because a type
180/// error has to be reported somewhere: a caller with a path should pass it, and
181/// one without ([`Entry::anonymous`]) gets diagnostics that say so rather than
182/// diagnostics that guess. Imported packages name themselves through the
183/// loader either way.
184///
185/// # Errors
186///
187/// As [`run_with_loader`].
188pub 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
197/// [`run_in_surface`] held to `hold` when one is given: a host name it
198/// withholds, written in the entry file, refuses the program before it runs,
199/// and one reached through a bidama or a spawned process is a breach where it
200/// is called; a provided capability's names reach its provider. `None` runs
201/// with every capability.
202///
203/// # Errors
204///
205/// As [`run_with_loader`], and [`RunError::Types`] naming each host name the
206/// entry file writes that `hold` withholds.
207pub 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    // The interpreter is built BEFORE the check stage, because it is the check
215    // stage's name oracle: every builtin, macro and special form a program may
216    // name is read off the interpreter that will run it, never off a list.
217    // Building it is not a stage; nothing is evaluated until `eval_prepared`.
218    let (mut interp, mut prepared) = prepared_on(entry, inputs, loader, surface)?;
219    hold_to_frame(&mut interp, &mut prepared, hold)?;
220    let mut value = crate::timings::stage(crate::timings::Stage::Eval, || {
221        eval_prepared(&mut interp, &prepared, &mut ())
222    })?;
223    if let Some(v) = crate::timings::stage(crate::timings::Stage::Main, || {
224        call_main(&mut interp, &prepared, &mut ())
225    })? {
226        value = v;
227    }
228    Ok(Run {
229        value,
230        visited: prepared.visited,
231        typed_decls: prepared.typed_decls,
232        seams: prepared.seams,
233    })
234}
235
236/// Bind `hold` into `interp` and its processes, with each scoped provider's
237/// packages moved onto it (`Hold::rebind_scoped`); a withheld name the entry
238/// file writes refuses the program before it runs.
239fn hold_to_frame(
240    interp: &mut tatara_lisp_eval::Interpreter<()>,
241    prepared: &mut Prepared,
242    hold: Option<&crate::Hold>,
243) -> Result<(), RunError> {
244    let Some(hold) = hold else {
245        return Ok(());
246    };
247    let withheld = hold.rebind_scoped(&prepared.program, &mut prepared.erased);
248    let escapes = entry_escapes(&prepared.program, &withheld, &|cap| {
249        format!("this run does not allow; pass --allow {cap} to grant it")
250    });
251    if !escapes.is_empty() {
252        return Err(RunError::Types(escapes));
253    }
254    hold.bind(interp, "this run's frame");
255    crate::attach_node_held(interp, hold);
256    Ok(())
257}
258
259fn entry_escapes(
260    program: &crate::uses::ResolvedProgram,
261    refused: &[(&'static str, blue_lang_waku::Capability)],
262    grant: &dyn Fn(&str) -> String,
263) -> Vec<String> {
264    use crate::uses::ResolvedProgram;
265    let forms: Vec<tatara_lisp::Spanned> = program
266        .forms()
267        .iter()
268        .enumerate()
269        .filter(|(i, _)| program.owner_of(*i) == Some(ResolvedProgram::ENTRY))
270        .map(|(_, f)| f.clone())
271        .collect();
272    let label = |name: &str| {
273        refused
274            .iter()
275            .find(|(n, _)| *n == name)
276            .map(|(_, c)| c.label())
277    };
278    blue_lang_waku::free_names(&forms, &|name| label(name).is_none())
279        .into_iter()
280        .filter_map(|name| {
281            let cap = label(&name)?;
282            let at = program
283                .file(ResolvedProgram::ENTRY)
284                .and_then(|f| f.path.clone())
285                .map_or_else(|| "<entry>".to_string(), |p| p.display().to_string());
286            Some(format!(
287                "{at}: refused: `{name}` reaches the host ({cap}), which {}",
288                grant(cap)
289            ))
290        })
291        .collect()
292}
293
294/// The interpreter a program runs on, with `inputs` bound for its macros.
295fn runtime(inputs: Inputs) -> tatara_lisp_eval::Interpreter<()> {
296    let mut interp =
297        crate::timings::stage(crate::timings::Stage::Runtime, crate::interpreter_hostless);
298    crate::inputs::install_input_primitives(&mut interp, inputs);
299    interp
300}
301
302/// [`runtime`], and the program prepared against that interpreter's names.
303fn prepared_on(
304    entry: Entry<'_>,
305    inputs: Inputs,
306    loader: &dyn crate::uses::Loader,
307    surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
308) -> Result<(tatara_lisp_eval::Interpreter<()>, Prepared), RunError> {
309    let interp = runtime(inputs);
310    let builtins = crate::timings::stage(crate::timings::Stage::Names, || builtin_names(&interp));
311    let prepared = prepare(entry, loader, surface, &builtins)?;
312    Ok((interp, prepared))
313}
314
315/// A program past every stage before evaluation, kept as data: the tree
316/// `def_hash` reads (`lowered`), the tree that runs (`erased`), one form per
317/// top-level form of `program` in both, and the runtime key of the entry
318/// file's `main`. What `blue bundle` seals into an appliance, and what
319/// [`run_compiled`] evaluates with no loader and no checker.
320pub struct Compiled {
321    pub program: crate::uses::ResolvedProgram,
322    pub lowered: Vec<tatara_lisp::Spanned>,
323    pub erased: Vec<tatara_lisp::Spanned>,
324    pub main: Option<String>,
325}
326
327/// Parse, resolve, check and erase `entry`, and stop before evaluation. Under
328/// a `hold`, a host name the entry file writes that the hold withholds
329/// refuses the program here, as [`run_framed`] refuses it before it runs.
330///
331/// # Errors
332///
333/// As [`run_framed`], with no runtime error: nothing is evaluated.
334pub fn compile(
335    entry: Entry<'_>,
336    loader: &dyn crate::uses::Loader,
337    surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
338    hold: Option<&crate::Hold>,
339) -> Result<Compiled, RunError> {
340    let (_, prepared) = prepared_on(entry, Inputs::new(), loader, surface)?;
341    if let Some(hold) = hold {
342        let escapes = entry_escapes(&prepared.program, &hold.withheld_host_names(), &|cap| {
343            format!("this bundle's policy does not allow; declare allow(%i[{cap}]) in the project's Bluefile and run `blue lock`")
344        });
345        if !escapes.is_empty() {
346            return Err(RunError::Types(escapes));
347        }
348    }
349    let main = entry_main(&prepared.program).map(|(top_level, _)| {
350        blue_lang_check::names::key(&namespace_of(&prepared.program, top_level), MAIN)
351    });
352    let Prepared {
353        program,
354        lowered,
355        erased,
356        ..
357    } = prepared;
358    Ok(Compiled {
359        program,
360        lowered,
361        erased,
362        main,
363    })
364}
365
366/// Evaluate compiled forms under `hold`, then call `main` when one is named.
367/// The hold is not optional: a compiled program has no ambient run. `who`
368/// names the hold in a breach's message. Errors carry no position: the
369/// forms carry no source.
370///
371/// # Errors
372///
373/// [`RunError::Eval`] when a form or `main` raises, a breach included
374/// ([`crate::observe::Fault::breach`]).
375pub fn run_compiled(
376    erased: &[tatara_lisp::Spanned],
377    main: Option<&str>,
378    hold: &crate::Hold,
379    who: &'static str,
380) -> Result<Value, RunError> {
381    let mut interp = runtime(Inputs::new());
382    hold.bind(&mut interp, who);
383    crate::attach_node_held(&mut interp, hold);
384    let fault = |e: &tatara_lisp_eval::EvalError| {
385        RunError::Eval(Box::new(crate::observe::Fault::unplaced(e, erased)))
386    };
387    let mut value = Value::Nil;
388    for form in erased {
389        value = interp.eval_top_form(form, &mut ()).map_err(|e| fault(&e))?;
390    }
391    if let Some(key) = main {
392        let at = tatara_lisp::Span::synthetic();
393        let call = tatara_lisp::Spanned::new(
394            at,
395            tatara_lisp::SpannedForm::List(vec![tatara_lisp::Spanned::new(
396                at,
397                tatara_lisp::SpannedForm::Atom(tatara_lisp::Atom::Symbol(key.to_string())),
398            )]),
399        );
400        value = interp
401            .eval_top_form(&call, &mut ())
402            .map_err(|e| fault(&e))?;
403    }
404    Ok(value)
405}
406
407/// A run watched by a call observer, and the definitions the observer was
408/// built over.
409pub struct Observed {
410    pub definitions: std::sync::Arc<crate::observe::Definitions>,
411    /// The run itself: its value, or the error it stopped on.
412    pub outcome: Result<Run, RunError>,
413}
414
415/// What a call observer is built from: the program past every stage before
416/// evaluation, its tree before erasure (what `def_hash` reads) and after it
417/// (what runs), and the definitions read off it.
418pub struct Watched<'a> {
419    pub definitions: &'a std::sync::Arc<crate::observe::Definitions>,
420    pub program: &'a crate::uses::ResolvedProgram,
421    pub lowered: &'a [tatara_lisp::Spanned],
422    pub erased: &'a [tatara_lisp::Spanned],
423}
424
425/// [`run_in_surface`] with a call observer installed for the evaluation.
426/// `watch` builds the observer once every stage before evaluation has
427/// passed; the profiler, the tracer, coverage and the call memo are all built
428/// this way (`crate::observe::Recorder`, `blue-lang-mondou`'s
429/// `recall::Recall`).
430///
431/// # Errors
432///
433/// A parse, import or check error, which leaves nothing to run. A runtime
434/// error is the [`Observed::outcome`], so what ran before it is still read.
435pub fn run_observed(
436    entry: Entry<'_>,
437    inputs: Inputs,
438    loader: &dyn crate::uses::Loader,
439    surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
440    watch: impl FnOnce(&Watched<'_>) -> std::sync::Arc<dyn crate::observe::CallObserver>,
441) -> Result<Observed, RunError> {
442    run_observed_framed(entry, inputs, loader, surface, None, watch)
443}
444
445/// [`run_observed`] held to `frame` as [`run_framed`] holds a run.
446///
447/// # Errors
448///
449/// As [`run_observed`], and as [`run_framed`] for a host name the entry file
450/// writes that `frame` does not grant.
451pub fn run_observed_framed(
452    entry: Entry<'_>,
453    inputs: Inputs,
454    loader: &dyn crate::uses::Loader,
455    surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
456    hold: Option<&crate::Hold>,
457    watch: impl FnOnce(&Watched<'_>) -> std::sync::Arc<dyn crate::observe::CallObserver>,
458) -> Result<Observed, RunError> {
459    let (mut interp, mut prepared) = prepared_on(entry, inputs, loader, surface)?;
460    hold_to_frame(&mut interp, &mut prepared, hold)?;
461    let definitions = std::sync::Arc::new(crate::observe::Definitions::of(
462        &prepared.program,
463        &prepared.erased,
464    ));
465    interp.set_call_observer(Some(watch(&Watched {
466        definitions: &definitions,
467        program: &prepared.program,
468        lowered: &prepared.lowered,
469        erased: &prepared.erased,
470    })));
471    let outcome = crate::timings::stage(crate::timings::Stage::Eval, || {
472        eval_prepared(&mut interp, &prepared, &mut ())
473    })
474    .and_then(|value| {
475        Ok(crate::timings::stage(crate::timings::Stage::Main, || {
476            call_main(&mut interp, &prepared, &mut ())
477        })?
478        .unwrap_or(value))
479    })
480    .map(|value| Run {
481        value,
482        visited: prepared.visited,
483        typed_decls: prepared.typed_decls,
484        seams: prepared.seams,
485    });
486    interp.set_call_observer(None);
487    Ok(Observed {
488        definitions,
489        outcome,
490    })
491}
492
493/// A program past every stage before evaluation: parsed, its imports
494/// resolved, type-checked and erased. What [`run_in_surface`] and
495/// [`crate::hosted::load_hosted`] both evaluate, so the stage order this
496/// module exists to own is written once for both.
497pub(crate) struct Prepared {
498    program: crate::uses::ResolvedProgram,
499    lowered: Vec<tatara_lisp::Spanned>,
500    erased: Vec<tatara_lisp::Spanned>,
501    visited: usize,
502    typed_decls: usize,
503    seams: usize,
504}
505
506impl Prepared {
507    /// The forms the evaluator will run: every file's, resolved and erased.
508    pub(crate) fn erased(&self) -> &[tatara_lisp::Spanned] {
509        &self.erased
510    }
511
512    /// The entry file's namespace.
513    pub(crate) fn entry_namespace(&self) -> Namespace {
514        let first = (0..self.program.forms().len())
515            .find(|i| self.program.owner_of(*i) == Some(crate::uses::ResolvedProgram::ENTRY));
516        match first {
517            Some(i) => namespace_of(&self.program, i),
518            None => match self
519                .program
520                .file(crate::uses::ResolvedProgram::ENTRY)
521                .and_then(|f| f.package.clone())
522            {
523                Some(p) => Namespace::Bidama(p),
524                None => Namespace::File(String::new()),
525            },
526        }
527    }
528}
529
530/// Parse, resolve, check and erase, in that order.
531///
532/// `builtins` is the name table of the interpreter that will evaluate the
533/// result ([`builtin_names`]); the check stage resolves the program's names
534/// against it.
535pub(crate) fn prepare(
536    entry: Entry<'_>,
537    loader: &dyn crate::uses::Loader,
538    surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
539    builtins: &NameTable,
540) -> Result<Prepared, RunError> {
541    let mut program = parse_and_resolve(entry, loader, surface)?;
542
543    // `test` blocks are declarations for the harness, not code to run.
544    //
545    // Dropped here rather than in `resolve_uses`, because `blue test` calls
546    // the resolver and then NEEDS the entry file's blocks — so the two
547    // callers want different things and the split has to live at this level.
548    //
549    // Without this, `blue run` on a file containing its own tests fails with
550    // `unbound symbol: deftest`: every package in the bidama distribution
551    // carries tests, so every one of them was unrunnable.
552    //
553    // Through `retain`, which drops each form's owner with it. A plain filter
554    // over the forms alone would slide every later form onto the wrong file.
555    program.retain(|f| !crate::uses::is_test_form(f));
556
557    // CHECK, on the annotated tree — the only tree that has annotations.
558    //
559    // **On the REAL spanned tree, including every imported package's.** This
560    // was the one caller that checked a spanless lift, because `resolve_uses`
561    // flattened the entry file and its imports into one list and `Span` is a
562    // byte range with no file identity — so a real span here would have
563    // reported an imported package's error at that offset in the ENTRY file, a
564    // precise-looking answer pointing at unrelated code.
565    //
566    // The fix is not a wider `Span` (that type is upstream, and its own docs
567    // put file identity on the caller: spans "are meaningful only relative to
568    // the string that produced them, which the caller is responsible for
569    // holding onto"). blue holds onto it BESIDE the span, per top-level form —
570    // see `uses::ResolvedProgram`.
571    let (outcome, names) = crate::timings::stage(crate::timings::Stage::Check, || {
572        check_stage(&program, builtins, false)
573    });
574    if !outcome.ok() {
575        return Err(RunError::Types(
576            outcome
577                .errors()
578                // `file:line:col: error[CODE]: message`, resolved against the
579                // file the form actually came from, then its help line.
580                .map(|d| render(&program, d))
581                .collect(),
582        ));
583    }
584
585    // ERASE, so the interpreter never sees a type — **on the spanned tree,
586    // and back out as one.**
587    //
588    // This used to be `erase_types(&program.sexps())` followed by a
589    // `lower_to_spanned` that stamped `Span::synthetic()` over every node, and
590    // the comment here said carrying real positions through was "a larger
591    // piece and is NOT built". It was not larger: erasure only ever DELETES
592    // nodes — the single node it invents is the `define` replacing
593    // `define-typed` — so there was never anything for a synthetic span to
594    // stand in for. See `crate::erase`.
595    //
596    // No lowering step follows. The tree the evaluator receives IS the tree the
597    // parser built, minus annotations, with the author's byte offsets intact.
598    let (lowered, erased) = crate::timings::stage(crate::timings::Stage::Erase, || {
599        let lowered = lower(&program, &names);
600        let erased = erase_types(&lowered);
601        (lowered, erased)
602    });
603
604    Ok(Prepared {
605        program,
606        lowered,
607        erased,
608        visited: outcome.stats.visited,
609        typed_decls: outcome.stats.typed_decls,
610        seams: outcome.seams.len(),
611    })
612}
613
614/// Parse the entry (in `surface`, if one is given) and splice in its imports.
615pub(crate) fn parse_and_resolve(
616    entry: Entry<'_>,
617    loader: &dyn crate::uses::Loader,
618    surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
619) -> Result<crate::uses::ResolvedProgram, RunError> {
620    let forms = crate::timings::stage(crate::timings::Stage::Parse, || {
621        let mut forms = match surface {
622            Some(pack) => blue_lang_syntax::parse_program_tree_in(entry.text, pack)
623                .map_err(|e| RunError::Parse(e.to_string()))?,
624            None => loader
625                .parse(entry.text)
626                .map_err(|e| RunError::Parse(e.to_string()))?,
627        };
628        // Every `@doc` example is a `test` block of the entry file: checked with
629        // the file's names, run by `blue test`, dropped by `run` with the others.
630        if surface.is_none() {
631            forms.extend(
632                blue_lang_syntax::doc::doctests(entry.text)
633                    .map_err(|e| RunError::Parse(e.to_string()))?,
634            );
635        }
636        Ok::<_, RunError>(forms)
637    })?;
638
639    // RESOLVE imports first, so everything below sees ONE program.
640    //
641    // Before the check on purpose: imported code is type-checked at the point
642    // its consumer imports it, rather than at whatever later moment its code
643    // first runs. A package that does not typecheck should break its importer's
644    // build, not their production run.
645    //
646    // One program, but not one FILE: the result records which file each
647    // top-level form came from, which is what lets a diagnostic below name a
648    // place instead of only a problem.
649    crate::timings::stage(crate::timings::Stage::Resolve, || {
650        crate::uses::resolve_uses(forms, entry, loader).map_err(RunError::Import)
651    })
652}
653
654/// **The check stage**: every rule in `blue_lang_check::RULES`, over a
655/// resolved program, with waivers applied.
656///
657/// One function, called by every door — `run`, the hosted loader, `blue test`
658/// and `blue check` — so a rule added to the registry is enforced by all of
659/// them at once. That is what makes an architecture rule a pipeline error
660/// rather than an optional report: there is no door that skips it.
661///
662/// Warnings (unused bindings, unused waivers) are reported for the ENTRY
663/// file only; an imported package's warnings belong to its author. Errors
664/// are reported for every file, because an imported package with one is
665/// broken for its importer.
666pub(crate) fn check_stage(
667    program: &crate::uses::ResolvedProgram,
668    builtins: &NameTable,
669    with_tests: bool,
670) -> (blue_lang_check::Outcome, NameTable) {
671    use crate::uses::ResolvedProgram;
672    let forms = program.forms();
673    let is_entry = |i: usize| program.owner_of(i) == Some(ResolvedProgram::ENTRY);
674
675    // Rules B0003–B0005: the typing ladder.
676    let mut outcome = blue_lang_check::check_program_in(forms, &|a, b| {
677        program.owner_of(a) == program.owner_of(b)
678    });
679    outcome
680        .diagnostics
681        .extend(data_law_diagnostics(program, &is_entry));
682
683    // Rules B0001–B0002: names. The program's own scopes come first (the
684    // entry file, then each bidama in import order), then the interpreter's.
685    let table = program_names(program, builtins);
686    let whole = |i: usize| with_tests && is_entry(i);
687    let (names, resolved) = blue_lang_check::names::check_names_whole(
688        forms,
689        &table,
690        &|i| namespace_of(program, i),
691        &is_entry,
692        &whole,
693    );
694    let stored = program
695        .file(ResolvedProgram::ENTRY)
696        .is_some_and(|f| f.path.is_some());
697    outcome.diagnostics.extend(names);
698    outcome.stats.names_resolved = resolved;
699    if stored {
700        let script = program
701            .file(ResolvedProgram::ENTRY)
702            .is_some_and(|f| f.package.is_none());
703        let found = blue_lang_check::form_rules::check(forms, &is_entry, script);
704        let silenced: Vec<tatara_lisp::Span> = found
705            .iter()
706            .filter(|d| d.code == blue_lang_check::Code::B0038)
707            .map(|d| d.span)
708            .collect();
709        outcome
710            .diagnostics
711            .retain(|d| !(d.code == blue_lang_check::Code::B0002 && silenced.contains(&d.span)));
712        outcome.diagnostics.extend(found);
713    }
714    outcome
715        .diagnostics
716        .extend(blue_lang_check::contracts::check(
717            forms,
718            &table,
719            &|i| namespace_of(program, i),
720            &is_entry,
721            &|i| {
722                program
723                    .owner_of(i)
724                    .and_then(|f| program.file(f))
725                    .map(|f| f.text.as_str())
726            },
727            crate::docs::signatures(),
728        ));
729
730    outcome.diagnostics.extend(doc_diagnostics(program));
731
732    // Waivers, per file: B0007 for a malformed one, B0008 for one that
733    // suppresses nothing.
734    let mut waivers = Vec::new();
735    for file in program.files() {
736        let spans: Vec<(usize, tatara_lisp::Span)> = forms
737            .iter()
738            .enumerate()
739            .filter(|(i, _)| program.owner_of(*i) == Some(file.id))
740            .map(|(i, f)| (i, f.span))
741            .collect();
742        let (found, malformed) = blue_lang_check::waiver::collect(&file.text, &spans);
743        waivers.extend(found);
744        outcome.diagnostics.extend(malformed);
745    }
746    outcome.waived = blue_lang_check::waiver::apply(&mut outcome.diagnostics, waivers, &is_entry);
747    if !stored {
748        outcome.diagnostics.retain(|d| {
749            !matches!(
750                d.code,
751                blue_lang_check::Code::B0023
752                    | blue_lang_check::Code::B0036
753                    | blue_lang_check::Code::B0037
754                    | blue_lang_check::Code::B0067
755                    | blue_lang_check::Code::B0061
756                    | blue_lang_check::Code::B0062
757                    | blue_lang_check::Code::B0063
758            )
759        });
760    }
761    let judge = blue_lang_check::evolution::FormJudge::from_env().unwrap_or_default();
762    outcome
763        .diagnostics
764        .retain(|d| judge.reports(d.code, is_entry(d.top_level)));
765    // A rule whose census has not reached zero is computed, and counted, but
766    // not enforced: its findings move to `census` (see `RULES`' `ratchet`).
767    let (enforced, census): (Vec<_>, Vec<_>) = std::mem::take(&mut outcome.diagnostics)
768        .into_iter()
769        .partition(|d| d.code.rule().active());
770    outcome.diagnostics = enforced;
771    outcome.census = census;
772    outcome
773        .diagnostics
774        .sort_by_key(|d| (d.top_level, d.span.start, d.code));
775    (outcome, table)
776}
777
778/// The documentation rules (B0056–B0060) over the entry file, when it is a
779/// bidama's: a bidama is judged at its own check, as its form is; an importer
780/// is not answerable for a dependency's documentation.
781fn doc_diagnostics(program: &crate::uses::ResolvedProgram) -> Vec<blue_lang_check::Diagnostic> {
782    use crate::uses::ResolvedProgram;
783    let Some(file) = program.file(ResolvedProgram::ENTRY) else {
784        return Vec::new();
785    };
786    let Some(package) = file.package.as_deref() else {
787        return Vec::new();
788    };
789    let forms: Vec<(usize, &tatara_lisp::Spanned)> = program
790        .forms()
791        .iter()
792        .enumerate()
793        .filter(|(i, f)| {
794            program.owner_of(*i) == Some(ResolvedProgram::ENTRY)
795                && !blue_lang_syntax::doc::is_doctest(f)
796        })
797        .collect();
798    let graph = file.path.as_deref().map(crate::doc_sources::graph_meanings);
799    let meaning = |name: &str| {
800        graph
801            .as_ref()
802            .and_then(|g| g.get(&(package.to_string(), name.to_string())).cloned())
803    };
804    let names_line = |name: &str| crate::docs::doc_of(name).map(|d| d.doc.to_string());
805    blue_lang_check::doc_rules::check_file(
806        &file.text,
807        &forms,
808        package,
809        &blue_lang_check::doc_rules::Sources {
810            meaning: &meaning,
811            names_line: &names_line,
812        },
813    )
814}
815
816fn data_law_diagnostics(
817    program: &crate::uses::ResolvedProgram,
818    is_entry: &dyn Fn(usize) -> bool,
819) -> Vec<blue_lang_check::Diagnostic> {
820    use blue_lang_check::data_laws;
821    let forms = program.forms();
822    let fragments = data_laws::Fragments::collect(forms);
823    let frontend = program.frontend();
824    let mut out = Vec::new();
825    let schema = match frontend.and_then(|f| f.table.as_ref()) {
826        Some((label, text)) => match data_laws::Schema::read(text) {
827            Ok(s) => Some(s),
828            Err(why) => {
829                if let Some((i, first)) = forms.iter().enumerate().find(|(i, _)| is_entry(*i)) {
830                    out.push(
831                        blue_lang_check::Diagnostic::new(
832                            blue_lang_check::Code::B0050,
833                            format!("the generated schema table {label} does not read: {why}"),
834                            first.span,
835                        )
836                        .at_top_level(i)
837                        .with_help("regenerate it: `nix run .#regen`"),
838                    );
839                }
840                None
841            }
842        },
843        None => None,
844    };
845    if fragments.is_empty() && schema.is_none() && !frontend.is_some_and(|f| f.view) {
846        return out;
847    }
848    out.extend(data_laws::check(
849        forms,
850        &data_laws::Context {
851            fragments: &fragments,
852            schema: schema.as_ref(),
853            is_entry,
854            view: frontend.is_some_and(|f| f.view),
855        },
856    ));
857    out
858}
859
860/// The whole name table the check stage resolves against: the program's
861/// own scopes (each file and bidama, by namespace) followed by `builtins`.
862///
863/// **A top-level call to a builtin macro can define names**: `defflow(slug,
864/// trim, downcase)` expands to `(define slug …)`, and nothing in the source
865/// spells `define`. Such a call is expanded here, on a hostless fork, and what
866/// its expansion defines is bound in the calling file's namespace. Only
867/// BUILTIN macros are expanded: their bodies are the runtime's own
868/// templates. A program's own `defmacro` is not run at check time — checking a
869/// file (and an editor checking on every keystroke) must not execute the code
870/// it checks — so names a program macro generates are not seen; none of the
871/// corpus's macros generates one (measured 2026-09-29).
872#[must_use]
873pub fn program_names(program: &crate::uses::ResolvedProgram, builtins: &NameTable) -> NameTable {
874    use blue_lang_check::Binding;
875    let mut table = NameTable::new();
876    table.add_program(program.forms(), |i| namespace_of(program, i));
877    let builtin_macro = |head: &str| {
878        builtins
879            .scopes()
880            .iter()
881            .any(|s| s.namespace == Namespace::Macro && s.get(head).is_some())
882    };
883    let mut expander: Option<tatara_lisp_eval::Interpreter<()>> = None;
884    for (i, form) in program.forms().iter().enumerate() {
885        let Some(head) = form
886            .as_list()
887            .and_then(|items| items.first())
888            .and_then(tatara_lisp::Spanned::as_symbol)
889        else {
890            continue;
891        };
892        if !builtin_macro(head) {
893            continue;
894        }
895        let interp = expander.get_or_insert_with(crate::interpreter_hostless);
896        // A failed expansion defines nothing here; the evaluator will report
897        // it, with its position, when the form runs.
898        let Ok(expanded) = interp.fully_expand(form, &mut ()) else {
899            continue;
900        };
901        let nullary = blue_lang_syntax::scope::nullary_definition(&expanded);
902        for (name, span, kind) in blue_lang_check::names::definitions_of(&expanded) {
903            table.define(
904                namespace_of(program, i),
905                Binding {
906                    nullary: nullary == Some(name.as_str()),
907                    name,
908                    kind,
909                    span: Some(span),
910                    top_level: Some(i),
911                },
912            );
913        }
914    }
915    for scope in builtins.scopes() {
916        table.push(scope.clone());
917    }
918    table.spell_bare_like(builtins);
919    let form_file: Vec<usize> = (0..program.forms().len())
920        .map(|i| program.owner_of(i).map_or(usize::MAX, |f| f.index()))
921        .collect();
922    let mut imports: std::collections::BTreeMap<usize, blue_lang_check::names::FileImports> =
923        std::collections::BTreeMap::new();
924    for (file, import) in program.imports() {
925        let entry = imports.entry(file.index()).or_default();
926        entry.add(&import.package, import.names.iter().map(|(n, _)| n.clone()));
927        let at = import.span.start;
928        entry.use_at = Some(entry.use_at.map_or(at, |a| a.min(at)));
929    }
930    // A FACADE — a bidama that defines nothing and `use`s others (zenbu) —
931    // exists so a consumer can depend on "everything": `use("zenbu")` makes
932    // every member's QUALIFIER reachable (`toukei::median`), never a bare
933    // name. Followed to a fixpoint, so a facade of facades works too.
934    let facade_uses =
935        |pkg: &str,
936         imports: &std::collections::BTreeMap<usize, blue_lang_check::names::FileImports>| {
937            let defines = table.bidama(pkg).is_some_and(|s| !s.is_empty());
938            if defines {
939                return std::collections::BTreeSet::new();
940            }
941            program
942                .files()
943                .iter()
944                .filter(|f| f.package.as_deref() == Some(pkg))
945                .filter_map(|f| imports.get(&f.id.index()))
946                .flat_map(|i| i.uses.iter().cloned())
947                .collect::<std::collections::BTreeSet<String>>()
948        };
949    loop {
950        let mut grew = false;
951        let files: Vec<usize> = imports.keys().copied().collect();
952        for f in files {
953            let used: Vec<String> = imports[&f].uses.iter().cloned().collect();
954            for p in used {
955                for member in facade_uses(&p, &imports) {
956                    if let Some(i) = imports.get_mut(&f) {
957                        if i.uses.insert(member.clone()) {
958                            i.via.insert(member, p.clone());
959                            grew = true;
960                        }
961                    }
962                }
963            }
964        }
965        if !grew {
966            break;
967        }
968    }
969    let mut pkg_uses: std::collections::BTreeMap<String, std::collections::BTreeSet<String>> =
970        std::collections::BTreeMap::new();
971    for (file, import) in program.imports() {
972        if let Some(p) = program.file(*file).and_then(|f| f.package.clone()) {
973            pkg_uses
974                .entry(p)
975                .or_default()
976                .insert(import.package.clone());
977        }
978    }
979    table.attach_files(form_file, imports);
980    table.attach_package_uses(pkg_uses);
981    table.attach_needs(program.needs().clone());
982    // Where each file lives and which distribution each bidama ships in: a
983    // bridge reference (B0021) is an error inside the bidama's own
984    // distribution and a warning outside it.
985    let canonical = |p: &std::path::Path| {
986        std::fs::canonicalize(p)
987            .unwrap_or_else(|_| p.to_path_buf())
988            .display()
989            .to_string()
990    };
991    let mut file_paths = std::collections::BTreeMap::new();
992    let mut pkg_roots = std::collections::BTreeMap::new();
993    for f in program.files() {
994        if let Some(path) = &f.path {
995            file_paths.insert(f.id.index(), canonical(path));
996            if let (Some(pkg), Some(root)) =
997                (&f.package, path.parent().and_then(std::path::Path::parent))
998            {
999                pkg_roots
1000                    .entry(pkg.clone())
1001                    .or_insert_with(|| canonical(root));
1002            }
1003        }
1004    }
1005    table.attach_paths(file_paths, pkg_roots);
1006    let versions = program.versions().clone();
1007    table.apply_legacy(program.forms(), &|i| namespace_of(program, i), &|p| {
1008        versions.get(p).cloned()
1009    });
1010    table
1011}
1012
1013/// The forms the evaluator runs, from a checked program: every `use`
1014/// declaration inert (`nil`, so each index still names its top-level form)
1015/// and every name at the key it runs under — the RESOLVED tree under
1016/// per-bidama namespaces (`blue_lang_check::names::Resolved::resolved_tree`).
1017///
1018/// A definition runs as `retsu/first` (a script's as `%root/f`), a
1019/// reference to it as the same key, and a builtin as its bare name. Keys
1020/// cannot collide across namespaces, so a bidama's `first` no longer
1021/// replaces the builtin for anyone who did not import it, and an importer's
1022/// `helper` no longer replaces a bidama's own: the runtime binds exactly what
1023/// the check stage resolved.
1024#[must_use]
1025pub fn lower(
1026    program: &crate::uses::ResolvedProgram,
1027    names: &NameTable,
1028) -> Vec<tatara_lisp::Spanned> {
1029    lower_with(program, names, true)
1030}
1031
1032#[must_use]
1033pub fn lower_with(
1034    program: &crate::uses::ResolvedProgram,
1035    names: &NameTable,
1036    simplify_chains: bool,
1037) -> Vec<tatara_lisp::Spanned> {
1038    let mut resolved = blue_lang_check::names::resolve_program(program.forms(), names, &|i| {
1039        namespace_of(program, i)
1040    });
1041    let simplified = if simplify_chains {
1042        chain_simplified(program.forms(), &resolved)
1043    } else {
1044        None
1045    };
1046    let forms: &[tatara_lisp::Spanned] = simplified.as_deref().unwrap_or(program.forms());
1047    if simplified.is_some() {
1048        resolved =
1049            blue_lang_check::names::resolve_program(forms, names, &|i| namespace_of(program, i));
1050    }
1051    resolved
1052        .resolved_tree(forms, blue_lang_check::names::Rule::Namespaced)
1053        .into_iter()
1054        .zip(forms)
1055        .enumerate()
1056        .map(|(i, (resolved, written))| {
1057            if crate::uses::use_target(written).is_some()
1058                || blue_lang_syntax::scope::legacy_target(written).is_some()
1059            {
1060                tatara_lisp::Spanned::new(written.span, tatara_lisp::SpannedForm::Nil)
1061            } else {
1062                let file = program.owner_of(i).and_then(|id| program.file(id));
1063                match file {
1064                    Some(f) => locate_raises(&resolved, &raise_site(f)),
1065                    None => resolved,
1066                }
1067            }
1068        })
1069        .collect()
1070}
1071
1072fn chain_simplified(
1073    forms: &[tatara_lisp::Spanned],
1074    resolved: &blue_lang_check::names::Resolved,
1075) -> Option<Vec<tatara_lisp::Spanned>> {
1076    let respell: std::collections::HashMap<usize, blue_lang_check::respell::Respelling> = resolved
1077        .respellings
1078        .iter()
1079        .filter(|(_, r)| matches!(r.respell, blue_lang_check::respell::Respell::Chain(_)))
1080        .cloned()
1081        .collect();
1082    if respell.is_empty() {
1083        return None;
1084    }
1085    let bare = std::collections::HashSet::new();
1086    let canon = Canon {
1087        bare: &bare,
1088        respell: &respell,
1089    };
1090    Some(forms.iter().map(|f| canon.form(f)).collect())
1091}
1092
1093fn raise_site(file: &crate::uses::SourceFile) -> impl Fn(tatara_lisp::Span) -> String + '_ {
1094    move |span| {
1095        let (line, _) =
1096            blue_lang_syntax::Span::line_col(&file.text, span.start.min(file.text.len()));
1097        let shown = file
1098            .path
1099            .as_ref()
1100            .map_or_else(|| "<source>".to_string(), |p| p.display().to_string());
1101        format!("{shown}:{line}")
1102    }
1103}
1104
1105fn locate_raises(
1106    form: &tatara_lisp::Spanned,
1107    site: &dyn Fn(tatara_lisp::Span) -> String,
1108) -> tatara_lisp::Spanned {
1109    use tatara_lisp::{Atom, Spanned, SpannedForm};
1110    let inner = match &form.form {
1111        SpannedForm::List(items) => {
1112            let mut out: Vec<Spanned> = items.iter().map(|i| locate_raises(i, site)).collect();
1113            let raising = items.first().and_then(Spanned::as_symbol) == Some("raise")
1114                && (3..=4).contains(&items.len());
1115            if raising {
1116                out[0] = Spanned::new(
1117                    items[0].span,
1118                    SpannedForm::Atom(Atom::Symbol(crate::errors::RAISE_AT.to_string())),
1119                );
1120                out.insert(
1121                    1,
1122                    Spanned::new(items[0].span, SpannedForm::Atom(Atom::Str(site(form.span)))),
1123                );
1124            }
1125            SpannedForm::List(out)
1126        }
1127        SpannedForm::Quote(_) | SpannedForm::Quasiquote(_) => return form.clone(),
1128        other => other.clone(),
1129    };
1130    Spanned::new(form.span, inner)
1131}
1132
1133/// The namespace top-level form `i` of `program` defines into.
1134#[must_use]
1135pub fn namespace_of(program: &crate::uses::ResolvedProgram, i: usize) -> Namespace {
1136    let file = program.owner_of(i).and_then(|id| program.file(id));
1137    match file {
1138        Some(f) => match &f.package {
1139            Some(p) => Namespace::Bidama(p.clone()),
1140            None => Namespace::File(
1141                f.path
1142                    .as_ref()
1143                    .map_or_else(|| "<anonymous>".to_string(), |p| p.display().to_string()),
1144            ),
1145        },
1146        None => Namespace::File("<unknown file>".to_string()),
1147    }
1148}
1149
1150/// Names only the test harness binds.
1151///
1152/// In scope for EVERY check, not only for test blocks. A bidama may carry a
1153/// test helper — a plain `def` that asserts, called only from its `test`
1154/// blocks (`heni`'s `hn_end_to_end` is one) — and every program importing that
1155/// bidama would otherwise be refused for a function it never calls. The
1156/// refusal would be scoped to the importer's whole program, not to the fault.
1157/// The cost, stated: an `assert` outside any test that `blue run` actually
1158/// reaches is caught at runtime (`blue-assert` is unbound there), not here.
1159///
1160/// `blue-lang-test` registers these on its own interpreter, and this crate
1161/// cannot depend on it (it depends on this one), so the list lives beside the
1162/// only name in it: the lowered `assert`, which `blue-lang-syntax` owns.
1163pub const HARNESS_NAMES: &[&str] = &[blue_lang_syntax::LOWERED_ASSERT];
1164
1165/// The check stage's builtin scopes, read off the interpreter that will run
1166/// the program: the test harness's names ([`HARNESS_NAMES`]), then its special
1167/// forms, its macros, and every value it binds — the last three in the order
1168/// the evaluator consults for a head.
1169///
1170/// Read from a live interpreter rather than a list, for the reason
1171/// `Interpreter::resolve_head` gives: a list is a second copy of the truth and
1172/// goes stale on the next installer that lands. An embedder's own
1173/// primitives are in scope here because the embedder installs them first.
1174#[must_use]
1175pub fn builtin_names<H: 'static>(interp: &tatara_lisp_eval::Interpreter<H>) -> NameTable {
1176    builtin_names_with(interp, &[])
1177}
1178
1179#[must_use]
1180pub fn builtin_names_with<H: 'static>(
1181    interp: &tatara_lisp_eval::Interpreter<H>,
1182    host: &[crate::host_surface::HostName],
1183) -> NameTable {
1184    use blue_lang_check::{Binding, Scope, ScopeKind};
1185    use tatara_lisp_eval::HeadBinding;
1186    let mut special = Scope::new(Namespace::SpecialForm);
1187    let mut macros = Scope::new(Namespace::Macro);
1188    let mut values = Scope::new(Namespace::Builtin);
1189    // `reserved_head_names` reads the ROOT frame only, so a name installed on
1190    // a fork (the input primitives, on every `run`) is invisible to it; those
1191    // are asked about by name instead, which `resolve_head` answers through
1192    // every frame.
1193    let mut names: Vec<std::sync::Arc<str>> = interp.reserved_head_names().into_iter().collect();
1194    names.extend(
1195        crate::inputs::INPUT_PRIMITIVES
1196            .iter()
1197            .filter(|n| interp.resolve_head(n).is_some())
1198            .map(|n| std::sync::Arc::from(*n)),
1199    );
1200    for name in names {
1201        let (scope, kind) = match interp.resolve_head(&name) {
1202            Some(HeadBinding::SpecialForm) => (&mut special, ScopeKind::SpecialForm),
1203            Some(HeadBinding::Macro) => (&mut macros, ScopeKind::Macro),
1204            _ => (&mut values, ScopeKind::Value),
1205        };
1206        let nullary = kind == ScopeKind::Value
1207            && interp.lookup_global(&name).is_some_and(|v| {
1208                nullary_value(&v)
1209                    || (crate::docs::doc_of(&name).is_some_and(|d| d.signature == &*name)
1210                        && matches!(
1211                            v,
1212                            tatara_lisp_eval::Value::NativeFn(_)
1213                                | tatara_lisp_eval::Value::Closure(_)
1214                        ))
1215            });
1216        scope.bind(Binding {
1217            name: name.to_string(),
1218            kind,
1219            span: None,
1220            top_level: None,
1221            nullary,
1222        });
1223    }
1224    for h in host {
1225        values.bind(Binding {
1226            name: h.name.clone(),
1227            kind: ScopeKind::Value,
1228            span: None,
1229            top_level: None,
1230            nullary: h.nullary(),
1231        });
1232    }
1233    let mut table = NameTable::new();
1234    let mut harness = Scope::new(Namespace::Harness);
1235    for n in HARNESS_NAMES {
1236        harness.bind(Binding {
1237            name: (*n).to_string(),
1238            kind: ScopeKind::Value,
1239            span: None,
1240            top_level: None,
1241            nullary: false,
1242        });
1243    }
1244    table.push(harness);
1245    table.push(special);
1246    table.push(macros);
1247    table.push(values);
1248    table.spell_bare_only(
1249        crate::docs::NAMES
1250            .iter()
1251            .filter(|d| d.signature == d.name)
1252            .map(|d| d.name.to_string())
1253            .chain(host.iter().filter(|h| h.nullary()).map(|h| h.name.clone()))
1254            .collect(),
1255    );
1256    table
1257}
1258
1259#[must_use]
1260pub fn nullary_value(value: &tatara_lisp_eval::Value) -> bool {
1261    match value {
1262        tatara_lisp_eval::Value::NativeFn(f) => {
1263            matches!(f.arity, tatara_lisp_eval::ffi::Arity::Exact(0))
1264        }
1265        tatara_lisp_eval::Value::Closure(c) => c.params.is_empty() && c.rest.is_none(),
1266        _ => false,
1267    }
1268}
1269
1270/// A diagnostic as a person reads it: `file:line:col: error[B0001]: message`,
1271/// then an indented `help:` line when it has one, and a `fix:` line naming
1272/// the one command that applies blue's fixer for it.
1273#[must_use]
1274pub fn render(program: &crate::uses::ResolvedProgram, d: &blue_lang_check::Diagnostic) -> String {
1275    let head = d.to_string();
1276    let mut out = program.locate(d.top_level, d.span, &head).to_string();
1277    if let Some(help) = &d.help {
1278        out.push_str("\n  help: ");
1279        out.push_str(help);
1280    }
1281    let file = program
1282        .owner_of(d.top_level)
1283        .and_then(|id| program.file(id))
1284        .and_then(|f| f.path.as_ref());
1285    if let Some(cmd) =
1286        file.and_then(|p| blue_lang_check::evolution::fix_command(d, &p.display().to_string()))
1287    {
1288        out.push_str("\n  fix: ");
1289        out.push_str(&cmd);
1290    }
1291    out
1292}
1293
1294/// What [`check_entry`] checks.
1295#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1296pub enum Checking {
1297    /// The program `blue run` would evaluate: test blocks dropped.
1298    Program,
1299    /// The program plus the entry file's test blocks, as `blue test` and
1300    /// `blue check` see it.
1301    WithTests,
1302}
1303
1304/// A program through the check stage, with everything the stage found.
1305pub struct Checked {
1306    pub program: crate::uses::ResolvedProgram,
1307    pub outcome: blue_lang_check::Outcome,
1308    /// The name table the stage resolved against: every program scope, then
1309    /// the builtins. What an editor completes from.
1310    pub names: NameTable,
1311}
1312
1313impl Checked {
1314    /// The forms an evaluator runs: [`lower`]ed, then erased — the tree
1315    /// `run` evaluates, for a door that brings its own evaluator (the VM).
1316    #[must_use]
1317    pub fn erased(&self) -> Vec<tatara_lisp::Spanned> {
1318        erase_types(&lower(&self.program, &self.names))
1319    }
1320
1321    /// What `blue test` hands the harness: the program [`lower`]ed, spans
1322    /// projected away.
1323    #[must_use]
1324    pub fn evaluable(&self) -> Vec<tatara_lisp::Sexp> {
1325        lower(&self.program, &self.names)
1326            .iter()
1327            .map(tatara_lisp::Spanned::to_sexp)
1328            .collect()
1329    }
1330
1331    /// Every non-local reference, resolved under both rules — today's flat
1332    /// one and per-bidama namespaces — and the resolved trees built from them
1333    /// (`blue ast --resolved`).
1334    #[must_use]
1335    pub fn resolve(&self) -> blue_lang_check::names::Resolved {
1336        blue_lang_check::names::resolve_program(self.program.forms(), &self.names, &|i| {
1337            namespace_of(&self.program, i)
1338        })
1339    }
1340}
1341
1342impl Checked {
1343    #[must_use]
1344    pub fn call_explicit_forms(&self) -> Vec<tatara_lisp::Spanned> {
1345        self.resolve().call_explicit_tree(self.program.forms())
1346    }
1347
1348    #[must_use]
1349    pub fn canonical_entry(&self, src: &str) -> Option<Vec<tatara_lisp::Spanned>> {
1350        use crate::uses::ResolvedProgram;
1351        let program = &self.program;
1352        let entry: Vec<&tatara_lisp::Spanned> = program
1353            .forms()
1354            .iter()
1355            .enumerate()
1356            .filter(|(i, _)| program.owner_of(*i) == Some(ResolvedProgram::ENTRY))
1357            .map(|(_, f)| f)
1358            .filter(|f| !blue_lang_syntax::doc::is_doctest(f))
1359            .collect();
1360        let written = blue_lang_syntax::parse_program_tree(src).ok()?;
1361        let same = written.len() == entry.len()
1362            && written
1363                .iter()
1364                .zip(&entry)
1365                .all(|(w, e)| w.to_sexp() == e.to_sexp() && w.span == e.span);
1366        if !same {
1367            return None;
1368        }
1369        let resolved = self.resolve();
1370        let bare: std::collections::HashSet<usize> = resolved
1371            .parenthesized_nullary_calls()
1372            .into_iter()
1373            .filter(|r| program.owner_of(r.top_level) == Some(ResolvedProgram::ENTRY))
1374            .map(blue_lang_check::names::Reference::node_address)
1375            .collect();
1376        let waived: Vec<tatara_lisp::Span> = self
1377            .outcome
1378            .waived
1379            .iter()
1380            .map(|w| w.diagnostic.span)
1381            .collect();
1382        let respell: std::collections::HashMap<usize, blue_lang_check::respell::Respelling> =
1383            resolved
1384                .respellings
1385                .iter()
1386                .filter(|(_, r)| !waived.contains(&r.span))
1387                .cloned()
1388                .collect();
1389        let canon = Canon {
1390            bare: &bare,
1391            respell: &respell,
1392        };
1393        let forms: Vec<tatara_lisp::Spanned> = entry.into_iter().map(|f| canon.form(f)).collect();
1394        let script = program
1395            .file(ResolvedProgram::ENTRY)
1396            .is_some_and(|f| f.package.is_none());
1397        let forms = if script {
1398            blue_lang_check::form_rules::without_prefix(&forms).unwrap_or(forms)
1399        } else {
1400            forms
1401        };
1402        let forms = blue_lang_check::form_rules::without_main_guard(&forms).unwrap_or(forms);
1403        Some(blue_lang_check::form_rules::eta_reduced(&forms).unwrap_or(forms))
1404    }
1405}
1406
1407struct Canon<'a> {
1408    bare: &'a std::collections::HashSet<usize>,
1409    respell: &'a std::collections::HashMap<usize, blue_lang_check::respell::Respelling>,
1410}
1411
1412impl Canon<'_> {
1413    fn form(&self, form: &tatara_lisp::Spanned) -> tatara_lisp::Spanned {
1414        use tatara_lisp::{Spanned, SpannedForm};
1415        let inner = match &form.form {
1416            SpannedForm::List(items) => match items.as_slice() {
1417                [head] if self.bare.contains(&(std::ptr::from_ref(head) as usize)) => {
1418                    return Spanned::new(form.span, head.form.clone());
1419                }
1420                [head, ..]
1421                    if self
1422                        .respell
1423                        .contains_key(&(std::ptr::from_ref(head) as usize)) =>
1424                {
1425                    let r = &self.respell[&(std::ptr::from_ref(head) as usize)];
1426                    let walked: Vec<Spanned> = items.iter().map(|i| self.form(i)).collect();
1427                    return blue_lang_check::respell::apply(form, &walked, r);
1428                }
1429                _ => SpannedForm::List(items.iter().map(|i| self.form(i)).collect()),
1430            },
1431            SpannedForm::Quote(i) => SpannedForm::Quote(Box::new(self.form(i))),
1432            SpannedForm::Quasiquote(i) => SpannedForm::Quasiquote(Box::new(self.form(i))),
1433            SpannedForm::Unquote(i) => SpannedForm::Unquote(Box::new(self.form(i))),
1434            SpannedForm::UnquoteSplice(i) => SpannedForm::UnquoteSplice(Box::new(self.form(i))),
1435            other => other.clone(),
1436        };
1437        Spanned::new(form.span, inner)
1438    }
1439}
1440
1441/// Parse, resolve and CHECK, without running: the door for `blue check`,
1442/// `blue test` and the language server.
1443///
1444/// Returns every diagnostic, errors and warnings alike, rather than failing on
1445/// the first error: a reader fixing a file wants the whole list, and an agent
1446/// repairing one wants it in one pass. Whether errors stop anything is the
1447/// caller's decision — `blue test` refuses to run tests over a program with
1448/// one, exactly as `run` refuses to evaluate it.
1449///
1450/// The name oracle is the interpreter `blue run` would build: hostless plus
1451/// input primitives.
1452///
1453/// # Errors
1454///
1455/// A parse or import error, which leaves no program to check.
1456pub fn check_entry(
1457    entry: Entry<'_>,
1458    loader: &dyn crate::uses::Loader,
1459    surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
1460    checking: Checking,
1461) -> Result<Checked, RunError> {
1462    let mut program = parse_and_resolve(entry, loader, surface)?;
1463    if checking == Checking::Program {
1464        program.retain(|f| !crate::uses::is_test_form(f));
1465    }
1466    let host = match entry.path {
1467        Some(path) => crate::host_surface::near(path)
1468            .map_err(|e| RunError::Import(e.to_string()))?
1469            .map(|s| s.functions)
1470            .unwrap_or_default(),
1471        None => Vec::new(),
1472    };
1473    let interp = runtime(Inputs::new());
1474    let builtins = crate::timings::stage(crate::timings::Stage::Names, || {
1475        builtin_names_with(&interp, &host)
1476    });
1477    let (outcome, names) = crate::timings::stage(crate::timings::Stage::Check, || {
1478        check_stage(&program, &builtins, checking == Checking::WithTests)
1479    });
1480    Ok(Checked {
1481        program,
1482        outcome,
1483        names,
1484    })
1485}
1486
1487#[must_use]
1488pub fn spelling_code(spelling: blue_lang_syntax::Spelling) -> blue_lang_check::Code {
1489    use blue_lang_check::Code;
1490    use blue_lang_syntax::Spelling;
1491    match spelling {
1492        Spelling::LogicalSymbol => Code::B0025,
1493        Spelling::Unless => Code::B0026,
1494        Spelling::ElseIf => Code::B0027,
1495        Spelling::CallForm => Code::B0028,
1496        Spelling::KeywordRocket => Code::B0029,
1497        Spelling::ParameterList => Code::B0030,
1498        Spelling::OperatorSpelling => Code::B0031,
1499        Spelling::TrailingComma => Code::B0032,
1500        Spelling::NegatedComparison => Code::B0034,
1501        Spelling::ElseNil => Code::B0035,
1502        Spelling::TryCatch => Code::B0037,
1503        Spelling::Apply => Code::B0042,
1504        Spelling::MapPun => Code::B0046,
1505        Spelling::NilTest => Code::B0047,
1506        Spelling::NewlineEscape => Code::B0065,
1507        Spelling::Index => Code::B0066,
1508    }
1509}
1510
1511#[must_use]
1512pub fn form_diagnostics(text: &str) -> Vec<blue_lang_check::Diagnostic> {
1513    match blue_lang_fmt::format_source_lossless(text) {
1514        Ok(formatted) => form_diagnostics_against(text, &formatted),
1515        Err(_) => Vec::new(),
1516    }
1517}
1518
1519#[must_use]
1520pub fn form_diagnostics_against(text: &str, formatted: &str) -> Vec<blue_lang_check::Diagnostic> {
1521    use blue_lang_check::{Code, Diagnostic};
1522    if formatted.trim_end() == text.trim_end() {
1523        return Vec::new();
1524    }
1525    let help = "`blue fmt --write FILE` writes it; no compile door writes a file (`BLUE_LAYOUT=rewrite` restores formatting in place)";
1526    let mut out: Vec<Diagnostic> = blue_lang_syntax::spelling_notes(text)
1527        .unwrap_or_default()
1528        .into_iter()
1529        .map(|n| {
1530            let message = if n.canonical.is_empty() {
1531                format!("`{}` is not written", n.written)
1532            } else {
1533                format!("`{}` is written `{}`", n.written, n.canonical)
1534            };
1535            Diagnostic::new(spelling_code(n.spelling), message, n.span)
1536                .at_top_level(0)
1537                .with_help(help)
1538        })
1539        .collect();
1540    out.extend(
1541        literal_diagnostics(text)
1542            .into_iter()
1543            .map(|d| d.with_help(help)),
1544    );
1545    if out.is_empty() {
1546        let at = text
1547            .lines()
1548            .zip(formatted.lines())
1549            .position(|(a, b)| a != b)
1550            .unwrap_or_else(|| text.lines().count().min(formatted.lines().count()));
1551        let start: usize = text.lines().take(at).map(|l| l.len() + 1).sum();
1552        let start = start.min(text.len());
1553        let end = text[start..].find('\n').map_or(text.len(), |e| start + e);
1554        out.push(
1555            Diagnostic::new(
1556                Code::B0024,
1557                format!("line {} is not laid out as blue writes it", at + 1),
1558                tatara_lisp::Span::new(start, end),
1559            )
1560            .at_top_level(0)
1561            .with_help(help),
1562        );
1563    }
1564    out.sort_by_key(|d| (d.span.start, d.code));
1565    out
1566}
1567
1568fn literal_diagnostics(text: &str) -> Vec<blue_lang_check::Diagnostic> {
1569    use blue_lang_syntax::{Atom, TokenKind};
1570    let Ok(toks) = blue_lang_syntax::lex(text) else {
1571        return Vec::new();
1572    };
1573    toks.iter()
1574        .filter_map(|t| {
1575            let atom = match &t.kind {
1576                TokenKind::Int(v) => Atom::Int(*v),
1577                TokenKind::Float(v) => Atom::Float(*v),
1578                TokenKind::Str(v) => Atom::Str(v.clone()),
1579                _ => return None,
1580            };
1581            let written = text.get(t.span.start..t.span.end)?;
1582            let canonical = blue_lang_fmt::render_literal(&atom);
1583            (written != canonical).then(|| {
1584                blue_lang_check::Diagnostic::new(
1585                    blue_lang_check::Code::B0033,
1586                    format!("`{written}` is written `{canonical}`"),
1587                    t.span,
1588                )
1589                .at_top_level(0)
1590            })
1591        })
1592        .collect()
1593}
1594
1595/// A syntax error in `text` as a [`B0006`](blue_lang_check::Code::B0006)
1596/// diagnostic, so a tool reading `--format json` gets a parse failure in the
1597/// same shape as every other finding. `None` when `text` parses.
1598#[must_use]
1599pub fn syntax_diagnostic(text: &str) -> Option<blue_lang_check::Diagnostic> {
1600    let e = blue_lang_syntax::parse_program_tree(text).err()?;
1601    Some(
1602        blue_lang_check::Diagnostic::new(blue_lang_check::Code::B0006, e.message, e.span)
1603            .at_top_level(0),
1604    )
1605}
1606
1607/// A runtime message as the author wrote the names in it: a definition runs
1608/// under its key (`retsu/first`, `%root/f`), and a message naming one in
1609/// backticks names it `retsu::first`, or `f` for the author's own script.
1610#[must_use]
1611pub fn display_keys(message: &str) -> String {
1612    let mut out = String::with_capacity(message.len());
1613    let mut parts = message.split('`');
1614    if let Some(first) = parts.next() {
1615        out.push_str(first);
1616    }
1617    for (i, part) in parts.enumerate() {
1618        out.push('`');
1619        // Odd pieces (0-based here) are inside backticks.
1620        if i % 2 == 0 {
1621            out.push_str(&crate::observe::display_key(part));
1622        } else {
1623            out.push_str(part);
1624        }
1625    }
1626    out
1627}
1628
1629/// Evaluate a prepared program's top-level forms into `interp`, with `host`.
1630pub(crate) fn eval_prepared<H: 'static>(
1631    interp: &mut tatara_lisp_eval::Interpreter<H>,
1632    prepared: &Prepared,
1633    host: &mut H,
1634) -> Result<Value, RunError> {
1635    let Prepared {
1636        program, erased, ..
1637    } = prepared;
1638
1639    // RUN, one top-level form at a time, **counting them**.
1640    //
1641    // This is literally the loop `Interpreter::eval_program` runs internally
1642    // (tatara-lisp-eval `eval.rs`) — unrolled here for exactly one reason: the
1643    // INDEX. `resolve_uses` flattens N files into one form list and a `Span` is
1644    // a byte range with no file identity, so an offset alone is ambiguous
1645    // across files — 66 means something in every one of them. The index is the
1646    // join key back to `ResolvedProgram`'s file table, and `eval_program`
1647    // consumes it internally and hands back only the error.
1648    //
1649    // The same key, the same renderer and the same file table the type errors
1650    // above already use. A runtime error was the one diagnostic in this
1651    // function still reporting a bare message.
1652    let mut value = Value::Nil;
1653    for (top_level, form) in erased.iter().enumerate() {
1654        value = interp.eval_top_form(form, host).map_err(|e| {
1655            // The message, its position and the blue call stack, read off the
1656            // frames the error unwound through (`observe::Fault`). The message
1657            // is `short_message` rather than upstream's `Display`, whose
1658            // trailing ` at {span}` is a bare byte range with no file, through
1659            // `messages::describe`, which names the function a wrong argument
1660            // count was called on.
1661            RunError::Eval(Box::new(crate::observe::Fault::of(
1662                program,
1663                erased,
1664                top_level,
1665                &e,
1666                &interp.take_trace(),
1667            )))
1668        })?;
1669    }
1670    Ok(value)
1671}
1672
1673pub const MAIN: &str = "main";
1674
1675#[must_use]
1676pub fn entry_main(program: &crate::uses::ResolvedProgram) -> Option<(usize, tatara_lisp::Span)> {
1677    use crate::uses::ResolvedProgram;
1678    let stored = program
1679        .file(ResolvedProgram::ENTRY)
1680        .is_some_and(|f| f.path.is_some());
1681    if !stored {
1682        return None;
1683    }
1684    program.forms().iter().enumerate().find_map(|(i, f)| {
1685        if program.owner_of(i) != Some(ResolvedProgram::ENTRY) {
1686            return None;
1687        }
1688        let items = f.as_list()?;
1689        let sig = items.get(1)?.as_list()?;
1690        let named_main = matches!(
1691            items.first().and_then(tatara_lisp::Spanned::as_symbol),
1692            Some("define" | "define-typed")
1693        ) && sig.len() == 1
1694            && sig[0].as_symbol() == Some(MAIN);
1695        named_main.then_some((i, sig[0].span))
1696    })
1697}
1698
1699pub(crate) fn call_main<H: 'static>(
1700    interp: &mut tatara_lisp_eval::Interpreter<H>,
1701    prepared: &Prepared,
1702    host: &mut H,
1703) -> Result<Option<Value>, RunError> {
1704    let Some((top_level, span)) = entry_main(&prepared.program) else {
1705        return Ok(None);
1706    };
1707    let ns = namespace_of(&prepared.program, top_level);
1708    let call = tatara_lisp::Spanned::new(
1709        span,
1710        tatara_lisp::SpannedForm::List(vec![tatara_lisp::Spanned::new(
1711            span,
1712            tatara_lisp::SpannedForm::Atom(tatara_lisp::Atom::Symbol(blue_lang_check::names::key(
1713                &ns, MAIN,
1714            ))),
1715        )]),
1716    );
1717    interp.eval_top_form(&call, host).map(Some).map_err(|e| {
1718        RunError::Eval(Box::new(crate::observe::Fault::of(
1719            &prepared.program,
1720            &prepared.erased,
1721            top_level,
1722            &e,
1723            &interp.take_trace(),
1724        )))
1725    })
1726}
1727
1728/// **A runtime error names a place.** The gates for the last stage that
1729/// reported a bare message.
1730///
1731/// Each fixture is sized so that resolving the failing span against ANY file
1732/// but the right one lands somewhere else — a different path and a different
1733/// line — so a green run here is evidence about the join and not about the
1734/// renderer being called at all.
1735///
1736/// **Each raise is an unbound name, WAIVED.** Since the check stage resolves
1737/// names (B0001), an unbound call no longer reaches the evaluator; these
1738/// fixtures need exactly that runtime raise, so each carries
1739/// `# waive B0001: …` above the definition holding it — the one escape hatch,
1740/// used for what it is for. The waiver adds a line above each failing call,
1741/// which is why the positions below are one line later than their red runs
1742/// (recorded against the unwaived fixtures) show.
1743#[cfg(test)]
1744mod position_tests {
1745    use super::*;
1746    use crate::uses::{Entry, Loader};
1747    use std::collections::BTreeMap;
1748    use std::path::Path;
1749
1750    struct MemLoader(BTreeMap<&'static str, &'static str>);
1751
1752    impl Loader for MemLoader {
1753        fn load(&self, name: &str) -> Result<Vec<(String, String)>, String> {
1754            self.0
1755                .get(name)
1756                .map(|s| vec![(format!("{name}.b"), (*s).to_owned())])
1757                .ok_or_else(|| format!("no bidama named \"{name}\""))
1758        }
1759    }
1760
1761    /// A package whose OWN top-level code raises, deep inside a file that is
1762    /// deliberately taller than either of the other two.
1763    ///
1764    /// Byte 66 — where `kore_wa_sonzai_shinai` starts — is past the end of the
1765    /// entry file (30 bytes) and past the end of `kotae.b` (21 bytes). So the
1766    /// only file this span can honestly resolve against is this one, and the
1767    /// other two now refuse to answer rather than walking off their ends.
1768    const BAKUHATSU: &str = "\
1769def tsukawanai_a()
1770  1
1771end
1772
1773def tsukawanai_b()
1774  2
1775end
1776
1777# waive B0001: the fixture needs a raise at runtime
1778def bakuhatsu()
1779  kore_wa_sonzai_shinai()
1780end
1781
1782bakuhatsu";
1783
1784    const KOTAE: &str = "def kotae()\n  42\nend";
1785
1786    /// The same failing call, but only ever reached by a CALLER in another
1787    /// file — the shape the call stack resolved (the last position test).
1788    const BAKUHATSU2: &str =
1789        "# waive B0001: a raise at runtime\ndef yobu()\n  kore_wa_sonzai_shinai()\nend";
1790
1791    fn loader() -> MemLoader {
1792        MemLoader(BTreeMap::from([
1793            ("kotae", KOTAE),
1794            ("bakuhatsu", BAKUHATSU),
1795            ("bakuhatsu2", BAKUHATSU2),
1796        ]))
1797    }
1798
1799    fn run_named(path: &str, src: &str) -> RunError {
1800        run_in_surface(
1801            Entry {
1802                path: Some(Path::new(path)),
1803                text: src,
1804            },
1805            Inputs::new(),
1806            &loader(),
1807            None,
1808        )
1809        .expect_err("the fixture must raise")
1810    }
1811
1812    /// **The load-bearing gate: a raise inside an IMPORTED package reports the
1813    /// IMPORTED file's path and line.**
1814    ///
1815    /// Hand-computed. In `bakuhatsu.b` the failing call sits on line 11 at
1816    /// column 3, and the whole point of the fixture's height is that no other
1817    /// file in the program can produce that pair: the entry file has 2 lines
1818    /// and `kotae.b` has 3, so *any* mis-attribution is visible as a different
1819    /// path AND a different position, never as a coincidence.
1820    ///
1821    /// **Three red runs, one per edit this change makes** — each isolates a
1822    /// different half of the mechanism, which is what stops the gate being a
1823    /// tautology over "some renderer ran".
1824    ///
1825    /// 1. `eval_program` + `.map_err(|e| RunError::Eval(e.to_string()))`
1826    ///    restored (the pre-change code):
1827    ///    ```text
1828    ///    left:  "runtime error: unbound symbol: kore_wa_sonzai_shinai at 74..95"
1829    ///    right: "runtime error: bakuhatsu.b:10:3: unbound symbol `kore_wa_sonzai_shinai`"
1830    ///    ```
1831    ///    No place at all — the state this change is fixing — and the trailing
1832    ///    `at 74..95` is upstream's `Display`, an offset with no file, which is
1833    ///    why the loop renders `short_message` instead.
1834    ///
1835    /// 2. The loop kept, but `0` passed to `locate` in place of `top_level`:
1836    ///    ```text
1837    ///    left:  "runtime error: kotae.b: unbound symbol `kore_wa_sonzai_shinai`"
1838    ///    right: "runtime error: bakuhatsu.b:10:3: unbound symbol `kore_wa_sonzai_shinai`"
1839    ///    ```
1840    ///    **This is the one that proves the INDEX is load-bearing** rather than
1841    ///    the renderer: form 0 belongs to `kotae.b`, so the wrong file is named
1842    ///    — and named without a position, because the guard in `locate` sees
1843    ///    that byte 74 is not a range in a 21-byte file and declines to invent
1844    ///    one. A fixture with one imported package could not tell these apart.
1845    ///
1846    /// 3. `crate::lower_to_spanned(&crate::to_sexps(&erased))` reinstated
1847    ///    between erasure and the loop:
1848    ///    ```text
1849    ///    left:  "runtime error: bakuhatsu.b: unbound symbol `kore_wa_sonzai_shinai`"
1850    ///    right: "runtime error: bakuhatsu.b:10:3: unbound symbol `kore_wa_sonzai_shinai`"
1851    ///    ```
1852    ///    The right FILE with no position — which is exactly the shape of the
1853    ///    bug: the index survived the lift and every span did not.
1854    /// kotae loads first so bakuhatsu's forms do not start at index 0: the
1855    /// order is the fixture, and the waiver says so (B0015 wants them sorted).
1856    const TWO_IMPORTS: &str =
1857        "use(\"kotae\")\n# waive B0015: kotae loads first on purpose\nuse(\"bakuhatsu\")\n";
1858
1859    #[test]
1860    fn a_raise_inside_an_imported_package_names_that_package() {
1861        let err = run_named("entry.b", TWO_IMPORTS);
1862        assert_eq!(
1863            err.to_string(),
1864            "runtime error: bakuhatsu.b:11:3: unbound symbol `kore_wa_sonzai_shinai`\n  \
1865             at bakuhatsu::bakuhatsu (bakuhatsu.b:11:3)\n  \
1866             at <top level> (bakuhatsu.b:14:1)"
1867        );
1868    }
1869
1870    /// Anti-vacuity for the fixture itself: the position asserted above is a
1871    /// FACT ABOUT `bakuhatsu.b`, checked against the source text rather than
1872    /// against the thing that produced it.
1873    ///
1874    /// Without this, `11:3` is just a number that happened to come out of the
1875    /// code under test, and a change that moved every reported line by one
1876    /// would move this assertion with it.
1877    #[test]
1878    fn line_11_column_3_of_the_fixture_is_the_failing_call() {
1879        let line = BAKUHATSU.lines().nth(10).expect("line 11 exists");
1880        assert_eq!(line, "  kore_wa_sonzai_shinai()");
1881        assert_eq!(
1882            &line[2..],
1883            "kore_wa_sonzai_shinai()",
1884            "column 3 (1-indexed) is where the call starts"
1885        );
1886        // And the other two files cannot reach that far, which is what makes a
1887        // mis-attribution loud instead of plausible.
1888        let offset = BAKUHATSU.find("kore_wa_sonzai_shinai").expect("in fixture");
1889        assert!(
1890            offset > KOTAE.len(),
1891            "kotae.b could answer for byte {offset}"
1892        );
1893        assert!(
1894            offset > TWO_IMPORTS.len(),
1895            "the entry file could answer for byte {offset}"
1896        );
1897    }
1898
1899    /// **The `FileId(0)` path: a raise in the ENTRY file reports the entry
1900    /// file.** The single-file case, which is every program that imports
1901    /// nothing — and the one where the index and the span cannot disagree.
1902    ///
1903    /// **Red run** (2026-08-12), `eval_program` +
1904    /// `.map_err(|e| RunError::Eval(e.to_string()))` restored:
1905    /// ```text
1906    /// left:  "runtime error: unbound symbol: nani_mo_nai at 17..28"
1907    /// right: "runtime error: honmono.b:5:1: unbound symbol `nani_mo_nai`"
1908    /// ```
1909    ///
1910    /// **Second red run**, `lower_to_spanned` reinstated after erasure — the
1911    /// one that isolates the SPAN half from the index half, since a
1912    /// single-file program cannot get its file wrong:
1913    /// ```text
1914    /// left:  "runtime error: honmono.b: unbound symbol `nani_mo_nai`"
1915    /// right: "runtime error: honmono.b:5:1: unbound symbol `nani_mo_nai`"
1916    /// ```
1917    #[test]
1918    fn a_raise_in_the_entry_file_names_the_entry_file() {
1919        let err = run_named(
1920            "honmono.b",
1921            "def f()\n  1\nend\n\n# waive B0001: a raise at runtime\nnani_mo_nai()\n",
1922        );
1923        assert_eq!(
1924            err.to_string(),
1925            "runtime error: honmono.b:6:1: unbound symbol `nani_mo_nai`"
1926        );
1927    }
1928
1929    /// A raise from a form that came through the SURFACE-level erasure still
1930    /// reports a real position — the annotated path, where erasure rewrites
1931    /// the node rather than passing it through.
1932    ///
1933    /// Separate from the two above because the erasure rewrite is the only
1934    /// place a node is built rather than kept, and a synthetic span there
1935    /// would be invisible to a fixture whose failing form is untouched.
1936    ///
1937    /// **Red run** (2026-08-12), `lower_to_spanned` reinstated after erasure:
1938    /// ```text
1939    /// left:  "runtime error: chuu.b: unbound symbol `mada_nai`"
1940    /// right: "runtime error: chuu.b:2:3: unbound symbol `mada_nai`"
1941    /// ```
1942    #[test]
1943    fn a_raise_inside_an_annotated_def_still_names_its_line() {
1944        let err = run_named(
1945            "chuu.b",
1946            "# waive B0001: a raise at runtime\ndef f(a: Int) -> Int\n  mada_nai(a)\nend\n\nf(1)\n",
1947        );
1948        assert_eq!(
1949            err.to_string(),
1950            "runtime error: chuu.b:3:3: unbound symbol `mada_nai`\n  \
1951             at f (chuu.b:3:3)\n  \
1952             at <top level> (chuu.b:6:1)"
1953        );
1954    }
1955
1956    /// **The limit this module used to state, closed by the call stack.**
1957    ///
1958    /// A top-level form in the entry file calls a function defined in an
1959    /// imported one, and the raise happens inside the callee. The executing
1960    /// form is the entry's and the span is the callee's bytes, so until the
1961    /// evaluator reported the frames an error unwound through, the best blue
1962    /// could do was name the caller's file with no position (the guard in
1963    /// `uses::ResolvedProgram::locate` refusing an offset it could not
1964    /// justify). The innermost frame's definition now names the file the
1965    /// offsets belong to, and the stack shows the call that led there.
1966    ///
1967    /// **Red run** (2026-10-10): `Fault::of` locating against the executing
1968    /// top-level form instead of the innermost frame's definition:
1969    /// ```text
1970    /// left:  "runtime error: yobidashi.b: unbound symbol `kore_wa_sonzai_shinai`\n  at bakuhatsu2::yobu (bakuhatsu2.b:3:3)\n  at <top level> (yobidashi.b:3:1)"
1971    /// ```
1972    /// The old answer, kept by the guard: the caller's file and no position.
1973    #[test]
1974    fn a_raise_in_a_callee_from_another_file_names_the_callees_file() {
1975        // The comment is padding, and load-bearing: `bakuhatsu2.b`'s failing
1976        // call starts at byte 47 (after its waiver line) and ends at 70, and
1977        // this file is 57 bytes, so the start is in range here and the end is
1978        // not — a mis-attribution would still be refused rather than placed.
1979        let err = run_named(
1980            "yobidashi.b",
1981            "use(\"bakuhatsu2\")\n# pad: 47's in range\nbakuhatsu2::yobu\n",
1982        );
1983        assert_eq!(
1984            err.to_string(),
1985            "runtime error: bakuhatsu2.b:3:3: unbound symbol `kore_wa_sonzai_shinai`\n  \
1986             at bakuhatsu2::yobu (bakuhatsu2.b:3:3)\n  \
1987             at <top level> (yobidashi.b:3:1)"
1988        );
1989    }
1990}
1991
1992#[cfg(test)]
1993mod tests {
1994    use super::*;
1995
1996    fn int(src: &str) -> i64 {
1997        match run(src).unwrap_or_else(|e| panic!("{src:?}: {e}")).value {
1998            Value::Int(v) => v,
1999            other => panic!("{src:?} produced {other:?}"),
2000        }
2001    }
2002
2003    /// **The sliding scale, as one assertion.** Annotating changes the
2004    /// analysis and nothing else.
2005    #[test]
2006    fn annotating_buys_analysis_and_changes_nothing_else() {
2007        let plain = run("def add(a, b)\n  a + b\nend\nadd(2, 3)").expect("plain");
2008        let typed = run("def add(a: Int, b: Int) -> Int\n  a + b\nend\nadd(2, 3)").expect("typed");
2009
2010        assert!(matches!(plain.value, Value::Int(5)));
2011        assert!(
2012            matches!(typed.value, Value::Int(5)),
2013            "the annotated program must compute the same answer"
2014        );
2015        assert_eq!(plain.visited, 0, "no annotations means no analysis");
2016        assert!(
2017            typed.visited > 0,
2018            "an annotation must actually buy analysis, not just decorate"
2019        );
2020        assert_eq!(plain.typed_decls, 0);
2021        assert_eq!(typed.typed_decls, 1);
2022    }
2023
2024    /// **Checking happens before erasure.** This is the test that catches the
2025    /// reordering: a program with a declared-type violation must be rejected,
2026    /// and it can only be rejected while the annotations still exist.
2027    #[test]
2028    fn a_type_error_is_reported_and_the_program_does_not_run() {
2029        let err = run("def add(a: Int, b: Int) -> Str\n  a + b\nend\nadd(1, 2)")
2030            .expect_err("a declared Str return from an Int body must be rejected");
2031        assert!(
2032            matches!(err, RunError::Types(ref d) if !d.is_empty()),
2033            "expected type diagnostics, got {err}"
2034        );
2035    }
2036
2037    /// And the untyped version of the same program runs, so the rejection
2038    /// above is the annotation's doing rather than a parse failure.
2039    #[test]
2040    fn the_same_program_without_annotations_runs() {
2041        assert_eq!(int("def add(a, b)\n  a + b\nend\nadd(1, 2)"), 3);
2042    }
2043
2044    #[test]
2045    fn a_parse_error_is_reported_as_one() {
2046        assert!(matches!(run("def (").unwrap_err(), RunError::Parse(_)));
2047    }
2048
2049    /// Every stage reports in its own vocabulary, so a failure names which
2050    /// stage failed rather than surfacing as a generic error.
2051    #[test]
2052    fn a_runtime_error_is_reported_as_one() {
2053        let err = run("throw(error(\"boom\"))").expect_err("a raise");
2054        assert!(matches!(err, RunError::Eval(_)), "got {err}");
2055    }
2056
2057    /// Stdlib and primitives are both reachable through the pipeline — the
2058    /// gap that made `6 % 3` fail.
2059    #[test]
2060    fn the_pipeline_reaches_both_runtime_layers() {
2061        assert_eq!(int("6 % 3"), 0);
2062        assert_eq!(int("7 % 3"), 1);
2063        assert_eq!(int("2 + 3 * 4"), 14);
2064    }
2065
2066    /// **The deleted hop was a no-op on everything blue emits — so removing it
2067    /// is a swap, not a behaviour change.**
2068    ///
2069    /// The old lowering printed the erased tree and read it back through
2070    /// `tatara_lisp::read_spanned`. This walks a corpus and asserts the two
2071    /// paths land on the same tree, which is the equivalence the swap rests on.
2072    /// It is stated as a *measurement over this corpus*, not as a theorem:
2073    /// the round trip is not identity in general (that is precisely why it had
2074    /// to go), it merely happened to be identity for the bytes blue emits.
2075    #[test]
2076    fn the_deleted_round_trip_agreed_with_the_direct_lowering() {
2077        let corpus = [
2078            "def add(a, b)\n  a + b\nend\nadd(2, 3)",
2079            "def fact(n)\n  if n < 2\n    1\n  else\n    n * fact(n - 1)\n  end\nend\nfact(5)",
2080            "def f(a, b)\n  c = a + b\n  c * 2\nend\nf(1, 2)",
2081            "defmacro sq(e)\n  quote\n    unquote(e) * unquote(e)\n  end\nend\nsq(2 + 3)",
2082            "\"a string with spaces, a ( and a )\"",
2083            "def g(a: Int) -> Int\n  a + 1\nend\ng(1)",
2084            "6 % 3",
2085            "1.5 + 2.25",
2086        ];
2087        for src in corpus {
2088            // Projected to `Sexp` because that is what the deleted hop
2089            // operated on. Erasure itself no longer goes near it.
2090            let erased = crate::to_sexps(&erase_types(&parse_tree(src).expect("parse")));
2091
2092            let direct: Vec<Sexp> = crate::lower_to_spanned(&erased)
2093                .iter()
2094                .map(tatara_lisp::Spanned::to_sexp)
2095                .collect();
2096            assert_eq!(direct, erased, "the direct lowering must be the identity");
2097
2098            let text = erased
2099                .iter()
2100                .map(ToString::to_string)
2101                .collect::<Vec<_>>()
2102                .join("\n");
2103            let round_tripped: Vec<Sexp> = tatara_lisp::read_spanned(&text)
2104                .unwrap_or_else(|e| panic!("{src:?}: the old path could not read back: {e:?}"))
2105                .iter()
2106                .map(tatara_lisp::Spanned::to_sexp)
2107                .collect();
2108            assert_eq!(
2109                round_tripped, erased,
2110                "{src:?}: the old print-and-reparse path changed the tree"
2111            );
2112        }
2113    }
2114
2115    /// Anti-vacuity for the test above: the round trip really is *not* the
2116    /// identity in general, so agreeing on the corpus was a property of what
2117    /// blue happens to emit rather than a property of the reader.
2118    ///
2119    /// **`Atom::Symbol`'s `Display` writes the name raw, with no escaping.**
2120    /// `Atom::Str` escapes and its docs explain at length why; the symbol arm
2121    /// is `f.write_str(s)`. So print-then-read is not inverse over the symbol
2122    /// domain, and the failure is *silent*: a symbol containing a space prints
2123    /// as two tokens, reads back as two symbols, and the result is a perfectly
2124    /// well-formed tree with a different meaning. No error, nothing to catch.
2125    ///
2126    /// Measured 2026-08-02 across the separators: `a b` and `x'y` come back
2127    /// `Ok` with a different tree; `x)y`, `x"y` and `x;y` come back `Err`;
2128    /// `x{y` and `x[y` DO round-trip at this level — those two are one symbol
2129    /// in and one symbol out, so the brace-fusion reported in tatara *source*
2130    /// is not what bites a printed tree. The silent pair is what makes this a
2131    /// miscompile class rather than a noisy one.
2132    #[test]
2133    fn the_round_trip_is_not_the_identity_in_general() {
2134        let tree = Sexp::List(vec![
2135            Sexp::Atom(tatara_lisp::Atom::Symbol("f".into())),
2136            Sexp::Atom(tatara_lisp::Atom::Symbol("a b".into())),
2137        ]);
2138        let text = tree.to_string();
2139        let back: Vec<Sexp> = tatara_lisp::read_spanned(&text)
2140            .expect("it reads back cleanly — that IS the problem")
2141            .iter()
2142            .map(tatara_lisp::Spanned::to_sexp)
2143            .collect();
2144        assert_ne!(
2145            back,
2146            vec![tree.clone()],
2147            "if print-then-read became inverse over symbols, the class would be \
2148             closed upstream and this test should be deleted rather than relaxed"
2149        );
2150        // …and the direct lowering is unaffected by any of it.
2151        let direct: Vec<Sexp> = crate::lower_to_spanned(std::slice::from_ref(&tree))
2152            .iter()
2153            .map(tatara_lisp::Spanned::to_sexp)
2154            .collect();
2155        assert_eq!(direct, vec![tree]);
2156    }
2157}
2158
2159#[cfg(test)]
2160mod macro_tests {
2161    use super::*;
2162
2163    fn int(src: &str) -> i64 {
2164        match run(src).unwrap_or_else(|e| panic!("{src:?}: {e}")).value {
2165            Value::Int(v) => v,
2166            other => panic!("{src:?} produced {other:?}"),
2167        }
2168    }
2169
2170    /// **A blue macro expands and runs.** Tenet 2's surface, end to end.
2171    #[test]
2172    fn a_macro_expands_and_runs() {
2173        assert_eq!(
2174            int("defmacro double(x)\n  quote\n    unquote(x) + unquote(x)\n  end\nend\ndouble(21)"),
2175            42
2176        );
2177    }
2178
2179    /// A macro receives *source forms*, not values — so it can duplicate its
2180    /// argument, which a function cannot do without re-evaluating it.
2181    #[test]
2182    fn a_macro_operates_on_syntax_not_values() {
2183        assert_eq!(
2184            int("defmacro sq(e)\n  quote\n    unquote(e) * unquote(e)\n  end\nend\nsq(2 + 3)"),
2185            25,
2186            "the argument form `2 + 3` must be substituted twice"
2187        );
2188    }
2189
2190    /// **A runaway macro is a typed error, not a dead compiler.** This is the
2191    /// property that makes the metaprogramming surface safe to hand to a user.
2192    ///
2193    /// The assertion was `contains("expansion limit")` and now reads
2194    /// `contains("expansion")`, because the pipeline renders `short_message`
2195    /// rather than `Display`: upstream words the same fact as "exceeded 256
2196    /// expansion steps" instead of "exceeded the expansion limit of 256
2197    /// rewrites". The PROPERTY is unchanged and still asserted — the macro is
2198    /// named, and the failure is attributed to expansion rather than to
2199    /// evaluation. **This reword was the whole measured blast radius of that
2200    /// switch**, one test in the workspace.
2201    ///
2202    /// The number is deliberately NOT pinned. It is upstream's constant, this
2203    /// test owns none of it, and matching it would turn an upstream bump into
2204    /// a red run here that proves nothing about blue.
2205    ///
2206    /// The third clause is new capability rather than repair: the message now
2207    /// carries `<anonymous>:6:1`, so a runaway macro says WHERE it ran away.
2208    #[test]
2209    fn a_runaway_macro_fails_the_compilation_rather_than_the_process() {
2210        let err =
2211            run("defmacro forever(x)\n  quote\n    forever(unquote(x))\n  end\nend\nforever(1)")
2212                .expect_err("a self-referential macro must be rejected");
2213        let msg = err.to_string();
2214        assert!(
2215            msg.contains("forever") && msg.contains("expansion"),
2216            "the error must name the macro and the limit it hit: {msg}"
2217        );
2218        assert!(
2219            msg.contains("<anonymous>:6:1"),
2220            "and must place the call that ran away — line 6 is `forever(1)`: {msg}"
2221        );
2222    }
2223}
2224
2225#[cfg(test)]
2226mod input_tests {
2227    use super::*;
2228    use crate::inputs::{Declaration, Inputs};
2229
2230    /// A schema a macro will generate code from.
2231    const SCHEMA: &[u8] = b"3";
2232
2233    fn with_schema(src: &str) -> Result<Run, RunError> {
2234        let hash = Inputs::hash_of(SCHEMA);
2235        let mut inputs = Inputs::new();
2236        inputs
2237            .bind(
2238                &Declaration {
2239                    name: "schema".to_string(),
2240                    hash,
2241                },
2242                SCHEMA.to_vec(),
2243            )
2244            .expect("bind");
2245        run_with_inputs(src, inputs)
2246    }
2247
2248    fn decl_line() -> String {
2249        let mut s = String::from("definput(\"schema\", \"");
2250        s.push_str(&Inputs::hash_of(SCHEMA));
2251        s.push_str("\")\n");
2252        s
2253    }
2254
2255    /// **A macro reads a declared build input.** This is §VI OPEN #6 closed —
2256    /// the spec names it as gating blue's whole "stronger than Ruby's
2257    /// metaprogramming" claim, because a macro that cannot read a schema cannot
2258    /// generate code from one.
2259    #[test]
2260    fn a_macro_can_read_a_declared_build_input() {
2261        let src = decl_line() + "input(\"schema\")";
2262        let out = with_schema(&src).expect("run");
2263        assert!(
2264            matches!(out.value, Value::Str(ref s) if &**s == "3"),
2265            "got {:?}",
2266            out.value
2267        );
2268    }
2269
2270    /// **An undeclared input is an error, not a file read and not nil.**
2271    /// Returning nil is how a macro generates an empty table and nobody notices
2272    /// until runtime.
2273    #[test]
2274    fn an_undeclared_input_is_an_error() {
2275        let err = with_schema("input(\"not_declared\")").expect_err("must fail");
2276        let msg = err.to_string();
2277        assert!(msg.contains("not_declared"), "must name it: {msg}");
2278        assert!(msg.contains("definput"), "and say how to declare it: {msg}");
2279    }
2280
2281    /// **There is no path-based read at all.** The capability is the absence of
2282    /// the primitive, not a check inside one — so this is an unbound symbol.
2283    ///
2284    /// Holds for the DEFAULT surface — the one every embedder gets. The `sys`
2285    /// cargo feature (CLI only) is the one declared exception: it is the
2286    /// operator's own trusted host surface, and is asserted in
2287    /// `sys_read_file_is_the_trusted_cli_only_exception` below.
2288    #[cfg(not(feature = "sys"))]
2289    #[test]
2290    fn there_is_no_ambient_file_read() {
2291        for attempt in [
2292            "read_file(\"/etc/passwd\")",
2293            "File(\"/etc/passwd\")",
2294            "slurp(\"/etc/passwd\")",
2295            "open(\"/etc/passwd\")",
2296        ] {
2297            let err = with_schema(attempt).expect_err("must not resolve");
2298            assert!(
2299                err.to_string().contains("unbound"),
2300                "{attempt} must be UNBOUND — a capability removed by absence, \
2301                 not guarded by a check: {err}"
2302            );
2303        }
2304    }
2305
2306    /// With the `sys` feature compiled in, `read_file` IS bound — that is the
2307    /// point of the feature. The doctrine does not move: this is the operator's
2308    /// own machine (the CLI), not an embedder's sandbox. Pin the boundary so a
2309    /// future default-build change is heard, and assert that `input()` still
2310    /// works beside it.
2311    #[cfg(feature = "sys")]
2312    #[test]
2313    fn sys_read_file_is_the_trusted_cli_only_exception() {
2314        let err = with_schema("definitely_not_a_primitive(\"x\")").expect_err("must not resolve");
2315        assert!(err.to_string().contains("unbound"), "{err}");
2316        assert!(
2317            with_schema("read_file(\"/etc/passwd\")").is_ok(),
2318            "with `sys` on, read_file is the trusted CLI surface"
2319        );
2320        let out = with_schema("input(\"schema\")").expect("run");
2321        assert!(
2322            matches!(out.value, Value::Str(ref s) if &**s == "3"),
2323            "input() still binds beside the sys surface: {:?}",
2324            out.value
2325        );
2326    }
2327
2328    /// Anti-vacuity: with no inputs supplied at all, even a declared name fails
2329    /// — so the success above is the binding's doing.
2330    #[test]
2331    fn a_declared_input_with_no_bytes_supplied_fails() {
2332        let src = decl_line() + "input(\"schema\")";
2333        assert!(run(&src).is_err(), "no bytes were supplied");
2334    }
2335}
2336
2337#[cfg(test)]
2338mod tier2_tests {
2339    use super::*;
2340    use crate::inputs::{Declaration, Inputs};
2341
2342    /// **The Tier-2 conversion §V.6.3 said was gated: a macro that emits real
2343    /// declarations FROM A SCHEMA.**
2344    ///
2345    /// `theory/BLUE.md` §VI OPEN #6 states the blocker plainly — "tenet 2
2346    /// installs a `NoLoader`, so a macro cannot read a schema — which gates
2347    /// every Tier-2 conversion in §V.6 and therefore blue's whole 'stronger than
2348    /// Ruby's metaprogramming' claim."
2349    ///
2350    /// Here the schema supplies a *value the generated code depends on*, read at
2351    /// expansion time. Ruby and Elixir can both do this — with the whole
2352    /// filesystem open. blue does it through a name bound to a content hash.
2353    #[test]
2354    fn a_macro_generates_code_from_a_schema() {
2355        let schema = b"7";
2356        let mut inputs = Inputs::new();
2357        inputs
2358            .bind(
2359                &Declaration {
2360                    name: "arity".to_string(),
2361                    hash: Inputs::hash_of(schema),
2362                },
2363                schema.to_vec(),
2364            )
2365            .expect("bind");
2366
2367        // The macro reads the input at EXPANSION time and splices the value it
2368        // found into the code it emits.
2369        let mut src = String::from("definput(\"arity\", \"");
2370        src.push_str(&Inputs::hash_of(schema));
2371        src.push_str("\")\n");
2372        src.push_str(
2373            "defmacro from_schema()\n  quote\n    unquote(to_int(input(\"arity\")))\n  end\nend\n\
2374             from_schema() * 6",
2375        );
2376
2377        let out = run_with_inputs(&src, inputs).expect("run");
2378        assert!(
2379            matches!(out.value, Value::Int(42)),
2380            "the schema's 7 must reach the generated code: got {:?}",
2381            out.value
2382        );
2383    }
2384}