1
//! Script-raise host native: the boundary bridge for in-guest raises.
2
//!
3
//! Since Tier 3.2, `(error 'code "msg")` and engine errors (commodity
4
//! mismatch) `throw $nomi_error` in-guest; this host fn is no longer the
5
//! raise primitive but the BOUNDARY BRIDGE (ADR-0026): the compiler wraps each
6
//! host-invoked body in a `try_table` that catches any uncaught `$nomi_error`,
7
//! reads its `code`/`message`, and `call $__nomi_raise`s here. The fn never
8
//! returns normally; it always produces `Err(wasmtime::Error::msg(...))`
9
//! carrying the `__nomi_raise:CODE:MSG` marker that
10
//! [`scripting::runtime::classify_runtime_error`] parses into a
11
//! `ScriptRaised { code, message }` for the wire envelope.
12

            
13
use scripting::runtime::{NOMI_RAISE_MARKER, read_string_arg};
14
use wasmtime::{ArrayRef, Caller, Linker, Rooted};
15

            
16
use crate::session::SessionData;
17

            
18
pub const REGISTERED_COMMANDS: &[&str] = &["error"];
19

            
20
2659
pub fn register(linker: &mut Linker<SessionData>) -> wasmtime::Result<()> {
21
2659
    linker.func_wrap(
22
2659
        "nomi",
23
2659
        "__nomi_raise",
24
        |mut caller: Caller<'_, SessionData>,
25
         code_arg: Option<Rooted<ArrayRef>>,
26
         msg_arg: Option<Rooted<ArrayRef>>|
27
144
         -> wasmtime::Result<()> {
28
144
            let code = read_string_arg(&mut caller, code_arg)?
29
144
                .ok_or_else(|| wasmtime::Error::msg("error: missing :code arg"))?;
30
144
            let message = read_string_arg(&mut caller, msg_arg)?.unwrap_or_default();
31
144
            Err(raise_error(&code, &message))
32
144
        },
33
    )?;
34
2659
    Ok(())
35
2659
}
36

            
37
/// Builds the marker-prefixed `wasmtime::Error` the classifier will parse.
38
/// Extracted so the special form's compile-time tests can assert the exact
39
/// wire format without spinning up a wasm engine.
40
147
pub fn raise_error(code: &str, message: &str) -> wasmtime::Error {
41
147
    wasmtime::Error::msg(format!("{NOMI_RAISE_MARKER}{code}:{message}"))
42
147
}
43

            
44
#[cfg(test)]
45
mod tests {
46
    use super::*;
47
    use scripting::runtime::{EngineError, classify_runtime_error};
48

            
49
    #[test]
50
1
    fn raise_error_round_trips_through_classifier() {
51
1
        let err = raise_error("no-such-account", "id=42");
52
1
        match classify_runtime_error(&err) {
53
1
            EngineError::ScriptRaised { code, message } => {
54
1
                assert_eq!(code, "no-such-account");
55
1
                assert_eq!(message, "id=42");
56
            }
57
            other => panic!("expected ScriptRaised, got {other:?}"),
58
        }
59
1
    }
60

            
61
    #[test]
62
1
    fn raise_error_message_can_contain_colons() {
63
1
        let err = raise_error("parse", "expected ':' at column 7");
64
1
        match classify_runtime_error(&err) {
65
1
            EngineError::ScriptRaised { code, message } => {
66
1
                assert_eq!(code, "parse");
67
1
                assert_eq!(message, "expected ':' at column 7");
68
            }
69
            other => panic!("expected ScriptRaised, got {other:?}"),
70
        }
71
1
    }
72

            
73
    #[test]
74
1
    fn raise_error_empty_message_keeps_code() {
75
1
        let err = raise_error("oops", "");
76
1
        match classify_runtime_error(&err) {
77
1
            EngineError::ScriptRaised { code, message } => {
78
1
                assert_eq!(code, "oops");
79
1
                assert_eq!(message, "");
80
            }
81
            other => panic!("expected ScriptRaised, got {other:?}"),
82
        }
83
1
    }
84
}