Skip to main content

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 cli;
34pub mod crypto_extra;
35pub mod encoding;
36pub mod env;
37pub mod fs;
38pub mod hash;
39pub mod http;
40pub mod http_server;
41pub mod io;
42pub mod json;
43pub mod kube;
44pub mod list_ext;
45pub mod log;
46pub mod module;
47pub mod os;
48pub mod process;
49pub mod regex;
50pub mod sops;
51pub mod string;
52pub mod string_ext;
53pub mod time;
54pub mod toml;
55pub mod uuid;
56pub mod yaml;
57
58/// Install every stdlib family. Three layers, in order:
59///
60///   1. Rust primitives (`install_primitives`): arithmetic, comparison,
61///      list/string/IO — `+`, `=`, `car`, `cons`, `length`, `reverse`,
62///      `string-format`, `print-line`, `read-file`, …
63///   2. Higher-order Rust primitives (`install_hof`): `map`, `filter`,
64///      `reduce`, `foldl`, `foldr`, `for-each`, `apply`, `find`,
65///      `every?`, `any?`, `partition`, `group-by`, `sort-by`,
66///      `iterate`, `take-while`, `drop-while`, `remove`, `count-if`,
67///      `find-index`, `scan-left`, `repeatedly`, `some`.
68///   3. Typed map/dict primitives (`install_map`): `(map ...)` value
69///      constructor + accessors (alist-get, etc.).
70///
71/// Then the FFI families (cli/fs/http/json/regex/sops/...).
72/// Finally, the pure-Lisp stdlib (`install_lisp_stdlib_with`) which adds
73/// `compose`, `pipe`, `->`, `->>`, `when-let`, `dotimes`, `dolist`,
74/// `defflow`, `inc`, `dec`, `even?`, `odd?`, `first`/`second`/`third`,
75/// `range`, `zip`, `interleave`, `flatten`, `distinct`, `max-by`,
76/// `min-by`, `partial`, `juxt`, `tap`, `not=`, `some?`, `not-empty?`.
77///
78/// Scripts see the full Clojure-flavored environment without calling
79/// any of these installers themselves.
80pub fn install_stdlib(interp: &mut Interpreter<ScriptCtx>, ctx: &mut ScriptCtx) {
81    install_primitives(interp);
82    install_hof(interp);
83    install_map(interp);
84    cli::install(interp);
85    crypto_extra::install(interp);
86    encoding::install(interp);
87    env::install(interp);
88    fs::install(interp);
89    hash::install(interp);
90    http::install(interp);
91    http_server::install(interp);
92    io::install(interp);
93    kube::install(interp);
94    json::install(interp);
95    list_ext::install(interp);
96    log::install(interp);
97    module::install(interp);
98    os::install(interp);
99    process::install(interp);
100    regex::install(interp);
101    sops::install(interp);
102    string::install(interp);
103    string_ext::install(interp);
104    time::install(interp);
105    toml::install(interp);
106    uuid::install(interp);
107    yaml::install(interp);
108    // Pure-Lisp layer LAST — it depends on every Rust primitive above
109    // (compose calls foldr, juxt calls map, threading macros use list?,
110    // etc.). Loading earlier would error on unbound primitives.
111    install_lisp_stdlib_with(interp, ctx);
112}
113
114#[cfg(test)]
115mod surface_tests {
116    //! Surface-area regression tests for `install_stdlib`.
117    //!
118    //! Every name in this module is something a script writer reasonably
119    //! expects to be in scope after `install_stdlib`. If one of these
120    //! tests fails, it means a previously-claimed primitive is gone — a
121    //! breaking change for every consumer .tlisp file in the org.
122    //!
123    //! Categories covered:
124    //!   1. Core Rust primitives (`+`, `=`, `car`, `cons`, `length`, `modulo`, `abs`, `min`, `max`, …)
125    //!   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`)
126    //!   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?)
127    //!   4. Clojure-flavored aliases (`fn`, `true`, `false`, `mod`, `rem`, `nil?`, `==`, `next`).
128
129    use crate::{eval_str, Value};
130
131    /// Helper: eval and assert against a stringified result.
132    fn eval_eq(src: &str, expected: &str) {
133        let v = eval_str(src).expect(src);
134        assert_eq!(format!("{v:?}"), expected, "src: {src}");
135    }
136
137    fn eval_ok(src: &str) -> Value {
138        eval_str(src).unwrap_or_else(|e| panic!("eval failed for {src:?}: {e}"))
139    }
140
141    // ── Core primitives ─────────────────────────────────────────────
142
143    #[test]
144    fn arith_modulo_and_friends() {
145        // The `mod`/`rem` aliases must agree with the canonical `modulo`.
146        eval_eq("(modulo 7 3)", "Int(1)");
147        eval_eq("(mod 7 3)", "Int(1)");
148        eval_eq("(rem 7 3)", "Int(1)");
149    }
150
151    #[test]
152    fn arith_min_max_abs() {
153        eval_eq("(min 1 2 3)", "Int(1)");
154        eval_eq("(max 1 2 3)", "Int(3)");
155        eval_eq("(abs -5)", "Int(5)");
156    }
157
158    #[test]
159    fn list_primitives() {
160        eval_eq("(car (list 1 2 3))", "Int(1)");
161        eval_eq("(length (list 1 2 3 4))", "Int(4)");
162    }
163
164    // ── Higher-order Rust primitives (install_hof) ─────────────────
165
166    #[test]
167    fn hof_filter_evens() {
168        // The headline regression: filter MUST be bound after install_stdlib.
169        let v = eval_ok("(filter (lambda (x) (= 0 (modulo x 2))) (list 1 2 3 4 5))");
170        assert_eq!(format!("{v:?}"), "[Int(2), Int(4)]");
171    }
172
173    #[test]
174    fn hof_map_double() {
175        let v = eval_ok("(map (lambda (x) (* x 2)) (list 1 2 3))");
176        assert_eq!(format!("{v:?}"), "[Int(2), Int(4), Int(6)]");
177    }
178
179    #[test]
180    fn hof_foldl_sum() {
181        eval_eq("(foldl + 0 (list 1 2 3 4 5))", "Int(15)");
182    }
183
184    #[test]
185    fn hof_foldr_sum() {
186        eval_eq("(foldr + 0 (list 1 2 3 4 5))", "Int(15)");
187    }
188
189    #[test]
190    fn hof_reduce_sum() {
191        eval_eq("(reduce + (list 1 2 3 4 5))", "Int(15)");
192    }
193
194    #[test]
195    fn hof_find_first_match() {
196        eval_eq(
197            "(find (lambda (x) (> x 2)) (list 1 2 3 4))",
198            "Int(3)",
199        );
200    }
201
202    #[test]
203    fn hof_every_and_any() {
204        eval_eq("(every? (lambda (x) (> x 0)) (list 1 2 3))", "Bool(true)");
205        eval_eq("(any? (lambda (x) (> x 5)) (list 1 2 3))", "Bool(false)");
206    }
207
208    #[test]
209    fn hof_remove_inverse_of_filter() {
210        let v = eval_ok("(remove (lambda (x) (= 0 (modulo x 2))) (list 1 2 3 4 5))");
211        assert_eq!(format!("{v:?}"), "[Int(1), Int(3), Int(5)]");
212    }
213
214    #[test]
215    fn hof_apply() {
216        eval_eq("(apply + (list 1 2 3 4))", "Int(10)");
217    }
218
219    // ── Pure-Lisp stdlib (install_lisp_stdlib_with) ────────────────
220
221    #[test]
222    fn lisp_compose_and_pipe() {
223        eval_eq("((compose inc inc) 5)", "Int(7)");
224        eval_eq("((pipe inc inc inc) 5)", "Int(8)");
225    }
226
227    #[test]
228    fn lisp_seq_helpers() {
229        eval_eq("(first (list 10 20 30))", "Int(10)");
230        eval_eq("(last (list 10 20 30))", "Int(30)");
231        let v = eval_ok("(rest (list 10 20 30))");
232        assert_eq!(format!("{v:?}"), "[Int(20), Int(30)]");
233    }
234
235    #[test]
236    fn lisp_range() {
237        let v = eval_ok("(range 5)");
238        assert_eq!(format!("{v:?}"), "[Int(0), Int(1), Int(2), Int(3), Int(4)]");
239    }
240
241    #[test]
242    fn lisp_concat_and_distinct() {
243        let cat = eval_ok("(concat (list 1 2) (list 3 4))");
244        assert_eq!(format!("{cat:?}"), "[Int(1), Int(2), Int(3), Int(4)]");
245        let uniq = eval_ok("(distinct (list 1 2 1 3 2 4))");
246        assert_eq!(format!("{uniq:?}"), "[Int(1), Int(2), Int(3), Int(4)]");
247    }
248
249    #[test]
250    fn lisp_numeric_helpers() {
251        eval_eq("(inc 5)", "Int(6)");
252        eval_eq("(dec 5)", "Int(4)");
253        eval_eq("(even? 4)", "Bool(true)");
254        eval_eq("(odd? 3)", "Bool(true)");
255        eval_eq("(zero? 0)", "Bool(true)");
256        eval_eq("(positive? 1)", "Bool(true)");
257        eval_eq("(negative? -1)", "Bool(true)");
258    }
259
260    #[test]
261    fn lisp_threading_macros() {
262        // `(-> 5 inc inc inc)` ≡ `(inc (inc (inc 5)))`
263        eval_eq("(-> 5 inc inc inc)", "Int(8)");
264        // `(->> 5 inc inc)` puts the value in the LAST position. inc is
265        // unary so position doesn't matter — confirms the macro fires.
266        eval_eq("(->> 5 inc inc)", "Int(7)");
267    }
268
269    // ── Clojure aliases ────────────────────────────────────────────
270
271    #[test]
272    fn alias_fn_for_lambda() {
273        eval_eq("((fn (x) (* x x)) 4)", "Int(16)");
274    }
275
276    #[test]
277    fn alias_true_false() {
278        eval_eq("true", "Bool(true)");
279        eval_eq("false", "Bool(false)");
280    }
281
282    #[test]
283    fn alias_nil_predicate() {
284        eval_eq("(nil? (list))", "Bool(true)");
285        eval_eq("(nil? (list 1))", "Bool(false)");
286    }
287
288    #[test]
289    fn alias_double_equals() {
290        eval_eq("(== 1 1)", "Bool(true)");
291        eval_eq("(== 1 2)", "Bool(false)");
292    }
293
294    #[test]
295    fn alias_next_is_rest() {
296        let v = eval_ok("(next (list 1 2 3))");
297        assert_eq!(format!("{v:?}"), "[Int(2), Int(3)]");
298    }
299
300    // ── End-to-end script-style smoke ───────────────────────────────
301
302    #[test]
303    fn end_to_end_small_pipeline() {
304        // Idiomatic "filter even, double, sum" — the smallest realistic
305        // shape that exercises filter + map + foldl/reduce in one breath.
306        eval_eq(
307            "(reduce + (map (fn (x) (* x 2)) (filter even? (range 10))))",
308            "Int(40)", // 2+4+6+8 = 20, doubled = 40
309        );
310    }
311
312    #[test]
313    fn end_to_end_threading_with_aliases() {
314        eval_eq(
315            "(-> 1 inc inc inc inc)",
316            "Int(5)",
317        );
318    }
319}