Skip to main content

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}