Catch Typed Jerboa Rust ABI panics

ober

60aa985e50449aa7a42f0fb1dd17f294da9f167c

diff --git a/docs/jerboa-to-rust.md b/docs/jerboa-to-rust.md
index 8a83233..ebaaa83 100644
--- a/docs/jerboa-to-rust.md
+++ b/docs/jerboa-to-rust.md
@@ -235,7 +235,9 @@ Current landing: scalar ABI-safe exported functions get generated Rust
 Chez `foreign-procedure`. `String` and `Bytes` arguments can cross this
 boundary for scalar-return functions as bytevector-plus-length pairs, with
 `String` using UTF-8 conversion. Non-scalar returns and owned values still wait
-for opaque handles and conversion records.
+for opaque handles and conversion records. ABI wrappers catch Rust panics before
+they cross Chez FFI and currently return conservative default scalar values;
+structured error returns are still future work.
 
 ## Generics
 
@@ -394,6 +396,10 @@ pub extern "C" fn jt_call(...) -> JtResultHandle {
 }
 ```
 
+Current landing catches panics in scalar wrappers but does not yet return
+`JtResultHandle`; panic paths return conservative defaults such as `0`, `#f`
+equivalent booleans, or replacement-character code points.
+
 ## Unsafe Policy
 
 Generated Rust should be safe by default.
diff --git a/docs/typed-jerboa.md b/docs/typed-jerboa.md
index 0ee0199..275056a 100644
--- a/docs/typed-jerboa.md
+++ b/docs/typed-jerboa.md
@@ -154,7 +154,9 @@ Current landing:
 - Scalar-only exported functions get initial `extern "C"` Rust wrappers. Rust
   requires the symbol export marker to be written as `#[unsafe(no_mangle)]`, so
   generated crates deny unsafe operations rather than claiming a blanket
-  `forbid(unsafe_code)` once wrappers are present.
+  `forbid(unsafe_code)` once wrappers are present. ABI wrappers catch Rust
+  panics before they cross Chez FFI and return conservative default values until
+  the structured error ABI lands.
 - `(jerboa typed wrapper)`, `support/typed-wrappers.ss`, and `make
   typed-wrappers` generate `.ss` Jerboa wrapper files for ABI-safe exports.
   The wrappers load the compiled Rust cdylib from `JERBOA_TYPED_RUST_LIB`,
diff --git a/lib/jerboa/typed/rust.ss b/lib/jerboa/typed/rust.ss
index 4bbf19f..be309b9 100644
--- a/lib/jerboa/typed/rust.ss
+++ b/lib/jerboa/typed/rust.ss
@@ -370,7 +370,7 @@
     (let ([name (rust-symbol-name (typed-param-name param))])
       (case (typed-param-type param)
         [(Char)
-         (write-line port 1
+         (write-line port 2
            (string-append
              "let "
              name
@@ -378,29 +378,29 @@
              name
              ").unwrap_or('\\u{FFFD}');"))]
         [(String Bytes)
-         (write-line port 1 (string-append "let " name " = {"))
-         (write-line port 2
+         (write-line port 2 (string-append "let " name " = {"))
+         (write-line port 3
            (string-append
              "let bytes: &[u8] = if "
              name
              "_ptr.is_null() {"))
-         (write-line port 3 "&[]")
-         (write-line port 2 "} else {")
-         (write-line port 3
+         (write-line port 4 "&[]")
+         (write-line port 3 "} else {")
+         (write-line port 4
            "// unsafe: pointer and length are produced by the generated Jerboa wrapper.")
-         (write-line port 3
+         (write-line port 4
            (string-append
              "unsafe { std::slice::from_raw_parts("
              name
              "_ptr, "
              name
              "_len) }"))
-         (write-line port 2 "};")
-         (write-line port 2
+         (write-line port 3 "};")
+         (write-line port 3
            (if (eq? (typed-param-type param) 'String)
              "String::from_utf8_lossy(bytes).into_owned()"
              "bytes.to_vec()"))
-         (write-line port 1 "};")]
+         (write-line port 2 "};")]
         [else #f])))
 
   (def (abi-return-expression def call)
@@ -408,6 +408,17 @@
       [(Char) (string-append "(" call " as u32)")]
       [else call]))
 
+  (def (abi-default-expression type)
+    (case type
+      [(Unit) "()"]
+      [(Bool) "false"]
+      [(Char) "0xFFFDu32"]
+      [(Int) "0i64"]
+      [(Nat) "0u64"]
+      [(Fixnum) "0isize"]
+      [(Float) "0.0f64"]
+      [else (error 'typed-rust "unsupported ABI default type" type)]))
+
   (def (emit-abi-wrapper module def port)
     (when (abi-safe-def? def)
       (let* ([params (typed-def-params def)]
@@ -430,11 +441,21 @@
             ") -> "
             (abi-rust-type (typed-def-return-type def))
             " {"))
+        (write-line port 1
+          "match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {")
         (for-each
           (lambda (param)
             (emit-abi-param-conversion param port))
           params)
-        (write-line port 1 (abi-return-expression def call))
+        (write-line port 2 (abi-return-expression def call))
+        (write-line port 1 "})) {")
+        (write-line port 2 "Ok(value) => value,")
+        (write-line port 2
+          (string-append
+            "Err(_) => "
+            (abi-default-expression (typed-def-return-type def))
+            ","))
+        (write-line port 1 "}")
         (write-line port 0 "}")
         (newline port))))
 
diff --git a/tests/test-typed-rust.ss b/tests/test-typed-rust.ss
index e7aa1f5..fb94713 100644
--- a/tests/test-typed-rust.ss
+++ b/tests/test-typed-rust.ss
@@ -31,7 +31,7 @@
        (+ x 1))))
 
 (define calc-rust
-  "// Generated by Jerboa's typed Rust backend. Do not edit.\n#![deny(unsafe_op_in_unsafe_fn)]\n#![allow(unused_parens)]\n#![allow(unused_variables)]\n\npub fn private_zero() -> u64 {\n    0u64\n}\n\npub fn zero() -> u64 {\n    private_zero()\n}\n\npub fn add_one(x: u64) -> u64 {\n    (x + 1u64)\n}\n\n#[unsafe(no_mangle)]\npub extern \"C\" fn jt_sample_typed_calc_zero() -> u64 {\n    zero()\n}\n\n#[unsafe(no_mangle)]\npub extern \"C\" fn jt_sample_typed_calc_add_one(x: u64) -> u64 {\n    add_one(x)\n}\n\n")
+  "// Generated by Jerboa's typed Rust backend. Do not edit.\n#![deny(unsafe_op_in_unsafe_fn)]\n#![allow(unused_parens)]\n#![allow(unused_variables)]\n\npub fn private_zero() -> u64 {\n    0u64\n}\n\npub fn zero() -> u64 {\n    private_zero()\n}\n\npub fn add_one(x: u64) -> u64 {\n    (x + 1u64)\n}\n\n#[unsafe(no_mangle)]\npub extern \"C\" fn jt_sample_typed_calc_zero() -> u64 {\n    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {\n        zero()\n    })) {\n        Ok(value) => value,\n        Err(_) => 0u64,\n    }\n}\n\n#[unsafe(no_mangle)]\npub extern \"C\" fn jt_sample_typed_calc_add_one(x: u64) -> u64 {\n    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {\n        add_one(x)\n    })) {\n        Ok(value) => value,\n        Err(_) => 0u64,\n    }\n}\n\n")
 
 (define data-form
   '(typed-library (sample typed data)
@@ -54,7 +54,7 @@
        ch)))
 
 (define char-rust
-  "// Generated by Jerboa's typed Rust backend. Do not edit.\n#![deny(unsafe_op_in_unsafe_fn)]\n#![allow(unused_parens)]\n#![allow(unused_variables)]\n\npub fn same_char(ch: char) -> char {\n    ch\n}\n\n#[unsafe(no_mangle)]\npub extern \"C\" fn jt_sample_typed_char_same_char(ch: u32) -> u32 {\n    let ch = char::from_u32(ch).unwrap_or('\\u{FFFD}');\n    (same_char(ch) as u32)\n}\n\n")
+  "// Generated by Jerboa's typed Rust backend. Do not edit.\n#![deny(unsafe_op_in_unsafe_fn)]\n#![allow(unused_parens)]\n#![allow(unused_variables)]\n\npub fn same_char(ch: char) -> char {\n    ch\n}\n\n#[unsafe(no_mangle)]\npub extern \"C\" fn jt_sample_typed_char_same_char(ch: u32) -> u32 {\n    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {\n        let ch = char::from_u32(ch).unwrap_or('\\u{FFFD}');\n        (same_char(ch) as u32)\n    })) {\n        Ok(value) => value,\n        Err(_) => 0xFFFDu32,\n    }\n}\n\n")
 
 (define string-form
   '(typed-library (sample typed text)
@@ -63,7 +63,7 @@
        (string-length s))))
 
 (define string-rust
-  "// Generated by Jerboa's typed Rust backend. Do not edit.\n#![deny(unsafe_op_in_unsafe_fn)]\n#![allow(unused_parens)]\n#![allow(unused_variables)]\n\npub fn text_length(s: String) -> u64 {\n    (s).len() as u64\n}\n\n#[unsafe(no_mangle)]\npub extern \"C\" fn jt_sample_typed_text_text_length(s_ptr: *const u8, s_len: usize) -> u64 {\n    let s = {\n        let bytes: &[u8] = if s_ptr.is_null() {\n            &[]\n        } else {\n            // unsafe: pointer and length are produced by the generated Jerboa wrapper.\n            unsafe { std::slice::from_raw_parts(s_ptr, s_len) }\n        };\n        String::from_utf8_lossy(bytes).into_owned()\n    };\n    text_length(s)\n}\n\n")
+  "// Generated by Jerboa's typed Rust backend. Do not edit.\n#![deny(unsafe_op_in_unsafe_fn)]\n#![allow(unused_parens)]\n#![allow(unused_variables)]\n\npub fn text_length(s: String) -> u64 {\n    (s).len() as u64\n}\n\n#[unsafe(no_mangle)]\npub extern \"C\" fn jt_sample_typed_text_text_length(s_ptr: *const u8, s_len: usize) -> u64 {\n    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {\n        let s = {\n            let bytes: &[u8] = if s_ptr.is_null() {\n                &[]\n            } else {\n                // unsafe: pointer and length are produced by the generated Jerboa wrapper.\n                unsafe { std::slice::from_raw_parts(s_ptr, s_len) }\n            };\n            String::from_utf8_lossy(bytes).into_owned()\n        };\n        text_length(s)\n    })) {\n        Ok(value) => value,\n        Err(_) => 0u64,\n    }\n}\n\n")
 
 (define bytes-form
   '(typed-library (sample typed bytes)
@@ -72,7 +72,7 @@
        (bytevector-length data))))
 
 (define bytes-rust
-  "// Generated by Jerboa's typed Rust backend. Do not edit.\n#![deny(unsafe_op_in_unsafe_fn)]\n#![allow(unused_parens)]\n#![allow(unused_variables)]\n\npub fn bytes_length(data: Vec<u8>) -> u64 {\n    (data).len() as u64\n}\n\n#[unsafe(no_mangle)]\npub extern \"C\" fn jt_sample_typed_bytes_bytes_length(data_ptr: *const u8, data_len: usize) -> u64 {\n    let data = {\n        let bytes: &[u8] = if data_ptr.is_null() {\n            &[]\n        } else {\n            // unsafe: pointer and length are produced by the generated Jerboa wrapper.\n            unsafe { std::slice::from_raw_parts(data_ptr, data_len) }\n        };\n        bytes.to_vec()\n    };\n    bytes_length(data)\n}\n\n")
+  "// Generated by Jerboa's typed Rust backend. Do not edit.\n#![deny(unsafe_op_in_unsafe_fn)]\n#![allow(unused_parens)]\n#![allow(unused_variables)]\n\npub fn bytes_length(data: Vec<u8>) -> u64 {\n    (data).len() as u64\n}\n\n#[unsafe(no_mangle)]\npub extern \"C\" fn jt_sample_typed_bytes_bytes_length(data_ptr: *const u8, data_len: usize) -> u64 {\n    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {\n        let data = {\n            let bytes: &[u8] = if data_ptr.is_null() {\n                &[]\n            } else {\n                // unsafe: pointer and length are produced by the generated Jerboa wrapper.\n                unsafe { std::slice::from_raw_parts(data_ptr, data_len) }\n            };\n            bytes.to_vec()\n        };\n        bytes_length(data)\n    })) {\n        Ok(value) => value,\n        Err(_) => 0u64,\n    }\n}\n\n")
 
 (define ops-form
   '(typed-library (sample typed ops)