narju/io/
mod.rs

1//! The I/O boundary between the evaluator and the outside world.
2//!
3//! The floor's `read`, `read-file`, and `print` primitives, and the
4//! REPL prompt itself, all route through a single trait object held in
5//! [`crate::eval::EvalCtx`]. Everything above that boundary — the CK
6//! machine, staging, the Purple session — is I/O-agnostic; swapping the
7//! implementation swaps the whole language's connection to the world.
8//!
9//! Two implementations exist: [`terminal::TerminalIo`] wraps a
10//! rustyline editor for interactive use, and [`headless::HeadlessIo`]
11//! replays scripted input and records output for tests and other
12//! embeddings. Code that constructs a `TopLevel` outside a terminal
13//! must use `HeadlessIo`; `TerminalIo::new` fails without a TTY.
14
15pub mod headless;
16pub mod terminal;
17
18use std::path::Path;
19
20use crate::error::NarjuError;
21
22/// Read a source file. Every file the system opens — the `read-file`
23/// primitive, the CLI's file arguments and `--purple` boot script, the
24/// REPL's `:load` — comes through here.
25///
26/// The path is tried as given: absolute, or relative to the process
27/// working directory. If that fails and the path is relative, it is
28/// retried under the directory named by the `NARJU_LIB` environment
29/// variable. Installed builds set `NARJU_LIB` to the directory holding
30/// their `lib/*.naj` copies, so the Purple boot resolves its libraries
31/// from any working directory while a user's own relative paths keep
32/// taking precedence. On failure the error is the one from the path as
33/// given, so diagnostics name what the caller wrote.
34pub fn read_source(path: &str) -> std::io::Result<String> {
35    match std::fs::read_to_string(path) {
36        Ok(src) => Ok(src),
37        Err(err) => {
38            if !Path::new(path).is_absolute() {
39                if let Ok(root) = std::env::var("NARJU_LIB") {
40                    if let Ok(src) = std::fs::read_to_string(Path::new(&root).join(path)) {
41                        return Ok(src);
42                    }
43                }
44            }
45            Err(err)
46        }
47    }
48}
49
50/// Input and output as the evaluator sees them.
51///
52/// One boxed instance lives in [`crate::eval::EvalCtx`] for the life of
53/// a session. The trait is deliberately minimal: a line in, a line out.
54/// Prompts, colour, history, and completion are implementation details
55/// behind it.
56pub trait NarjuIo {
57    /// Read one line of input, displaying `prompt` where that makes
58    /// sense. Returns `Ok(None)` at end of input (EOF, Ctrl-D, or an
59    /// exhausted script), which the floor's `read` maps to `nil`.
60    fn read_line(&mut self, prompt: &str) -> Result<Option<String>, NarjuError>;
61
62    /// Write one line of output (no trailing newline in `s`; the
63    /// implementation supplies it).
64    fn print(&mut self, s: &str) -> Result<(), NarjuError>;
65}
66
67#[cfg(test)]
68mod tests {
69    use super::read_source;
70
71    #[test]
72    fn narju_lib_is_a_fallback_root_for_relative_paths() {
73        let root = std::env::temp_dir().join("narju-read-source-test");
74        std::fs::create_dir_all(root.join("lib")).unwrap();
75        std::fs::write(root.join("lib/fallback-probe.naj"), "(+ 1 2)\n").unwrap();
76
77        std::env::set_var("NARJU_LIB", &root);
78        assert_eq!(read_source("lib/fallback-probe.naj").unwrap(), "(+ 1 2)\n");
79        // The working directory wins when the path exists there.
80        assert!(read_source("lib/prelude.naj").unwrap().contains("append"));
81
82        std::env::remove_var("NARJU_LIB");
83        assert!(read_source("lib/fallback-probe.naj").is_err());
84    }
85}