1
//! Caller-supplied host fn codegen — `compiler/native/host_fn.rs`.
2
//! Builds a `Compiler` with a custom `HostFnSpec` and exercises the
3
//! `compile_host_fn_for_stack` / `compile_host_fn_for_effect` paths
4
//! plus the arg-coercion and arity validation.
5

            
6
use nomiscript::{Compiler, HostFnSpec, Program, Reader, SymbolTable, WasmType};
7
use wasmparser::{Validator, WasmFeatures};
8

            
9
14
fn validate(wasm: &[u8]) {
10
14
    let f = WasmFeatures::default()
11
14
        | WasmFeatures::GC
12
14
        | WasmFeatures::REFERENCE_TYPES
13
14
        | WasmFeatures::FUNCTION_REFERENCES
14
14
        | WasmFeatures::EXCEPTIONS;
15
14
    Validator::new_with_features(f)
16
14
        .validate_all(wasm)
17
14
        .expect("wasm validation failed");
18
14
}
19

            
20
14
fn compile_eval_with_specs(src: &str, specs: Vec<HostFnSpec>) -> Vec<u8> {
21
14
    let program: Program = Reader::parse(src).expect("parse");
22
14
    let mut compiler = Compiler::with_host_fns(specs.clone());
23
14
    let mut symbols = SymbolTable::with_builtins();
24
14
    symbols.register_host_fns(&specs);
25
14
    let (bytes, _ty) = compiler
26
14
        .compile_eval_with_type(&program, &mut symbols)
27
14
        .unwrap_or_else(|e| panic!("compile {src:?}: {e}"));
28
14
    validate(&bytes);
29
14
    bytes
30
14
}
31

            
32
3
fn compile_eval_expect_error(src: &str, specs: Vec<HostFnSpec>) -> String {
33
3
    let program: Program = Reader::parse(src).expect("parse");
34
3
    let mut compiler = Compiler::with_host_fns(specs.clone());
35
3
    let mut symbols = SymbolTable::with_builtins();
36
3
    symbols.register_host_fns(&specs);
37
3
    match compiler.compile_eval_with_type(&program, &mut symbols) {
38
        Ok(_) => panic!("expected compile error for {src:?}"),
39
3
        Err(e) => e.to_string(),
40
    }
41
3
}
42

            
43
#[test]
44
1
fn host_fn_no_args_returning_i32() {
45
1
    let spec = HostFnSpec::new("rpc-protocol-version", "nomi", "rpc_protocol_version")
46
1
        .returns(WasmType::I32);
47
1
    compile_eval_with_specs("(rpc-protocol-version)", vec![spec]);
48
1
}
49

            
50
#[test]
51
1
fn host_fn_no_return_at_value_position_errors() {
52
    // A host fn with `result: None` cannot produce a stack value;
53
    // calling it at value position must surface a structured error.
54
1
    let spec = HostFnSpec::new("rpc-log", "nomi", "rpc_log");
55
1
    let err = compile_eval_expect_error("(rpc-log)", vec![spec]);
56
1
    assert!(
57
1
        err.contains("RPC-LOG") || err.contains("no return type") || err.contains("stack"),
58
        "got: {err}",
59
    );
60
1
}
61

            
62
#[test]
63
1
fn void_host_fn_in_effect_position_compiles() {
64
    // A `result: None` host fn called in EFFECT position (a non-tail form in a
65
    // body) has no stack value: the effect-position dispatch must route it
66
    // through the host-fn effect compiler (emit the import call), not through
67
    // `compile_for_stack` (which rejects a void host fn).
68
1
    let spec = HostFnSpec::new("rpc-log", "nomi", "rpc_log");
69
1
    compile_eval_with_specs("(begin (rpc-log) 1)", vec![spec]);
70
1
}
71

            
72
#[test]
73
1
fn host_fn_with_ratio_arg_and_ratio_return() {
74
1
    let spec = HostFnSpec::new("rpc-double", "nomi", "rpc_double")
75
1
        .with_params(vec![WasmType::Ratio])
76
1
        .returns(WasmType::Ratio);
77
1
    compile_eval_with_specs("(rpc-double (/ 1 2))", vec![spec]);
78
1
}
79

            
80
#[test]
81
1
fn host_fn_arity_mismatch_errors() {
82
1
    let spec =
83
1
        HostFnSpec::new("rpc-takes-one", "nomi", "rpc_takes_one").with_params(vec![WasmType::I32]);
84
1
    let err = compile_eval_expect_error("(rpc-takes-one)", vec![spec]);
85
1
    assert!(
86
1
        err.contains("rpc-takes-one") || err.contains("RPC-TAKES-ONE"),
87
        "got: {err}"
88
    );
89
1
}
90

            
91
#[test]
92
1
fn host_fn_type_mismatch_errors() {
93
1
    let spec = HostFnSpec::new("rpc-needs-i32", "nomi", "rpc_needs_i32")
94
1
        .with_params(vec![WasmType::I32])
95
1
        .returns(WasmType::I32);
96
    // `1/2` is a fractional Scalar literal — it cannot cross to the i32 the
97
    // spec wants. (`(/ 1 2)` would now be Index integer division → 0, which
98
    // *would* satisfy an i32 param.)
99
1
    let err = compile_eval_expect_error("(rpc-needs-i32 1/2)", vec![spec]);
100
1
    assert!(
101
1
        err.contains("RPC-NEEDS-I32") || err.contains("i32") || err.contains("scalar"),
102
        "got: {err}"
103
    );
104
1
}
105

            
106
#[test]
107
1
fn host_fn_with_string_arg() {
108
1
    let spec = HostFnSpec::new("rpc-takes-string", "nomi", "rpc_takes_string")
109
1
        .with_params(vec![WasmType::StringRef])
110
1
        .returns(WasmType::I32);
111
1
    compile_eval_with_specs("(rpc-takes-string \"hello\")", vec![spec]);
112
1
}
113

            
114
#[test]
115
1
fn host_fn_returning_string() {
116
1
    let spec = HostFnSpec::new("rpc-greet", "nomi", "rpc_greet").returns(WasmType::StringRef);
117
1
    compile_eval_with_specs("(rpc-greet)", vec![spec]);
118
1
}
119

            
120
#[test]
121
1
fn host_fn_returning_pair() {
122
1
    let spec = HostFnSpec::new("rpc-list", "nomi", "rpc_list")
123
1
        .returns(WasmType::PairRef(nomiscript::PairElement::I32));
124
1
    compile_eval_with_specs("(rpc-list)", vec![spec]);
125
1
}
126

            
127
#[test]
128
1
fn host_fn_returning_entity_ref() {
129
1
    let spec = HostFnSpec::new("rpc-get-account", "nomi", "rpc_get_account")
130
1
        .returns(WasmType::EntityRef(nomiscript::EntityKind::Account));
131
1
    compile_eval_with_specs("(rpc-get-account)", vec![spec]);
132
1
}
133

            
134
#[test]
135
1
fn host_fn_at_value_position_returning_commodity() {
136
1
    let spec = HostFnSpec::new("rpc-balance", "nomi", "rpc_balance").returns(WasmType::Commodity);
137
1
    compile_eval_with_specs("(rpc-balance)", vec![spec]);
138
1
}
139

            
140
#[test]
141
1
fn commodity_times_commodity_compiles_to_compound() {
142
    // ADR-0028 E2: money × money is valid (it was a compile error before) and
143
    // lowers through `commodity_mul` (unit terms multiply). Validate the wasm.
144
1
    let spec = HostFnSpec::new("rpc-balance", "nomi", "rpc_balance").returns(WasmType::Commodity);
145
1
    compile_eval_with_specs("(* (rpc-balance) (rpc-balance))", vec![spec]);
146
1
}
147

            
148
#[test]
149
1
fn commodity_div_commodity_compiles_to_money() {
150
    // ADR-0028 E2: money ÷ money stays Money (dimensionless / compound unit
151
    // term) via `commodity_div`, not a bare Ratio.
152
1
    let spec = HostFnSpec::new("rpc-balance", "nomi", "rpc_balance").returns(WasmType::Commodity);
153
1
    compile_eval_with_specs("(/ (rpc-balance) (rpc-balance))", vec![spec]);
154
1
}
155

            
156
#[test]
157
1
fn compound_commodity_arg_emits_host_border_guard() {
158
    // Passing a (possibly compound) money to a host fn compiles: the
159
    // `commodity_assert_atomic` guard is emitted before the import call. The
160
    // throw only fires at runtime for a genuinely non-atomic value.
161
1
    let bal = HostFnSpec::new("rpc-balance", "nomi", "rpc_balance").returns(WasmType::Commodity);
162
1
    let sink = HostFnSpec::new("rpc-sink", "nomi", "rpc_sink")
163
1
        .with_params(vec![WasmType::Commodity])
164
1
        .returns(WasmType::I32);
165
1
    compile_eval_with_specs(
166
1
        "(rpc-sink (* (rpc-balance) (rpc-balance)))",
167
1
        vec![bal, sink],
168
    );
169
1
}
170

            
171
/// P4 A1 invariant: a host fn returning Commodity emits `ref.cast` after
172
/// the import call — recovering the concrete `$commodity` struct from the
173
/// abstract `(ref null struct)` the import declaration uses. Locks out a
174
/// regression that would re-introduce a synthesized `ratio_new` /
175
/// `commodity_new` post-call wrap (the path the P4 design retired in
176
/// favor of host-allocated Rooted GC refs).
177
#[test]
178
1
fn host_fn_commodity_return_emits_ref_cast_not_wrap_call() {
179
    use wasmparser::{Parser, Payload};
180
1
    let spec = HostFnSpec::new("rpc-balance", "nomi", "rpc_balance").returns(WasmType::Commodity);
181
1
    let wasm = compile_eval_with_specs("(rpc-balance)", vec![spec]);
182
1
    let mut saw_ref_cast = false;
183
1
    let mut saw_call = false;
184
50
    for payload in Parser::new(0).parse_all(&wasm) {
185
50
        let Payload::CodeSectionEntry(body) = payload.unwrap() else {
186
11
            continue;
187
        };
188
39
        let mut reader = body.get_operators_reader().unwrap();
189
1171
        while let Ok(op) = reader.read() {
190
1132
            match op {
191
44
                wasmparser::Operator::Call { .. } => saw_call = true,
192
                wasmparser::Operator::RefCastNonNull { .. }
193
3
                | wasmparser::Operator::RefCastNullable { .. } => saw_ref_cast = true,
194
1085
                _ => {}
195
            }
196
        }
197
    }
198
1
    assert!(saw_call, "expected (call $rpc_balance) in emitted wasm");
199
1
    assert!(
200
1
        saw_ref_cast,
201
        "expected `ref.cast` after Commodity-returning host fn call \
202
         (universal-result convention recovers the concrete struct type)"
203
    );
204
1
}
205

            
206
/// Tier 1.5: confirms today's inline `compile_lambda_call` path
207
/// handles a non-recursive defun called with a runtime arg (host-fn
208
/// return). Locks in that the inline const-fold path is *not*
209
/// vestigial — runtime args ride through it via `compile_arg_for_param`'s
210
/// `WasmLocal` promotion (`compiler/expr/call.rs:237`).
211
#[test]
212
1
fn defun_with_runtime_arg_via_host_fn_compiles() {
213
1
    let spec = HostFnSpec::new("rpc-balance", "nomi", "rpc_balance").returns(WasmType::Ratio);
214
1
    compile_eval_with_specs(
215
1
        "(defun double (x) (* x 2)) (double (rpc-balance))",
216
1
        vec![spec],
217
    );
218
1
}
219

            
220
/// Same as above, but the host-fn return rides through two defun
221
/// frames (no recursion). Locks in cross-frame `WasmLocal` propagation.
222
#[test]
223
1
fn nested_defuns_with_runtime_arg_compile() {
224
1
    let spec = HostFnSpec::new("rpc-balance", "nomi", "rpc_balance").returns(WasmType::Ratio);
225
1
    compile_eval_with_specs(
226
1
        "(defun inner (x) (* x 3))
227
1
         (defun outer (y) (inner (* y 2)))
228
1
         (outer (rpc-balance))",
229
1
        vec![spec],
230
    );
231
1
}