Skip to main content

tatara_lisp_script/stdlib/
module.rs

1//! Module system — `(require "path.tlisp")`.
2//!
3//! Evaluates another .tlisp file in the current interpreter, exposing
4//! its top-level `(define …)` forms as globals in the caller. Relative
5//! paths resolve against the directory of the *current* file being
6//! evaluated (or cwd if there isn't one — e.g. from a REPL).
7//!
8//! The require cache canonicalizes paths before storing so that
9//! `(require "./util.tlisp")` and `(require "../scripts/util.tlisp")`
10//! from sibling files converge on the same entry — no double-eval.
11//!
12//! Implementation note: because tatara-lisp-eval does not expose an
13//! "eval a new form inside my interpreter" from the FFI layer, we
14//! install `require` as a **macro-like** native fn that READS + EXPANDS
15//! the target file but hands the forms back to the caller as a list
16//! the caller will then have to splice. To keep this ergonomic we
17//! wrap the top-level require behavior behind the
18//! `install_require_with` API (called from main.rs), which takes the
19//! interpreter by pointer and re-enters eval_program.
20
21use std::path::{Path, PathBuf};
22
23use tatara_lisp_eval::{Arity, EvalError, Interpreter, Value};
24
25use crate::script_ctx::ScriptCtx;
26
27/// Install `(require PATH)` on the given interpreter. Because `require`
28/// has to re-enter evaluation on the same interpreter, we capture an
29/// `Arc<Mutex<Interpreter>>` (or equivalent) via a closure — but since
30/// `Interpreter<H>` is not `Send+Sync`, we instead install a stub here
31/// and let main.rs override it with the real function after the
32/// interpreter is constructed.
33///
34/// This stub exists so `install_stdlib` can be called without a ready
35/// interpreter-to-self reference; scripts get a clear error if they
36/// call `(require)` without the real binding having been patched in.
37pub fn install(interp: &mut Interpreter<ScriptCtx>) {
38    interp.register_fn(
39        "require",
40        Arity::Exact(1),
41        |_args: &[Value], _ctx: &mut ScriptCtx, sp| {
42            Err(EvalError::native_fn(
43                "require",
44                "require is only available when the interpreter is driven \
45                 by `tatara-script` (or an embedder that installs the \
46                 require hook). See tatara-lisp-script/src/require_hook.rs.",
47                sp,
48            ))
49        },
50    );
51}
52
53/// Resolve a `(require)` target string into an absolute canonical path.
54/// Relative paths resolve against the directory of `ctx.current_file`,
55/// or `cwd` if that's unset. Absolute paths pass through unchanged.
56pub fn resolve_require_path(ctx: &ScriptCtx, target: &str) -> Result<PathBuf, String> {
57    let candidate = Path::new(target);
58    let absolute = if candidate.is_absolute() {
59        candidate.to_path_buf()
60    } else if let Some(current) = ctx.current_file.as_ref().and_then(|p| p.parent()) {
61        current.join(candidate)
62    } else {
63        std::env::current_dir()
64            .map_err(|e| format!("cwd: {e}"))?
65            .join(candidate)
66    };
67    std::fs::canonicalize(&absolute).map_err(|e| format!("require {absolute:?}: {e}"))
68}
69
70/// Utility for main.rs: take a `(require PATH)` string, resolve +
71/// canonicalize, return None if already required (caller should no-op),
72/// or the canonical path otherwise.
73pub fn plan_require(ctx: &mut ScriptCtx, target: &str) -> Result<Option<PathBuf>, String> {
74    let canonical = resolve_require_path(ctx, target)?;
75    if ctx.required.contains(&canonical) {
76        return Ok(None);
77    }
78    ctx.required.insert(canonical.clone());
79    Ok(Some(canonical))
80}
81
82/// Convenience wrapper: given an already-resolved path, read + parse it
83/// and return the spanned forms, ready for the caller to `eval_program`.
84pub fn read_forms(path: &Path) -> Result<Vec<tatara_lisp::Spanned>, String> {
85    let src = std::fs::read_to_string(path).map_err(|e| format!("read {path:?}: {e}"))?;
86    tatara_lisp::read_spanned(&src).map_err(|e| format!("parse {path:?}: {e:?}"))
87}