tatara_lisp_script/stdlib/mod.rs
1//! The tatara-script stdlib. Each module registers a family of FFI
2//! primitives on `Interpreter<ScriptCtx>`; `install_stdlib` is the
3//! single entry point — call after `Interpreter::new()`.
4//!
5//! `install_stdlib` also calls `tatara_lisp_eval::install_primitives`,
6//! `install_hof`, and `install_map` up front so arithmetic, comparison,
7//! list primitives (`+`, `-`, `=`, `car`, `cdr`, `cons`, `list`, …) AND
8//! higher-order list ops (`map`, `filter`, `reduce`, `foldl`, `foldr`,
9//! `for-each`, `apply`, `find`, `every?`, `any?`, `partition`,
10//! `group-by`, `sort-by`, `iterate`, `take-while`, `drop-while`,
11//! `remove`, `count-if`, `find-index`, `scan-left`, `repeatedly`,
12//! `some`) and `(map …)` constructors / accessors are available to any
13//! script. Without these, `Interpreter::new()` is intentionally bare —
14//! the evaluator leaves primitive registration to the embedder, and
15//! historically `install_stdlib` here only wired primitives so HOFs
16//! were silently absent at script runtime.
17//!
18//! Anything beyond these (Lisp-source stdlib like `compose`, `pipe`,
19//! `->`, `->>`, `defflow`, `dotimes`, `distinct`, `group-by` helpers)
20//! lives in `tatara-lisp-eval::install_lisp_stdlib_with` and requires a
21//! host context — it's deliberately NOT installed here today since
22//! tatara-script's per-script ScriptCtx isn't available at
23//! install-time. Call `install_lisp_stdlib_with` from your binary if
24//! you need it.
25
26use tatara_lisp_eval::{
27 install_hof, install_lisp_stdlib_with, install_map, install_primitives, Interpreter,
28};
29
30use crate::script_ctx::ScriptCtx;
31
32// Core scripting families
33pub mod profile;
34pub use profile::{Capability, Profile};
35
36pub mod cli;
37pub mod crypto_extra;
38pub mod dns;
39pub mod encoding;
40pub mod env;
41pub mod fs;
42pub mod hash;
43pub mod http;
44pub mod http_server;
45pub mod io;
46pub mod json;
47pub mod kube;
48pub mod list_ext;
49pub mod log;
50pub mod module;
51pub mod os;
52pub mod process;
53pub mod regex;
54pub mod sops;
55pub mod string;
56pub mod string_ext;
57pub mod time;
58pub mod toml;
59pub mod uuid;
60pub mod yaml;
61
62/// Install every stdlib family. Three layers, in order:
63///
64/// 1. Rust primitives (`install_primitives`): arithmetic, comparison,
65/// list/string/IO — `+`, `=`, `car`, `cons`, `length`, `reverse`,
66/// `string-format`, `print-line`, `read-file`, …
67/// 2. Higher-order Rust primitives (`install_hof`): `map`, `filter`,
68/// `reduce`, `foldl`, `foldr`, `for-each`, `apply`, `find`,
69/// `every?`, `any?`, `partition`, `group-by`, `sort-by`,
70/// `iterate`, `take-while`, `drop-while`, `remove`, `count-if`,
71/// `find-index`, `scan-left`, `repeatedly`, `some`.
72/// 3. Typed map/dict primitives (`install_map`): `(map ...)` value
73/// constructor + accessors (alist-get, etc.).
74///
75/// Then the FFI families (cli/fs/http/json/regex/sops/...).
76/// Finally, the pure-Lisp stdlib (`install_lisp_stdlib_with`) which adds
77/// `compose`, `pipe`, `->`, `->>`, `when-let`, `dotimes`, `dolist`,
78/// `defflow`, `inc`, `dec`, `even?`, `odd?`, `first`/`second`/`third`,
79/// `range`, `zip`, `interleave`, `flatten`, `distinct`, `max-by`,
80/// `min-by`, `partial`, `juxt`, `tap`, `not=`, `some?`, `not-empty?`.
81///
82/// Scripts see the full Clojure-flavored environment without calling
83/// any of these installers themselves.
84pub fn install_stdlib(interp: &mut Interpreter<ScriptCtx>, ctx: &mut ScriptCtx) {
85 install_stdlib_with(interp, ctx, &Profile::ambient());
86}
87
88/// Install the stdlib under a capability [`Profile`].
89///
90/// Families outside the profile are never installed, so their names are
91/// simply unbound and calling one is an unbound-symbol error from the
92/// evaluator. There is no runtime policy check to consult or to get
93/// wrong — the containment IS the absence.
94///
95/// [`install_stdlib`] is this with [`Profile::ambient`], which grants
96/// everything, so no existing embedder changes behaviour.
97///
98/// Reach for [`Profile::sealed`] whenever the source being evaluated is
99/// not source you wrote: a controller reconciling a CR, a renderer
100/// inside a compliance boundary. The ambient set includes `sh-exec`
101/// (a literal `sh -c`), `rm-rf`, `env-set`, `kube-bearer-token` and
102/// `sops-extract` — correct for an operator running a deploy script,
103/// wrong for anything else.
104pub fn install_stdlib_with(
105 interp: &mut Interpreter<ScriptCtx>,
106 ctx: &mut ScriptCtx,
107 profile: &Profile,
108) {
109 install_primitives(interp);
110 install_hof(interp);
111 install_map(interp);
112 profile::install_families(interp, profile);
113 // Pure-Lisp layer LAST — it depends on every Rust primitive above
114 // (compose calls foldr, juxt calls map, threading macros use list?,
115 // etc.). Loading earlier would error on unbound primitives.
116 install_lisp_stdlib_with(interp, ctx);
117}
118
119#[cfg(test)]
120mod surface_tests {
121 //! Surface-area regression tests for `install_stdlib`.
122 //!
123 //! Every name in this module is something a script writer reasonably
124 //! expects to be in scope after `install_stdlib`. If one of these
125 //! tests fails, it means a previously-claimed primitive is gone — a
126 //! breaking change for every consumer .tlisp file in the org.
127 //!
128 //! Categories covered:
129 //! 1. Core Rust primitives (`+`, `=`, `car`, `cons`, `length`, `modulo`, `abs`, `min`, `max`, …)
130 //! 2. Higher-order Rust primitives (`map`, `filter`, `reduce`, `foldl`, `foldr`, `for-each`, `apply`, `find`, `every?`, `any?`, `partition`, `group-by`, `sort-by`, `iterate`, `take-while`, `drop-while`, `remove`)
131 //! 3. Pure-Lisp stdlib: identity / comp / pipe / partial / juxt / tap, `->` / `->>` threading, `when-let` / `if-let` / `dotimes` / `dolist`, sequence helpers (first/second/third/rest/last/butlast, range, repeat-list, concat, member?, position, zip, interleave, intersperse, flatten, distinct, max-by, min-by), numeric helpers (inc, dec, zero?, positive?, negative?, even?, odd?), predicates (not=, some?, not-empty?)
132 //! 4. Clojure-flavored aliases (`fn`, `true`, `false`, `mod`, `rem`, `nil?`, `==`, `next`).
133
134 use crate::{eval_str, Value};
135
136 /// Helper: eval and assert against a stringified result.
137 fn eval_eq(src: &str, expected: &str) {
138 let v = eval_str(src).expect(src);
139 assert_eq!(format!("{v:?}"), expected, "src: {src}");
140 }
141
142 fn eval_ok(src: &str) -> Value {
143 eval_str(src).unwrap_or_else(|e| panic!("eval failed for {src:?}: {e}"))
144 }
145
146 // ── Core primitives ─────────────────────────────────────────────
147
148 #[test]
149 fn arith_modulo_and_friends() {
150 // The `mod`/`rem` aliases must agree with the canonical `modulo`.
151 eval_eq("(modulo 7 3)", "Int(1)");
152 eval_eq("(mod 7 3)", "Int(1)");
153 eval_eq("(rem 7 3)", "Int(1)");
154 }
155
156 #[test]
157 fn arith_min_max_abs() {
158 eval_eq("(min 1 2 3)", "Int(1)");
159 eval_eq("(max 1 2 3)", "Int(3)");
160 eval_eq("(abs -5)", "Int(5)");
161 }
162
163 #[test]
164 fn list_primitives() {
165 eval_eq("(car (list 1 2 3))", "Int(1)");
166 eval_eq("(length (list 1 2 3 4))", "Int(4)");
167 }
168
169 // ── Higher-order Rust primitives (install_hof) ─────────────────
170
171 #[test]
172 fn hof_filter_evens() {
173 // The headline regression: filter MUST be bound after install_stdlib.
174 let v = eval_ok("(filter (lambda (x) (= 0 (modulo x 2))) (list 1 2 3 4 5))");
175 assert_eq!(format!("{v:?}"), "[Int(2), Int(4)]");
176 }
177
178 #[test]
179 fn hof_map_double() {
180 let v = eval_ok("(map (lambda (x) (* x 2)) (list 1 2 3))");
181 assert_eq!(format!("{v:?}"), "[Int(2), Int(4), Int(6)]");
182 }
183
184 #[test]
185 fn hof_foldl_sum() {
186 eval_eq("(foldl + 0 (list 1 2 3 4 5))", "Int(15)");
187 }
188
189 #[test]
190 fn hof_foldr_sum() {
191 eval_eq("(foldr + 0 (list 1 2 3 4 5))", "Int(15)");
192 }
193
194 #[test]
195 fn hof_reduce_sum() {
196 eval_eq("(reduce + (list 1 2 3 4 5))", "Int(15)");
197 }
198
199 #[test]
200 fn hof_find_first_match() {
201 eval_eq("(find (lambda (x) (> x 2)) (list 1 2 3 4))", "Int(3)");
202 }
203
204 #[test]
205 fn hof_every_and_any() {
206 eval_eq("(every? (lambda (x) (> x 0)) (list 1 2 3))", "Bool(true)");
207 eval_eq("(any? (lambda (x) (> x 5)) (list 1 2 3))", "Bool(false)");
208 }
209
210 #[test]
211 fn hof_remove_inverse_of_filter() {
212 let v = eval_ok("(remove (lambda (x) (= 0 (modulo x 2))) (list 1 2 3 4 5))");
213 assert_eq!(format!("{v:?}"), "[Int(1), Int(3), Int(5)]");
214 }
215
216 #[test]
217 fn hof_apply() {
218 eval_eq("(apply + (list 1 2 3 4))", "Int(10)");
219 }
220
221 // ── Pure-Lisp stdlib (install_lisp_stdlib_with) ────────────────
222
223 #[test]
224 fn lisp_compose_and_pipe() {
225 eval_eq("((compose inc inc) 5)", "Int(7)");
226 eval_eq("((pipe inc inc inc) 5)", "Int(8)");
227 }
228
229 #[test]
230 fn lisp_seq_helpers() {
231 eval_eq("(first (list 10 20 30))", "Int(10)");
232 eval_eq("(last (list 10 20 30))", "Int(30)");
233 let v = eval_ok("(rest (list 10 20 30))");
234 assert_eq!(format!("{v:?}"), "[Int(20), Int(30)]");
235 }
236
237 #[test]
238 fn lisp_range() {
239 let v = eval_ok("(range 5)");
240 assert_eq!(format!("{v:?}"), "[Int(0), Int(1), Int(2), Int(3), Int(4)]");
241 }
242
243 #[test]
244 fn lisp_concat_and_distinct() {
245 let cat = eval_ok("(concat (list 1 2) (list 3 4))");
246 assert_eq!(format!("{cat:?}"), "[Int(1), Int(2), Int(3), Int(4)]");
247 let uniq = eval_ok("(distinct (list 1 2 1 3 2 4))");
248 assert_eq!(format!("{uniq:?}"), "[Int(1), Int(2), Int(3), Int(4)]");
249 }
250
251 #[test]
252 fn lisp_numeric_helpers() {
253 eval_eq("(inc 5)", "Int(6)");
254 eval_eq("(dec 5)", "Int(4)");
255 eval_eq("(even? 4)", "Bool(true)");
256 eval_eq("(odd? 3)", "Bool(true)");
257 eval_eq("(zero? 0)", "Bool(true)");
258 eval_eq("(positive? 1)", "Bool(true)");
259 eval_eq("(negative? -1)", "Bool(true)");
260 }
261
262 #[test]
263 fn lisp_threading_macros() {
264 // `(-> 5 inc inc inc)` ≡ `(inc (inc (inc 5)))`
265 eval_eq("(-> 5 inc inc inc)", "Int(8)");
266 // `(->> 5 inc inc)` puts the value in the LAST position. inc is
267 // unary so position doesn't matter — confirms the macro fires.
268 eval_eq("(->> 5 inc inc)", "Int(7)");
269 }
270
271 // ── Clojure aliases ────────────────────────────────────────────
272
273 #[test]
274 fn alias_fn_for_lambda() {
275 eval_eq("((fn (x) (* x x)) 4)", "Int(16)");
276 }
277
278 #[test]
279 fn alias_true_false() {
280 eval_eq("true", "Bool(true)");
281 eval_eq("false", "Bool(false)");
282 }
283
284 #[test]
285 fn alias_nil_predicate() {
286 eval_eq("(nil? (list))", "Bool(true)");
287 eval_eq("(nil? (list 1))", "Bool(false)");
288 }
289
290 #[test]
291 fn alias_double_equals() {
292 eval_eq("(== 1 1)", "Bool(true)");
293 eval_eq("(== 1 2)", "Bool(false)");
294 }
295
296 #[test]
297 fn alias_next_is_rest() {
298 let v = eval_ok("(next (list 1 2 3))");
299 assert_eq!(format!("{v:?}"), "[Int(2), Int(3)]");
300 }
301
302 // ── End-to-end script-style smoke ───────────────────────────────
303
304 #[test]
305 fn end_to_end_small_pipeline() {
306 // Idiomatic "filter even, double, sum" — the smallest realistic
307 // shape that exercises filter + map + foldl/reduce in one breath.
308 eval_eq(
309 "(reduce + (map (fn (x) (* x 2)) (filter even? (range 10))))",
310 "Int(40)", // 2+4+6+8 = 20, doubled = 40
311 );
312 }
313
314 #[test]
315 fn end_to_end_threading_with_aliases() {
316 eval_eq("(-> 1 inc inc inc inc)", "Int(5)");
317 }
318}