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