Lines
97.21 %
Functions
89.19 %
Branches
100 %
use super::*;
mod bool_decode_tests;
#[test]
fn err_code_uses_script_raised_symbol_verbatim() {
let (code, msg) = err_code_and_message(&EngineError::ScriptRaised {
code: "no-such-account".to_string(),
message: "id=42".to_string(),
});
assert_eq!(code, "no-such-account");
assert_eq!(msg, "id=42");
}
fn err_code_maps_commodity_mismatch_script_raise_to_symbol() {
// Commodity mismatch now `throw`s `$nomi_error` in-guest (ADR-0026);
// uncaught, the boundary wrapper bridges it to `__nomi_raise` and the
// classifier yields `ScriptRaised`. The code is the reader-folded
// (upper-cased) symbol `COMMODITY-MISMATCH`, like any script raise —
// `err_code_and_message` passes a `ScriptRaised` code through verbatim.
code: "COMMODITY-MISMATCH".to_string(),
message: "USD vs EUR".to_string(),
assert_eq!(code, "COMMODITY-MISMATCH");
assert_eq!(msg, "USD vs EUR");
fn err_code_maps_no_conversion_to_kebab_symbol() {
let (code, msg) = err_code_and_message(&EngineError::NoConversion("missing price".to_string()));
assert_eq!(code, "no-conversion");
assert_eq!(msg, "missing price");
fn err_code_falls_back_to_runtime_for_generic_traps() {
let (code, msg) = err_code_and_message(&EngineError::Trap("oops".to_string()));
assert_eq!(code, "runtime");
assert_eq!(msg, "oops");
fn err_code_maps_out_of_fuel_to_runtime_with_diagnostic_message() {
let (code, msg) = err_code_and_message(&EngineError::OutOfFuel);
assert_eq!(msg, "fuel exhausted");
fn store_with_fuel<T: Default>(engine: &Engine, fuel: u64) -> Store<T> {
let mut store = Store::new(engine, T::default());
store
.set_fuel(fuel)
.expect("set_fuel must succeed for fresh store");
store.set_epoch_deadline(1);
fn baseline_engine_omits_fuel() {
let opts = EngineOpts::baseline();
assert!(!opts.fuel);
let _engine = build_engine(opts).expect("baseline engine must build");
fn with_fuel_engine_supports_set_fuel() {
let engine = build_engine(EngineOpts::baseline().with_fuel()).unwrap();
let mut store: Store<()> = Store::new(&engine, ());
.set_fuel(1_000)
.expect("set_fuel works only when consume_fuel is on");
fn module_cache_returns_same_module_for_same_bytecode() {
let engine = build_engine(EngineOpts::baseline()).unwrap();
let cache = ModuleCache::new();
let wat = r#"(module (func (export "answer") (result i64) (i64.const 42)))"#;
let bytes = wat::parse_str(wat).unwrap();
assert_eq!(cache.len().unwrap(), 0);
let _first = cache.get_or_compile(&engine, &bytes).unwrap();
assert_eq!(cache.len().unwrap(), 1);
let _second = cache.get_or_compile(&engine, &bytes).unwrap();
fn module_cache_clones_share_storage() {
let cache_a = ModuleCache::new();
let cache_b = cache_a.clone();
let _ = cache_a.get_or_compile(&engine, &bytes).unwrap();
assert_eq!(cache_b.len().unwrap(), 1);
fn runs_trivial_i64_export() {
let module = compile_wat(
&engine,
r#"(module (func (export "answer") (result i64) (i64.const 42)))"#,
)
.unwrap();
let mut store: Store<()> = store_with_fuel(&engine, 100_000);
let result = call_i64_export(&engine, &mut store, &module, "answer").unwrap();
assert_eq!(result, 42);
fn missing_export_returns_typed_error() {
let err = call_i64_export(&engine, &mut store, &module, "missing").unwrap_err();
assert!(matches!(err, EngineError::MissingExport(name) if name == "missing"));
fn fuel_exhaustion_yields_typed_error() {
r#"
(module
(func (export "spin") (result i64)
(loop (br 0))
(i64.const 0)))
"#,
let mut store: Store<()> = store_with_fuel(&engine, 1_000);
let err = call_i64_export(&engine, &mut store, &module, "spin").unwrap_err();
assert!(matches!(err, EngineError::OutOfFuel), "got: {err:?}");
fn epoch_interrupt_yields_typed_error() {
store.set_fuel(1_000_000_000).unwrap();
engine.increment_epoch();
assert!(
matches!(err, EngineError::EpochInterrupt | EngineError::OutOfFuel),
"got: {err:?}"
);
fn malformed_module_bytes_yield_compile_error() {
let err = compile_module(&engine, b"not wasm bytes").unwrap_err();
assert!(matches!(err, EngineError::Compile(_)));
#[tokio::test(flavor = "current_thread")]
async fn alloc_pair_chain_builds_list_head_in_order() {
use wasmtime::I31;
// Self-recursive $pair shape matching `CompileContext::new_skeleton`.
// The module exports `pair_new` (the helper alloc_pair_chain re-enters
// for each element) and a `go` entry that asks the test host fn for a
// 3-element chain, then walks it to confirm the host-built structure.
let wat = r#"
(rec
(type $pair (struct (field anyref) (field (ref null $pair)))))
(import "test" "make_chain"
(func $make_chain (result (ref null struct))))
(func $pair_new (export "pair_new")
(param $car anyref) (param $cdr (ref null $pair))
(result (ref null $pair))
(struct.new $pair (local.get $car) (local.get $cdr)))
(func $length (param $head (ref null $pair)) (result i32)
(local $count i32)
(block $exit
(loop $more
(br_if $exit (ref.is_null (local.get $head)))
(local.set $count (i32.add (local.get $count) (i32.const 1)))
(local.set $head
(struct.get $pair 1 (local.get $head)))
(br $more)))
(local.get $count))
(func (export "go") (result i32)
(local $head (ref null $pair))
(ref.cast (ref null $pair) (call $make_chain)))
(call $length (local.get $head))))
"#;
let module = compile_wat(&engine, wat).unwrap();
let mut linker: Linker<()> = Linker::new(&engine);
linker
.func_wrap_async("test", "make_chain", |mut caller: Caller<'_, ()>, ()| {
Box::new(async move {
let items: Vec<Rooted<AnyRef>> = (0..3)
.map(|i| AnyRef::from_i31(caller.as_context_mut(), I31::wrapping_u32(i)))
.collect();
alloc_pair_chain(&mut caller, items).await
})
store.set_epoch_deadline(1_000);
let instance = linker.instantiate_async(&mut store, &module).await.unwrap();
let go = instance.get_func(&mut store, "go").unwrap();
let mut results = [Val::I32(0)];
go.call_async(&mut store, &[], &mut results).await.unwrap();
assert_eq!(results[0].i32(), Some(3));
async fn alloc_pair_chain_errors_without_pair_new_export() {
use wasmtime::Func;
// No `pair_new` export — the host fn must surface the missing-export
// contract violation rather than panic or silently succeed.
(import "test" "try_chain"
(func $try))
(func (export "go") (call $try)))
.func_wrap_async("test", "try_chain", |mut caller: Caller<'_, ()>, ()| {
let empty: Vec<Rooted<AnyRef>> = Vec::new();
let result = alloc_pair_chain(&mut caller, empty).await;
match result {
Err(e) => {
let msg = e.to_string();
msg.contains("pair_new"),
"expected pair_new-missing error, got: {msg}"
Ok(())
Ok(_) => Err(wasmtime::Error::msg(
"alloc_pair_chain unexpectedly succeeded without pair_new",
)),
let go: Func = instance.get_func(&mut store, "go").unwrap();
let mut results: [Val; 0] = [];
async fn alloc_commodity_ref_builds_atomic_via_reentry() {
// ADR-0028 E0: the host builds a commodity by re-entering the module's
// exported `commodity_new`, which writes a NULL unit-term (= atomic).
// The `$commodity` shape matches `CompileContext::new_skeleton` (5
// fields, the 5th a `(ref null $unit_term)`). `go` asks the host for a
// 7/2 commodity with UUID hi=1/lo=2, then reads numer, hi, lo, and
// whether the term is null.
(type $unit_term (array (mut i64)))
(type $commodity
(struct (field i64) (field i64) (field i64) (field i64)
(field (ref null $unit_term))))
(import "test" "make_commodity"
(func $make_commodity (result (ref null struct))))
(func $commodity_new (export "commodity_new")
(param $n i64) (param $d i64) (param $hi i64) (param $lo i64)
(result (ref $commodity))
(struct.new $commodity
(local.get $n) (local.get $d) (local.get $hi) (local.get $lo)
(ref.null $unit_term)))
(func (export "go") (result i64 i64 i64 i32)
(local $c (ref $commodity))
(local.set $c
(ref.cast (ref $commodity) (call $make_commodity)))
(struct.get $commodity 0 (local.get $c))
(struct.get $commodity 2 (local.get $c))
(struct.get $commodity 3 (local.get $c))
(ref.is_null (struct.get $commodity 4 (local.get $c)))))
.func_wrap_async(
"test",
"make_commodity",
|mut caller: Caller<'_, ()>, ()| {
let id = Uuid::from_u128((1u128 << 64) | 2u128);
Ok(Some(alloc_commodity_ref(&mut caller, 7, 2, id).await?))
},
let mut results = [Val::I64(0), Val::I64(0), Val::I64(0), Val::I32(0)];
assert_eq!(results[0].i64(), Some(7), "numer");
assert_eq!(results[1].i64(), Some(1), "commodity_hi");
assert_eq!(results[2].i64(), Some(2), "commodity_lo");
assert_eq!(results[3].i32(), Some(1), "atomic ⇒ null unit-term");
async fn alloc_commodity_ref_errors_without_commodity_new_export() {
// No `commodity_new` export — the host fn must surface the missing-
// export contract violation rather than panic or silently succeed.
(import "test" "try_make"
.func_wrap_async("test", "try_make", |mut caller: Caller<'_, ()>, ()| {
let id = Uuid::from_u128(0);
match alloc_commodity_ref(&mut caller, 1, 1, id).await {
msg.contains("commodity_new"),
"expected commodity_new-missing error, got: {msg}"
"alloc_commodity_ref unexpectedly succeeded without commodity_new",
fn unit_term_algebra_merges_sorts_and_cancels() {
use nomiscript::{Compiler, Reader, SymbolTable};
fn ar(a: Rooted<AnyRef>) -> Val {
Val::AnyRef(Some(a))
// Compile a trivial program just to obtain the skeleton, which exports
// the unit-term helpers. Then exercise the sorted-merge directly: it is
// the riskiest hand-written wasm in ADR-0028 E1/E2.
let mut compiler = Compiler::new();
let mut symbols = SymbolTable::with_builtins();
let program = Reader::parse("0").unwrap();
let (bytes, _) = compiler
.compile_eval_with_type(&program, &mut symbols)
.expect("eval compile");
let module = compile_module(&engine, &bytes).expect("module");
link_nomi_raise_stub(&mut linker, &engine);
store.set_fuel(100_000_000).unwrap();
let instance = linker.instantiate(&mut store, &module).unwrap();
let call_ref = |store: &mut Store<()>, name: &str, args: &[Val]| -> Rooted<AnyRef> {
let f = instance.get_func(&mut *store, name).unwrap();
let mut res = [Val::AnyRef(None)];
f.call(&mut *store, args, &mut res).unwrap();
match &res[0] {
Val::AnyRef(Some(a)) => *a,
other => panic!("{name} returned {other:?}"),
};
let read_term = |store: &mut Store<()>, t: Rooted<AnyRef>| -> Vec<i64> {
let arr = t.unwrap_array(&mut *store).unwrap();
let len = arr.len(&mut *store).unwrap();
(0..len)
.map(|i| arr.get(&mut *store, i).unwrap().i64().unwrap())
.collect()
let singleton = |store: &mut Store<()>, hi: i64, lo: i64| -> Rooted<AnyRef> {
call_ref(store, "unit_singleton", &[Val::I64(hi), Val::I64(lo)])
let usd = singleton(&mut store, 10, 20);
let eur = singleton(&mut store, 30, 40);
assert_eq!(read_term(&mut store, usd), vec![10, 20, 1]);
assert_eq!(read_term(&mut store, eur), vec![30, 40, 1]);
// Disjoint merge is sorted by (hi,lo), order-independent.
let usd_eur = call_ref(&mut store, "unit_mul", &[ar(usd), ar(eur)]);
assert_eq!(read_term(&mut store, usd_eur), vec![10, 20, 1, 30, 40, 1]);
let eur_usd = call_ref(&mut store, "unit_mul", &[ar(eur), ar(usd)]);
assert_eq!(read_term(&mut store, eur_usd), vec![10, 20, 1, 30, 40, 1]);
// Matching key sums exponents.
let usd2 = call_ref(&mut store, "unit_mul", &[ar(usd), ar(usd)]);
assert_eq!(read_term(&mut store, usd2), vec![10, 20, 2]);
// Cancellation drops the zero-exponent entry → empty (dimensionless).
let canceled = call_ref(&mut store, "unit_div", &[ar(usd), ar(usd)]);
assert_eq!(read_term(&mut store, canceled), Vec::<i64>::new());
// negate flips exponents.
let neg_usd = call_ref(&mut store, "unit_negate", &[ar(usd)]);
assert_eq!(read_term(&mut store, neg_usd), vec![10, 20, -1]);
let eq = |store: &mut Store<()>, a: Rooted<AnyRef>, b: Rooted<AnyRef>| -> i32 {
let f = instance.get_func(&mut *store, "unit_eq").unwrap();
let mut res = [Val::I32(0)];
f.call(&mut *store, &[ar(a), ar(b)], &mut res).unwrap();
res[0].i32().unwrap()
assert_eq!(eq(&mut store, usd, usd), 1);
assert_eq!(eq(&mut store, usd, eur), 0);
// Same multiset, built two ways, compares equal.
assert_eq!(eq(&mut store, usd_eur, eur_usd), 1);
async fn compound_money_arithmetic_end_to_end() {
use nomiscript::{Compiler, HostFnSpec, Reader, SymbolTable, WasmType};
// Two distinct currencies, each produced atomic at 3/1 by a host fn.
const USD: u128 = 0x1111_1111_1111_1111_2222_2222_2222_2222;
const EUR: u128 = 0x3333_3333_3333_3333_4444_4444_4444_4444;
async fn run(src: &str) -> Result<EvalValue, String> {
let specs = vec![
HostFnSpec::new("usd", "test", "usd").returns(WasmType::Commodity),
HostFnSpec::new("eur", "test", "eur").returns(WasmType::Commodity),
HostFnSpec::new("sink", "test", "sink")
.with_params(vec![WasmType::Commodity])
.returns(WasmType::I32),
];
let program = Reader::parse(src).unwrap();
let mut compiler = Compiler::with_host_fns(specs.clone());
symbols.register_host_fns(&specs);
let (bytes, result_ty) = compiler
.map_err(|e| e.to_string())?;
let module = compile_module(&engine, &bytes).map_err(|e| format!("{e:?}"))?;
.func_wrap_async("test", "usd", |mut caller: Caller<'_, ()>, ()| {
Ok(Some(
alloc_commodity_ref(&mut caller, 3, 1, Uuid::from_u128(USD)).await?,
))
.func_wrap_async("test", "eur", |mut caller: Caller<'_, ()>, ()| {
alloc_commodity_ref(&mut caller, 3, 1, Uuid::from_u128(EUR)).await?,
// The sink only reaches its body for ATOMIC args — a compound arg is
// rejected by the `commodity_assert_atomic` guard before this runs.
"sink",
|mut caller: Caller<'_, ()>, (arg,): (Option<Rooted<StructRef>>,)| {
read_commodity_arg(&mut caller, arg)?;
Ok(0i32)
let instance = linker
.instantiate_async(&mut store, &module)
.await
.map_err(|e| format!("{e:?}"))?;
let func = instance.get_func(&mut store, "nomi-eval").unwrap();
let mut results = [Val::AnyRef(None)];
func.call_async(&mut store, &[], &mut results)
let any = match &results[0] {
Val::AnyRef(a) => *a,
_ => return Err("nomi-eval returned non-anyref".to_string()),
decode_eval_result(&mut store, any, result_ty).map_err(|e| e.to_string())
// money ÷ money, same currency → dimensionless → decodes as a Number.
assert_eq!(
run("(/ (usd) (usd))").await,
Ok(EvalValue::Ratio { numer: 1, denom: 1 })
// money + money, same currency → atomic money (the null-term fast path).
match run("(+ (usd) (usd))").await {
Ok(EvalValue::Commodity { numer, denom, .. }) => assert_eq!((numer, denom), (6, 1)),
other => panic!("expected atomic commodity 6/1, got {other:?}"),
// money + money, different currency → COMMODITY-MISMATCH throw.
assert!(run("(+ (usd) (eur))").await.is_err());
// money × money → compound, no host wire form yet → decode error.
run("(* (usd) (usd))")
.unwrap_err()
.contains("compound")
// an ATOMIC money passes the host-border guard.
assert_eq!(run("(sink (usd))").await, Ok(EvalValue::I32(0)));
// a COMPOUND money is rejected at the host border by the guard.
assert!(run("(sink (* (usd) (usd)))").await.is_err());
// a COMPOUND money riding a $pair cell is rejected by the pair-car
// renderer too — it must NOT slip through as id-zero atomic money
// (the pair-decode border is field-4-aware, same as the top-level one).
run("(list (* (usd) (usd)))")
// an atomic money in a list cell still renders (sanity: the field-4
// gate doesn't reject the null-term atomic case).
assert!(run("(list (usd))").await.is_ok());
// dimensionless × atomic → a `[(usd,1)]` singleton term that
// `commodity_new_with_term` canonicalizes back to ATOMIC usd: it decodes
// as an atomic commodity (3/1) and passes the host-border atomic guard.
match run("(* (/ (usd) (usd)) (usd))").await {
Ok(EvalValue::Commodity { numer, denom, .. }) => assert_eq!((numer, denom), (3, 1)),
other => panic!("expected atomic commodity 3/1, got {other:?}"),
run("(sink (* (/ (usd) (usd)) (usd)))").await,
Ok(EvalValue::I32(0))
/// The eval-mode `CompileContext` declares `nomi.__nomi_raise` for
/// `(error 'code "msg")` lowering even when no `(error)` form is
/// present in the program. The host side lives in the rpc crate;
/// for the scripting-crate runtime tests we link a never-called
/// stub so `instantiate()` resolves the import.
fn link_nomi_raise_stub(linker: &mut Linker<()>, engine: &wasmtime::Engine) {
.func_new(
"nomi",
"__nomi_raise",
wasmtime::FuncType::new(
engine,
[
wasmtime::ValType::Ref(wasmtime::RefType::ARRAYREF),
],
[],
),
|_, _, _| {
Err(wasmtime::Error::msg(
"__nomi_raise stub: not linked in this test",
link_log_stub(linker, engine);
link_nomi_catch_each_stub(linker, engine);
/// `env.log` `(i32 level, i32 ptr, i32 len) -> ()` stub. Eval-mode modules
/// import `env.log` (PRINT / DISPLAY / NEWLINE / DEBUG lower to it); a test
/// linker that instantiates an eval module must define it or instantiation
/// fails with "unknown import". Production wires it via `scripting::host`
/// (script mode) / `rpc::natives::env_io` (eval mode); this no-op stub
/// suffices for tests that don't assert on logged output.
fn link_log_stub(linker: &mut Linker<()>, engine: &wasmtime::Engine) {
"env",
"log",
wasmtime::ValType::I32,
|_, _, _| Ok(()),
/// Companion to `link_nomi_raise_stub`. The eval compile context now
/// declares `__nomi_catch_each` up-front (so its import index is
/// stable before any user host fn is wired — matches `__nomi_raise`'s
/// shape), so even programs that don't use `(catch-each ...)` still
/// need the import resolvable at instantiation time. The stub traps
/// on call so a regression that accidentally invokes catch-each in
/// these unit tests surfaces loudly rather than silently no-oping.
fn link_nomi_catch_each_stub(linker: &mut Linker<()>, engine: &wasmtime::Engine) {
let abstract_struct =
wasmtime::ValType::Ref(wasmtime::RefType::new(true, wasmtime::HeapType::Struct));
let funcref = wasmtime::ValType::Ref(wasmtime::RefType::FUNCREF);
let anyref = wasmtime::ValType::Ref(wasmtime::RefType::ANYREF);
"__nomi_catch_each",
[funcref, anyref, abstract_struct.clone()],
[abstract_struct],
"__nomi_catch_each stub: not linked in this test",
/// End-to-end: nomiscript Compiler emits eval-mode bytecode that
/// returns the form's final value via nomi-eval's `(ref null any)`
/// return slot, the runtime instantiates it (no capture host fns
/// linked — they retired in A6.c), and `decode_eval_result` walks
/// the anyref into the structured `EvalValue` the rest of the host
/// renders from.
fn run_nomiscript_eval(program: &nomiscript::Program) -> Option<EvalValue> {
use nomiscript::{Compiler, SymbolTable};
.compile_eval_with_type(program, &mut symbols)
store.set_fuel(10_000_000).unwrap();
func.call(&mut store, &[], &mut results).unwrap();
_ => panic!("nomi-eval returned non-anyref"),
Some(decode_eval_result(&mut store, any, result_ty).expect("decode"))
fn nomiscript_eval_captures_integer_literal() {
use nomiscript::{Expr, Fraction, Program};
// ADR-0028: an integer literal is an Index (I32), decoding as `I32`,
// not the dimensionless `Ratio` it conflated with before the flip.
let program = Program::new(vec![Expr::Number(Fraction::from_integer(7))]);
assert_eq!(run_nomiscript_eval(&program), Some(EvalValue::I32(7)));
fn nomiscript_eval_captures_arithmetic_result() {
// All-integer (Index) arithmetic stays in the Index stratum: `(+ 1 2)`
// decodes as `I32(3)`.
let program = Program::new(vec![Expr::List(vec![
Expr::Symbol("+".into()),
Expr::Number(Fraction::from_integer(1)),
Expr::Number(Fraction::from_integer(2)),
])]);
assert_eq!(run_nomiscript_eval(&program), Some(EvalValue::I32(3)));
fn nomiscript_eval_captures_fractional_result() {
// A Scalar operand keeps rational division: `(/ 1/2 2) → 1/4`, decoding
// as `Ratio`. (All-integer `(/ 1 4)` would be Index `0`.)
Expr::Symbol("/".into()),
Expr::Number(Fraction::new(1, 2)),
run_nomiscript_eval(&program),
Some(EvalValue::Ratio { numer: 1, denom: 4 })
fn nomiscript_eval_captures_nil_for_empty_program() {
let program = nomiscript::Program::default();
assert_eq!(run_nomiscript_eval(&program), Some(EvalValue::Nil));
fn nomiscript_eval_decodes_bool_as_bool() {
use nomiscript::{Expr, Program};
let program = Program::new(vec![Expr::T]);
// `t` carries `WasmType::Bool` (i31-boxed); the decoder surfaces a
// truthy bool as `Bool(true)` (a falsy one would be `Nil`), not the
// raw integer the old i32-conflated path produced.
assert_eq!(run_nomiscript_eval(&program), Some(EvalValue::T));
/// Drift-detector: compile-eval each script source, run it, and
/// verify the static `result_ty` hint reported by
/// `compile_eval_with_type` matches the decoded `EvalValue`'s
/// variant. Catches eval-vs-codegen drift across the compiler —
/// the exact bug class that produced the tag-sync test regressions.
/// Add a row whenever a new WasmType or Expr-shape is supported.
fn nomiscript_eval_type_hint_matches_value_variant() {
use nomiscript::{Compiler, Program, Reader, SymbolTable, WasmType};
let cases: &[(&str, Option<WasmType>)] = &[
// ADR-0028: integer literals + all-integer arithmetic are Index
// (I32); a fractional literal is Scalar (Ratio).
("42", Some(WasmType::I32)),
("(+ 1 2)", Some(WasmType::I32)),
("(/ 1 4)", Some(WasmType::I32)),
("(/ 1/2 2)", Some(WasmType::Ratio)),
("(= 1 1)", Some(WasmType::Bool)),
("(< 1 2)", Some(WasmType::Bool)),
("t", Some(WasmType::Bool)),
("\"hello\"", Some(WasmType::StringRef)),
("(let ((x 1)) (+ x 1))", Some(WasmType::I32)),
("(let ((x 1)) \"tail\")", Some(WasmType::StringRef)),
("(if (= 1 1) 2 3)", Some(WasmType::I32)),
for (src, expected_ty) in cases {
let program: Program = Reader::parse(src).expect("parse");
.unwrap_or_else(|e| panic!("compile {src:?}: {e}"));
&result_ty, expected_ty,
"compile_eval_with_type reported wrong static type for {src:?}",
func.call(&mut store, &[], &mut results)
.unwrap_or_else(|e| panic!("run {src:?}: {e}"));
_ => panic!("nomi-eval returned non-anyref for {src:?}"),
let decoded = decode_eval_result(&mut store, any, result_ty)
.unwrap_or_else(|e| panic!("decode {src:?}: {e}"));
// The mapping below is the canonical EvalValue ↔ WasmType
// contract. Any drift fails here.
let ok = matches!(
(&result_ty, &decoded),
(None, EvalValue::Nil)
| (Some(WasmType::I32), EvalValue::I32(_))
// A Bool decodes to Bool(true) when truthy or Nil when falsy.
| (Some(WasmType::Bool), EvalValue::T | EvalValue::Nil)
| (Some(WasmType::Ratio), EvalValue::Ratio { .. })
| (Some(WasmType::Commodity), EvalValue::Commodity { .. })
| (
Some(WasmType::StringRef),
EvalValue::String(_) | EvalValue::Bytes(_)
ok,
"type/value drift for {src:?}: hint={result_ty:?}, decoded={decoded:?}",
async fn render_entity_emits_named_field_plist() {
// Build a $commodity-shaped struct (3 string fields, matching the
// Commodity layout's id/symbol/name slots) via WAT, then decode it with
// `render_entity` — the same call the host eval path makes for a returned
// entity. Proves the spec-driven decoder reads each slot by name.
(type $i8 (array (mut i8)))
(type $commodity (struct
(field (ref null $i8))
(field (ref null $i8))))
(data $id "uuid-123")
(data $sym "USD")
(data $name "US Dollar")
(func (export "go") (result (ref null struct))
(array.new_data $i8 $id (i32.const 0) (i32.const 8))
(array.new_data $i8 $sym (i32.const 0) (i32.const 3))
(array.new_data $i8 $name (i32.const 0) (i32.const 9)))))
let linker: Linker<()> = Linker::new(&engine);
let entity = match results[0] {
Val::AnyRef(Some(a)) => a.unwrap_struct(&mut store).unwrap(),
other => panic!("go did not return a struct: {other:?}"),
let rendered = render_entity(&mut store, entity, nomiscript::EntityKind::Commodity).unwrap();
rendered,
"(:commodity :id \"uuid-123\" :symbol \"USD\" :name \"US Dollar\")"
async fn read_entity_string_field_reads_named_slot() {
// Same Commodity-shaped struct; prove the field reader resolves a named
// slot from the layout (id = slot 0, name = slot 2) — the call
// `draft-split` makes to turn an entity ref into a stable uuid string.
let id =
read_entity_string_field_ctx(&mut store, entity, nomiscript::EntityKind::Commodity, "id")
assert_eq!(id, "uuid-123");
let name = read_entity_string_field_ctx(
&mut store,
entity,
nomiscript::EntityKind::Commodity,
"name",
assert_eq!(name, "US Dollar");
// A field that isn't a String slot in the layout is rejected.
let bad = read_entity_string_field_ctx(
"nonexistent",
assert!(bad.is_err());