Skip to main content

sim_lib_server/repl/
spec.rs

1use std::path::PathBuf;
2
3use sim_kernel::{CapabilityName, Error, Expr, Result, Symbol};
4
5use crate::{keyword, line_driver_factories};
6
7use super::drivers::{BufferDriver, ExternalDriver, StdioLineDriver};
8
9const AGENT_DRIVE_CAPABILITY: &str = "agent-drive";
10
11/// Source of REPL input lines and sink for rendered output.
12pub trait LineDriver: Send + Sync {
13    /// Reads the next input line, showing `prompt`; returns `None` at end of input.
14    fn read_line(&mut self, cx: &mut sim_kernel::Cx, prompt: &str) -> Result<Option<String>>;
15    /// Writes rendered `output` to the driver.
16    fn write_output(&mut self, cx: &mut sim_kernel::Cx, output: &str) -> Result<()>;
17    /// Reports whether the driver reads multi-line input. Defaults to `false`.
18    fn supports_multiline(&self) -> bool {
19        false
20    }
21    /// Returns the capabilities the driver requires. Defaults to none.
22    fn capabilities(&self) -> &[CapabilityName] {
23        &[]
24    }
25}
26
27/// Declarative description of a REPL line driver, resolved into a concrete
28/// [`LineDriver`] by [`DriverSpec::create_driver`].
29#[derive(Clone, Debug, PartialEq, Eq)]
30pub enum DriverSpec {
31    /// Single-line stdio driver.
32    Line,
33    /// Multi-line stdio driver.
34    Multiline,
35    /// External process driver invoking `cmd`.
36    External {
37        /// Command line to spawn as the driver.
38        cmd: String,
39    },
40    /// Buffer-file driver reading from `path`.
41    Buffer {
42        /// Path to the buffer file.
43        path: PathBuf,
44        /// Symbol naming the trigger event (for example `save`).
45        on: Symbol,
46    },
47    /// A driver named in the spec but not built in; resolved from the registry
48    /// at creation time or reported as unavailable.
49    Unavailable {
50        /// The original driver spec expression.
51        spec: Expr,
52    },
53}
54
55impl DriverSpec {
56    /// Parses a driver spec from `expr`: a driver symbol (`line`, `multiline`,
57    /// ...) or a `(kind ...)` option list. Returns an error for malformed specs.
58    pub fn from_expr(expr: &Expr) -> Result<Self> {
59        match expr {
60            Expr::Symbol(symbol) if symbol.name.as_ref() == "line" => Ok(Self::Line),
61            Expr::Symbol(symbol) if symbol.name.as_ref() == "multiline" => Ok(Self::Multiline),
62            Expr::Symbol(symbol)
63                if symbol.name.as_ref() == "readline"
64                    || symbol.name.as_ref() == "browser"
65                    || symbol.name.as_ref() == "agent" =>
66            {
67                Ok(Self::Unavailable { spec: expr.clone() })
68            }
69            Expr::List(items) | Expr::Vector(items) => Self::from_items(items),
70            _ => Err(Error::Eval(
71                "server/repl :driver expects a driver symbol or spec list".to_owned(),
72            )),
73        }
74    }
75
76    fn from_items(items: &[Expr]) -> Result<Self> {
77        let Some(Expr::Symbol(kind)) = items.first() else {
78            return Err(Error::Eval(
79                "server/repl :driver list must start with a symbol".to_owned(),
80            ));
81        };
82        match kind.name.as_ref() {
83            "external" => {
84                let cmd = find_string_option(
85                    items,
86                    "cmd",
87                    "external driver requires :cmd",
88                    "external :cmd expects a string",
89                )?;
90                Ok(Self::External { cmd })
91            }
92            "buffer" => {
93                let path = PathBuf::from(find_string_option(
94                    items,
95                    "path",
96                    "buffer driver requires :path",
97                    "buffer :path expects a string",
98                )?);
99                let on = find_symbol_option(items, "on", "buffer :on expects a symbol")?
100                    .unwrap_or_else(|| Symbol::new("save"));
101                Ok(Self::Buffer { path, on })
102            }
103            "readline" | "browser" | "agent" => Ok(Self::Unavailable {
104                spec: Expr::List(items.to_vec()),
105            }),
106            other => Err(Error::Eval(format!(
107                "server/repl: unknown driver kind {other}"
108            ))),
109        }
110    }
111
112    /// Renders this spec back into its `Expr` form, the inverse of
113    /// [`DriverSpec::from_expr`].
114    pub fn as_expr(&self) -> Expr {
115        match self {
116            Self::Line => Expr::Symbol(Symbol::new("line")),
117            Self::Multiline => Expr::Symbol(Symbol::new("multiline")),
118            Self::External { cmd } => Expr::List(vec![
119                Expr::Symbol(Symbol::new("external")),
120                Expr::Symbol(Symbol::new(":cmd")),
121                Expr::String(cmd.clone()),
122            ]),
123            Self::Buffer { path, on } => Expr::List(vec![
124                Expr::Symbol(Symbol::new("buffer")),
125                Expr::Symbol(Symbol::new(":path")),
126                Expr::String(path.display().to_string()),
127                Expr::Symbol(Symbol::new(":on")),
128                Expr::Quote {
129                    mode: sim_kernel::QuoteMode::Quote,
130                    expr: Box::new(Expr::Symbol(on.clone())),
131                },
132            ]),
133            Self::Unavailable { spec } => spec.clone(),
134        }
135    }
136
137    /// Builds the concrete [`LineDriver`] for this spec, consulting the driver
138    /// registry for [`DriverSpec::Unavailable`] specs and erroring if none is found.
139    pub fn create_driver(&self, cx: &mut sim_kernel::Cx) -> Result<Box<dyn LineDriver>> {
140        match self {
141            Self::Line => Ok(Box::new(StdioLineDriver::new(false))),
142            Self::Multiline => Ok(Box::new(StdioLineDriver::new(true))),
143            Self::External { cmd } => Ok(Box::new(ExternalDriver::new(cmd.clone()))),
144            Self::Buffer { path, on } => {
145                if on.name.as_ref() != "save" {
146                    return Err(Error::Eval(format!(
147                        "buffer driver does not support :on {} yet",
148                        on
149                    )));
150                }
151                Ok(Box::new(BufferDriver::new(path.clone())))
152            }
153            Self::Unavailable { spec } => {
154                if let Some(driver) = lookup_registered_driver(cx, spec)? {
155                    return Ok(driver);
156                }
157                Err(unavailable_driver_error(spec))
158            }
159        }
160    }
161
162    /// Returns the capabilities required to use this driver spec.
163    pub fn required_capabilities(&self) -> Vec<CapabilityName> {
164        match self {
165            Self::Unavailable { spec } => unavailable_driver_capabilities(spec),
166            _ => Vec::new(),
167        }
168    }
169}
170
171fn find_string_option(
172    items: &[Expr],
173    name: &str,
174    missing: &'static str,
175    wrong: &'static str,
176) -> Result<String> {
177    let mut iter = items.iter().skip(1);
178    while let Some(key_expr) = iter.next() {
179        let Some(value) = iter.next() else {
180            return Err(Error::Eval(
181                "driver options must be key/value pairs".to_owned(),
182            ));
183        };
184        if keyword(key_expr)? == name {
185            return match value {
186                Expr::String(text) => Ok(text.clone()),
187                _ => Err(Error::Eval(wrong.to_owned())),
188            };
189        }
190    }
191    Err(Error::Eval(missing.to_owned()))
192}
193
194fn find_symbol_option(items: &[Expr], name: &str, wrong: &'static str) -> Result<Option<Symbol>> {
195    let mut iter = items.iter().skip(1);
196    while let Some(key_expr) = iter.next() {
197        let Some(value) = iter.next() else {
198            return Err(Error::Eval(
199                "driver options must be key/value pairs".to_owned(),
200            ));
201        };
202        if keyword(key_expr)? == name {
203            return match value {
204                Expr::Symbol(symbol) => Ok(Some(symbol.clone())),
205                Expr::Quote { expr, .. } => match expr.as_ref() {
206                    Expr::Symbol(symbol) => Ok(Some(symbol.clone())),
207                    _ => Err(Error::Eval(wrong.to_owned())),
208                },
209                _ => Err(Error::Eval(wrong.to_owned())),
210            };
211        }
212    }
213    Ok(None)
214}
215
216fn unavailable_driver_capabilities(spec: &Expr) -> Vec<CapabilityName> {
217    match spec {
218        Expr::Symbol(symbol) if symbol.name.as_ref() == "agent" => {
219            vec![CapabilityName::new(AGENT_DRIVE_CAPABILITY)]
220        }
221        Expr::List(items) | Expr::Vector(items) if matches!(items.first(), Some(Expr::Symbol(symbol)) if symbol.name.as_ref() == "agent") =>
222        {
223            vec![CapabilityName::new(AGENT_DRIVE_CAPABILITY)]
224        }
225        _ => Vec::new(),
226    }
227}
228
229fn lookup_registered_driver(
230    cx: &mut sim_kernel::Cx,
231    spec: &Expr,
232) -> Result<Option<Box<dyn LineDriver>>> {
233    let Some(name) = unavailable_driver_name(spec) else {
234        return Ok(None);
235    };
236    let factory = {
237        let factories = line_driver_factories()
238            .lock()
239            .map_err(|_| Error::HostError("line driver registry mutex poisoned".to_owned()))?;
240        factories.get(name).copied()
241    };
242    let Some(factory) = factory else {
243        return Ok(None);
244    };
245    factory(cx, spec)
246}
247
248fn unavailable_driver_name(spec: &Expr) -> Option<&str> {
249    match spec {
250        Expr::Symbol(symbol) => Some(symbol.name.as_ref()),
251        Expr::List(items) | Expr::Vector(items) => match items.first() {
252            Some(Expr::Symbol(symbol)) => Some(symbol.name.as_ref()),
253            _ => None,
254        },
255        _ => None,
256    }
257}
258
259fn unavailable_driver_error(spec: &Expr) -> Error {
260    match unavailable_driver_name(spec) {
261        Some("browser") => Error::Eval("browser driver not available".to_owned()),
262        Some("readline") => Error::Eval("readline driver not available".to_owned()),
263        _ => Error::Eval("driver not available".to_owned()),
264    }
265}