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
53fn 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 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 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 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}