Lines
83.33 %
Functions
8.82 %
Branches
100 %
use scripting::host::{WasmHost, define_host_functions};
use scripting::nomiscript::{
Compiler, Expr, Fraction, GIT_REVISION, Program, Reader, Symbol, SymbolKind, SymbolTable, Value,
};
use scripting_format::{
ContextType, DebugValueData, EntityType, GlobalHeader, OUTPUT_HEADER_SIZE, OutputHeader,
ValueType,
use thiserror::Error;
use tracing::debug;
use tracing_subscriber::EnvFilter;
use tracing_subscriber::prelude::*;
use tracing_subscriber::reload;
use wasmtime::{Config, Engine, Linker, Module, Store};
#[derive(Error, Debug)]
pub enum Error {
#[error("{0}")]
Script(#[from] scripting::nomiscript::Error),
Runtime(String),
}
impl Error {
#[must_use]
pub fn render(&self, use_color: bool) -> String {
match self {
Error::Script(e) => e.render(use_color),
Error::Runtime(msg) => {
if use_color {
format!("\x1b[31merror:\x1b[0m {msg}")
} else {
format!("error: {msg}")
pub type Result<T> = std::result::Result<T, Error>;
pub struct Interpreter {
host: WasmHost,
compiler: Compiler,
reload_handle: reload::Handle<EnvFilter, tracing_subscriber::Registry>,
const DEFAULT_OUTPUT_SIZE: u32 = 64 * 1024;
const WASM_PAGE_SIZE: u32 = 65536;
impl Interpreter {
pub fn new(debug_mode: bool) -> anyhow::Result<Self> {
let mut config = Config::new();
config.wasm_gc(true);
let engine = Engine::new(&config)?;
let mut symbols = SymbolTable::with_builtins();
symbols.define(
Symbol::new("REVISION", SymbolKind::Variable)
.with_value(Expr::String(GIT_REVISION.to_string())),
);
let default_filter = if debug_mode { "debug" } else { "warn" };
let filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_filter));
let (filter_layer, reload_handle) = reload::Layer::new(filter);
tracing_subscriber::registry()
.with(filter_layer)
.with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr))
.try_init()
.ok();
let mut interp = Self {
host: WasmHost::new(engine, symbols),
compiler: Compiler::new(),
reload_handle,
interp.load_stdlib()?;
Ok(interp)
fn load_stdlib(&mut self) -> Result<()> {
const STDLIB: &str = include_str!("stdlib.lisp");
let program = Reader::parse(STDLIB)?;
let mut symbols = self
.host
.symbol_table()
.write()
.map_err(|e| Error::Runtime(format!("failed to write symbol table: {e}")))?;
self.compiler.compile(&program, &mut symbols)?;
Ok(())
pub fn eval(&mut self, input: &str) -> Result<Vec<Value>> {
let program = Reader::parse(input)?;
let debug_mode = program.annotations.iter().any(|a| a.name == "debug");
if debug_mode {
self.reload_handle
.modify(|f| *f = EnvFilter::new("debug"))
let result = self.eval_program(&program);
.modify(|f| {
*f = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("warn"));
})
result
fn eval_program(&mut self, program: &Program) -> Result<Vec<Value>> {
if program.exprs.is_empty() {
return Ok(vec![]);
let wasm = {
self.compiler.compile(program, &mut symbols)?
let value = self.run_wasm(&wasm)?;
Ok(vec![value])
pub fn struct_fields(&self, name: &str) -> Option<Vec<String>> {
self.host
.read()
.ok()
.and_then(|st| st.struct_fields(name).map(<[std::string::String]>::to_vec))
pub fn compile_to_wasm(&mut self, input: &str) -> Result<Vec<u8>> {
return Err(Error::Runtime("nothing to compile".to_string()));
Ok(self.compiler.compile(&program, &mut symbols)?)
pub fn run_wasm(&self, wasm: &[u8]) -> Result<Value> {
debug!(wasm_size = wasm.len(), "creating WASM module");
let module =
Module::new(self.host.engine(), wasm).map_err(|e| Error::Runtime(e.to_string()))?;
let output_size = DEFAULT_OUTPUT_SIZE;
let input = build_minimal_input(output_size);
let input_offset = scripting_format::BASE_OFFSET;
let output_offset = input_offset + input.len() as u32;
let strings_offset = {
let header = GlobalHeader::from_bytes(&input).expect("minimal input must be valid");
header.strings_pool_offset
debug!(
input_offset,
output_offset, strings_offset, "memory layout offsets"
let exec_state = self
.execution_state(input_offset, output_offset, strings_offset);
let mut store = Store::new(self.host.engine(), exec_state);
let mut linker = Linker::new(self.host.engine());
define_host_functions(&mut linker).map_err(|e| Error::Runtime(e.to_string()))?;
let instance = linker
.instantiate(&mut store, &module)
.map_err(|e| Error::Runtime(e.to_string()))?;
let memory = instance
.get_memory(&mut store, "memory")
.ok_or_else(|| Error::Runtime("no memory export".to_string()))?;
store.data_mut().memory = Some(memory);
let total_size = input.len() + output_size as usize;
let required_pages = (input_offset as usize + total_size).div_ceil(WASM_PAGE_SIZE as usize);
let current_pages = memory.size(&store) as usize;
if required_pages > current_pages {
debug!(current_pages, required_pages, "growing memory");
memory
.grow(&mut store, (required_pages - current_pages) as u64)
let mem_data = memory.data_mut(&mut store);
let input_start = input_offset as usize;
mem_data[input_start..input_start + input.len()].copy_from_slice(&input);
let output_start = output_offset as usize;
let output_header = OutputHeader::new(0);
mem_data[output_start..output_start + OUTPUT_HEADER_SIZE]
.copy_from_slice(&output_header.to_bytes());
let should_apply = instance
.get_typed_func::<(), i32>(&mut store, "should_apply")
debug!("calling should_apply");
let apply = should_apply
.call(&mut store, ())
debug!(result = apply, "should_apply returned");
if apply != 1 {
return Err(Error::Runtime(format!(
"should_apply returned {apply}, expected 1"
)));
let process = instance
.get_typed_func::<(), ()>(&mut store, "process")
debug!("calling process");
process
let mem_data = memory.data(&store);
let output_data = &mem_data[output_start..];
let result = decode_result(output_data);
debug!("result decoded");
impl Default for Interpreter {
fn default() -> Self {
Self::new(false).expect("failed to create Interpreter")
fn build_minimal_input(output_size: u32) -> Vec<u8> {
use scripting::MemorySerializer;
let mut ser = MemorySerializer::new();
ser.set_context(ContextType::BatchProcess, EntityType::Transaction);
ser.finalize(output_size)
fn decode_result(data: &[u8]) -> Result<Value> {
let output_header = OutputHeader::from_bytes(data)
.ok_or_else(|| Error::Runtime("invalid output header".to_string()))?;
if output_header.output_entity_count == 0 {
return Err(Error::Runtime("no output entities".to_string()));
let entity_header = scripting_format::EntityHeader::from_bytes(&data[OUTPUT_HEADER_SIZE..])
.ok_or_else(|| Error::Runtime("invalid entity header".to_string()))?;
let data_offset = entity_header.data_offset as usize;
let value_data = DebugValueData::from_bytes(&data[data_offset..])
.ok_or_else(|| Error::Runtime("invalid debug value data".to_string()))?;
let value_type = ValueType::try_from(value_data.value_type)
.map_err(|()| Error::Runtime("unknown value type".to_string()))?;
match value_type {
ValueType::Nil => Ok(Value::Nil),
ValueType::Bool => Ok(Value::Bool(value_data.data1 != 0)),
ValueType::Number => Ok(Value::Number(Fraction::new(
value_data.data1,
value_data.data2,
))),
ValueType::String | ValueType::Symbol => {
let pool_offset = value_data.data1 as usize;
let len = value_data.data2 as usize;
let strings_offset = output_header.strings_offset as usize;
let start = strings_offset + pool_offset;
let end = start + len;
if end > data.len() {
return Err(Error::Runtime("string data out of bounds".to_string()));
let s = std::str::from_utf8(&data[start..end])
.map_err(|_| Error::Runtime("invalid UTF-8 in string".to_string()))?;
if value_type == ValueType::Symbol {
Ok(Value::Symbol(s.to_string()))
Ok(Value::String(s.to_string()))