1
use scripting::host::{WasmHost, define_host_functions};
2
use scripting::nomiscript::{
3
    Compiler, Expr, Fraction, GIT_REVISION, Program, Reader, Symbol, SymbolKind, SymbolTable, Value,
4
};
5
use scripting::runtime::{EngineOpts, ProfilerStrategy, build_engine};
6
use scripting_format::{
7
    ContextType, DEBUG_VALUE_DATA_SIZE, DebugValueData, ENTITY_HEADER_SIZE, EntityType,
8
    GlobalHeader, OUTPUT_HEADER_SIZE, OutputHeader, ValueType,
9
};
10
use thiserror::Error;
11
use tracing::debug;
12
use tracing_subscriber::EnvFilter;
13
use tracing_subscriber::prelude::*;
14
use tracing_subscriber::reload;
15
use wasmtime::{Linker, Module, Store};
16

            
17
#[derive(Error, Debug)]
18
pub enum Error {
19
    #[error("{0}")]
20
    Script(#[from] scripting::nomiscript::Error),
21

            
22
    #[error("{0}")]
23
    Runtime(String),
24
}
25

            
26
impl 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

            
42
pub type Result<T> = std::result::Result<T, Error>;
43

            
44
pub struct Interpreter {
45
    host: WasmHost,
46
    compiler: Compiler,
47
    reload_handle: reload::Handle<EnvFilter, tracing_subscriber::Registry>,
48
}
49

            
50
const DEFAULT_OUTPUT_SIZE: u32 = 64 * 1024;
51
const 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.
59
4455
fn set_interactive_epoch_deadline<T>(store: &mut Store<T>) {
60
4455
    store.set_epoch_deadline(u64::MAX);
61
4455
}
62

            
63
impl Interpreter {
64
4590
    pub fn new(debug_mode: bool) -> anyhow::Result<Self> {
65
4590
        Self::with_profiler(debug_mode, ProfilerStrategy::None)
66
4590
    }
67

            
68
4605
    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
4605
        let engine = build_engine(EngineOpts::baseline().with_profiler(profiler))?;
73

            
74
4605
        let mut symbols = SymbolTable::with_builtins();
75
4605
        symbols.define(
76
4605
            Symbol::new("REVISION", SymbolKind::Variable)
77
4605
                .with_value(Expr::String(GIT_REVISION.to_string())),
78
        );
79

            
80
4605
        let default_filter = if debug_mode { "debug" } else { "warn" };
81
4605
        let filter =
82
4605
            EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_filter));
83
4605
        let (filter_layer, reload_handle) = reload::Layer::new(filter);
84
4605
        tracing_subscriber::registry()
85
4605
            .with(filter_layer)
86
4605
            .with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr))
87
4605
            .try_init()
88
4605
            .ok();
89

            
90
4605
        let mut interp = Self {
91
4605
            host: WasmHost::new(engine, symbols),
92
4605
            compiler: Compiler::new(),
93
4605
            reload_handle,
94
4605
        };
95
4605
        interp.load_stdlib()?;
96
4605
        Ok(interp)
97
4605
    }
98

            
99
4605
    fn load_stdlib(&mut self) -> Result<()> {
100
        const STDLIB: &str = include_str!("stdlib.lisp");
101
4605
        let program = Reader::parse(STDLIB)?;
102
4605
        let mut symbols = self
103
4605
            .host
104
4605
            .symbol_table()
105
4605
            .write()
106
4605
            .map_err(|e| Error::Runtime(format!("failed to write symbol table: {e}")))?;
107
4605
        self.compiler.compile(&program, &mut symbols)?;
108
4605
        Ok(())
109
4605
    }
110

            
111
4650
    pub fn eval(&mut self, input: &str) -> Result<Vec<Value>> {
112
4650
        let program = Reader::parse(input)?;
113
4650
        let debug_mode = program.annotations.iter().any(|a| a.name == "debug");
114
4650
        if debug_mode {
115
            self.reload_handle
116
                .modify(|f| *f = EnvFilter::new("debug"))
117
                .ok();
118
4650
        }
119
4650
        let result = self.eval_program(&program);
120
4650
        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
4650
        }
128
4650
        result
129
4650
    }
130

            
131
4650
    fn eval_program(&mut self, program: &Program) -> Result<Vec<Value>> {
132
4650
        if program.exprs.is_empty() {
133
15
            return Ok(vec![]);
134
4635
        }
135

            
136
4050
        let wasm = {
137
4635
            let mut symbols = self
138
4635
                .host
139
4635
                .symbol_table()
140
4635
                .write()
141
4635
                .map_err(|e| Error::Runtime(format!("failed to write symbol table: {e}")))?;
142
4635
            self.compiler.compile(program, &mut symbols)?
143
        };
144

            
145
4050
        let value = self.run_wasm(&wasm)?;
146
3990
        Ok(vec![value])
147
4650
    }
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
630
    pub fn compile_to_wasm(&mut self, input: &str) -> Result<Vec<u8>> {
159
630
        let program = Reader::parse(input)?;
160
630
        if program.exprs.is_empty() {
161
            return Err(Error::Runtime("nothing to compile".to_string()));
162
630
        }
163
630
        let mut symbols = self
164
630
            .host
165
630
            .symbol_table()
166
630
            .write()
167
630
            .map_err(|e| Error::Runtime(format!("failed to write symbol table: {e}")))?;
168
630
        Ok(self.compiler.compile(&program, &mut symbols)?)
169
630
    }
170

            
171
4155
    pub fn run_wasm(&self, wasm: &[u8]) -> Result<Value> {
172
4155
        let input = build_minimal_input(DEFAULT_OUTPUT_SIZE);
173
4155
        self.run_wasm_with_input(wasm, &input)
174
4155
    }
175

            
176
4455
    pub fn run_wasm_with_input(&self, wasm: &[u8], input: &[u8]) -> Result<Value> {
177
4455
        debug!(wasm_size = wasm.len(), "creating WASM module");
178
4455
        let module =
179
4455
            Module::new(self.host.engine(), wasm).map_err(|e| Error::Runtime(e.to_string()))?;
180

            
181
4455
        let output_size = DEFAULT_OUTPUT_SIZE;
182
4455
        let input_offset = scripting_format::BASE_OFFSET;
183
4455
        let output_offset = input_offset + input.len() as u32;
184
4455
        let strings_offset = {
185
4455
            let header = GlobalHeader::from_bytes(input).expect("minimal input must be valid");
186
4455
            header.strings_pool_offset
187
        };
188
4455
        debug!(
189
            input_offset,
190
            output_offset, strings_offset, "memory layout offsets"
191
        );
192

            
193
4455
        let exec_state = self
194
4455
            .host
195
4455
            .execution_state(input_offset, output_offset, strings_offset);
196
4455
        let mut store = Store::new(self.host.engine(), exec_state);
197
4455
        set_interactive_epoch_deadline(&mut store);
198

            
199
4455
        let mut linker = Linker::new(self.host.engine());
200
4455
        define_host_functions(&mut linker).map_err(|e| Error::Runtime(e.to_string()))?;
201

            
202
4455
        let instance = linker
203
4455
            .instantiate(&mut store, &module)
204
4455
            .map_err(|e| Error::Runtime(e.to_string()))?;
205

            
206
4455
        let memory = instance
207
4455
            .get_memory(&mut store, "memory")
208
4455
            .ok_or_else(|| Error::Runtime("no memory export".to_string()))?;
209

            
210
4455
        store.data_mut().memory = Some(memory);
211

            
212
4455
        let total_size = input.len() + output_size as usize;
213
4455
        let required_pages = (input_offset as usize + total_size).div_ceil(WASM_PAGE_SIZE as usize);
214
4455
        let current_pages = memory.size(&store) as usize;
215

            
216
4455
        if required_pages > current_pages {
217
4455
            debug!(current_pages, required_pages, "growing memory");
218
4455
            memory
219
4455
                .grow(&mut store, (required_pages - current_pages) as u64)
220
4455
                .map_err(|e| Error::Runtime(e.to_string()))?;
221
        }
222

            
223
4455
        let mem_data = memory.data_mut(&mut store);
224
4455
        let input_start = input_offset as usize;
225
4455
        mem_data[input_start..input_start + input.len()].copy_from_slice(input);
226

            
227
4455
        let output_start = output_offset as usize;
228
4455
        let output_header = OutputHeader::new(0);
229
4455
        mem_data[output_start..output_start + OUTPUT_HEADER_SIZE]
230
4455
            .copy_from_slice(&output_header.to_bytes());
231

            
232
4455
        let should_apply = instance
233
4455
            .get_typed_func::<(), i32>(&mut store, "should_apply")
234
4455
            .map_err(|e| Error::Runtime(e.to_string()))?;
235
4455
        debug!("calling should_apply");
236
4455
        let apply = should_apply
237
4455
            .call(&mut store, ())
238
4455
            .map_err(|e| Error::Runtime(e.to_string()))?;
239
4455
        debug!(result = apply, "should_apply returned");
240
4455
        if apply != 1 {
241
            return Err(Error::Runtime(format!(
242
                "should_apply returned {apply}, expected 1"
243
            )));
244
4455
        }
245

            
246
4455
        let process = instance
247
4455
            .get_typed_func::<(), ()>(&mut store, "process")
248
4455
            .map_err(|e| Error::Runtime(e.to_string()))?;
249
4455
        debug!("calling process");
250
4455
        process
251
4455
            .call(&mut store, ())
252
4455
            .map_err(|e| Error::Runtime(e.to_string()))?;
253

            
254
4380
        let mem_data = memory.data(&store);
255
4380
        let output_data = &mem_data[output_start..];
256

            
257
4380
        let result = decode_result(output_data);
258
4380
        debug!("result decoded");
259
4380
        result
260
4455
    }
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

            
336
impl Default for Interpreter {
337
    fn default() -> Self {
338
        Self::new(false).expect("failed to create Interpreter")
339
    }
340
}
341

            
342
4155
fn build_minimal_input(output_size: u32) -> Vec<u8> {
343
    use scripting::MemorySerializer;
344
4155
    let mut ser = MemorySerializer::new();
345
4155
    ser.set_context(ContextType::BatchProcess, EntityType::Transaction);
346
4155
    ser.finalize(output_size)
347
4155
}
348

            
349
4380
fn decode_result(data: &[u8]) -> Result<Value> {
350
4380
    let output_header = OutputHeader::from_bytes(data)
351
4380
        .ok_or_else(|| Error::Runtime("invalid output header".to_string()))?;
352

            
353
4380
    if output_header.output_entity_count == 0 {
354
        return Err(Error::Runtime("no output entities".to_string()));
355
4380
    }
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
4380
    let mut offset = OUTPUT_HEADER_SIZE;
365
4380
    let mut found: Option<usize> = None;
366
4380
    for _ in 0..output_header.output_entity_count {
367
4395
        let header = scripting_format::EntityHeader::from_bytes(&data[offset..])
368
4395
            .ok_or_else(|| Error::Runtime("invalid entity header".to_string()))?;
369
4395
        if header.entity_type == EntityType::DebugValue as u8 {
370
4380
            found = Some(header.data_offset as usize);
371
4380
        }
372
4395
        offset += ENTITY_HEADER_SIZE + header.data_size as usize;
373
    }
374
4380
    let data_offset =
375
4380
        found.ok_or_else(|| Error::Runtime("no debug value in output".to_string()))?;
376

            
377
4380
    let value_data = DebugValueData::from_bytes(&data[data_offset..])
378
4380
        .ok_or_else(|| Error::Runtime("invalid debug value data".to_string()))?;
379

            
380
4380
    let value_type = ValueType::try_from(value_data.value_type)
381
4380
        .map_err(|()| Error::Runtime("unknown value type".to_string()))?;
382

            
383
4380
    match value_type {
384
540
        ValueType::Nil => Ok(Value::Nil),
385
675
        ValueType::Bool => Ok(Value::Bool(value_data.data1 != 0)),
386
2265
        ValueType::Number => Ok(Value::Number(Fraction::new(
387
2265
            value_data.data1,
388
2265
            value_data.data2,
389
2265
        ))),
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
900
            let start = data_offset + DEBUG_VALUE_DATA_SIZE + value_data.data1 as usize;
394
900
            let len = value_data.data2 as usize;
395
900
            let end = start + len;
396
900
            if end > data.len() {
397
                return Err(Error::Runtime("string data out of bounds".to_string()));
398
900
            }
399
900
            let s = std::str::from_utf8(&data[start..end])
400
900
                .map_err(|_| Error::Runtime("invalid UTF-8 in string".to_string()))?;
401
900
            if value_type == ValueType::Symbol {
402
225
                Ok(Value::Symbol(s.to_string()))
403
            } else {
404
675
                Ok(Value::String(s.to_string()))
405
            }
406
        }
407
    }
408
4380
}