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}