Skip to main content

tatara_lisp_script/
lib.rs

1//! tatara-lisp-script — scripting surface for tatara-lisp.
2//!
3//! Wraps `tatara-lisp-eval::Interpreter<ScriptCtx>` with a batteries-included
4//! stdlib (http, json, yaml, sops, file I/O, env, sha256, string ops) so a
5//! `.tlisp` file can replace a bash script. The binary (`tatara-script`)
6//! parses a .tlisp file, expands macros via tatara-lisp, and evaluates each
7//! form against this stdlib.
8//!
9//! # Usage from nix-run
10//!
11//! ```nix
12//! apps.tatara-script = {
13//!   type = "app";
14//!   program = "${tataraScript}/bin/tatara-script path/to/script.tlisp";
15//! };
16//! ```
17//!
18//! # Library surface
19//!
20//! Embedders that want to add domain-specific FFI on top of the stdlib can:
21//!
22//! ```rust,ignore
23//! use tatara_lisp_script::{Interpreter, ScriptCtx, install_stdlib};
24//!
25//! let mut interp: Interpreter<ScriptCtx> = Interpreter::new();
26//! let mut ctx = ScriptCtx::default();
27//! install_stdlib(&mut interp, &mut ctx);
28//! // Register more fns before eval_program.
29//! ```
30
31/// Owned temp-path registry — makes the `(tmp-dir)` / `(tmp-file)` leak
32/// unrepresentable. See the module docs for the incident it closes.
33pub mod scratch;
34pub mod script_ctx;
35pub mod stdlib;
36
37pub use script_ctx::ScriptCtx;
38pub use stdlib::install_stdlib;
39
40// Re-export the evaluator so embedders don't have to depend on tatara-lisp-eval
41// directly.
42pub use tatara_lisp::{read_spanned, Spanned};
43pub use tatara_lisp_eval::{Arity, EvalError, Interpreter, Value};
44
45/// Convenience: read + evaluate a tatara-lisp source string against a fresh
46/// interpreter with the full stdlib installed.
47///
48/// Primarily for tests and one-liner invocations. Binary entry points
49/// should construct the `Interpreter` directly to keep the host context
50/// available across calls.
51pub fn eval_str(src: &str) -> Result<Value, anyhow::Error> {
52    let forms = read_spanned(src).map_err(|e| anyhow::anyhow!("parse error: {e}"))?;
53    let mut interp: Interpreter<ScriptCtx> = Interpreter::new();
54    let mut ctx = ScriptCtx::default();
55    install_stdlib(&mut interp, &mut ctx);
56    interp
57        .eval_program(&forms, &mut ctx)
58        .map_err(|e| anyhow::anyhow!("eval error: {e:?}"))
59}
60
61/// Every name a script may call on THIS binary: special forms, macros, and
62/// every top-level global the real stdlib installs.
63///
64/// ── ONE SOURCE, TWO READERS ──────────────────────────────────────────────
65/// `tatara-script lint`'s `unbound-symbol` rule and `tatara-script symbols`
66/// both read this. They must never disagree: if `symbols` listed a name the
67/// lint rejects (or the reverse), the listing would send an author to a name
68/// that fails. So there is exactly one computation, and it comes from a real
69/// interpreter with the real stdlib — never a hand-kept table, which would
70/// drift the first time a primitive landed.
71///
72/// ── WHY `symbols` EXISTS ─────────────────────────────────────────────────
73/// The lint's own diagnostic says "check the spelling against the installed
74/// primitives", but until this there was no way to LIST them; `--help` pointed
75/// at crate docs unreachable from the binary. The cost was measured
76/// 2026-09-15: `member?` was hand-rolled ~10 times under 5 names across the
77/// fleet while being a builtin the whole time, and
78/// `hardened-images/tools/classic-crypto-gate.tlisp:77` records probing for a
79/// function by trial and error. Authors re-derive what they cannot see.
80///
81/// A `BTreeSet`, so the output is sorted and duplicate-free:
82/// `reserved_head_names` already folds in top-level globals, and the lint's
83/// original construction added them a second time.
84#[must_use]
85pub fn bound_symbol_names() -> std::collections::BTreeSet<String> {
86    let mut interp: Interpreter<ScriptCtx> = Interpreter::new();
87    let mut ctx = ScriptCtx::with_argv(Vec::<String>::new());
88    install_stdlib(&mut interp, &mut ctx);
89    let mut names: std::collections::BTreeSet<String> = interp
90        .reserved_head_names()
91        .iter()
92        .map(ToString::to_string)
93        .collect();
94    names.extend(
95        interp
96            .globals_snapshot()
97            .iter_top_level()
98            .into_iter()
99            .map(|(name, _)| name.to_string()),
100    );
101    names
102}