Skip to main content

nms/
interpreter.rs

1use scripting::host::{WasmHost, define_host_functions};
2use scripting::nomiscript::{
3    Compiler, Expr, Fraction, GIT_REVISION, Program, Reader, Symbol, SymbolKind, SymbolTable, Value,
4};
5use scripting::runtime::{EngineOpts, ProfilerStrategy, build_engine};
6use scripting_format::{
7    ContextType, DEBUG_VALUE_DATA_SIZE, DebugValueData, ENTITY_HEADER_SIZE, EntityType,
8    GlobalHeader, OUTPUT_HEADER_SIZE, OutputHeader, ValueType,
9};
10use thiserror::Error;
11use tracing::debug;
12use tracing_subscriber::EnvFilter;
13use tracing_subscriber::prelude::*;
14use tracing_subscriber::reload;
15use wasmtime::{Linker, Module, Store};
16
17#[derive(Error, Debug)]
18pub enum Error {
19    #[error("{0}")]
20    Script(#[from] scripting::nomiscript::Error),
21
22    #[error("{0}")]
23    Runtime(String),
24}
25
26impl Error {
27    #[must_use]
28    pub fn render(&self, use_color: bool) -> String {
29        match self {
30            Error::Script(e) => e.render(use_color),
31            Error::Runtime(msg) => {
32                if use_color {
33                    format!("\x1b[31merror:\x1b[0m {msg}")
34                } else {
35                    format!("error: {msg}")
36                }
37            }
38        }
39    }
40}
41
42pub type Result<T> = std::result::Result<T, Error>;
43
44pub struct Interpreter {
45    host: WasmHost,
46    compiler: Compiler,
47    reload_handle: reload::Handle<EnvFilter, tracing_subscriber::Registry>,
48}
49
50const DEFAULT_OUTPUT_SIZE: u32 = 64 * 1024;
51const WASM_PAGE_SIZE: u32 = 65536;
52
53/// `build_engine` turns on epoch interruption, so every store needs a
54/// deadline or it traps at the default tick 0. The interactive
55/// interpreter has no wall-clock budget to enforce (unlike the rpc
56/// `Session`, which spawns an epoch bumper) — it just runs the user's
57/// REPL input to completion. A deadline that's never advanced past
58/// gives the old no-epoch-limit behaviour back.
59fn set_interactive_epoch_deadline<T>(store: &mut Store<T>) {
60    store.set_epoch_deadline(u64::MAX);
61}
62
63impl Interpreter {
64    pub fn new(debug_mode: bool) -> anyhow::Result<Self> {
65        Self::with_profiler(debug_mode, ProfilerStrategy::None)
66    }
67
68    pub fn with_profiler(debug_mode: bool, profiler: ProfilerStrategy) -> anyhow::Result<Self> {
69        // Route through `build_engine` so the wasm feature set (GC,
70        // function-references, exceptions) stays single-sourced — a flag
71        // added there can't silently miss the nms interpreter path.
72        let engine = build_engine(EngineOpts::baseline().with_profiler(profiler))?;
73
74        let mut symbols = SymbolTable::with_builtins();
75        symbols.define(
76            Symbol::new("REVISION", SymbolKind::Variable)
77                .with_value(Expr::String(GIT_REVISION.to_string())),
78        );
79
80        let default_filter = if debug_mode { "debug" } else { "warn" };
81        let filter =
82            EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_filter));
83        let (filter_layer, reload_handle) = reload::Layer::new(filter);
84        tracing_subscriber::registry()
85            .with(filter_layer)
86            .with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr))
87            .try_init()
88            .ok();
89
90        let mut interp = Self {
91            host: WasmHost::new(engine, symbols),
92            compiler: Compiler::new(),
93            reload_handle,
94        };
95        interp.load_stdlib()?;
96        Ok(interp)
97    }
98
99    fn load_stdlib(&mut self) -> Result<()> {
100        const STDLIB: &str = include_str!("stdlib.lisp");
101        let program = Reader::parse(STDLIB)?;
102        let mut symbols = self
103            .host
104            .symbol_table()
105            .write()
106            .map_err(|e| Error::Runtime(format!("failed to write symbol table: {e}")))?;
107        self.compiler.compile(&program, &mut symbols)?;
108        Ok(())
109    }
110
111    pub fn eval(&mut self, input: &str) -> Result<Vec<Value>> {
112        let program = Reader::parse(input)?;
113        let debug_mode = program.annotations.iter().any(|a| a.name == "debug");
114        if debug_mode {
115            self.reload_handle
116                .modify(|f| *f = EnvFilter::new("debug"))
117                .ok();
118        }
119        let result = self.eval_program(&program);
120        if debug_mode {
121            self.reload_handle
122                .modify(|f| {
123                    *f = EnvFilter::try_from_default_env()
124                        .unwrap_or_else(|_| EnvFilter::new("warn"));
125                })
126                .ok();
127        }
128        result
129    }
130
131    fn eval_program(&mut self, program: &Program) -> Result<Vec<Value>> {
132        if program.exprs.is_empty() {
133            return Ok(vec![]);
134        }
135
136        let wasm = {
137            let mut symbols = self
138                .host
139                .symbol_table()
140                .write()
141                .map_err(|e| Error::Runtime(format!("failed to write symbol table: {e}")))?;
142            self.compiler.compile(program, &mut symbols)?
143        };
144
145        let value = self.run_wasm(&wasm)?;
146        Ok(vec![value])
147    }
148
149    #[must_use]
150    pub fn struct_fields(&self, name: &str) -> Option<Vec<String>> {
151        self.host
152            .symbol_table()
153            .read()
154            .ok()
155            .and_then(|st| st.struct_fields(name).map(<[std::string::String]>::to_vec))
156    }
157
158    pub fn compile_to_wasm(&mut self, input: &str) -> Result<Vec<u8>> {
159        let program = Reader::parse(input)?;
160        if program.exprs.is_empty() {
161            return Err(Error::Runtime("nothing to compile".to_string()));
162        }
163        let mut symbols = self
164            .host
165            .symbol_table()
166            .write()
167            .map_err(|e| Error::Runtime(format!("failed to write symbol table: {e}")))?;
168        Ok(self.compiler.compile(&program, &mut symbols)?)
169    }
170
171    pub fn run_wasm(&self, wasm: &[u8]) -> Result<Value> {
172        let input = build_minimal_input(DEFAULT_OUTPUT_SIZE);
173        self.run_wasm_with_input(wasm, &input)
174    }
175
176    pub fn run_wasm_with_input(&self, wasm: &[u8], input: &[u8]) -> Result<Value> {
177        debug!(wasm_size = wasm.len(), "creating WASM module");
178        let module =
179            Module::new(self.host.engine(), wasm).map_err(|e| Error::Runtime(e.to_string()))?;
180
181        let output_size = DEFAULT_OUTPUT_SIZE;
182        let input_offset = scripting_format::BASE_OFFSET;
183        let output_offset = input_offset + input.len() as u32;
184        let strings_offset = {
185            let header = GlobalHeader::from_bytes(input).expect("minimal input must be valid");
186            header.strings_pool_offset
187        };
188        debug!(
189            input_offset,
190            output_offset, strings_offset, "memory layout offsets"
191        );
192
193        let exec_state = self
194            .host
195            .execution_state(input_offset, output_offset, strings_offset);
196        let mut store = Store::new(self.host.engine(), exec_state);
197        set_interactive_epoch_deadline(&mut store);
198
199        let mut linker = Linker::new(self.host.engine());
200        define_host_functions(&mut linker).map_err(|e| Error::Runtime(e.to_string()))?;
201
202        let instance = linker
203            .instantiate(&mut store, &module)
204            .map_err(|e| Error::Runtime(e.to_string()))?;
205
206        let memory = instance
207            .get_memory(&mut store, "memory")
208            .ok_or_else(|| Error::Runtime("no memory export".to_string()))?;
209
210        store.data_mut().memory = Some(memory);
211
212        let total_size = input.len() + output_size as usize;
213        let required_pages = (input_offset as usize + total_size).div_ceil(WASM_PAGE_SIZE as usize);
214        let current_pages = memory.size(&store) as usize;
215
216        if required_pages > current_pages {
217            debug!(current_pages, required_pages, "growing memory");
218            memory
219                .grow(&mut store, (required_pages - current_pages) as u64)
220                .map_err(|e| Error::Runtime(e.to_string()))?;
221        }
222
223        let mem_data = memory.data_mut(&mut store);
224        let input_start = input_offset as usize;
225        mem_data[input_start..input_start + input.len()].copy_from_slice(input);
226
227        let output_start = output_offset as usize;
228        let output_header = OutputHeader::new(0);
229        mem_data[output_start..output_start + OUTPUT_HEADER_SIZE]
230            .copy_from_slice(&output_header.to_bytes());
231
232        let should_apply = instance
233            .get_typed_func::<(), i32>(&mut store, "should_apply")
234            .map_err(|e| Error::Runtime(e.to_string()))?;
235        debug!("calling should_apply");
236        let apply = should_apply
237            .call(&mut store, ())
238            .map_err(|e| Error::Runtime(e.to_string()))?;
239        debug!(result = apply, "should_apply returned");
240        if apply != 1 {
241            return Err(Error::Runtime(format!(
242                "should_apply returned {apply}, expected 1"
243            )));
244        }
245
246        let process = instance
247            .get_typed_func::<(), ()>(&mut store, "process")
248            .map_err(|e| Error::Runtime(e.to_string()))?;
249        debug!("calling process");
250        process
251            .call(&mut store, ())
252            .map_err(|e| Error::Runtime(e.to_string()))?;
253
254        let mem_data = memory.data(&store);
255        let output_data = &mem_data[output_start..];
256
257        let result = decode_result(output_data);
258        debug!("result decoded");
259        result
260    }
261
262    pub fn run_wasm_with_input_raw(&self, wasm: &[u8], input: &[u8]) -> Result<Vec<u8>> {
263        let module =
264            Module::new(self.host.engine(), wasm).map_err(|e| Error::Runtime(e.to_string()))?;
265
266        let output_size = DEFAULT_OUTPUT_SIZE;
267        let input_offset = scripting_format::BASE_OFFSET;
268        let output_offset = input_offset + input.len() as u32;
269        let strings_offset = {
270            let header = GlobalHeader::from_bytes(input).expect("minimal input must be valid");
271            header.strings_pool_offset
272        };
273
274        let exec_state = self
275            .host
276            .execution_state(input_offset, output_offset, strings_offset);
277        let mut store = Store::new(self.host.engine(), exec_state);
278        set_interactive_epoch_deadline(&mut store);
279
280        let mut linker = Linker::new(self.host.engine());
281        define_host_functions(&mut linker).map_err(|e| Error::Runtime(e.to_string()))?;
282
283        let instance = linker
284            .instantiate(&mut store, &module)
285            .map_err(|e| Error::Runtime(e.to_string()))?;
286
287        let memory = instance
288            .get_memory(&mut store, "memory")
289            .ok_or_else(|| Error::Runtime("no memory export".to_string()))?;
290
291        store.data_mut().memory = Some(memory);
292
293        let total_size = input.len() + output_size as usize;
294        let required_pages = (input_offset as usize + total_size).div_ceil(WASM_PAGE_SIZE as usize);
295        let current_pages = memory.size(&store) as usize;
296
297        if required_pages > current_pages {
298            memory
299                .grow(&mut store, (required_pages - current_pages) as u64)
300                .map_err(|e| Error::Runtime(e.to_string()))?;
301        }
302
303        let mem_data = memory.data_mut(&mut store);
304        let input_start = input_offset as usize;
305        mem_data[input_start..input_start + input.len()].copy_from_slice(input);
306
307        let output_start = output_offset as usize;
308        let output_header = OutputHeader::new(0);
309        mem_data[output_start..output_start + OUTPUT_HEADER_SIZE]
310            .copy_from_slice(&output_header.to_bytes());
311
312        let should_apply = instance
313            .get_typed_func::<(), i32>(&mut store, "should_apply")
314            .map_err(|e| Error::Runtime(e.to_string()))?;
315        let apply = should_apply
316            .call(&mut store, ())
317            .map_err(|e| Error::Runtime(e.to_string()))?;
318        if apply != 1 {
319            return Err(Error::Runtime(format!(
320                "should_apply returned {apply}, expected 1"
321            )));
322        }
323
324        let process = instance
325            .get_typed_func::<(), ()>(&mut store, "process")
326            .map_err(|e| Error::Runtime(e.to_string()))?;
327        process
328            .call(&mut store, ())
329            .map_err(|e| Error::Runtime(e.to_string()))?;
330
331        let mem_data = memory.data(&store);
332        Ok(mem_data[output_start..output_start + output_size as usize].to_vec())
333    }
334}
335
336impl Default for Interpreter {
337    fn default() -> Self {
338        Self::new(false).expect("failed to create Interpreter")
339    }
340}
341
342fn build_minimal_input(output_size: u32) -> Vec<u8> {
343    use scripting::MemorySerializer;
344    let mut ser = MemorySerializer::new();
345    ser.set_context(ContextType::BatchProcess, EntityType::Transaction);
346    ser.finalize(output_size)
347}
348
349fn decode_result(data: &[u8]) -> Result<Value> {
350    let output_header = OutputHeader::from_bytes(data)
351        .ok_or_else(|| Error::Runtime("invalid output header".to_string()))?;
352
353    if output_header.output_entity_count == 0 {
354        return Err(Error::Runtime("no output entities".to_string()));
355    }
356
357    // The eval result is the program's tail value — a `DebugValue` entity. Other
358    // entities (a side-effecting `create-tag`) may precede it in the same output
359    // stream, so walk to the DebugValue rather than assuming entity 0. Its data
360    // is at `data_offset`; any string payload is inline right after the fixed
361    // `DebugValueData` (the writer counts those bytes in `data_size`), so it is
362    // resolved per-entity, never via the shared `strings_offset` the entity
363    // parser uses for tag strings.
364    let mut offset = OUTPUT_HEADER_SIZE;
365    let mut found: Option<usize> = None;
366    for _ in 0..output_header.output_entity_count {
367        let header = scripting_format::EntityHeader::from_bytes(&data[offset..])
368            .ok_or_else(|| Error::Runtime("invalid entity header".to_string()))?;
369        if header.entity_type == EntityType::DebugValue as u8 {
370            found = Some(header.data_offset as usize);
371        }
372        offset += ENTITY_HEADER_SIZE + header.data_size as usize;
373    }
374    let data_offset =
375        found.ok_or_else(|| Error::Runtime("no debug value in output".to_string()))?;
376
377    let value_data = DebugValueData::from_bytes(&data[data_offset..])
378        .ok_or_else(|| Error::Runtime("invalid debug value data".to_string()))?;
379
380    let value_type = ValueType::try_from(value_data.value_type)
381        .map_err(|()| Error::Runtime("unknown value type".to_string()))?;
382
383    match value_type {
384        ValueType::Nil => Ok(Value::Nil),
385        ValueType::Bool => Ok(Value::Bool(value_data.data1 != 0)),
386        ValueType::Number => Ok(Value::Number(Fraction::new(
387            value_data.data1,
388            value_data.data2,
389        ))),
390        ValueType::String | ValueType::Symbol => {
391            // String bytes are inline right after DebugValueData, at
392            // `data_offset + DEBUG_VALUE_DATA_SIZE + data1` (data1 is 0 today).
393            let start = data_offset + DEBUG_VALUE_DATA_SIZE + value_data.data1 as usize;
394            let len = value_data.data2 as usize;
395            let end = start + len;
396            if end > data.len() {
397                return Err(Error::Runtime("string data out of bounds".to_string()));
398            }
399            let s = std::str::from_utf8(&data[start..end])
400                .map_err(|_| Error::Runtime("invalid UTF-8 in string".to_string()))?;
401            if value_type == ValueType::Symbol {
402                Ok(Value::Symbol(s.to_string()))
403            } else {
404                Ok(Value::String(s.to_string()))
405            }
406        }
407    }
408}