Lines
87.64 %
Functions
27.5 %
Branches
100 %
//! Compile-time emit for caller-supplied host fn calls.
//!
//! When `(rpc-protocol-version)` (or any other registered `HostFnSpec`)
//! shows up in nomiscript source, the dispatcher in `super::mod`
//! routes here. We push each argument onto the wasm stack via
//! `compile_for_stack`, validate the type matches the spec, and emit
//! `(call $idx)` against the import index recorded by
//! [`CompileContext::register_host_fn`].
use super::CompileContext;
use crate::ast::{Expr, WasmType};
use crate::compiler::emit::FunctionEmitter;
use crate::compiler::expr::compile_for_stack_as;
use crate::error::{Error, Result};
use crate::runtime::SymbolTable;
pub(in crate::compiler) fn compile_host_fn_for_stack(
ctx: &mut CompileContext,
emit: &mut FunctionEmitter,
symbols: &mut SymbolTable,
name: &str,
args: &[Expr],
) -> Result<WasmType> {
let entry = expect_entry(ctx, name)?;
let result = entry.result.ok_or_else(|| {
Error::Compile(format!(
"host fn '{name}' has no return type and cannot produce a stack value"
))
})?;
let func_idx = entry.func_idx;
let params = entry.params;
push_args(ctx, emit, symbols, name, ¶ms, args)?;
symbols.mark_native_referenced(name);
emit.call(func_idx);
// Host imports declare GC-ref results with the abstract heap type
// (`(ref null struct)` / `(ref null array)`) because that's what
// `Rooted<StructRef>` / `Rooted<ArrayRef>` report through
// `WasmTy::valtype()`. Cast the result back to the concrete type
// the rest of the pipeline expects.
emit_concrete_cast(ctx, emit, result);
Ok(result)
}
pub(in crate::compiler) fn compile_host_fn_for_effect(
) -> Result<()> {
let result = entry.result;
if let Some(ty) = result {
emit_concrete_cast(ctx, emit, ty);
emit.drop_value();
Ok(())
/// Casts the abstract `(ref null struct)` / `(ref null array)` left on
/// the stack by a host import to the concrete type the result's
/// `WasmType` declares. Primitive returns (I32) pass through unchanged.
/// Nullable variant: wasmtime surfaces `Option<Rooted<…>>` returns as
/// `(ref null …)`, so an empty `list-accounts` / not-found
/// `get-account` returns null and the cast must preserve it. The
/// non-null `ref_cast` would trap on every nil-shaped result.
fn emit_concrete_cast(ctx: &CompileContext, emit: &mut FunctionEmitter, ty: WasmType) {
match ty {
WasmType::I32 | WasmType::Bool => {}
WasmType::Ratio => emit.ref_cast_nullable(ctx.ids.ty_ratio),
WasmType::Commodity => emit.ref_cast_nullable(ctx.ids.ty_commodity),
WasmType::StringRef => emit.ref_cast_nullable(ctx.ids.ty_i8_array),
WasmType::PairRef(_) => emit.ref_cast_nullable(ctx.ids.ty_pair),
WasmType::EntityRef(kind) => emit.ref_cast_nullable(ctx.ids.entity_type(kind)),
WasmType::Closure(sig) => emit.ref_cast_nullable(ctx.closure_sig(sig).closure_type_idx),
WasmType::AnyRef => {}
fn expect_entry(ctx: &CompileContext, name: &str) -> Result<HostFnSnapshot> {
let entry = ctx.lookup_host_fn(name).ok_or_else(|| {
"host fn dispatch reached for unregistered name '{name}'"
Ok(HostFnSnapshot {
func_idx: entry.func_idx,
params: entry.params.clone(),
result: entry.result,
})
struct HostFnSnapshot {
func_idx: u32,
params: Vec<WasmType>,
result: Option<WasmType>,
fn push_args(
params: &[WasmType],
if args.len() != params.len() {
return Err(Error::Arity {
name: name.to_string(),
expected: params.len(),
actual: args.len(),
});
for (idx, (arg, expected_ty)) in args.iter().zip(params.iter()).enumerate() {
// Coerce each argument to the host fn's declared parameter type; an
// integer/fractional literal crosses the sanctioned Index↔Scalar
// boundary so e.g. a `Ratio` arg accepts a bare literal.
compile_for_stack_as(ctx, emit, symbols, arg, *expected_ty).map_err(|_| Error::Type {
expected: format!("{expected_ty} for argument {idx} of '{name}'"),
actual: "an incompatible value".to_string(),
// ADR-0028 E2: the wasm↔host border is unit-erased — host fns only see
// ATOMIC single-currency money (fields 0-3). Guard every Commodity arg:
// `commodity_assert_atomic` returns it unchanged when its unit term is
// null, else throws a catchable `NON-ATOMIC-COMMODITY`, so a compound
// value can never be misread as `id = 0` money.
if *expected_ty == WasmType::Commodity {
emit.call(ctx.ids.commodity_assert_atomic);