1use std::collections::HashMap;
11use std::sync::{Arc, Mutex};
12
13use thiserror::Error;
14use uuid::Uuid;
15use wasmtime::{
16 AnyRef, AsContextMut, Caller, Config, Engine, FieldType, Linker, Module, Mutability, Rooted,
17 StorageType, Store, StructRef, StructRefPre, StructType, Val, ValType,
18};
19
20#[derive(Debug, Error)]
21pub enum EngineError {
22 #[error("engine config rejected: {0}")]
23 Config(String),
24 #[error("module cache lock poisoned")]
25 CachePoisoned,
26 #[error("module compilation failed: {0}")]
27 Compile(String),
28 #[error("module instantiation failed: {0}")]
29 Instantiate(String),
30 #[error("fuel configuration failed: {0}")]
31 Fuel(String),
32 #[error("missing export `{0}`")]
33 MissingExport(String),
34 #[error("fuel exhausted before completion")]
35 OutOfFuel,
36 #[error("epoch deadline reached before completion")]
37 EpochInterrupt,
38 #[error("no conversion: {0}")]
43 NoConversion(String),
44 #[error("script raised {code}: {message}")]
55 ScriptRaised { code: String, message: String },
56 #[error("execution trapped: {0}")]
57 Trap(String),
58}
59
60pub const NOMI_RAISE_MARKER: &str = "__nomi_raise:";
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
73pub enum ProfilerStrategy {
74 #[default]
75 None,
76 JitDump,
77 PerfMap,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub struct EngineOpts {
82 pub fuel: bool,
83 pub profiler: ProfilerStrategy,
84}
85
86impl EngineOpts {
87 #[must_use]
88 pub const fn baseline() -> Self {
89 Self {
90 fuel: false,
91 profiler: ProfilerStrategy::None,
92 }
93 }
94
95 #[must_use]
96 pub const fn with_fuel(mut self) -> Self {
97 self.fuel = true;
98 self
99 }
100
101 #[must_use]
102 pub const fn with_profiler(mut self, strategy: ProfilerStrategy) -> Self {
103 self.profiler = strategy;
104 self
105 }
106}
107
108impl Default for EngineOpts {
109 fn default() -> Self {
110 Self::baseline()
111 }
112}
113
114pub fn build_engine(opts: EngineOpts) -> Result<Engine, EngineError> {
115 let mut config = Config::new();
116 config.wasm_gc(true);
117 config.wasm_function_references(true);
118 config.wasm_exceptions(true);
124 config.epoch_interruption(true);
125 if opts.fuel {
126 config.consume_fuel(true);
127 }
128 match opts.profiler {
129 ProfilerStrategy::None => {}
130 ProfilerStrategy::JitDump => {
131 config.profiler(wasmtime::ProfilingStrategy::JitDump);
132 }
133 ProfilerStrategy::PerfMap => {
134 config.profiler(wasmtime::ProfilingStrategy::PerfMap);
135 }
136 }
137 Engine::new(&config).map_err(|e| EngineError::Config(e.to_string()))
138}
139
140pub fn compile_module(engine: &Engine, bytes: &[u8]) -> Result<Module, EngineError> {
141 Module::new(engine, bytes).map_err(|e| EngineError::Compile(e.to_string()))
142}
143
144pub fn compile_wat(engine: &Engine, source: &str) -> Result<Module, EngineError> {
145 Module::new(engine, source).map_err(|e| EngineError::Compile(e.to_string()))
146}
147
148#[derive(Debug, Default, Clone)]
153pub struct ModuleCache {
154 inner: Arc<Mutex<HashMap<Vec<u8>, Module>>>,
155}
156
157impl ModuleCache {
158 #[must_use]
159 pub fn new() -> Self {
160 Self::default()
161 }
162
163 pub fn get_or_compile(&self, engine: &Engine, bytecode: &[u8]) -> Result<Module, EngineError> {
164 if let Some(module) = self.lookup(bytecode)? {
165 return Ok(module);
166 }
167 let module = compile_module(engine, bytecode)?;
168 self.store(bytecode, module.clone())?;
169 Ok(module)
170 }
171
172 fn lookup(&self, bytecode: &[u8]) -> Result<Option<Module>, EngineError> {
173 let guard = self.inner.lock().map_err(|_| EngineError::CachePoisoned)?;
174 Ok(guard.get(bytecode).cloned())
175 }
176
177 fn store(&self, bytecode: &[u8], module: Module) -> Result<(), EngineError> {
178 let mut guard = self.inner.lock().map_err(|_| EngineError::CachePoisoned)?;
179 guard.insert(bytecode.to_vec(), module);
180 Ok(())
181 }
182
183 pub fn is_empty(&self) -> Result<bool, EngineError> {
184 let guard = self.inner.lock().map_err(|_| EngineError::CachePoisoned)?;
185 Ok(guard.is_empty())
186 }
187
188 pub fn len(&self) -> Result<usize, EngineError> {
189 let guard = self.inner.lock().map_err(|_| EngineError::CachePoisoned)?;
190 Ok(guard.len())
191 }
192}
193
194pub fn classify_runtime_error(err: &wasmtime::Error) -> EngineError {
198 if let Some(trap) = err.downcast_ref::<wasmtime::Trap>() {
199 match *trap {
200 wasmtime::Trap::OutOfFuel => return EngineError::OutOfFuel,
201 wasmtime::Trap::Interrupt => return EngineError::EpochInterrupt,
202 _ => {}
203 }
204 }
205 let mut combined = err.to_string();
211 for cause in err.chain().skip(1) {
212 combined.push_str(": ");
213 combined.push_str(&cause.to_string());
214 }
215 if let Some(raised) = parse_nomi_raise_marker(err) {
224 return raised;
225 }
226 if combined.contains("convert-commodity: no Price row")
227 || combined.contains("convert-commodity: inverse price has zero numerator")
228 {
229 return EngineError::NoConversion(combined);
230 }
231 EngineError::Trap(combined)
232}
233
234fn parse_nomi_raise_marker(err: &wasmtime::Error) -> Option<EngineError> {
240 err.chain()
241 .map(|cause| cause.to_string())
242 .find_map(|cause_str| split_marker(&cause_str))
243}
244
245fn split_marker(text: &str) -> Option<EngineError> {
246 let rest = text.strip_prefix(NOMI_RAISE_MARKER)?;
247 let (code, message) = rest.split_once(':')?;
248 Some(EngineError::ScriptRaised {
249 code: code.to_string(),
250 message: message.to_string(),
251 })
252}
253
254#[must_use]
266pub fn err_code_and_message(err: &EngineError) -> (String, String) {
267 match err {
268 EngineError::ScriptRaised { code, message } => (code.clone(), message.clone()),
269 EngineError::NoConversion(msg) => ("no-conversion".to_string(), msg.clone()),
270 EngineError::Trap(msg) => ("runtime".to_string(), msg.clone()),
271 EngineError::Compile(msg) => ("compile".to_string(), msg.clone()),
272 EngineError::Instantiate(msg) => ("runtime".to_string(), msg.clone()),
273 EngineError::Fuel(msg) => ("runtime".to_string(), msg.clone()),
274 EngineError::MissingExport(msg) => ("runtime".to_string(), msg.clone()),
275 EngineError::Config(msg) => ("runtime".to_string(), msg.clone()),
276 EngineError::CachePoisoned => (
277 "runtime".to_string(),
278 "module cache lock poisoned".to_string(),
279 ),
280 EngineError::OutOfFuel => ("runtime".to_string(), "fuel exhausted".to_string()),
281 EngineError::EpochInterrupt => ("runtime".to_string(), "epoch deadline".to_string()),
282 }
283}
284
285pub async fn alloc_commodity_ref<T>(
293 caller: &mut Caller<'_, T>,
294 numer: i64,
295 denom: i64,
296 commodity_id: Uuid,
297) -> wasmtime::Result<Rooted<StructRef>>
298where
299 T: Send,
300{
301 let commodity_new = caller
302 .get_export("commodity_new")
303 .and_then(|e| e.into_func())
304 .ok_or_else(|| {
305 wasmtime::Error::msg(
306 "module missing 'commodity_new' export — host commodity allocation \
307 requires the nomiscript compiler skeleton's exported commodity_new",
308 )
309 })?;
310 let (hi, lo) = commodity_id.as_u64_pair();
311 let mut results = [Val::AnyRef(None)];
312 commodity_new
313 .call_async(
314 caller.as_context_mut(),
315 &[
316 Val::I64(numer),
317 Val::I64(denom),
318 Val::I64(hi as i64),
319 Val::I64(lo as i64),
320 ],
321 &mut results,
322 )
323 .await?;
324 match &results[0] {
325 Val::AnyRef(Some(any)) => any.unwrap_struct(caller.as_context_mut()),
326 Val::AnyRef(None) => Err(wasmtime::Error::msg("commodity_new returned null")),
327 _ => Err(wasmtime::Error::msg(
328 "commodity_new returned non-anyref Val variant",
329 )),
330 }
331}
332
333pub fn alloc_string_ref<T>(
340 caller: &mut Caller<'_, T>,
341 bytes: &[u8],
342) -> wasmtime::Result<Rooted<wasmtime::ArrayRef>> {
343 let engine = caller.engine().clone();
344 let ty = wasmtime::ArrayType::new(&engine, FieldType::new(Mutability::Var, StorageType::I8));
349 let pre = wasmtime::ArrayRefPre::new(caller.as_context_mut(), ty);
350 let vals: Vec<Val> = bytes.iter().map(|b| Val::I32(i32::from(*b))).collect();
351 wasmtime::ArrayRef::new_fixed(caller.as_context_mut(), &pre, &vals)
352}
353
354pub fn alloc_ratio_ref<T>(
359 caller: &mut Caller<'_, T>,
360 numer: i64,
361 denom: i64,
362) -> wasmtime::Result<Rooted<StructRef>> {
363 let engine = caller.engine().clone();
364 let ty = StructType::new(
365 &engine,
366 std::iter::repeat_n(
367 FieldType::new(Mutability::Const, StorageType::ValType(ValType::I64)),
368 2,
369 ),
370 )?;
371 let pre = StructRefPre::new(caller.as_context_mut(), ty);
372 StructRef::new(
373 caller.as_context_mut(),
374 &pre,
375 &[Val::I64(numer), Val::I64(denom)],
376 )
377}
378
379pub async fn alloc_entity_via_export<T>(
393 caller: &mut Caller<'_, T>,
394 export_name: &str,
395 args: &[Val],
396) -> wasmtime::Result<Rooted<StructRef>>
397where
398 T: Send,
399{
400 let alloc = caller
401 .get_export(export_name)
402 .and_then(|e| e.into_func())
403 .ok_or_else(|| {
404 wasmtime::Error::msg(format!(
405 "module missing '{export_name}' export — host entity allocation requires \
406 the nomiscript compiler skeleton's exported alloc_<kind> function"
407 ))
408 })?;
409 let mut results = [Val::AnyRef(None)];
410 alloc
411 .call_async(caller.as_context_mut(), args, &mut results)
412 .await?;
413 let new_entity_any = match &results[0] {
414 Val::AnyRef(any) => *any,
415 _ => {
416 return Err(wasmtime::Error::msg(format!(
417 "{export_name} returned non-anyref Val variant"
418 )));
419 }
420 };
421 new_entity_any
422 .ok_or_else(|| {
423 wasmtime::Error::msg(format!(
424 "{export_name} returned null when allocating entity"
425 ))
426 })?
427 .unwrap_struct(caller.as_context_mut())
428}
429
430pub fn read_string_arg<T>(
437 caller: &mut Caller<'_, T>,
438 arg: Option<Rooted<wasmtime::ArrayRef>>,
439) -> wasmtime::Result<Option<String>> {
440 let Some(arr) = arg else {
441 return Ok(None);
442 };
443 let len = arr.len(caller.as_context_mut())?;
444 let mut bytes = Vec::with_capacity(len as usize);
445 for i in 0..len {
446 let val = arr.get(caller.as_context_mut(), i)?;
447 let byte_i32 = val
448 .i32()
449 .ok_or_else(|| wasmtime::Error::msg("string arg element is not i32"))?;
450 bytes.push(byte_i32 as u8);
451 }
452 String::from_utf8(bytes)
453 .map(Some)
454 .map_err(|err| wasmtime::Error::msg(format!("string arg is not valid UTF-8: {err}")))
455}
456
457pub fn read_commodity_arg<T>(
462 caller: &mut Caller<'_, T>,
463 arg: Option<Rooted<StructRef>>,
464) -> wasmtime::Result<Option<(i64, i64, Uuid)>> {
465 let Some(s) = arg else {
466 return Ok(None);
467 };
468 let read_i64 = |c: &mut Caller<'_, T>, idx: usize| -> wasmtime::Result<i64> {
469 let v = s.field(c.as_context_mut(), idx)?;
470 v.i64()
471 .ok_or_else(|| wasmtime::Error::msg(format!("commodity field {idx} is not i64")))
472 };
473 let numer = read_i64(caller, 0)?;
474 let denom = read_i64(caller, 1)?;
475 let hi = read_i64(caller, 2)?;
476 let lo = read_i64(caller, 3)?;
477 let raw = ((hi as u64 as u128) << 64) | (lo as u64 as u128);
478 Ok(Some((numer, denom, Uuid::from_u128(raw))))
479}
480
481pub fn read_ratio_arg<T>(
486 caller: &mut Caller<'_, T>,
487 arg: Option<Rooted<StructRef>>,
488) -> wasmtime::Result<Option<(i64, i64)>> {
489 let Some(s) = arg else {
490 return Ok(None);
491 };
492 let numer = s
493 .field(caller.as_context_mut(), 0)?
494 .i64()
495 .ok_or_else(|| wasmtime::Error::msg("ratio field 0 (numer) is not i64"))?;
496 let denom = s
497 .field(caller.as_context_mut(), 1)?
498 .i64()
499 .ok_or_else(|| wasmtime::Error::msg("ratio field 1 (denom) is not i64"))?;
500 if denom == 0 {
501 return Err(wasmtime::Error::msg("ratio has zero denominator"));
502 }
503 Ok(Some((numer, denom)))
504}
505
506pub fn read_entity_string_field<T>(
513 caller: &mut Caller<'_, T>,
514 arg: Option<Rooted<StructRef>>,
515 kind: nomiscript::EntityKind,
516 field_name: &str,
517) -> wasmtime::Result<Option<String>> {
518 let Some(s) = arg else {
519 return Ok(None);
520 };
521 read_entity_string_field_ctx(caller.as_context_mut(), s, kind, field_name).map(Some)
522}
523
524pub fn read_entity_string_field_ctx(
528 mut store: impl AsContextMut,
529 entity: Rooted<StructRef>,
530 kind: nomiscript::EntityKind,
531 field_name: &str,
532) -> wasmtime::Result<String> {
533 let layout = nomiscript::entity_layout(kind)
534 .ok_or_else(|| wasmtime::Error::msg(format!("no entity layout for {kind:?}")))?;
535 let idx = layout
536 .fields
537 .iter()
538 .position(|f| f.name == field_name && f.kind == nomiscript::EntityFieldKind::String)
539 .ok_or_else(|| {
540 wasmtime::Error::msg(format!(
541 "entity {kind:?} has no String field named '{field_name}'"
542 ))
543 })?;
544 let arr = match entity.field(store.as_context_mut(), idx)? {
545 Val::AnyRef(Some(any)) => any.unwrap_array(store.as_context_mut())?,
546 _ => {
547 return Err(wasmtime::Error::msg(format!(
548 "entity {kind:?} field '{field_name}' (slot {idx}) is not an i8_array"
549 )));
550 }
551 };
552 let len = arr.len(store.as_context_mut())?;
553 let mut bytes = Vec::with_capacity(len as usize);
554 for i in 0..len {
555 let byte = arr
556 .get(store.as_context_mut(), i)?
557 .i32()
558 .ok_or_else(|| wasmtime::Error::msg("entity string element is not i32"))?;
559 bytes.push(byte as u8);
560 }
561 String::from_utf8(bytes)
562 .map_err(|err| wasmtime::Error::msg(format!("entity string field not utf-8: {err}")))
563}
564
565pub async fn alloc_pair_chain<T>(
583 caller: &mut Caller<'_, T>,
584 items: impl IntoIterator<Item = Rooted<AnyRef>>,
585) -> wasmtime::Result<Option<Rooted<StructRef>>>
586where
587 T: Send,
588{
589 let pair_new = caller
590 .get_export("pair_new")
591 .and_then(|e| e.into_func())
592 .ok_or_else(|| {
593 wasmtime::Error::msg(
594 "module missing 'pair_new' export — host pair allocation requires \
595 the nomiscript compiler skeleton's exported pair_new",
596 )
597 })?;
598
599 let items: Vec<Rooted<AnyRef>> = items.into_iter().collect();
600 let mut head: Option<Rooted<StructRef>> = None;
601 for item in items.into_iter().rev() {
602 let cdr_any = head.map(|p| p.to_anyref());
603 let mut results = [Val::AnyRef(None)];
604 pair_new
605 .call_async(
606 caller.as_context_mut(),
607 &[Val::AnyRef(Some(item)), Val::AnyRef(cdr_any)],
608 &mut results,
609 )
610 .await?;
611 let new_pair_any = match &results[0] {
612 Val::AnyRef(any) => *any,
613 _ => {
614 return Err(wasmtime::Error::msg(
615 "pair_new returned non-anyref Val variant",
616 ));
617 }
618 };
619 head = Some(
620 new_pair_any
621 .ok_or_else(|| {
622 wasmtime::Error::msg("pair_new returned null when chaining elements")
623 })?
624 .unwrap_struct(caller.as_context_mut())?,
625 );
626 }
627 Ok(head)
628}
629
630pub fn call_i64_export<T>(
634 engine: &Engine,
635 store: &mut Store<T>,
636 module: &Module,
637 export: &str,
638) -> Result<i64, EngineError> {
639 let linker = Linker::<T>::new(engine);
640 let instance = linker
641 .instantiate(&mut *store, module)
642 .map_err(|e| classify_runtime_error(&e))?;
643 let func = instance
644 .get_typed_func::<(), i64>(&mut *store, export)
645 .map_err(|_| EngineError::MissingExport(export.to_string()))?;
646 func.call(&mut *store, ())
647 .map_err(|e| classify_runtime_error(&e))
648}
649
650#[derive(Debug, Clone, PartialEq)]
657pub enum EvalValue {
658 Nil,
659 Bool(bool),
660 I32(i32),
661 Ratio {
662 numer: i64,
663 denom: i64,
664 },
665 Commodity {
670 numer: i64,
671 denom: i64,
672 commodity_hi: i64,
673 commodity_lo: i64,
674 },
675 String(String),
676 Bytes(Vec<u8>),
677}
678
679impl From<EvalValue> for nomiscript::Value {
680 fn from(value: EvalValue) -> Self {
681 match value {
682 EvalValue::Nil => nomiscript::Value::Nil,
683 EvalValue::Bool(b) => nomiscript::Value::Bool(b),
684 EvalValue::I32(n) => {
685 nomiscript::Value::Number(nomiscript::Fraction::from_integer(i64::from(n)))
686 }
687 EvalValue::Ratio { numer, denom } => {
688 nomiscript::Value::Number(nomiscript::Fraction::new(numer, denom))
689 }
690 EvalValue::Commodity {
691 numer,
692 denom,
693 commodity_hi,
694 commodity_lo,
695 } => {
696 let raw = ((commodity_hi as u64 as u128) << 64) | (commodity_lo as u64 as u128);
700 nomiscript::Value::Commodity {
701 amount: nomiscript::Fraction::new(numer, denom),
702 commodity_id: uuid::Uuid::from_u128(raw),
703 }
704 }
705 EvalValue::String(s) => nomiscript::Value::String(s),
706 EvalValue::Bytes(b) => nomiscript::Value::Bytes(b),
707 }
708 }
709}
710
711pub fn decode_eval_result(
722 mut store: impl AsContextMut,
723 value: Option<Rooted<AnyRef>>,
724 result_ty: Option<nomiscript::WasmType>,
725) -> wasmtime::Result<EvalValue> {
726 let Some(ty) = result_ty else {
727 return Ok(EvalValue::Nil);
728 };
729 let Some(any) = value else {
735 return match ty {
736 nomiscript::WasmType::I32 => Err(wasmtime::Error::msg(
737 "nomi-eval returned null for declared result type i32",
738 )),
739 nomiscript::WasmType::PairRef(_) => Ok(EvalValue::String("()".into())),
740 _ => Ok(EvalValue::Nil),
741 };
742 };
743 decode_anyref(&mut store, any, ty)
744}
745
746fn decode_anyref(
747 mut store: impl AsContextMut,
748 any: Rooted<AnyRef>,
749 ty: nomiscript::WasmType,
750) -> wasmtime::Result<EvalValue> {
751 use nomiscript::WasmType;
752 match ty {
753 WasmType::I32 => {
754 let i31 = any
755 .unwrap_i31(&mut store)
756 .map_err(|err| wasmtime::Error::msg(format!("expected i31, got {err}")))?;
757 Ok(EvalValue::I32(i31.get_i32()))
758 }
759 WasmType::Bool => {
760 let i31 = any
764 .unwrap_i31(&mut store)
765 .map_err(|err| wasmtime::Error::msg(format!("expected i31, got {err}")))?;
766 if i31.get_i32() == 0 {
767 Ok(EvalValue::Nil)
768 } else {
769 Ok(EvalValue::Bool(true))
770 }
771 }
772 WasmType::Ratio => {
773 let s = any.unwrap_struct(&mut store)?;
774 let numer = s
775 .field(&mut store, 0)?
776 .i64()
777 .ok_or_else(|| wasmtime::Error::msg("ratio field 0 (numer) is not i64"))?;
778 let denom = s
779 .field(&mut store, 1)?
780 .i64()
781 .ok_or_else(|| wasmtime::Error::msg("ratio field 1 (denom) is not i64"))?;
782 Ok(EvalValue::Ratio { numer, denom })
783 }
784 WasmType::Commodity => {
785 let s = any.unwrap_struct(&mut store)?;
786 let numer = s.field(&mut store, 0)?.i64().unwrap_or(0);
787 let denom = s.field(&mut store, 1)?.i64().unwrap_or(1);
788 match s.field(&mut store, 4)? {
793 Val::AnyRef(None) => {
794 let hi = s.field(&mut store, 2)?.i64().unwrap_or(0);
795 let lo = s.field(&mut store, 3)?.i64().unwrap_or(0);
796 Ok(EvalValue::Commodity {
797 numer,
798 denom,
799 commodity_hi: hi,
800 commodity_lo: lo,
801 })
802 }
803 Val::AnyRef(Some(term)) => {
804 let arr = term.unwrap_array(&mut store)?;
805 if arr.len(&mut store)? == 0 {
806 Ok(EvalValue::Ratio { numer, denom })
807 } else {
808 Err(wasmtime::Error::msg(
809 "compound commodity (e.g. money × money) has no host \
810 representation yet",
811 ))
812 }
813 }
814 _ => Err(wasmtime::Error::msg(
815 "commodity field 4 (unit term) is not a ref",
816 )),
817 }
818 }
819 WasmType::StringRef => {
820 let arr = any.unwrap_array(&mut store)?;
821 let len = arr.len(&mut store)?;
822 let mut bytes = Vec::with_capacity(len as usize);
823 for i in 0..len {
824 let v = arr.get(&mut store, i)?;
825 let byte = v
826 .i32()
827 .ok_or_else(|| wasmtime::Error::msg("string element is not i32"))?;
828 bytes.push(byte as u8);
829 }
830 let s = String::from_utf8(bytes)
831 .map_err(|err| wasmtime::Error::msg(format!("not valid utf-8: {err}")))?;
832 Ok(EvalValue::String(s))
833 }
834 WasmType::PairRef(elem) => {
835 let head = render_pair_as_string(&mut store, any, elem)?;
836 Ok(EvalValue::String(head))
837 }
838 WasmType::EntityRef(kind) => {
839 let entity = any.unwrap_struct(&mut store)?;
840 Ok(EvalValue::String(render_entity(&mut store, entity, kind)?))
841 }
842 WasmType::Closure(_) => {
843 let _ = any;
844 Ok(EvalValue::String("<closure>".into()))
845 }
846 WasmType::AnyRef => {
847 let _ = any;
853 Ok(EvalValue::String("<anyref>".into()))
854 }
855 }
856}
857
858fn render_pair_as_string(
862 mut store: impl AsContextMut,
863 head_any: Rooted<AnyRef>,
864 elem: nomiscript::PairElement,
865) -> wasmtime::Result<String> {
866 let mut out = String::from("(");
867 let mut cur: Option<Rooted<StructRef>> = Some(head_any.unwrap_struct(&mut store)?);
868 let mut first = true;
869 while let Some(node) = cur {
870 if !first {
871 out.push(' ');
872 }
873 first = false;
874 let car_val = node.field(&mut store, 0)?;
875 let car_any = match car_val {
876 Val::AnyRef(Some(a)) => a,
877 Val::AnyRef(None) => {
878 out.push_str("nil");
879 let cdr_val = node.field(&mut store, 1)?;
880 cur = match cdr_val {
881 Val::AnyRef(Some(a)) => Some(a.unwrap_struct(&mut store)?),
882 _ => None,
883 };
884 continue;
885 }
886 _ => {
887 return Err(wasmtime::Error::msg("pair car is not anyref"));
888 }
889 };
890 let car_str = render_car(&mut store, car_any, elem)?;
891 out.push_str(&car_str);
892 let cdr_val = node.field(&mut store, 1)?;
893 cur = match cdr_val {
894 Val::AnyRef(Some(a)) => Some(a.unwrap_struct(&mut store)?),
895 _ => None,
896 };
897 }
898 out.push(')');
899 Ok(out)
900}
901
902fn render_car(
903 mut store: impl AsContextMut,
904 car_any: Rooted<AnyRef>,
905 elem: nomiscript::PairElement,
906) -> wasmtime::Result<String> {
907 use nomiscript::PairElement;
908 match elem {
909 PairElement::I32 => {
910 let i31 = car_any.unwrap_i31(&mut store)?;
911 Ok(i31.get_i32().to_string())
912 }
913 PairElement::Bool => {
914 let i31 = car_any.unwrap_i31(&mut store)?;
917 Ok(if i31.get_i32() == 0 { "nil" } else { "t" }.to_string())
918 }
919 PairElement::Ratio => {
920 let s = car_any.unwrap_struct(&mut store)?;
921 let n = s.field(&mut store, 0)?.i64().unwrap_or(0);
922 let d = s.field(&mut store, 1)?.i64().unwrap_or(1);
923 if d == 1 {
924 Ok(n.to_string())
925 } else {
926 Ok(format!("{n}/{d}"))
927 }
928 }
929 PairElement::Commodity => {
930 let s = car_any.unwrap_struct(&mut store)?;
931 let n = s.field(&mut store, 0)?.i64().unwrap_or(0);
932 let d = s.field(&mut store, 1)?.i64().unwrap_or(1);
933 match s.field(&mut store, 4)? {
939 Val::AnyRef(None) => {
940 let hi = s.field(&mut store, 2)?.i64().unwrap_or(0);
941 let lo = s.field(&mut store, 3)?.i64().unwrap_or(0);
942 let raw = ((hi as u64 as u128) << 64) | (lo as u64 as u128);
943 let id = Uuid::from_u128(raw);
944 if d == 1 {
945 Ok(format!("(:commodity {n} :id \"{id}\")"))
946 } else {
947 Ok(format!("(:commodity {n}/{d} :id \"{id}\")"))
948 }
949 }
950 Val::AnyRef(Some(term)) => {
951 let arr = term.unwrap_array(&mut store)?;
952 if arr.len(&mut store)? == 0 {
953 Ok(if d == 1 {
954 n.to_string()
955 } else {
956 format!("{n}/{d}")
957 })
958 } else {
959 Err(wasmtime::Error::msg(
960 "compound commodity (e.g. money × money) has no host \
961 representation yet",
962 ))
963 }
964 }
965 _ => Err(wasmtime::Error::msg(
966 "commodity field 4 (unit term) is not a ref",
967 )),
968 }
969 }
970 PairElement::StringRef => {
971 let arr = car_any.unwrap_array(&mut store)?;
972 let len = arr.len(&mut store)?;
973 let mut bytes = Vec::with_capacity(len as usize);
974 for i in 0..len {
975 let v = arr.get(&mut store, i)?;
976 bytes.push(v.i32().unwrap_or(0) as u8);
977 }
978 let s = String::from_utf8(bytes).unwrap_or_else(|_| "<invalid-utf8>".into());
979 Ok(format!("\"{s}\""))
980 }
981 PairElement::Entity(kind) => {
982 let entity = car_any.unwrap_struct(&mut store)?;
983 render_entity(&mut store, entity, kind)
984 }
985 PairElement::AnyRef => Ok("<anyref>".into()),
986 }
987}
988
989fn render_entity(
997 mut store: impl AsContextMut,
998 entity: Rooted<StructRef>,
999 kind: nomiscript::EntityKind,
1000) -> wasmtime::Result<String> {
1001 use nomiscript::EntityFieldKind;
1002
1003 let Some(layout) = nomiscript::entity_layout(kind) else {
1004 return Ok(format!("(:{kind:?})"));
1006 };
1007 let mut out = format!("(:{}", layout.label);
1008 for (slot, field) in layout.fields.iter().enumerate() {
1009 let rendered = match field.kind {
1010 EntityFieldKind::String => read_string_slot(&mut store, entity, slot)?,
1011 EntityFieldKind::Ratio => read_ratio_slot(&mut store, entity, slot)?,
1012 EntityFieldKind::I32 => entity
1013 .field(&mut store, slot)?
1014 .i32()
1015 .unwrap_or(0)
1016 .to_string(),
1017 EntityFieldKind::Pair => "(...)".to_string(),
1020 };
1021 out.push_str(&format!(" :{} {rendered}", field.name));
1022 }
1023 out.push(')');
1024 Ok(out)
1025}
1026
1027fn read_string_slot(
1030 mut store: impl AsContextMut,
1031 entity: Rooted<StructRef>,
1032 slot: usize,
1033) -> wasmtime::Result<String> {
1034 match entity.field(&mut store, slot)? {
1035 Val::AnyRef(Some(a)) => {
1036 let arr = a.unwrap_array(&mut store)?;
1037 let len = arr.len(&mut store)?;
1038 let mut bytes = Vec::with_capacity(len as usize);
1039 for i in 0..len {
1040 bytes.push(arr.get(&mut store, i)?.i32().unwrap_or(0) as u8);
1041 }
1042 let s = String::from_utf8(bytes).unwrap_or_else(|_| "<invalid-utf8>".into());
1043 Ok(format!("\"{s}\""))
1044 }
1045 _ => Ok("\"\"".to_string()),
1046 }
1047}
1048
1049fn read_ratio_slot(
1052 mut store: impl AsContextMut,
1053 entity: Rooted<StructRef>,
1054 slot: usize,
1055) -> wasmtime::Result<String> {
1056 match entity.field(&mut store, slot)? {
1057 Val::AnyRef(Some(a)) => {
1058 let s = a.unwrap_struct(&mut store)?;
1059 let n = s.field(&mut store, 0)?.i64().unwrap_or(0);
1060 let d = s.field(&mut store, 1)?.i64().unwrap_or(1);
1061 Ok(if d == 1 {
1062 n.to_string()
1063 } else {
1064 format!("{n}/{d}")
1065 })
1066 }
1067 _ => Ok("0".to_string()),
1068 }
1069}
1070
1071#[cfg(test)]
1072mod tests {
1073 use super::*;
1074
1075 #[test]
1076 fn err_code_uses_script_raised_symbol_verbatim() {
1077 let (code, msg) = err_code_and_message(&EngineError::ScriptRaised {
1078 code: "no-such-account".to_string(),
1079 message: "id=42".to_string(),
1080 });
1081 assert_eq!(code, "no-such-account");
1082 assert_eq!(msg, "id=42");
1083 }
1084
1085 #[test]
1086 fn err_code_maps_commodity_mismatch_script_raise_to_symbol() {
1087 let (code, msg) = err_code_and_message(&EngineError::ScriptRaised {
1093 code: "COMMODITY-MISMATCH".to_string(),
1094 message: "USD vs EUR".to_string(),
1095 });
1096 assert_eq!(code, "COMMODITY-MISMATCH");
1097 assert_eq!(msg, "USD vs EUR");
1098 }
1099
1100 #[test]
1101 fn err_code_maps_no_conversion_to_kebab_symbol() {
1102 let (code, msg) =
1103 err_code_and_message(&EngineError::NoConversion("missing price".to_string()));
1104 assert_eq!(code, "no-conversion");
1105 assert_eq!(msg, "missing price");
1106 }
1107
1108 #[test]
1109 fn err_code_falls_back_to_runtime_for_generic_traps() {
1110 let (code, msg) = err_code_and_message(&EngineError::Trap("oops".to_string()));
1111 assert_eq!(code, "runtime");
1112 assert_eq!(msg, "oops");
1113 }
1114
1115 #[test]
1116 fn err_code_maps_out_of_fuel_to_runtime_with_diagnostic_message() {
1117 let (code, msg) = err_code_and_message(&EngineError::OutOfFuel);
1118 assert_eq!(code, "runtime");
1119 assert_eq!(msg, "fuel exhausted");
1120 }
1121
1122 fn store_with_fuel<T: Default>(engine: &Engine, fuel: u64) -> Store<T> {
1123 let mut store = Store::new(engine, T::default());
1124 store
1125 .set_fuel(fuel)
1126 .expect("set_fuel must succeed for fresh store");
1127 store.set_epoch_deadline(1);
1128 store
1129 }
1130
1131 #[test]
1132 fn baseline_engine_omits_fuel() {
1133 let opts = EngineOpts::baseline();
1134 assert!(!opts.fuel);
1135 let _engine = build_engine(opts).expect("baseline engine must build");
1136 }
1137
1138 #[test]
1139 fn with_fuel_engine_supports_set_fuel() {
1140 let engine = build_engine(EngineOpts::baseline().with_fuel()).unwrap();
1141 let mut store: Store<()> = Store::new(&engine, ());
1142 store
1143 .set_fuel(1_000)
1144 .expect("set_fuel works only when consume_fuel is on");
1145 }
1146
1147 #[test]
1148 fn module_cache_returns_same_module_for_same_bytecode() {
1149 let engine = build_engine(EngineOpts::baseline()).unwrap();
1150 let cache = ModuleCache::new();
1151 let wat = r#"(module (func (export "answer") (result i64) (i64.const 42)))"#;
1152 let bytes = wat::parse_str(wat).unwrap();
1153 assert_eq!(cache.len().unwrap(), 0);
1154 let _first = cache.get_or_compile(&engine, &bytes).unwrap();
1155 assert_eq!(cache.len().unwrap(), 1);
1156 let _second = cache.get_or_compile(&engine, &bytes).unwrap();
1157 assert_eq!(cache.len().unwrap(), 1);
1158 }
1159
1160 #[test]
1161 fn module_cache_clones_share_storage() {
1162 let engine = build_engine(EngineOpts::baseline()).unwrap();
1163 let cache_a = ModuleCache::new();
1164 let cache_b = cache_a.clone();
1165 let wat = r#"(module (func (export "answer") (result i64) (i64.const 42)))"#;
1166 let bytes = wat::parse_str(wat).unwrap();
1167 let _ = cache_a.get_or_compile(&engine, &bytes).unwrap();
1168 assert_eq!(cache_b.len().unwrap(), 1);
1169 }
1170
1171 #[test]
1172 fn runs_trivial_i64_export() {
1173 let engine = build_engine(EngineOpts::baseline().with_fuel()).unwrap();
1174 let module = compile_wat(
1175 &engine,
1176 r#"(module (func (export "answer") (result i64) (i64.const 42)))"#,
1177 )
1178 .unwrap();
1179 let mut store: Store<()> = store_with_fuel(&engine, 100_000);
1180 let result = call_i64_export(&engine, &mut store, &module, "answer").unwrap();
1181 assert_eq!(result, 42);
1182 }
1183
1184 #[test]
1185 fn missing_export_returns_typed_error() {
1186 let engine = build_engine(EngineOpts::baseline().with_fuel()).unwrap();
1187 let module = compile_wat(
1188 &engine,
1189 r#"(module (func (export "answer") (result i64) (i64.const 42)))"#,
1190 )
1191 .unwrap();
1192 let mut store: Store<()> = store_with_fuel(&engine, 100_000);
1193 let err = call_i64_export(&engine, &mut store, &module, "missing").unwrap_err();
1194 assert!(matches!(err, EngineError::MissingExport(name) if name == "missing"));
1195 }
1196
1197 #[test]
1198 fn fuel_exhaustion_yields_typed_error() {
1199 let engine = build_engine(EngineOpts::baseline().with_fuel()).unwrap();
1200 let module = compile_wat(
1201 &engine,
1202 r#"
1203 (module
1204 (func (export "spin") (result i64)
1205 (loop (br 0))
1206 (i64.const 0)))
1207 "#,
1208 )
1209 .unwrap();
1210 let mut store: Store<()> = store_with_fuel(&engine, 1_000);
1211 let err = call_i64_export(&engine, &mut store, &module, "spin").unwrap_err();
1212 assert!(matches!(err, EngineError::OutOfFuel), "got: {err:?}");
1213 }
1214
1215 #[test]
1216 fn epoch_interrupt_yields_typed_error() {
1217 let engine = build_engine(EngineOpts::baseline().with_fuel()).unwrap();
1218 let module = compile_wat(
1219 &engine,
1220 r#"
1221 (module
1222 (func (export "spin") (result i64)
1223 (loop (br 0))
1224 (i64.const 0)))
1225 "#,
1226 )
1227 .unwrap();
1228 let mut store: Store<()> = Store::new(&engine, ());
1229 store.set_fuel(1_000_000_000).unwrap();
1230 store.set_epoch_deadline(1);
1231 engine.increment_epoch();
1232 engine.increment_epoch();
1233 let err = call_i64_export(&engine, &mut store, &module, "spin").unwrap_err();
1234 assert!(
1235 matches!(err, EngineError::EpochInterrupt | EngineError::OutOfFuel),
1236 "got: {err:?}"
1237 );
1238 }
1239
1240 #[test]
1241 fn malformed_module_bytes_yield_compile_error() {
1242 let engine = build_engine(EngineOpts::baseline()).unwrap();
1243 let err = compile_module(&engine, b"not wasm bytes").unwrap_err();
1244 assert!(matches!(err, EngineError::Compile(_)));
1245 }
1246
1247 #[tokio::test(flavor = "current_thread")]
1248 async fn alloc_pair_chain_builds_list_head_in_order() {
1249 use wasmtime::I31;
1250
1251 let wat = r#"
1256 (module
1257 (rec
1258 (type $pair (struct (field anyref) (field (ref null $pair)))))
1259 (import "test" "make_chain"
1260 (func $make_chain (result (ref null struct))))
1261 (func $pair_new (export "pair_new")
1262 (param $car anyref) (param $cdr (ref null $pair))
1263 (result (ref null $pair))
1264 (struct.new $pair (local.get $car) (local.get $cdr)))
1265 (func $length (param $head (ref null $pair)) (result i32)
1266 (local $count i32)
1267 (block $exit
1268 (loop $more
1269 (br_if $exit (ref.is_null (local.get $head)))
1270 (local.set $count (i32.add (local.get $count) (i32.const 1)))
1271 (local.set $head
1272 (struct.get $pair 1 (local.get $head)))
1273 (br $more)))
1274 (local.get $count))
1275 (func (export "go") (result i32)
1276 (local $head (ref null $pair))
1277 (local.set $head
1278 (ref.cast (ref null $pair) (call $make_chain)))
1279 (call $length (local.get $head))))
1280 "#;
1281
1282 let engine = build_engine(EngineOpts::baseline()).unwrap();
1283 let module = compile_wat(&engine, wat).unwrap();
1284 let mut linker: Linker<()> = Linker::new(&engine);
1285 linker
1286 .func_wrap_async("test", "make_chain", |mut caller: Caller<'_, ()>, ()| {
1287 Box::new(async move {
1288 let items: Vec<Rooted<AnyRef>> = (0..3)
1289 .map(|i| AnyRef::from_i31(caller.as_context_mut(), I31::wrapping_u32(i)))
1290 .collect();
1291 alloc_pair_chain(&mut caller, items).await
1292 })
1293 })
1294 .unwrap();
1295 let mut store: Store<()> = Store::new(&engine, ());
1296 store.set_epoch_deadline(1_000);
1297 let instance = linker.instantiate_async(&mut store, &module).await.unwrap();
1298 let go = instance.get_func(&mut store, "go").unwrap();
1299 let mut results = [Val::I32(0)];
1300 go.call_async(&mut store, &[], &mut results).await.unwrap();
1301 assert_eq!(results[0].i32(), Some(3));
1302 }
1303
1304 #[tokio::test(flavor = "current_thread")]
1305 async fn alloc_pair_chain_errors_without_pair_new_export() {
1306 use wasmtime::Func;
1307
1308 let wat = r#"
1311 (module
1312 (import "test" "try_chain"
1313 (func $try))
1314 (func (export "go") (call $try)))
1315 "#;
1316 let engine = build_engine(EngineOpts::baseline()).unwrap();
1317 let module = compile_wat(&engine, wat).unwrap();
1318 let mut linker: Linker<()> = Linker::new(&engine);
1319 linker
1320 .func_wrap_async("test", "try_chain", |mut caller: Caller<'_, ()>, ()| {
1321 Box::new(async move {
1322 let empty: Vec<Rooted<AnyRef>> = Vec::new();
1323 let result = alloc_pair_chain(&mut caller, empty).await;
1324 match result {
1325 Err(e) => {
1326 let msg = e.to_string();
1327 assert!(
1328 msg.contains("pair_new"),
1329 "expected pair_new-missing error, got: {msg}"
1330 );
1331 Ok(())
1332 }
1333 Ok(_) => Err(wasmtime::Error::msg(
1334 "alloc_pair_chain unexpectedly succeeded without pair_new",
1335 )),
1336 }
1337 })
1338 })
1339 .unwrap();
1340 let mut store: Store<()> = Store::new(&engine, ());
1341 store.set_epoch_deadline(1_000);
1342 let instance = linker.instantiate_async(&mut store, &module).await.unwrap();
1343 let go: Func = instance.get_func(&mut store, "go").unwrap();
1344 let mut results: [Val; 0] = [];
1345 go.call_async(&mut store, &[], &mut results).await.unwrap();
1346 }
1347
1348 #[tokio::test(flavor = "current_thread")]
1349 async fn alloc_commodity_ref_builds_atomic_via_reentry() {
1350 let wat = r#"
1357 (module
1358 (type $unit_term (array (mut i64)))
1359 (type $commodity
1360 (struct (field i64) (field i64) (field i64) (field i64)
1361 (field (ref null $unit_term))))
1362 (import "test" "make_commodity"
1363 (func $make_commodity (result (ref null struct))))
1364 (func $commodity_new (export "commodity_new")
1365 (param $n i64) (param $d i64) (param $hi i64) (param $lo i64)
1366 (result (ref $commodity))
1367 (struct.new $commodity
1368 (local.get $n) (local.get $d) (local.get $hi) (local.get $lo)
1369 (ref.null $unit_term)))
1370 (func (export "go") (result i64 i64 i64 i32)
1371 (local $c (ref $commodity))
1372 (local.set $c
1373 (ref.cast (ref $commodity) (call $make_commodity)))
1374 (struct.get $commodity 0 (local.get $c))
1375 (struct.get $commodity 2 (local.get $c))
1376 (struct.get $commodity 3 (local.get $c))
1377 (ref.is_null (struct.get $commodity 4 (local.get $c)))))
1378 "#;
1379
1380 let engine = build_engine(EngineOpts::baseline()).unwrap();
1381 let module = compile_wat(&engine, wat).unwrap();
1382 let mut linker: Linker<()> = Linker::new(&engine);
1383 linker
1384 .func_wrap_async(
1385 "test",
1386 "make_commodity",
1387 |mut caller: Caller<'_, ()>, ()| {
1388 Box::new(async move {
1389 let id = Uuid::from_u128((1u128 << 64) | 2u128);
1390 Ok(Some(alloc_commodity_ref(&mut caller, 7, 2, id).await?))
1391 })
1392 },
1393 )
1394 .unwrap();
1395 let mut store: Store<()> = Store::new(&engine, ());
1396 store.set_epoch_deadline(1_000);
1397 let instance = linker.instantiate_async(&mut store, &module).await.unwrap();
1398 let go = instance.get_func(&mut store, "go").unwrap();
1399 let mut results = [Val::I64(0), Val::I64(0), Val::I64(0), Val::I32(0)];
1400 go.call_async(&mut store, &[], &mut results).await.unwrap();
1401 assert_eq!(results[0].i64(), Some(7), "numer");
1402 assert_eq!(results[1].i64(), Some(1), "commodity_hi");
1403 assert_eq!(results[2].i64(), Some(2), "commodity_lo");
1404 assert_eq!(results[3].i32(), Some(1), "atomic ⇒ null unit-term");
1405 }
1406
1407 #[tokio::test(flavor = "current_thread")]
1408 async fn alloc_commodity_ref_errors_without_commodity_new_export() {
1409 use wasmtime::Func;
1410
1411 let wat = r#"
1414 (module
1415 (import "test" "try_make"
1416 (func $try))
1417 (func (export "go") (call $try)))
1418 "#;
1419 let engine = build_engine(EngineOpts::baseline()).unwrap();
1420 let module = compile_wat(&engine, wat).unwrap();
1421 let mut linker: Linker<()> = Linker::new(&engine);
1422 linker
1423 .func_wrap_async("test", "try_make", |mut caller: Caller<'_, ()>, ()| {
1424 Box::new(async move {
1425 let id = Uuid::from_u128(0);
1426 match alloc_commodity_ref(&mut caller, 1, 1, id).await {
1427 Err(e) => {
1428 let msg = e.to_string();
1429 assert!(
1430 msg.contains("commodity_new"),
1431 "expected commodity_new-missing error, got: {msg}"
1432 );
1433 Ok(())
1434 }
1435 Ok(_) => Err(wasmtime::Error::msg(
1436 "alloc_commodity_ref unexpectedly succeeded without commodity_new",
1437 )),
1438 }
1439 })
1440 })
1441 .unwrap();
1442 let mut store: Store<()> = Store::new(&engine, ());
1443 store.set_epoch_deadline(1_000);
1444 let instance = linker.instantiate_async(&mut store, &module).await.unwrap();
1445 let go: Func = instance.get_func(&mut store, "go").unwrap();
1446 let mut results: [Val; 0] = [];
1447 go.call_async(&mut store, &[], &mut results).await.unwrap();
1448 }
1449
1450 #[test]
1451 fn unit_term_algebra_merges_sorts_and_cancels() {
1452 use nomiscript::{Compiler, Reader, SymbolTable};
1453
1454 fn ar(a: Rooted<AnyRef>) -> Val {
1455 Val::AnyRef(Some(a))
1456 }
1457
1458 let engine = build_engine(EngineOpts::baseline().with_fuel()).unwrap();
1462 let mut compiler = Compiler::new();
1463 let mut symbols = SymbolTable::with_builtins();
1464 let program = Reader::parse("0").unwrap();
1465 let (bytes, _) = compiler
1466 .compile_eval_with_type(&program, &mut symbols)
1467 .expect("eval compile");
1468 let module = compile_module(&engine, &bytes).expect("module");
1469 let mut linker: Linker<()> = Linker::new(&engine);
1470 link_nomi_raise_stub(&mut linker, &engine);
1471 let mut store: Store<()> = Store::new(&engine, ());
1472 store.set_fuel(100_000_000).unwrap();
1473 store.set_epoch_deadline(1);
1474 let instance = linker.instantiate(&mut store, &module).unwrap();
1475
1476 let call_ref = |store: &mut Store<()>, name: &str, args: &[Val]| -> Rooted<AnyRef> {
1477 let f = instance.get_func(&mut *store, name).unwrap();
1478 let mut res = [Val::AnyRef(None)];
1479 f.call(&mut *store, args, &mut res).unwrap();
1480 match &res[0] {
1481 Val::AnyRef(Some(a)) => *a,
1482 other => panic!("{name} returned {other:?}"),
1483 }
1484 };
1485 let read_term = |store: &mut Store<()>, t: Rooted<AnyRef>| -> Vec<i64> {
1486 let arr = t.unwrap_array(&mut *store).unwrap();
1487 let len = arr.len(&mut *store).unwrap();
1488 (0..len)
1489 .map(|i| arr.get(&mut *store, i).unwrap().i64().unwrap())
1490 .collect()
1491 };
1492 let singleton = |store: &mut Store<()>, hi: i64, lo: i64| -> Rooted<AnyRef> {
1493 call_ref(store, "unit_singleton", &[Val::I64(hi), Val::I64(lo)])
1494 };
1495
1496 let usd = singleton(&mut store, 10, 20);
1497 let eur = singleton(&mut store, 30, 40);
1498 assert_eq!(read_term(&mut store, usd), vec![10, 20, 1]);
1499 assert_eq!(read_term(&mut store, eur), vec![30, 40, 1]);
1500
1501 let usd_eur = call_ref(&mut store, "unit_mul", &[ar(usd), ar(eur)]);
1503 assert_eq!(read_term(&mut store, usd_eur), vec![10, 20, 1, 30, 40, 1]);
1504 let eur_usd = call_ref(&mut store, "unit_mul", &[ar(eur), ar(usd)]);
1505 assert_eq!(read_term(&mut store, eur_usd), vec![10, 20, 1, 30, 40, 1]);
1506
1507 let usd2 = call_ref(&mut store, "unit_mul", &[ar(usd), ar(usd)]);
1509 assert_eq!(read_term(&mut store, usd2), vec![10, 20, 2]);
1510
1511 let canceled = call_ref(&mut store, "unit_div", &[ar(usd), ar(usd)]);
1513 assert_eq!(read_term(&mut store, canceled), Vec::<i64>::new());
1514
1515 let neg_usd = call_ref(&mut store, "unit_negate", &[ar(usd)]);
1517 assert_eq!(read_term(&mut store, neg_usd), vec![10, 20, -1]);
1518
1519 let eq = |store: &mut Store<()>, a: Rooted<AnyRef>, b: Rooted<AnyRef>| -> i32 {
1520 let f = instance.get_func(&mut *store, "unit_eq").unwrap();
1521 let mut res = [Val::I32(0)];
1522 f.call(&mut *store, &[ar(a), ar(b)], &mut res).unwrap();
1523 res[0].i32().unwrap()
1524 };
1525 assert_eq!(eq(&mut store, usd, usd), 1);
1526 assert_eq!(eq(&mut store, usd, eur), 0);
1527 assert_eq!(eq(&mut store, usd_eur, eur_usd), 1);
1529 }
1530
1531 #[tokio::test(flavor = "current_thread")]
1532 async fn compound_money_arithmetic_end_to_end() {
1533 use nomiscript::{Compiler, HostFnSpec, Reader, SymbolTable, WasmType};
1534
1535 const USD: u128 = 0x1111_1111_1111_1111_2222_2222_2222_2222;
1537 const EUR: u128 = 0x3333_3333_3333_3333_4444_4444_4444_4444;
1538
1539 async fn run(src: &str) -> Result<EvalValue, String> {
1540 let specs = vec![
1541 HostFnSpec::new("usd", "test", "usd").returns(WasmType::Commodity),
1542 HostFnSpec::new("eur", "test", "eur").returns(WasmType::Commodity),
1543 HostFnSpec::new("sink", "test", "sink")
1544 .with_params(vec![WasmType::Commodity])
1545 .returns(WasmType::I32),
1546 ];
1547 let program = Reader::parse(src).unwrap();
1548 let mut compiler = Compiler::with_host_fns(specs.clone());
1549 let mut symbols = SymbolTable::with_builtins();
1550 symbols.register_host_fns(&specs);
1551 let (bytes, result_ty) = compiler
1552 .compile_eval_with_type(&program, &mut symbols)
1553 .map_err(|e| e.to_string())?;
1554 let engine = build_engine(EngineOpts::baseline().with_fuel()).unwrap();
1555 let module = compile_module(&engine, &bytes).map_err(|e| format!("{e:?}"))?;
1556 let mut linker: Linker<()> = Linker::new(&engine);
1557 link_nomi_raise_stub(&mut linker, &engine);
1558 linker
1559 .func_wrap_async("test", "usd", |mut caller: Caller<'_, ()>, ()| {
1560 Box::new(async move {
1561 Ok(Some(
1562 alloc_commodity_ref(&mut caller, 3, 1, Uuid::from_u128(USD)).await?,
1563 ))
1564 })
1565 })
1566 .unwrap();
1567 linker
1568 .func_wrap_async("test", "eur", |mut caller: Caller<'_, ()>, ()| {
1569 Box::new(async move {
1570 Ok(Some(
1571 alloc_commodity_ref(&mut caller, 3, 1, Uuid::from_u128(EUR)).await?,
1572 ))
1573 })
1574 })
1575 .unwrap();
1576 linker
1579 .func_wrap_async(
1580 "test",
1581 "sink",
1582 |mut caller: Caller<'_, ()>, (arg,): (Option<Rooted<StructRef>>,)| {
1583 Box::new(async move {
1584 read_commodity_arg(&mut caller, arg)?;
1585 Ok(0i32)
1586 })
1587 },
1588 )
1589 .unwrap();
1590 let mut store: Store<()> = Store::new(&engine, ());
1591 store.set_fuel(1_000_000_000).unwrap();
1592 store.set_epoch_deadline(1);
1593 let instance = linker
1594 .instantiate_async(&mut store, &module)
1595 .await
1596 .map_err(|e| format!("{e:?}"))?;
1597 let func = instance.get_func(&mut store, "nomi-eval").unwrap();
1598 let mut results = [Val::AnyRef(None)];
1599 func.call_async(&mut store, &[], &mut results)
1600 .await
1601 .map_err(|e| e.to_string())?;
1602 let any = match &results[0] {
1603 Val::AnyRef(a) => *a,
1604 _ => return Err("nomi-eval returned non-anyref".to_string()),
1605 };
1606 decode_eval_result(&mut store, any, result_ty).map_err(|e| e.to_string())
1607 }
1608
1609 assert_eq!(
1611 run("(/ (usd) (usd))").await,
1612 Ok(EvalValue::Ratio { numer: 1, denom: 1 })
1613 );
1614 match run("(+ (usd) (usd))").await {
1616 Ok(EvalValue::Commodity { numer, denom, .. }) => assert_eq!((numer, denom), (6, 1)),
1617 other => panic!("expected atomic commodity 6/1, got {other:?}"),
1618 }
1619 assert!(run("(+ (usd) (eur))").await.is_err());
1621 assert!(
1623 run("(* (usd) (usd))")
1624 .await
1625 .unwrap_err()
1626 .contains("compound")
1627 );
1628 assert_eq!(run("(sink (usd))").await, Ok(EvalValue::I32(0)));
1630 assert!(run("(sink (* (usd) (usd)))").await.is_err());
1632 assert!(
1636 run("(list (* (usd) (usd)))")
1637 .await
1638 .unwrap_err()
1639 .contains("compound")
1640 );
1641 assert!(run("(list (usd))").await.is_ok());
1644 match run("(* (/ (usd) (usd)) (usd))").await {
1648 Ok(EvalValue::Commodity { numer, denom, .. }) => assert_eq!((numer, denom), (3, 1)),
1649 other => panic!("expected atomic commodity 3/1, got {other:?}"),
1650 }
1651 assert_eq!(
1652 run("(sink (* (/ (usd) (usd)) (usd)))").await,
1653 Ok(EvalValue::I32(0))
1654 );
1655 }
1656
1657 fn link_nomi_raise_stub(linker: &mut Linker<()>, engine: &wasmtime::Engine) {
1663 linker
1664 .func_new(
1665 "nomi",
1666 "__nomi_raise",
1667 wasmtime::FuncType::new(
1668 engine,
1669 [
1670 wasmtime::ValType::Ref(wasmtime::RefType::ARRAYREF),
1671 wasmtime::ValType::Ref(wasmtime::RefType::ARRAYREF),
1672 ],
1673 [],
1674 ),
1675 |_, _, _| {
1676 Err(wasmtime::Error::msg(
1677 "__nomi_raise stub: not linked in this test",
1678 ))
1679 },
1680 )
1681 .unwrap();
1682 link_log_stub(linker, engine);
1683 link_nomi_catch_each_stub(linker, engine);
1684 }
1685
1686 fn link_log_stub(linker: &mut Linker<()>, engine: &wasmtime::Engine) {
1693 linker
1694 .func_new(
1695 "env",
1696 "log",
1697 wasmtime::FuncType::new(
1698 engine,
1699 [
1700 wasmtime::ValType::I32,
1701 wasmtime::ValType::I32,
1702 wasmtime::ValType::I32,
1703 ],
1704 [],
1705 ),
1706 |_, _, _| Ok(()),
1707 )
1708 .unwrap();
1709 }
1710
1711 fn link_nomi_catch_each_stub(linker: &mut Linker<()>, engine: &wasmtime::Engine) {
1719 let abstract_struct =
1720 wasmtime::ValType::Ref(wasmtime::RefType::new(true, wasmtime::HeapType::Struct));
1721 let funcref = wasmtime::ValType::Ref(wasmtime::RefType::FUNCREF);
1722 let anyref = wasmtime::ValType::Ref(wasmtime::RefType::ANYREF);
1723 linker
1724 .func_new(
1725 "nomi",
1726 "__nomi_catch_each",
1727 wasmtime::FuncType::new(
1728 engine,
1729 [funcref, anyref, abstract_struct.clone()],
1730 [abstract_struct],
1731 ),
1732 |_, _, _| {
1733 Err(wasmtime::Error::msg(
1734 "__nomi_catch_each stub: not linked in this test",
1735 ))
1736 },
1737 )
1738 .unwrap();
1739 }
1740
1741 fn run_nomiscript_eval(program: &nomiscript::Program) -> Option<EvalValue> {
1748 use nomiscript::{Compiler, SymbolTable};
1749 let engine = build_engine(EngineOpts::baseline().with_fuel()).unwrap();
1750 let mut compiler = Compiler::new();
1751 let mut symbols = SymbolTable::with_builtins();
1752 let (bytes, result_ty) = compiler
1753 .compile_eval_with_type(program, &mut symbols)
1754 .expect("eval compile");
1755 let module = compile_module(&engine, &bytes).expect("module");
1756 let mut linker: Linker<()> = Linker::new(&engine);
1757 link_nomi_raise_stub(&mut linker, &engine);
1758 let mut store: Store<()> = Store::new(&engine, ());
1759 store.set_fuel(10_000_000).unwrap();
1760 store.set_epoch_deadline(1);
1761 let instance = linker.instantiate(&mut store, &module).unwrap();
1762 let func = instance.get_func(&mut store, "nomi-eval").unwrap();
1763 let mut results = [Val::AnyRef(None)];
1764 func.call(&mut store, &[], &mut results).unwrap();
1765 let any = match &results[0] {
1766 Val::AnyRef(a) => *a,
1767 _ => panic!("nomi-eval returned non-anyref"),
1768 };
1769 Some(decode_eval_result(&mut store, any, result_ty).expect("decode"))
1770 }
1771
1772 #[test]
1773 fn nomiscript_eval_captures_integer_literal() {
1774 use nomiscript::{Expr, Fraction, Program};
1775 let program = Program::new(vec![Expr::Number(Fraction::from_integer(7))]);
1778 assert_eq!(run_nomiscript_eval(&program), Some(EvalValue::I32(7)));
1779 }
1780
1781 #[test]
1782 fn nomiscript_eval_captures_arithmetic_result() {
1783 use nomiscript::{Expr, Fraction, Program};
1784 let program = Program::new(vec![Expr::List(vec![
1787 Expr::Symbol("+".into()),
1788 Expr::Number(Fraction::from_integer(1)),
1789 Expr::Number(Fraction::from_integer(2)),
1790 ])]);
1791 assert_eq!(run_nomiscript_eval(&program), Some(EvalValue::I32(3)));
1792 }
1793
1794 #[test]
1795 fn nomiscript_eval_captures_fractional_result() {
1796 use nomiscript::{Expr, Fraction, Program};
1797 let program = Program::new(vec![Expr::List(vec![
1800 Expr::Symbol("/".into()),
1801 Expr::Number(Fraction::new(1, 2)),
1802 Expr::Number(Fraction::from_integer(2)),
1803 ])]);
1804 assert_eq!(
1805 run_nomiscript_eval(&program),
1806 Some(EvalValue::Ratio { numer: 1, denom: 4 })
1807 );
1808 }
1809
1810 #[test]
1811 fn nomiscript_eval_captures_nil_for_empty_program() {
1812 let program = nomiscript::Program::default();
1813 assert_eq!(run_nomiscript_eval(&program), Some(EvalValue::Nil));
1814 }
1815
1816 #[test]
1817 fn nomiscript_eval_decodes_bool_as_bool() {
1818 use nomiscript::{Expr, Program};
1819 let program = Program::new(vec![Expr::Bool(true)]);
1820 assert_eq!(run_nomiscript_eval(&program), Some(EvalValue::Bool(true)));
1824 }
1825
1826 #[test]
1833 fn nomiscript_eval_type_hint_matches_value_variant() {
1834 use nomiscript::{Compiler, Program, Reader, SymbolTable, WasmType};
1835 let cases: &[(&str, Option<WasmType>)] = &[
1836 ("42", Some(WasmType::I32)),
1839 ("(+ 1 2)", Some(WasmType::I32)),
1840 ("(/ 1 4)", Some(WasmType::I32)),
1841 ("(/ 1/2 2)", Some(WasmType::Ratio)),
1842 ("(= 1 1)", Some(WasmType::Bool)),
1843 ("(< 1 2)", Some(WasmType::Bool)),
1844 ("#t", Some(WasmType::Bool)),
1845 ("\"hello\"", Some(WasmType::StringRef)),
1846 ("(let ((x 1)) (+ x 1))", Some(WasmType::I32)),
1847 ("(let ((x 1)) \"tail\")", Some(WasmType::StringRef)),
1848 ("(if (= 1 1) 2 3)", Some(WasmType::I32)),
1849 ];
1850 for (src, expected_ty) in cases {
1851 let program: Program = Reader::parse(src).expect("parse");
1852 let engine = build_engine(EngineOpts::baseline().with_fuel()).unwrap();
1853 let mut compiler = Compiler::new();
1854 let mut symbols = SymbolTable::with_builtins();
1855 let (bytes, result_ty) = compiler
1856 .compile_eval_with_type(&program, &mut symbols)
1857 .unwrap_or_else(|e| panic!("compile {src:?}: {e}"));
1858 assert_eq!(
1859 &result_ty, expected_ty,
1860 "compile_eval_with_type reported wrong static type for {src:?}",
1861 );
1862 let module = compile_module(&engine, &bytes).expect("module");
1863 let mut linker: Linker<()> = Linker::new(&engine);
1864 link_nomi_raise_stub(&mut linker, &engine);
1865 let mut store: Store<()> = Store::new(&engine, ());
1866 store.set_fuel(10_000_000).unwrap();
1867 store.set_epoch_deadline(1);
1868 let instance = linker.instantiate(&mut store, &module).unwrap();
1869 let func = instance.get_func(&mut store, "nomi-eval").unwrap();
1870 let mut results = [Val::AnyRef(None)];
1871 func.call(&mut store, &[], &mut results)
1872 .unwrap_or_else(|e| panic!("run {src:?}: {e}"));
1873 let any = match &results[0] {
1874 Val::AnyRef(a) => *a,
1875 _ => panic!("nomi-eval returned non-anyref for {src:?}"),
1876 };
1877 let decoded = decode_eval_result(&mut store, any, result_ty)
1878 .unwrap_or_else(|e| panic!("decode {src:?}: {e}"));
1879 let ok = matches!(
1882 (&result_ty, &decoded),
1883 (None, EvalValue::Nil)
1884 | (Some(WasmType::I32), EvalValue::I32(_))
1885 | (Some(WasmType::Bool), EvalValue::Bool(_) | EvalValue::Nil)
1887 | (Some(WasmType::Ratio), EvalValue::Ratio { .. })
1888 | (Some(WasmType::Commodity), EvalValue::Commodity { .. })
1889 | (
1890 Some(WasmType::StringRef),
1891 EvalValue::String(_) | EvalValue::Bytes(_)
1892 ),
1893 );
1894 assert!(
1895 ok,
1896 "type/value drift for {src:?}: hint={result_ty:?}, decoded={decoded:?}",
1897 );
1898 }
1899 }
1900
1901 #[tokio::test(flavor = "current_thread")]
1902 async fn render_entity_emits_named_field_plist() {
1903 let wat = r#"
1908 (module
1909 (type $i8 (array (mut i8)))
1910 (type $commodity (struct
1911 (field (ref null $i8))
1912 (field (ref null $i8))
1913 (field (ref null $i8))))
1914 (data $id "uuid-123")
1915 (data $sym "USD")
1916 (data $name "US Dollar")
1917 (func (export "go") (result (ref null struct))
1918 (struct.new $commodity
1919 (array.new_data $i8 $id (i32.const 0) (i32.const 8))
1920 (array.new_data $i8 $sym (i32.const 0) (i32.const 3))
1921 (array.new_data $i8 $name (i32.const 0) (i32.const 9)))))
1922 "#;
1923 let engine = build_engine(EngineOpts::baseline()).unwrap();
1924 let module = compile_wat(&engine, wat).unwrap();
1925 let linker: Linker<()> = Linker::new(&engine);
1926 let mut store: Store<()> = Store::new(&engine, ());
1927 store.set_epoch_deadline(1_000);
1928 let instance = linker.instantiate_async(&mut store, &module).await.unwrap();
1929 let go = instance.get_func(&mut store, "go").unwrap();
1930 let mut results = [Val::AnyRef(None)];
1931 go.call_async(&mut store, &[], &mut results).await.unwrap();
1932 let entity = match results[0] {
1933 Val::AnyRef(Some(a)) => a.unwrap_struct(&mut store).unwrap(),
1934 other => panic!("go did not return a struct: {other:?}"),
1935 };
1936 let rendered =
1937 render_entity(&mut store, entity, nomiscript::EntityKind::Commodity).unwrap();
1938 assert_eq!(
1939 rendered,
1940 "(:commodity :id \"uuid-123\" :symbol \"USD\" :name \"US Dollar\")"
1941 );
1942 }
1943
1944 #[tokio::test(flavor = "current_thread")]
1945 async fn read_entity_string_field_reads_named_slot() {
1946 let wat = r#"
1950 (module
1951 (type $i8 (array (mut i8)))
1952 (type $commodity (struct
1953 (field (ref null $i8))
1954 (field (ref null $i8))
1955 (field (ref null $i8))))
1956 (data $id "uuid-123")
1957 (data $sym "USD")
1958 (data $name "US Dollar")
1959 (func (export "go") (result (ref null struct))
1960 (struct.new $commodity
1961 (array.new_data $i8 $id (i32.const 0) (i32.const 8))
1962 (array.new_data $i8 $sym (i32.const 0) (i32.const 3))
1963 (array.new_data $i8 $name (i32.const 0) (i32.const 9)))))
1964 "#;
1965 let engine = build_engine(EngineOpts::baseline()).unwrap();
1966 let module = compile_wat(&engine, wat).unwrap();
1967 let linker: Linker<()> = Linker::new(&engine);
1968 let mut store: Store<()> = Store::new(&engine, ());
1969 store.set_epoch_deadline(1_000);
1970 let instance = linker.instantiate_async(&mut store, &module).await.unwrap();
1971 let go = instance.get_func(&mut store, "go").unwrap();
1972 let mut results = [Val::AnyRef(None)];
1973 go.call_async(&mut store, &[], &mut results).await.unwrap();
1974 let entity = match results[0] {
1975 Val::AnyRef(Some(a)) => a.unwrap_struct(&mut store).unwrap(),
1976 other => panic!("go did not return a struct: {other:?}"),
1977 };
1978
1979 let id = read_entity_string_field_ctx(
1980 &mut store,
1981 entity,
1982 nomiscript::EntityKind::Commodity,
1983 "id",
1984 )
1985 .unwrap();
1986 assert_eq!(id, "uuid-123");
1987
1988 let name = read_entity_string_field_ctx(
1989 &mut store,
1990 entity,
1991 nomiscript::EntityKind::Commodity,
1992 "name",
1993 )
1994 .unwrap();
1995 assert_eq!(name, "US Dollar");
1996
1997 let bad = read_entity_string_field_ctx(
1999 &mut store,
2000 entity,
2001 nomiscript::EntityKind::Commodity,
2002 "nonexistent",
2003 );
2004 assert!(bad.is_err());
2005 }
2006}