1use std::collections::HashMap;
11use std::sync::{Arc, Mutex};
12
13use thiserror::Error;
14use uuid::Uuid;
15
16use crate::checked_ref::CheckedAnyRef;
17use wasmtime::{
18 AnyRef, AsContextMut, Caller, Config, Engine, FieldType, Linker, Module, Mutability, Rooted,
19 StorageType, Store, StructRef, StructRefPre, StructType, Val, ValType,
20};
21
22#[derive(Debug, Error)]
23pub enum EngineError {
24 #[error("engine config rejected: {0}")]
25 Config(String),
26 #[error("module cache lock poisoned")]
27 CachePoisoned,
28 #[error("module compilation failed: {0}")]
29 Compile(String),
30 #[error("module instantiation failed: {0}")]
31 Instantiate(String),
32 #[error("fuel configuration failed: {0}")]
33 Fuel(String),
34 #[error("missing export `{0}`")]
35 MissingExport(String),
36 #[error("fuel exhausted before completion")]
37 OutOfFuel,
38 #[error("epoch deadline reached before completion")]
39 EpochInterrupt,
40 #[error("no conversion: {0}")]
45 NoConversion(String),
46 #[error("script raised {code}: {message}")]
57 ScriptRaised { code: String, message: String },
58 #[error("execution trapped: {0}")]
59 Trap(String),
60}
61
62pub const NOMI_RAISE_MARKER: &str = "__nomi_raise:";
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
75pub enum ProfilerStrategy {
76 #[default]
77 None,
78 JitDump,
79 PerfMap,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub struct EngineOpts {
84 pub fuel: bool,
85 pub profiler: ProfilerStrategy,
86}
87
88impl EngineOpts {
89 #[must_use]
90 pub const fn baseline() -> Self {
91 Self {
92 fuel: false,
93 profiler: ProfilerStrategy::None,
94 }
95 }
96
97 #[must_use]
98 pub const fn with_fuel(mut self) -> Self {
99 self.fuel = true;
100 self
101 }
102
103 #[must_use]
104 pub const fn with_profiler(mut self, strategy: ProfilerStrategy) -> Self {
105 self.profiler = strategy;
106 self
107 }
108}
109
110impl Default for EngineOpts {
111 fn default() -> Self {
112 Self::baseline()
113 }
114}
115
116pub fn build_engine(opts: EngineOpts) -> Result<Engine, EngineError> {
117 let mut config = Config::new();
118 config.wasm_gc(true);
119 config.wasm_function_references(true);
120 config.wasm_exceptions(true);
126 config.epoch_interruption(true);
127 if opts.fuel {
128 config.consume_fuel(true);
129 }
130 match opts.profiler {
131 ProfilerStrategy::None => {}
132 ProfilerStrategy::JitDump => {
133 config.profiler(wasmtime::ProfilingStrategy::JitDump);
134 }
135 ProfilerStrategy::PerfMap => {
136 config.profiler(wasmtime::ProfilingStrategy::PerfMap);
137 }
138 }
139 Engine::new(&config).map_err(|e| EngineError::Config(e.to_string()))
140}
141
142pub fn compile_module(engine: &Engine, bytes: &[u8]) -> Result<Module, EngineError> {
143 Module::new(engine, bytes).map_err(|e| EngineError::Compile(e.to_string()))
144}
145
146pub fn compile_wat(engine: &Engine, source: &str) -> Result<Module, EngineError> {
147 Module::new(engine, source).map_err(|e| EngineError::Compile(e.to_string()))
148}
149
150#[derive(Debug, Default, Clone)]
155pub struct ModuleCache {
156 inner: Arc<Mutex<HashMap<Vec<u8>, Module>>>,
157}
158
159impl ModuleCache {
160 #[must_use]
161 pub fn new() -> Self {
162 Self::default()
163 }
164
165 pub fn get_or_compile(&self, engine: &Engine, bytecode: &[u8]) -> Result<Module, EngineError> {
166 if let Some(module) = self.lookup(bytecode)? {
167 return Ok(module);
168 }
169 let module = compile_module(engine, bytecode)?;
170 self.store(bytecode, module.clone())?;
171 Ok(module)
172 }
173
174 fn lookup(&self, bytecode: &[u8]) -> Result<Option<Module>, EngineError> {
175 let guard = self.inner.lock().map_err(|_| EngineError::CachePoisoned)?;
176 Ok(guard.get(bytecode).cloned())
177 }
178
179 fn store(&self, bytecode: &[u8], module: Module) -> Result<(), EngineError> {
180 let mut guard = self.inner.lock().map_err(|_| EngineError::CachePoisoned)?;
181 guard.insert(bytecode.to_vec(), module);
182 Ok(())
183 }
184
185 pub fn is_empty(&self) -> Result<bool, EngineError> {
186 let guard = self.inner.lock().map_err(|_| EngineError::CachePoisoned)?;
187 Ok(guard.is_empty())
188 }
189
190 pub fn len(&self) -> Result<usize, EngineError> {
191 let guard = self.inner.lock().map_err(|_| EngineError::CachePoisoned)?;
192 Ok(guard.len())
193 }
194}
195
196pub fn classify_runtime_error(err: &wasmtime::Error) -> EngineError {
200 if let Some(trap) = err.downcast_ref::<wasmtime::Trap>() {
201 match *trap {
202 wasmtime::Trap::OutOfFuel => return EngineError::OutOfFuel,
203 wasmtime::Trap::Interrupt => return EngineError::EpochInterrupt,
204 _ => {}
205 }
206 }
207 let mut combined = err.to_string();
213 for cause in err.chain().skip(1) {
214 combined.push_str(": ");
215 combined.push_str(&cause.to_string());
216 }
217 if let Some(raised) = parse_nomi_raise_marker(err) {
226 return raised;
227 }
228 if combined.contains("convert-commodity: no Price row")
229 || combined.contains("convert-commodity: inverse price has zero numerator")
230 {
231 return EngineError::NoConversion(combined);
232 }
233 EngineError::Trap(combined)
234}
235
236fn parse_nomi_raise_marker(err: &wasmtime::Error) -> Option<EngineError> {
242 err.chain()
243 .map(|cause| cause.to_string())
244 .find_map(|cause_str| split_marker(&cause_str))
245}
246
247fn split_marker(text: &str) -> Option<EngineError> {
248 let rest = text.strip_prefix(NOMI_RAISE_MARKER)?;
249 let (code, message) = rest.split_once(':')?;
250 Some(EngineError::ScriptRaised {
251 code: code.to_string(),
252 message: message.to_string(),
253 })
254}
255
256#[must_use]
268pub fn err_code_and_message(err: &EngineError) -> (String, String) {
269 match err {
270 EngineError::ScriptRaised { code, message } => (code.clone(), message.clone()),
271 EngineError::NoConversion(msg) => ("no-conversion".to_string(), msg.clone()),
272 EngineError::Trap(msg) => ("runtime".to_string(), msg.clone()),
273 EngineError::Compile(msg) => ("compile".to_string(), msg.clone()),
274 EngineError::Instantiate(msg) => ("runtime".to_string(), msg.clone()),
275 EngineError::Fuel(msg) => ("runtime".to_string(), msg.clone()),
276 EngineError::MissingExport(msg) => ("runtime".to_string(), msg.clone()),
277 EngineError::Config(msg) => ("runtime".to_string(), msg.clone()),
278 EngineError::CachePoisoned => (
279 "runtime".to_string(),
280 "module cache lock poisoned".to_string(),
281 ),
282 EngineError::OutOfFuel => ("runtime".to_string(), "fuel exhausted".to_string()),
283 EngineError::EpochInterrupt => ("runtime".to_string(), "epoch deadline".to_string()),
284 }
285}
286
287pub async fn alloc_commodity_ref<T>(
295 caller: &mut Caller<'_, T>,
296 numer: i64,
297 denom: i64,
298 commodity_id: Uuid,
299) -> wasmtime::Result<Rooted<StructRef>>
300where
301 T: Send,
302{
303 let commodity_new = caller
304 .get_export("commodity_new")
305 .and_then(|e| e.into_func())
306 .ok_or_else(|| {
307 wasmtime::Error::msg(
308 "module missing 'commodity_new' export — host commodity allocation \
309 requires the nomiscript compiler skeleton's exported commodity_new",
310 )
311 })?;
312 let (hi, lo) = commodity_id.as_u64_pair();
313 let mut results = [Val::AnyRef(None)];
314 commodity_new
315 .call_async(
316 caller.as_context_mut(),
317 &[
318 Val::I64(numer),
319 Val::I64(denom),
320 Val::I64(hi as i64),
321 Val::I64(lo as i64),
322 ],
323 &mut results,
324 )
325 .await?;
326 match &results[0] {
327 Val::AnyRef(Some(any)) => any.checked_struct(caller.as_context_mut()),
328 Val::AnyRef(None) => Err(wasmtime::Error::msg("commodity_new returned null")),
329 _ => Err(wasmtime::Error::msg(
330 "commodity_new returned non-anyref Val variant",
331 )),
332 }
333}
334
335pub fn alloc_string_ref<T>(
342 caller: &mut Caller<'_, T>,
343 bytes: &[u8],
344) -> wasmtime::Result<Rooted<wasmtime::ArrayRef>> {
345 let engine = caller.engine().clone();
346 let ty = wasmtime::ArrayType::new(&engine, FieldType::new(Mutability::Var, StorageType::I8));
351 let pre = wasmtime::ArrayRefPre::new(caller.as_context_mut(), ty);
352 let vals: Vec<Val> = bytes.iter().map(|b| Val::I32(i32::from(*b))).collect();
353 wasmtime::ArrayRef::new_fixed(caller.as_context_mut(), &pre, &vals)
354}
355
356pub fn alloc_ratio_ref<T>(
361 caller: &mut Caller<'_, T>,
362 numer: i64,
363 denom: i64,
364) -> wasmtime::Result<Rooted<StructRef>> {
365 let engine = caller.engine().clone();
366 let ty = StructType::new(
367 &engine,
368 std::iter::repeat_n(
369 FieldType::new(Mutability::Const, StorageType::ValType(ValType::I64)),
370 2,
371 ),
372 )?;
373 let pre = StructRefPre::new(caller.as_context_mut(), ty);
374 StructRef::new(
375 caller.as_context_mut(),
376 &pre,
377 &[Val::I64(numer), Val::I64(denom)],
378 )
379}
380
381pub async fn alloc_entity_via_export<T>(
395 caller: &mut Caller<'_, T>,
396 export_name: &str,
397 args: &[Val],
398) -> wasmtime::Result<Rooted<StructRef>>
399where
400 T: Send,
401{
402 let alloc = caller
403 .get_export(export_name)
404 .and_then(|e| e.into_func())
405 .ok_or_else(|| {
406 wasmtime::Error::msg(format!(
407 "module missing '{export_name}' export — host entity allocation requires \
408 the nomiscript compiler skeleton's exported alloc_<kind> function"
409 ))
410 })?;
411 let mut results = [Val::AnyRef(None)];
412 alloc
413 .call_async(caller.as_context_mut(), args, &mut results)
414 .await?;
415 let new_entity_any = match &results[0] {
416 Val::AnyRef(any) => *any,
417 _ => {
418 return Err(wasmtime::Error::msg(format!(
419 "{export_name} returned non-anyref Val variant"
420 )));
421 }
422 };
423 new_entity_any
424 .ok_or_else(|| {
425 wasmtime::Error::msg(format!(
426 "{export_name} returned null when allocating entity"
427 ))
428 })?
429 .checked_struct(caller.as_context_mut())
430}
431
432pub fn read_string_arg<T>(
439 caller: &mut Caller<'_, T>,
440 arg: Option<Rooted<wasmtime::ArrayRef>>,
441) -> wasmtime::Result<Option<String>> {
442 let Some(arr) = arg else {
443 return Ok(None);
444 };
445 let len = arr.len(caller.as_context_mut())?;
446 let mut bytes = Vec::with_capacity(len as usize);
447 for i in 0..len {
448 let val = arr.get(caller.as_context_mut(), i)?;
449 let byte_i32 = val
450 .i32()
451 .ok_or_else(|| wasmtime::Error::msg("string arg element is not i32"))?;
452 bytes.push(byte_i32 as u8);
453 }
454 String::from_utf8(bytes)
455 .map(Some)
456 .map_err(|err| wasmtime::Error::msg(format!("string arg is not valid UTF-8: {err}")))
457}
458
459pub fn read_commodity_arg<T>(
464 caller: &mut Caller<'_, T>,
465 arg: Option<Rooted<StructRef>>,
466) -> wasmtime::Result<Option<(i64, i64, Uuid)>> {
467 let Some(s) = arg else {
468 return Ok(None);
469 };
470 let read_i64 = |c: &mut Caller<'_, T>, idx: usize| -> wasmtime::Result<i64> {
471 let v = s.field(c.as_context_mut(), idx)?;
472 v.i64()
473 .ok_or_else(|| wasmtime::Error::msg(format!("commodity field {idx} is not i64")))
474 };
475 let numer = read_i64(caller, 0)?;
476 let denom = read_i64(caller, 1)?;
477 let hi = read_i64(caller, 2)?;
478 let lo = read_i64(caller, 3)?;
479 let raw = ((hi as u64 as u128) << 64) | (lo as u64 as u128);
480 Ok(Some((numer, denom, Uuid::from_u128(raw))))
481}
482
483pub fn read_ratio_arg<T>(
488 caller: &mut Caller<'_, T>,
489 arg: Option<Rooted<StructRef>>,
490) -> wasmtime::Result<Option<(i64, i64)>> {
491 let Some(s) = arg else {
492 return Ok(None);
493 };
494 let numer = s
495 .field(caller.as_context_mut(), 0)?
496 .i64()
497 .ok_or_else(|| wasmtime::Error::msg("ratio field 0 (numer) is not i64"))?;
498 let denom = s
499 .field(caller.as_context_mut(), 1)?
500 .i64()
501 .ok_or_else(|| wasmtime::Error::msg("ratio field 1 (denom) is not i64"))?;
502 if denom == 0 {
503 return Err(wasmtime::Error::msg("ratio has zero denominator"));
504 }
505 Ok(Some((numer, denom)))
506}
507
508pub fn read_entity_string_field<T>(
515 caller: &mut Caller<'_, T>,
516 arg: Option<Rooted<StructRef>>,
517 kind: nomiscript::EntityKind,
518 field_name: &str,
519) -> wasmtime::Result<Option<String>> {
520 let Some(s) = arg else {
521 return Ok(None);
522 };
523 read_entity_string_field_ctx(caller.as_context_mut(), s, kind, field_name).map(Some)
524}
525
526pub fn read_entity_string_field_ctx(
530 mut store: impl AsContextMut,
531 entity: Rooted<StructRef>,
532 kind: nomiscript::EntityKind,
533 field_name: &str,
534) -> wasmtime::Result<String> {
535 let layout = nomiscript::entity_layout(kind)
536 .ok_or_else(|| wasmtime::Error::msg(format!("no entity layout for {kind:?}")))?;
537 let idx = layout
538 .fields
539 .iter()
540 .position(|f| f.name == field_name && f.kind == nomiscript::EntityFieldKind::String)
541 .ok_or_else(|| {
542 wasmtime::Error::msg(format!(
543 "entity {kind:?} has no String field named '{field_name}'"
544 ))
545 })?;
546 let arr = match entity.field(store.as_context_mut(), idx)? {
547 Val::AnyRef(Some(any)) => any.checked_array(store.as_context_mut())?,
548 _ => {
549 return Err(wasmtime::Error::msg(format!(
550 "entity {kind:?} field '{field_name}' (slot {idx}) is not an i8_array"
551 )));
552 }
553 };
554 let len = arr.len(store.as_context_mut())?;
555 let mut bytes = Vec::with_capacity(len as usize);
556 for i in 0..len {
557 let byte = arr
558 .get(store.as_context_mut(), i)?
559 .i32()
560 .ok_or_else(|| wasmtime::Error::msg("entity string element is not i32"))?;
561 bytes.push(byte as u8);
562 }
563 String::from_utf8(bytes)
564 .map_err(|err| wasmtime::Error::msg(format!("entity string field not utf-8: {err}")))
565}
566
567pub async fn alloc_pair_chain<T>(
585 caller: &mut Caller<'_, T>,
586 items: impl IntoIterator<Item = Rooted<AnyRef>>,
587) -> wasmtime::Result<Option<Rooted<StructRef>>>
588where
589 T: Send,
590{
591 let pair_new = caller
592 .get_export("pair_new")
593 .and_then(|e| e.into_func())
594 .ok_or_else(|| {
595 wasmtime::Error::msg(
596 "module missing 'pair_new' export — host pair allocation requires \
597 the nomiscript compiler skeleton's exported pair_new",
598 )
599 })?;
600
601 let items: Vec<Rooted<AnyRef>> = items.into_iter().collect();
602 let mut head: Option<Rooted<StructRef>> = None;
603 for item in items.into_iter().rev() {
604 let cdr_any = head.map(|p| p.to_anyref());
605 let mut results = [Val::AnyRef(None)];
606 pair_new
607 .call_async(
608 caller.as_context_mut(),
609 &[Val::AnyRef(Some(item)), Val::AnyRef(cdr_any)],
610 &mut results,
611 )
612 .await?;
613 let new_pair_any = match &results[0] {
614 Val::AnyRef(any) => *any,
615 _ => {
616 return Err(wasmtime::Error::msg(
617 "pair_new returned non-anyref Val variant",
618 ));
619 }
620 };
621 head = Some(
622 new_pair_any
623 .ok_or_else(|| {
624 wasmtime::Error::msg("pair_new returned null when chaining elements")
625 })?
626 .checked_struct(caller.as_context_mut())?,
627 );
628 }
629 Ok(head)
630}
631
632pub fn call_i64_export<T>(
636 engine: &Engine,
637 store: &mut Store<T>,
638 module: &Module,
639 export: &str,
640) -> Result<i64, EngineError> {
641 let linker = Linker::<T>::new(engine);
642 let instance = linker
643 .instantiate(&mut *store, module)
644 .map_err(|e| classify_runtime_error(&e))?;
645 let func = instance
646 .get_typed_func::<(), i64>(&mut *store, export)
647 .map_err(|_| EngineError::MissingExport(export.to_string()))?;
648 func.call(&mut *store, ())
649 .map_err(|e| classify_runtime_error(&e))
650}
651
652#[derive(Debug, Clone, PartialEq)]
659pub enum EvalValue {
660 Nil,
661 T,
662 I32(i32),
663 Ratio {
664 numer: i64,
665 denom: i64,
666 },
667 Commodity {
672 numer: i64,
673 denom: i64,
674 commodity_hi: i64,
675 commodity_lo: i64,
676 },
677 String(String),
678 Bytes(Vec<u8>),
679}
680
681impl From<EvalValue> for nomiscript::Value {
682 fn from(value: EvalValue) -> Self {
683 match value {
684 EvalValue::Nil => nomiscript::Value::Nil,
685 EvalValue::T => nomiscript::Value::T,
686 EvalValue::I32(n) => {
687 nomiscript::Value::Number(nomiscript::Fraction::from_integer(i64::from(n)))
688 }
689 EvalValue::Ratio { numer, denom } => {
690 nomiscript::Value::Number(nomiscript::Fraction::new(numer, denom))
691 }
692 EvalValue::Commodity {
693 numer,
694 denom,
695 commodity_hi,
696 commodity_lo,
697 } => {
698 let raw = ((commodity_hi as u64 as u128) << 64) | (commodity_lo as u64 as u128);
702 nomiscript::Value::Commodity {
703 amount: nomiscript::Fraction::new(numer, denom),
704 commodity_id: uuid::Uuid::from_u128(raw),
705 }
706 }
707 EvalValue::String(s) => nomiscript::Value::String(s),
708 EvalValue::Bytes(b) => nomiscript::Value::Bytes(b),
709 }
710 }
711}
712
713pub fn decode_eval_result(
724 mut store: impl AsContextMut,
725 value: Option<Rooted<AnyRef>>,
726 result_ty: Option<nomiscript::WasmType>,
727) -> wasmtime::Result<EvalValue> {
728 let Some(ty) = result_ty else {
729 return Ok(EvalValue::Nil);
730 };
731 let Some(any) = value else {
737 return match ty {
738 nomiscript::WasmType::I32 => Err(wasmtime::Error::msg(
739 "nomi-eval returned null for declared result type i32",
740 )),
741 _ => Ok(EvalValue::Nil),
742 };
743 };
744 decode_anyref(&mut store, any, ty)
745}
746
747fn decode_index(mut store: impl AsContextMut, any: &Rooted<AnyRef>) -> wasmtime::Result<EvalValue> {
750 let numer = any
751 .checked_struct(&mut store)?
752 .field(&mut store, 0)?
753 .i64()
754 .ok_or_else(|| wasmtime::Error::msg("index result field 0 (numer) is not i64"))?;
755 i32::try_from(numer)
756 .map(EvalValue::I32)
757 .map_err(|_| wasmtime::Error::msg(format!("index result {numer} exceeds i32")))
758}
759
760fn decode_anyref(
761 mut store: impl AsContextMut,
762 any: Rooted<AnyRef>,
763 ty: nomiscript::WasmType,
764) -> wasmtime::Result<EvalValue> {
765 use nomiscript::WasmType;
766 match ty {
767 WasmType::I32 => decode_index(&mut store, &any),
768 WasmType::Bool => {
769 let i31 = any.checked_i31(&mut store)?;
772 if i31.get_i32() == 0 {
773 Ok(EvalValue::Nil)
774 } else {
775 Ok(EvalValue::T)
776 }
777 }
778 WasmType::Ratio => {
779 let s = any.checked_struct(&mut store)?;
780 let numer = s
781 .field(&mut store, 0)?
782 .i64()
783 .ok_or_else(|| wasmtime::Error::msg("ratio field 0 (numer) is not i64"))?;
784 let denom = s
785 .field(&mut store, 1)?
786 .i64()
787 .ok_or_else(|| wasmtime::Error::msg("ratio field 1 (denom) is not i64"))?;
788 Ok(EvalValue::Ratio { numer, denom })
789 }
790 WasmType::Commodity => {
791 let s = any.checked_struct(&mut store)?;
792 let numer = s.field(&mut store, 0)?.i64().unwrap_or(0);
793 let denom = s.field(&mut store, 1)?.i64().unwrap_or(1);
794 match s.field(&mut store, 4)? {
799 Val::AnyRef(None) => {
800 let hi = s.field(&mut store, 2)?.i64().unwrap_or(0);
801 let lo = s.field(&mut store, 3)?.i64().unwrap_or(0);
802 Ok(EvalValue::Commodity {
803 numer,
804 denom,
805 commodity_hi: hi,
806 commodity_lo: lo,
807 })
808 }
809 Val::AnyRef(Some(term)) => {
810 let arr = term.checked_array(&mut store)?;
811 if arr.len(&mut store)? == 0 {
812 Ok(EvalValue::Ratio { numer, denom })
813 } else {
814 Err(wasmtime::Error::msg(
815 "compound commodity (e.g. money × money) has no host \
816 representation yet",
817 ))
818 }
819 }
820 _ => Err(wasmtime::Error::msg(
821 "commodity field 4 (unit term) is not a ref",
822 )),
823 }
824 }
825 WasmType::StringRef => {
826 let arr = any.checked_array(&mut store)?;
827 let len = arr.len(&mut store)?;
828 let mut bytes = Vec::with_capacity(len as usize);
829 for i in 0..len {
830 let v = arr.get(&mut store, i)?;
831 let byte = v
832 .i32()
833 .ok_or_else(|| wasmtime::Error::msg("string element is not i32"))?;
834 bytes.push(byte as u8);
835 }
836 let s = String::from_utf8(bytes)
837 .map_err(|err| wasmtime::Error::msg(format!("not valid utf-8: {err}")))?;
838 Ok(EvalValue::String(s))
839 }
840 WasmType::PairRef(elem) => {
841 let head = render_pair_as_string(&mut store, any, elem)?;
842 Ok(EvalValue::String(head))
843 }
844 WasmType::EntityRef(kind) => {
845 let entity = any.checked_struct(&mut store)?;
846 Ok(EvalValue::String(render_entity(&mut store, entity, kind)?))
847 }
848 WasmType::Closure(_) => {
849 let _ = any;
850 Ok(EvalValue::String("<closure>".into()))
851 }
852 WasmType::AnyRef => {
853 let _ = any;
859 Ok(EvalValue::String("<anyref>".into()))
860 }
861 }
862}
863
864fn render_pair_as_string(
868 mut store: impl AsContextMut,
869 head_any: Rooted<AnyRef>,
870 elem: nomiscript::PairElement,
871) -> wasmtime::Result<String> {
872 let mut out = String::from("(");
873 let mut cur: Option<Rooted<StructRef>> = Some(head_any.checked_struct(&mut store)?);
874 let mut first = true;
875 while let Some(node) = cur {
876 if !first {
877 out.push(' ');
878 }
879 first = false;
880 let car_val = node.field(&mut store, 0)?;
881 let car_any = match car_val {
882 Val::AnyRef(Some(a)) => a,
883 Val::AnyRef(None) => {
884 out.push_str("nil");
885 let cdr_val = node.field(&mut store, 1)?;
886 cur = match cdr_val {
887 Val::AnyRef(Some(a)) => Some(a.checked_struct(&mut store)?),
888 _ => None,
889 };
890 continue;
891 }
892 _ => {
893 return Err(wasmtime::Error::msg("pair car is not anyref"));
894 }
895 };
896 let car_str = render_car(&mut store, car_any, elem)?;
897 out.push_str(&car_str);
898 let cdr_val = node.field(&mut store, 1)?;
899 cur = match cdr_val {
900 Val::AnyRef(Some(a)) => Some(a.checked_struct(&mut store)?),
901 _ => None,
902 };
903 }
904 out.push(')');
905 Ok(out)
906}
907
908fn render_car(
909 mut store: impl AsContextMut,
910 car_any: Rooted<AnyRef>,
911 elem: nomiscript::PairElement,
912) -> wasmtime::Result<String> {
913 use nomiscript::PairElement;
914 match elem {
915 PairElement::I32 => {
916 let i31 = car_any.checked_i31(&mut store)?;
917 Ok(i31.get_i32().to_string())
918 }
919 PairElement::Bool => {
920 let i31 = car_any.checked_i31(&mut store)?;
923 Ok(if i31.get_i32() == 0 { "nil" } else { "t" }.to_string())
924 }
925 PairElement::Ratio => {
926 let s = car_any.checked_struct(&mut store)?;
927 let n = s.field(&mut store, 0)?.i64().unwrap_or(0);
928 let d = s.field(&mut store, 1)?.i64().unwrap_or(1);
929 if d == 1 {
930 Ok(n.to_string())
931 } else {
932 Ok(format!("{n}/{d}"))
933 }
934 }
935 PairElement::Commodity => {
936 let s = car_any.checked_struct(&mut store)?;
937 let n = s.field(&mut store, 0)?.i64().unwrap_or(0);
938 let d = s.field(&mut store, 1)?.i64().unwrap_or(1);
939 match s.field(&mut store, 4)? {
945 Val::AnyRef(None) => {
946 let hi = s.field(&mut store, 2)?.i64().unwrap_or(0);
947 let lo = s.field(&mut store, 3)?.i64().unwrap_or(0);
948 let raw = ((hi as u64 as u128) << 64) | (lo as u64 as u128);
949 let id = Uuid::from_u128(raw);
950 if d == 1 {
951 Ok(format!("(:commodity {n} :id \"{id}\")"))
952 } else {
953 Ok(format!("(:commodity {n}/{d} :id \"{id}\")"))
954 }
955 }
956 Val::AnyRef(Some(term)) => {
957 let arr = term.checked_array(&mut store)?;
958 if arr.len(&mut store)? == 0 {
959 Ok(if d == 1 {
960 n.to_string()
961 } else {
962 format!("{n}/{d}")
963 })
964 } else {
965 Err(wasmtime::Error::msg(
966 "compound commodity (e.g. money × money) has no host \
967 representation yet",
968 ))
969 }
970 }
971 _ => Err(wasmtime::Error::msg(
972 "commodity field 4 (unit term) is not a ref",
973 )),
974 }
975 }
976 PairElement::StringRef => {
977 let arr = car_any.checked_array(&mut store)?;
978 let len = arr.len(&mut store)?;
979 let mut bytes = Vec::with_capacity(len as usize);
980 for i in 0..len {
981 let v = arr.get(&mut store, i)?;
982 bytes.push(v.i32().unwrap_or(0) as u8);
983 }
984 let s = String::from_utf8(bytes).unwrap_or_else(|_| "<invalid-utf8>".into());
985 Ok(nomiscript::format_value(&nomiscript::Value::String(s)))
986 }
987 PairElement::Entity(kind) => {
988 let entity = car_any.checked_struct(&mut store)?;
989 render_entity(&mut store, entity, kind)
990 }
991 PairElement::AnyRef => Ok("<anyref>".into()),
992 }
993}
994
995fn render_entity(
1002 mut store: impl AsContextMut,
1003 entity: Rooted<StructRef>,
1004 kind: nomiscript::EntityKind,
1005) -> wasmtime::Result<String> {
1006 use nomiscript::EntityFieldKind;
1007
1008 let Some(layout) = nomiscript::entity_layout(kind) else {
1009 return Ok(format!("(:{kind:?})"));
1011 };
1012 let mut out = format!("(:{}", layout.label);
1013 for (slot, field) in layout.fields.iter().enumerate() {
1014 let rendered = match field.kind {
1015 EntityFieldKind::String => read_string_slot(&mut store, entity, slot)?,
1016 EntityFieldKind::Ratio => read_ratio_slot(&mut store, entity, slot)?,
1017 EntityFieldKind::I32 => entity
1018 .field(&mut store, slot)?
1019 .i32()
1020 .unwrap_or(0)
1021 .to_string(),
1022 EntityFieldKind::Pair => "(...)".to_string(),
1023 };
1024 out.push_str(&format!(" :{} {rendered}", field.name));
1025 }
1026 out.push(')');
1027 Ok(out)
1028}
1029
1030fn read_string_slot(
1033 mut store: impl AsContextMut,
1034 entity: Rooted<StructRef>,
1035 slot: usize,
1036) -> wasmtime::Result<String> {
1037 match entity.field(&mut store, slot)? {
1038 Val::AnyRef(Some(a)) => {
1039 let arr = a.checked_array(&mut store)?;
1040 let len = arr.len(&mut store)?;
1041 let mut bytes = Vec::with_capacity(len as usize);
1042 for i in 0..len {
1043 bytes.push(arr.get(&mut store, i)?.i32().unwrap_or(0) as u8);
1044 }
1045 let s = String::from_utf8(bytes).unwrap_or_else(|_| "<invalid-utf8>".into());
1046 Ok(nomiscript::format_value(&nomiscript::Value::String(s)))
1047 }
1048 _ => Ok("\"\"".to_string()),
1049 }
1050}
1051
1052fn read_ratio_slot(
1055 mut store: impl AsContextMut,
1056 entity: Rooted<StructRef>,
1057 slot: usize,
1058) -> wasmtime::Result<String> {
1059 match entity.field(&mut store, slot)? {
1060 Val::AnyRef(Some(a)) => {
1061 let s = a.checked_struct(&mut store)?;
1062 let n = s.field(&mut store, 0)?.i64().unwrap_or(0);
1063 let d = s.field(&mut store, 1)?.i64().unwrap_or(1);
1064 Ok(if d == 1 {
1065 n.to_string()
1066 } else {
1067 format!("{n}/{d}")
1068 })
1069 }
1070 _ => Ok("0".to_string()),
1071 }
1072}
1073
1074#[cfg(test)]
1075mod tests;