Skip to main content

rpc/natives/
raise.rs

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
13use scripting::runtime::{NOMI_RAISE_MARKER, read_string_arg};
14use wasmtime::{ArrayRef, Caller, Linker, Rooted};
15
16use crate::session::SessionData;
17
18pub const REGISTERED_COMMANDS: &[&str] = &["error"];
19
20pub fn register(linker: &mut Linker<SessionData>) -> wasmtime::Result<()> {
21    linker.func_wrap(
22        "nomi",
23        "__nomi_raise",
24        |mut caller: Caller<'_, SessionData>,
25         code_arg: Option<Rooted<ArrayRef>>,
26         msg_arg: Option<Rooted<ArrayRef>>|
27         -> wasmtime::Result<()> {
28            let code = read_string_arg(&mut caller, code_arg)?
29                .ok_or_else(|| wasmtime::Error::msg("error: missing :code arg"))?;
30            let message = read_string_arg(&mut caller, msg_arg)?.unwrap_or_default();
31            Err(raise_error(&code, &message))
32        },
33    )?;
34    Ok(())
35}
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.
40pub fn raise_error(code: &str, message: &str) -> wasmtime::Error {
41    wasmtime::Error::msg(format!("{NOMI_RAISE_MARKER}{code}:{message}"))
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47    use scripting::runtime::{EngineError, classify_runtime_error};
48
49    #[test]
50    fn raise_error_round_trips_through_classifier() {
51        let err = raise_error("no-such-account", "id=42");
52        match classify_runtime_error(&err) {
53            EngineError::ScriptRaised { code, message } => {
54                assert_eq!(code, "no-such-account");
55                assert_eq!(message, "id=42");
56            }
57            other => panic!("expected ScriptRaised, got {other:?}"),
58        }
59    }
60
61    #[test]
62    fn raise_error_message_can_contain_colons() {
63        let err = raise_error("parse", "expected ':' at column 7");
64        match classify_runtime_error(&err) {
65            EngineError::ScriptRaised { code, message } => {
66                assert_eq!(code, "parse");
67                assert_eq!(message, "expected ':' at column 7");
68            }
69            other => panic!("expected ScriptRaised, got {other:?}"),
70        }
71    }
72
73    #[test]
74    fn raise_error_empty_message_keeps_code() {
75        let err = raise_error("oops", "");
76        match classify_runtime_error(&err) {
77            EngineError::ScriptRaised { code, message } => {
78                assert_eq!(code, "oops");
79                assert_eq!(message, "");
80            }
81            other => panic!("expected ScriptRaised, got {other:?}"),
82        }
83    }
84}