1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
//! The blue runtime — **one** definition of what a blue program runs against.
//!
//! Before this crate existed, every consumer hand-rolled its own
//! `Interpreter::new()` + `install_primitives(...)` pair — three of them, in
//! three crates — and they had silently drifted: none of them loaded the Lisp
//! stdlib. So `6 % 3` lowered correctly to `(mod 6 3)`, `mod` was genuinely
//! defined, and the program still died with `unbound symbol: mod`, because
//! the definition lived in a stdlib nobody loaded.
//!
//! That is the duplication tax in its usual shape: the bug is not in any one
//! copy, it is in there *being* copies. One function now owns the answer.
//!
//! ## Layers
//!
//! A blue interpreter is built in two layers, and both are required:
//!
//! 1. **Rust primitives** — arithmetic, comparison, list ops, I/O.
//! 2. **The full tatara stdlib** — primitives, higher-order functions
//! (`map`/`filter`/`fold`), maps, channels, fibers, type-check, and
//! everything tatara defines in tatara-lisp itself
//! (`mod`, `rem`, `first`, `inc`, `even?`, the actor and transducer
//! helpers, …). Loading it is not optional garnish: blue's own operator
//! lowering depends on it.
//! 3. **blue's own core** ([`stdlib`]) — strings and number conversion, which
//! tatara-lisp does not have in any form. A Ruby-surface language without
//! `length` or `upcase` is not usable, and the semantics are Ruby's
//! (characters, not bytes), which is why they are blue's and not the
//! substrate's.
//! 4. **JSON** ([`json`]) and **cryptography** ([`crypto`]) — pure
//! computation, installed unconditionally, so the wasm consumer keeps them.
//! 5. **The host layer** (`sys`, behind the `sys` feature) — every name in it
//! is a host import.
//! 6. **Processes** ([`attach_node`]) — `spawn`, `send`, `receive` and the
//! rest of `blue_lang_proc::node`'s words, bound to a node of the
//! interpreter's own, so the program is that node's root process.
pub use ;
pub use ;
pub use ;
pub use install_blue_stdlib;
use ;
/// Build an interpreter with the complete blue runtime installed.
///
/// This is the *only* sanctioned way to obtain one. A caller that builds an
/// `Interpreter` directly gets a partial runtime, and the failure shows up as
/// an unbound symbol at the far end of a program.
/// Give `interp` a process node of its own: its program becomes the node's
/// root, and `spawn` starts processes in this node and no other.
///
/// [`interpreter_hostless`] calls it on every fork, so two programs never
/// share processes. A door that forks an interpreter per unit of work (a test
/// per fork) calls it on each fork for the same reason.
/// [`attach_node`] for a program that runs under `frame`: every process it
/// starts is held to the same frame, so a host name the program may not call
/// is refused in its children too. Without this a spawned child would run on
/// the full runtime and a frame would be one `spawn` away from meaningless.
/// The node every interpreter's processes run under: the execution bounds
/// per incarnation, a scheduling quantum, and the host's clock when there is
/// a host (a node with no host has a virtual clock: time passes only when
/// every process waits).
/// A fresh interpreter for a child process: the hostless base, forked. The
/// node adds its child words to it.
/// The execution bounds every interpreter this crate builds runs under.
static EXECUTION_BOUNDS: OnceLock = new;
/// How much a blue program may do before it is refused: nested calls and
/// evaluation steps.
///
/// Both are BOUNDS in the sense `blue-lang-cli`'s `config` module admits: a
/// terminating program under bounds above its cost returns the same value, and
/// one that exceeds either is refused with a catchable `depth-exceeded` or
/// `fuel-exhausted` error naming the limit and the function — never a process
/// abort, which is what exceeding the host stack was before tatara-lisp-eval
/// 0.3.63. The walker and the VM run under the same bounds and refuse with the
/// same message.
/// Name the bounds for this process. The CLI calls this once with the resolved
/// `BlueConfig`, before any interpreter is built; an embedder that never calls
/// it runs under [`ExecutionBounds::DEFAULT`]. Only the first call counts,
/// like `sys::set_program_args`.
/// The bounds interpreters are built with.
/// The prebuilt hostless substrate, built once per process.
///
/// **Measured 2026-08-01, and this is the entire reason the fork exists.**
/// Profiling a trivial blue program found the run dominated by ONE thing:
///
/// ```text
/// parse 9.6 µs
/// check 1.6 µs
/// interpreter_hostless() 5 820 µs <- 98.4% of the run
/// ```
///
/// Splitting that further: `Interpreter::new()` is 24 µs and
/// `install_full_stdlib_with` is the rest — the stdlib was being rebuilt from
/// scratch on every single `run()`, and blue's whole surface (`map`, `filter`,
/// string ops) lives in it, so no program could avoid the cost.
///
/// `fork` already existed upstream for exactly this, and blue simply was not
/// using it. tatara-lisp-eval's own `fork_cost.rs` opens with *"cheapness is
/// the entire reason it exists"* — the capability was built, documented and
/// guarded, and the consumer kept paying full price beside it.
///
/// | | per call |
/// |---|---|
/// | rebuild (`Interpreter::new` + `install_full_stdlib_with`) | **9.03 ms** |
/// | `fork()` of a prebuilt base | **0.102 ms** |
///
/// **88× on the dominant cost.** This is purgatory's computation-layer clause
/// made real: the stdlib is the most-reused computation in the language, and
/// it was being discarded and recomputed rather than resurrected.
///
/// Isolation is `fork`'s contract, not an assumption made here — its
/// correctness gates live upstream in `fork.rs`, and `fork_cost.rs` guards
/// against a future change that deep-copies (which would keep every
/// correctness test green while silently restoring the cost this removes).
static HOSTLESS_BASE: LazyLock =
new;
/// The common host-free case.
///
/// Forks the process-wide base rather than rebuilding the stdlib. Falls back to
/// a full build if the lock is poisoned: a poisoned lock means another thread
/// panicked mid-fork, and answering a correct-but-slow interpreter beats
/// propagating someone else's panic into an unrelated caller.
/// Lift spanless forms into what the evaluator eats.
///
/// **No longer on the `blue run` path, and that is the point.**
/// [`erase::erase_types`] now takes and returns `Spanned`, so
/// `pipeline::run_in_surface` hands the evaluator the *parser's own* tree with
/// the author's positions on it — there is nothing left to lift. This
/// function survives for a caller that genuinely holds spanless `Sexp` and
/// has no position to preserve, and as the subject of the two tests below
/// that pin why the hop it replaced was unsafe.
///
/// `Interpreter::eval_program` takes `&[Spanned]`; blue's stages produced
/// `Sexp`. Two callers bridged that gap by *printing the tree and reading it
/// back* — `forms.map(ToString::to_string).join("\n")` into
/// `tatara_lisp::read_spanned`. Both stopped, and the round trip is gone from
/// the pipeline.
///
/// **Why it had to go.** Printing a tree we already hold and re-parsing it puts
/// the reader's lexer between blue and its own output, for nothing — and the
/// printer and the reader are **not inverses**. `Atom::Str`'s `Display` escapes
/// its payload and its own docs explain at length why; the `Atom::Symbol` arm
/// is a bare `write_str`. So a symbol whose text carries a separator prints as
/// several tokens and reads back as several symbols: a well-formed tree with a
/// different meaning, no error raised. Measured 2026-08-02 —
/// `pipeline::tests::the_round_trip_is_not_the_identity_in_general` pins which
/// separators are silent and which are loud. The only thing that kept this from
/// biting was blue happening not to emit those bytes. A stage that never
/// serialises cannot be mis-read.
///
/// **What is given up: spans.** The old path's spans pointed into the
/// re-printed lisp text, a buffer no human ever wrote and no diagnostic could
/// usefully cite — they were positions in blue's own output, not in the
/// author's source. `Span::synthetic` says the same thing honestly.
///
/// **Carrying real blue-source spans through erasure was the separate piece of
/// work, and it is done — but NOT the way this doc predicted.** It said the
/// work "would start from `Spanned::from_sexp_at`". It did not, and could not:
/// `from_sexp_at` is a *lift*, stamping ONE span across a whole subtree, so
/// starting there would have replaced a tree of synthetic spans with a tree of
/// identical wrong ones. The actual shape was to stop projecting in the first
/// place — erasure walks `Spanned` and keeps each node's own span, because it
/// only ever deletes (see [`erase`]). **A pointer at the nearest-looking API is
/// how a reader is sent to the wrong starting point**; the note is corrected
/// here rather than deleted, because the wrong lead was load-bearing enough to
/// be worth naming.