Skip to main content

outl_exec/runtimes/
lisp.rs

1//! `lisp` runtime — Scheme via [Steel](https://github.com/mattwparas/steel).
2//!
3//! Steel is a mature, embeddable Scheme dialect in pure Rust. We
4//! register `display` / `displayln` / `print` as native functions that
5//! funnel into our own buffer, run the source, and (if nothing was
6//! printed) auto-display the value of the last expression.
7//!
8//! Gated behind the `lang-lisp` feature.
9
10use std::sync::{Arc, Mutex};
11use std::time::Instant;
12
13use steel::steel_vm::engine::Engine;
14use steel::steel_vm::register_fn::RegisterFn;
15use steel::SteelVal;
16
17use crate::runtime::{ExecContext, ExecError, ExecOutput, ExitStatus, OutputFormat, Runtime};
18
19/// Steel-backed Scheme runtime.
20pub struct LispRuntime;
21
22impl Runtime for LispRuntime {
23    fn language(&self) -> &'static str {
24        "lisp"
25    }
26
27    fn execute(&self, source: &str, _ctx: &ExecContext) -> Result<ExecOutput, ExecError> {
28        let start = Instant::now();
29
30        let sink: Arc<Mutex<String>> = Arc::new(Mutex::new(String::new()));
31        let mut engine = Engine::new();
32
33        // Override the printing builtins so output lands in our buffer
34        // instead of the host's real stdout. Steel still has the
35        // originals available under different names if user code asks,
36        // but `(display ...)` / `(displayln ...)` / `(print ...)` —
37        // the muscle-memory forms — go through us.
38        install_printers(&mut engine, sink.clone());
39
40        match engine.run(source.to_string()) {
41            Ok(values) => {
42                let mut stdout = sink.lock().unwrap().clone();
43                if stdout.is_empty() {
44                    if let Some(last) = values.last() {
45                        stdout.push_str(&steel_value_to_string(last));
46                    }
47                }
48                Ok(ExecOutput {
49                    stdout,
50                    stderr: String::new(),
51                    duration: start.elapsed(),
52                    exit: ExitStatus::Ok,
53                    format: OutputFormat::Text,
54                })
55            }
56            Err(e) => Ok(ExecOutput {
57                stdout: sink.lock().unwrap().clone(),
58                stderr: format!("{e}"),
59                duration: start.elapsed(),
60                exit: ExitStatus::Trap("steel-error".into()),
61                format: OutputFormat::Text,
62            }),
63        }
64    }
65}
66
67fn install_printers(engine: &mut Engine, sink: Arc<Mutex<String>>) {
68    let s1 = sink.clone();
69    engine.register_fn("display", move |v: SteelVal| {
70        s1.lock().unwrap().push_str(&steel_value_to_string(&v));
71    });
72    let s2 = sink.clone();
73    engine.register_fn("displayln", move |v: SteelVal| {
74        let mut s = s2.lock().unwrap();
75        s.push_str(&steel_value_to_string(&v));
76        s.push('\n');
77    });
78    let s3 = sink.clone();
79    engine.register_fn("print", move |v: SteelVal| {
80        s3.lock().unwrap().push_str(&steel_value_to_string(&v));
81    });
82    let s4 = sink.clone();
83    engine.register_fn("println", move |v: SteelVal| {
84        let mut s = s4.lock().unwrap();
85        s.push_str(&steel_value_to_string(&v));
86        s.push('\n');
87    });
88    let s5 = sink;
89    engine.register_fn("newline", move || {
90        s5.lock().unwrap().push('\n');
91    });
92}
93
94/// Render a Steel value the way you'd see it at a REPL.
95fn steel_value_to_string(v: &SteelVal) -> String {
96    match v {
97        SteelVal::StringV(s) => s.to_string(),
98        other => other.to_string(),
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    fn run(src: &str) -> String {
107        LispRuntime
108            .execute(src, &ExecContext::default())
109            .unwrap()
110            .stdout
111    }
112
113    #[test]
114    fn simple_addition() {
115        assert_eq!(run("(+ 1 2)"), "3");
116    }
117
118    #[test]
119    fn nested_arithmetic() {
120        assert_eq!(run("(* (+ 1 2) (- 10 4))"), "18");
121    }
122
123    #[test]
124    fn explicit_display() {
125        assert_eq!(run("(display \"hello\")"), "hello");
126    }
127
128    #[test]
129    fn displayln_appends_newline() {
130        assert_eq!(run("(displayln \"hi\")"), "hi\n");
131    }
132
133    #[test]
134    fn list_operations() {
135        // Real Scheme — `map`, lambda all work out of the box because
136        // it's Steel under the hood, not our own toy.
137        let out = run("(map (lambda (x) (* x x)) (list 1 2 3))");
138        assert!(out.contains("1") && out.contains("4") && out.contains("9"));
139    }
140
141    #[test]
142    fn syntax_error_returns_trap() {
143        let out = LispRuntime
144            .execute("(+ 1", &ExecContext::default())
145            .unwrap();
146        assert!(matches!(out.exit, ExitStatus::Trap(_)));
147        assert!(!out.stderr.is_empty());
148    }
149}