tatara_lisp_script/script_ctx.rs
1//! `ScriptCtx` — the host context passed to every stdlib FFI call.
2//!
3//! Holds a shared HTTP agent (connection pooling across calls), the
4//! command-line argv, the current file being evaluated (for relative-
5//! path `require`), and the require cache (so repeated `(require …)`
6//! calls are no-ops). Embedders that want to extend the stdlib should
7//! either expose `ScriptCtx` directly or wrap it in their own type
8//! and re-register fn's against that.
9
10use std::collections::HashSet;
11use std::path::PathBuf;
12use std::time::Duration;
13
14#[derive(Default)]
15pub struct ScriptCtx {
16 /// Program arguments after the script path.
17 /// e.g. for `tatara-script imports.tlisp lilitu_io --all` → `["lilitu_io", "--all"]`.
18 pub argv: Vec<String>,
19
20 /// The file currently being evaluated (if any). Used by `(require)`
21 /// to resolve relative paths against the caller's directory.
22 pub current_file: Option<PathBuf>,
23
24 /// Set of canonical absolute paths already required. Prevents
25 /// re-evaluation on the second (or third, …) `(require …)` of the
26 /// same file — required forms define globals once.
27 pub required: HashSet<PathBuf>,
28
29 /// Lazily-initialized shared HTTP agent (connection pooling).
30 http_agent: Option<ureq::Agent>,
31
32 /// Recorded test cases when in `--test` mode. Each entry is a
33 /// (name, thunk-closure) pair — the closure captures the test body
34 /// for deferred execution.
35 pub tests: Vec<TestCase>,
36
37 /// Owns every temp path `(tmp-dir)` / `(tmp-file)` hands out, and removes
38 /// them when this context drops.
39 ///
40 /// PRIVATE on purpose. The old stdlib built its own path inline and
41 /// returned a bare string, so nothing owned it and nothing ever cleaned it
42 /// up — 21,608 dirs / 13 GB on rio, into a tmpfs, i.e. into RAM. Routing
43 /// every mint through [`ScriptCtx::scratch_dir`] / [`ScriptCtx::scratch_file`]
44 /// is what makes "created but never cleaned" unrepresentable rather than
45 /// merely discouraged: there is no longer a constructor that skips the
46 /// registry. See `scratch.rs` for the full incident.
47 scratch: crate::scratch::ScratchRegistry,
48}
49
50/// A collected `(deftest name body)` form awaiting `--test` execution.
51pub struct TestCase {
52 pub name: String,
53 pub body: Vec<tatara_lisp::Spanned>,
54}
55
56impl ScriptCtx {
57 /// Construct a context with the given argv. Used by the binary
58 /// entry point; embedders may prefer to start from `Default` and
59 /// populate argv directly.
60 pub fn with_argv<I, S>(argv: I) -> Self
61 where
62 I: IntoIterator<Item = S>,
63 S: Into<String>,
64 {
65 Self {
66 argv: argv.into_iter().map(Into::into).collect(),
67 current_file: None,
68 required: HashSet::new(),
69 http_agent: None,
70 tests: Vec::new(),
71 scratch: crate::scratch::ScratchRegistry::default(),
72 }
73 }
74
75 /// Mint a scratch DIRECTORY owned by this context.
76 ///
77 /// The returned path is removed when the context drops. This is the only
78 /// way the stdlib can obtain one.
79 ///
80 /// # Errors
81 /// Propagates any `create_dir_all` failure.
82 pub fn scratch_dir(&mut self) -> std::io::Result<std::path::PathBuf> {
83 self.scratch.dir()
84 }
85
86 /// Mint a scratch FILE (created empty) owned by this context.
87 ///
88 /// The returned path is removed when the context drops. This is the only
89 /// way the stdlib can obtain one.
90 ///
91 /// # Errors
92 /// Propagates any write failure.
93 pub fn scratch_file(&mut self) -> std::io::Result<std::path::PathBuf> {
94 self.scratch.file()
95 }
96
97 /// How many scratch entries this context currently owns. Test surface.
98 #[must_use]
99 pub fn scratch_len(&self) -> usize {
100 self.scratch.len()
101 }
102
103 /// Return a shared HTTP agent, initializing on first use.
104 pub fn http(&mut self) -> &ureq::Agent {
105 self.http_agent.get_or_insert_with(|| {
106 ureq::Agent::config_builder()
107 .timeout_global(Some(Duration::from_secs(30)))
108 .build()
109 .new_agent()
110 })
111 }
112}