1use serde::{Deserialize, Serialize};
26use tatara_lisp_eval::{Arity, Interpreter, Value, install_full_stdlib_with};
27use thiserror::Error;
28
29#[derive(Debug, Error)]
30pub enum VmError {
31 #[error("tatara-lisp read error: {0}")]
32 Read(#[from] tatara_lisp::LispError),
33 #[error("tatara-lisp eval error: {0}")]
34 Eval(#[from] tatara_lisp_eval::EvalError),
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
43pub enum HostEffect {
44 Message(String),
46 RunCommand { name: String, args: Vec<String> },
48 SetOption { name: String, value: String },
50 InsertText(String),
52}
53
54#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
58pub struct EditorSnapshot {
59 pub cursor_line: i64,
60 pub cursor_column: i64,
61 pub current_line: String,
62 pub mode: String,
63 pub buffer_name: String,
64}
65
66#[derive(Debug, Clone, Default, PartialEq, Eq)]
70pub struct EscribaHost {
71 pub snapshot: EditorSnapshot,
72 pub effects: Vec<HostEffect>,
73}
74
75impl EscribaHost {
76 #[must_use]
77 pub fn new() -> Self {
78 Self::default()
79 }
80
81 #[must_use]
83 pub fn with_snapshot(snapshot: EditorSnapshot) -> Self {
84 Self {
85 snapshot,
86 effects: Vec::new(),
87 }
88 }
89
90 pub fn take_effects(&mut self) -> Vec<HostEffect> {
93 std::mem::take(&mut self.effects)
94 }
95}
96
97pub struct EscribaVm {
101 interp: Interpreter<EscribaHost>,
102}
103
104impl Default for EscribaVm {
105 fn default() -> Self {
106 Self::new()
107 }
108}
109
110impl EscribaVm {
111 #[must_use]
112 pub fn new() -> Self {
113 let mut interp: Interpreter<EscribaHost> = Interpreter::new();
114 let mut bootstrap = EscribaHost::new();
120 install_full_stdlib_with(&mut interp, &mut bootstrap);
121 register_editor_fns(&mut interp);
122 Self { interp }
123 }
124
125 pub fn eval(&mut self, src: &str, host: &mut EscribaHost) -> Result<Value, VmError> {
129 let forms = tatara_lisp::read_spanned(src)?;
130 Ok(self.interp.eval_program(&forms, host)?)
131 }
132}
133
134fn register_editor_fns(interp: &mut Interpreter<EscribaHost>) {
137 interp.register_typed1(
139 "message",
140 |h: &mut EscribaHost, s: String| -> tatara_lisp_eval::Result<String> {
141 h.effects.push(HostEffect::Message(s.clone()));
142 Ok(s)
143 },
144 );
145 interp.register_typed1(
146 "insert",
147 |h: &mut EscribaHost, s: String| -> tatara_lisp_eval::Result<()> {
148 h.effects.push(HostEffect::InsertText(s));
149 Ok(())
150 },
151 );
152 interp.register_typed2(
153 "set-option",
154 |h: &mut EscribaHost, name: String, value: String| -> tatara_lisp_eval::Result<()> {
155 h.effects.push(HostEffect::SetOption { name, value });
156 Ok(())
157 },
158 );
159 interp.register_fn(
162 "run-command",
163 Arity::AtLeast(1),
164 |args: &[Value], h: &mut EscribaHost, span| {
165 let name = value_as_string(&args[0]).ok_or_else(|| {
166 tatara_lisp_eval::EvalError::native_fn(
167 "run-command",
168 "first argument must be a string command name",
169 span,
170 )
171 })?;
172 let rest = args[1..].iter().filter_map(value_as_string).collect();
173 h.effects.push(HostEffect::RunCommand { name, args: rest });
174 Ok(Value::Nil)
175 },
176 );
177
178 interp.register_typed0(
180 "cursor-line",
181 |h: &mut EscribaHost| -> tatara_lisp_eval::Result<i64> { Ok(h.snapshot.cursor_line) },
182 );
183 interp.register_typed0(
184 "cursor-column",
185 |h: &mut EscribaHost| -> tatara_lisp_eval::Result<i64> { Ok(h.snapshot.cursor_column) },
186 );
187 interp.register_typed0(
188 "current-line",
189 |h: &mut EscribaHost| -> tatara_lisp_eval::Result<String> {
190 Ok(h.snapshot.current_line.clone())
191 },
192 );
193 interp.register_typed0(
194 "editor-mode",
195 |h: &mut EscribaHost| -> tatara_lisp_eval::Result<String> { Ok(h.snapshot.mode.clone()) },
196 );
197 interp.register_typed0(
198 "buffer-name",
199 |h: &mut EscribaHost| -> tatara_lisp_eval::Result<String> {
200 Ok(h.snapshot.buffer_name.clone())
201 },
202 );
203}
204
205fn value_as_string(v: &Value) -> Option<String> {
209 match v {
210 Value::Str(s) | Value::Symbol(s) => Some(s.to_string()),
211 Value::Int(n) => Some(n.to_string()),
212 Value::Bool(b) => Some(b.to_string()),
213 _ => None,
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn evaluates_pure_arithmetic() {
223 let mut vm = EscribaVm::new();
224 let mut host = EscribaHost::new();
225 let v = vm.eval("(+ 1 2)", &mut host).unwrap();
226 assert!(matches!(v, Value::Int(3)), "got {v:?}");
227 assert!(host.effects.is_empty(), "pure compute emits no effects");
228 }
229
230 #[test]
231 fn full_stdlib_supports_let_binding() {
232 let mut vm = EscribaVm::new();
235 let mut host = EscribaHost::new();
236 let v = vm.eval("(let ((x 5)) (* x x))", &mut host).unwrap();
237 assert!(matches!(v, Value::Int(25)), "got {v:?}");
238 }
239
240 #[test]
241 fn message_emits_effect() {
242 let mut vm = EscribaVm::new();
243 let mut host = EscribaHost::new();
244 vm.eval(r#"(message "hello from lisp")"#, &mut host)
245 .unwrap();
246 assert_eq!(
247 host.effects,
248 vec![HostEffect::Message("hello from lisp".into())]
249 );
250 }
251
252 #[test]
253 fn run_command_with_args_emits_effect() {
254 let mut vm = EscribaVm::new();
255 let mut host = EscribaHost::new();
256 vm.eval(r#"(run-command "open" "README.md")"#, &mut host)
257 .unwrap();
258 assert_eq!(
259 host.effects,
260 vec![HostEffect::RunCommand {
261 name: "open".into(),
262 args: vec!["README.md".into()],
263 }]
264 );
265 }
266
267 #[test]
268 fn set_option_and_insert_emit_effects() {
269 let mut vm = EscribaVm::new();
270 let mut host = EscribaHost::new();
271 vm.eval(r#"(set-option "number" "true")"#, &mut host)
272 .unwrap();
273 vm.eval(r#"(insert "hello")"#, &mut host).unwrap();
274 assert_eq!(
275 host.effects,
276 vec![
277 HostEffect::SetOption {
278 name: "number".into(),
279 value: "true".into(),
280 },
281 HostEffect::InsertText("hello".into()),
282 ]
283 );
284 }
285
286 #[test]
287 fn reads_snapshot_and_branches() {
288 let mut vm = EscribaVm::new();
291 let mut host = EscribaHost::with_snapshot(EditorSnapshot {
292 cursor_line: 7,
293 ..Default::default()
294 });
295 vm.eval(
296 r#"(if (> (cursor-line) 0) (message "below-top") (message "at-top"))"#,
297 &mut host,
298 )
299 .unwrap();
300 assert_eq!(host.effects, vec![HostEffect::Message("below-top".into())]);
301 }
302
303 #[test]
304 fn multi_form_program_sequences_effects() {
305 let mut vm = EscribaVm::new();
306 let mut host = EscribaHost::new();
307 vm.eval(r#"(message "first") (run-command "save")"#, &mut host)
308 .unwrap();
309 assert_eq!(
310 host.effects,
311 vec![
312 HostEffect::Message("first".into()),
313 HostEffect::RunCommand {
314 name: "save".into(),
315 args: vec![],
316 },
317 ]
318 );
319 }
320
321 #[test]
322 fn take_effects_drains_log() {
323 let mut vm = EscribaVm::new();
324 let mut host = EscribaHost::new();
325 vm.eval(r#"(message "x")"#, &mut host).unwrap();
326 let drained = host.take_effects();
327 assert_eq!(drained.len(), 1);
328 assert!(host.effects.is_empty());
329 }
330
331 #[test]
332 fn read_error_surfaces_as_vm_error() {
333 let mut vm = EscribaVm::new();
334 let mut host = EscribaHost::new();
335 let err = vm.eval("(((", &mut host).unwrap_err();
336 assert!(matches!(err, VmError::Read(_)));
337 }
338}