1
//! Compile-time emit for caller-supplied host fn calls.
2
//!
3
//! When `(rpc-protocol-version)` (or any other registered `HostFnSpec`)
4
//! shows up in nomiscript source, the dispatcher in `super::mod`
5
//! routes here. We push each argument onto the wasm stack via
6
//! `compile_for_stack`, validate the type matches the spec, and emit
7
//! `(call $idx)` against the import index recorded by
8
//! [`CompileContext::register_host_fn`].
9

            
10
use super::CompileContext;
11
use crate::ast::{Expr, WasmType};
12
use crate::compiler::emit::FunctionEmitter;
13
use crate::compiler::expr::compile_for_stack_as;
14
use crate::error::{Error, Result};
15
use crate::runtime::SymbolTable;
16

            
17
131785
pub(in crate::compiler) fn compile_host_fn_for_stack(
18
131785
    ctx: &mut CompileContext,
19
131785
    emit: &mut FunctionEmitter,
20
131785
    symbols: &mut SymbolTable,
21
131785
    name: &str,
22
131785
    args: &[Expr],
23
131785
) -> Result<WasmType> {
24
131785
    let entry = expect_entry(ctx, name)?;
25
131785
    let result = entry.result.ok_or_else(|| {
26
136
        Error::Compile(format!(
27
136
            "host fn '{name}' has no return type and cannot produce a stack value"
28
136
        ))
29
136
    })?;
30
131649
    let func_idx = entry.func_idx;
31
131649
    let params = entry.params;
32
131649
    push_args(ctx, emit, symbols, name, &params, args)?;
33
131581
    symbols.mark_native_referenced(name);
34
131581
    emit.call(func_idx);
35
    // Host imports declare GC-ref results with the abstract heap type
36
    // (`(ref null struct)` / `(ref null array)`) because that's what
37
    // `Rooted<StructRef>` / `Rooted<ArrayRef>` report through
38
    // `WasmTy::valtype()`. Cast the result back to the concrete type
39
    // the rest of the pipeline expects.
40
131581
    emit_concrete_cast(ctx, emit, result);
41
131581
    Ok(result)
42
131785
}
43

            
44
408
pub(in crate::compiler) fn compile_host_fn_for_effect(
45
408
    ctx: &mut CompileContext,
46
408
    emit: &mut FunctionEmitter,
47
408
    symbols: &mut SymbolTable,
48
408
    name: &str,
49
408
    args: &[Expr],
50
408
) -> Result<()> {
51
408
    let entry = expect_entry(ctx, name)?;
52
408
    let result = entry.result;
53
408
    let func_idx = entry.func_idx;
54
408
    let params = entry.params;
55
408
    push_args(ctx, emit, symbols, name, &params, args)?;
56
408
    symbols.mark_native_referenced(name);
57
408
    emit.call(func_idx);
58
408
    if let Some(ty) = result {
59
340
        emit_concrete_cast(ctx, emit, ty);
60
340
        emit.drop_value();
61
340
    }
62
408
    Ok(())
63
408
}
64

            
65
/// Casts the abstract `(ref null struct)` / `(ref null array)` left on
66
/// the stack by a host import to the concrete type the result's
67
/// `WasmType` declares. Primitive returns (I32) pass through unchanged.
68
/// Nullable variant: wasmtime surfaces `Option<Rooted<…>>` returns as
69
/// `(ref null …)`, so an empty `list-accounts` / not-found
70
/// `get-account` returns null and the cast must preserve it. The
71
/// non-null `ref_cast` would trap on every nil-shaped result.
72
131921
fn emit_concrete_cast(ctx: &CompileContext, emit: &mut FunctionEmitter, ty: WasmType) {
73
131921
    match ty {
74
32028
        WasmType::I32 | WasmType::Bool => {}
75
272
        WasmType::Ratio => emit.ref_cast_nullable(ctx.ids.ty_ratio),
76
2720
        WasmType::Commodity => emit.ref_cast_nullable(ctx.ids.ty_commodity),
77
34340
        WasmType::StringRef => emit.ref_cast_nullable(ctx.ids.ty_i8_array),
78
50661
        WasmType::PairRef(_) => emit.ref_cast_nullable(ctx.ids.ty_pair),
79
11900
        WasmType::EntityRef(kind) => emit.ref_cast_nullable(ctx.ids.entity_type(kind)),
80
        WasmType::Closure(sig) => emit.ref_cast_nullable(ctx.closure_sig(sig).closure_type_idx),
81
        WasmType::AnyRef => {}
82
    }
83
131921
}
84

            
85
132193
fn expect_entry(ctx: &CompileContext, name: &str) -> Result<HostFnSnapshot> {
86
132193
    let entry = ctx.lookup_host_fn(name).ok_or_else(|| {
87
        Error::Compile(format!(
88
            "host fn dispatch reached for unregistered name '{name}'"
89
        ))
90
    })?;
91
132193
    Ok(HostFnSnapshot {
92
132193
        func_idx: entry.func_idx,
93
132193
        params: entry.params.clone(),
94
132193
        result: entry.result,
95
132193
    })
96
132193
}
97

            
98
struct HostFnSnapshot {
99
    func_idx: u32,
100
    params: Vec<WasmType>,
101
    result: Option<WasmType>,
102
}
103

            
104
132057
fn push_args(
105
132057
    ctx: &mut CompileContext,
106
132057
    emit: &mut FunctionEmitter,
107
132057
    symbols: &mut SymbolTable,
108
132057
    name: &str,
109
132057
    params: &[WasmType],
110
132057
    args: &[Expr],
111
132057
) -> Result<()> {
112
132057
    if args.len() != params.len() {
113
        return Err(Error::Arity {
114
            name: name.to_string(),
115
            expected: params.len(),
116
            actual: args.len(),
117
        });
118
132057
    }
119
132057
    for (idx, (arg, expected_ty)) in args.iter().zip(params.iter()).enumerate() {
120
        // Coerce each argument to the host fn's declared parameter type; an
121
        // integer/fractional literal crosses the sanctioned Index↔Scalar
122
        // boundary so e.g. a `Ratio` arg accepts a bare literal.
123
13124
        compile_for_stack_as(ctx, emit, symbols, arg, *expected_ty).map_err(|_| Error::Type {
124
68
            expected: format!("{expected_ty} for argument {idx} of '{name}'"),
125
68
            actual: "an incompatible value".to_string(),
126
68
        })?;
127
        // ADR-0028 E2: the wasm↔host border is unit-erased — host fns only see
128
        // ATOMIC single-currency money (fields 0-3). Guard every Commodity arg:
129
        // `commodity_assert_atomic` returns it unchanged when its unit term is
130
        // null, else throws a catchable `NON-ATOMIC-COMMODITY`, so a compound
131
        // value can never be misread as `id = 0` money.
132
13056
        if *expected_ty == WasmType::Commodity {
133
408
            emit.call(ctx.ids.commodity_assert_atomic);
134
12648
        }
135
    }
136
131989
    Ok(())
137
132057
}