Generate scalar Typed Jerboa ABI wrappers

ober

b2ac2610228f171a104e9b38dccac3c17f88bb82

diff --git a/docs/jerboa-to-rust.md b/docs/jerboa-to-rust.md
index e9d51d5..f076435 100644
--- a/docs/jerboa-to-rust.md
+++ b/docs/jerboa-to-rust.md
@@ -29,7 +29,10 @@ Jerboa code can call typed compiled code safely.
 - Do not translate arbitrary dynamic Jerboa to Rust.
 - Do not expose Rust lifetimes directly in Typed Jerboa syntax.
 - Do not require users to edit generated Rust.
-- Do not use `unsafe` except in small, generated runtime and FFI shims.
+- Do not use `unsafe` except in small, generated runtime and FFI shims. Modern
+  Rust treats exported symbol names as an unsafe attribute; generated C ABI
+  wrappers therefore use `#[unsafe(no_mangle)]` and deny unsafe operations in
+  unsafe functions.
 - Do not make Rust crates the canonical source of typed modules.
 
 ## Pipeline
diff --git a/docs/typed-jerboa.md b/docs/typed-jerboa.md
index 97573bd..a261335 100644
--- a/docs/typed-jerboa.md
+++ b/docs/typed-jerboa.md
@@ -151,6 +151,10 @@ Current landing:
   records, variants, primitive function bodies, record accessors/constructors,
   variant constructors, variant predicates, and exhaustive variant `match`;
   native artifact builds and FFI wrappers are still future work.
+- 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.
 - `support/typed-rust.ss`, `make typed-rust`, and `make typed-build` generate a
   disposable Cargo crate under `build/typed/rust`; `typed-build` runs
   `cargo build` against the generated crate.
diff --git a/lib/jerboa/typed/rust.ss b/lib/jerboa/typed/rust.ss
index 81cab0b..0c5ab01 100644
--- a/lib/jerboa/typed/rust.ss
+++ b/lib/jerboa/typed/rust.ss
@@ -304,6 +304,79 @@
       ": "
       (rust-type (typed-param-type param))))
 
+  (def (abi-safe-type? type)
+    (and (symbol? type)
+         (memq type '(Unit Bool Char Int Nat Fixnum Float))
+         #t))
+
+  (def (abi-rust-type type)
+    (case type
+      [(Unit) "()"]
+      [(Bool) "bool"]
+      [(Char) "u32"]
+      [(Int) "i64"]
+      [(Nat) "u64"]
+      [(Fixnum) "isize"]
+      [(Float) "f64"]
+      [else (error 'typed-rust "unsupported ABI type" type)]))
+
+  (def (abi-safe-def? def)
+    (and (abi-safe-type? (typed-def-return-type def))
+         (let loop ([params (typed-def-params def)])
+           (cond
+             [(null? params) #t]
+             [(abi-safe-type? (typed-param-type (car params)))
+              (loop (cdr params))]
+             [else #f]))))
+
+  (def (exported-def? module def)
+    (and (memq (typed-def-name def) (typed-module-exports module))
+         #t))
+
+  (def (module-abi-prefix module)
+    (join-strings
+      (map rust-symbol-name (typed-module-name module))
+      "_"))
+
+  (def (abi-wrapper-name module def)
+    (string-append
+      "jt_"
+      (module-abi-prefix module)
+      "_"
+      (rust-symbol-name (typed-def-name def))))
+
+  (def (emit-abi-param param)
+    (string-append
+      (rust-symbol-name (typed-param-name param))
+      ": "
+      (abi-rust-type (typed-param-type param))))
+
+  (def (emit-abi-wrapper module def port)
+    (when (abi-safe-def? def)
+      (let* ([params (typed-def-params def)]
+             [param-names (map (lambda (param)
+                                 (rust-symbol-name (typed-param-name param)))
+                               params)]
+             [call
+              (string-append
+                (rust-symbol-name (typed-def-name def))
+                "("
+                (join-strings param-names ", ")
+                ")")])
+        (write-line port 0 "#[unsafe(no_mangle)]")
+        (write-line port 0
+          (string-append
+            "pub extern \"C\" fn "
+            (abi-wrapper-name module def)
+            "("
+            (join-strings (map emit-abi-param params) ", ")
+            ") -> "
+            (abi-rust-type (typed-def-return-type def))
+            " {"))
+        (write-line port 1 call)
+        (write-line port 0 "}")
+        (newline port))))
+
   (def (emit-binary-chain op args)
     (cond
       [(null? args) (error 'typed-rust "operator needs operands" op)]
@@ -559,7 +632,7 @@
 
   (def (emit-rust-header port)
     (write-line port 0 "// Generated by Jerboa's typed Rust backend. Do not edit.")
-    (write-line port 0 "#![forbid(unsafe_code)]")
+    (write-line port 0 "#![deny(unsafe_op_in_unsafe_fn)]")
     (write-line port 0 "#![allow(unused_parens)]")
     (write-line port 0 "#![allow(unused_variables)]")
     (newline port))
@@ -568,6 +641,12 @@
     (for-each
       (lambda (decl)
         (emit-declaration decl port))
+      (typed-module-declarations module))
+    (for-each
+      (lambda (decl)
+        (when (and (typed-def? decl)
+                   (exported-def? module decl))
+          (emit-abi-wrapper module decl port)))
       (typed-module-declarations module)))
 
   (def (emit-module module port)
diff --git a/tests/test-typed-rust.ss b/tests/test-typed-rust.ss
index 1227ae4..cc44423 100644
--- a/tests/test-typed-rust.ss
+++ b/tests/test-typed-rust.ss
@@ -23,13 +23,15 @@
 (define calc-form
   '(typed-library (sample typed calc)
      (export zero add-one)
-     (def (zero) : Nat
+     (def (private-zero) : Nat
        0)
+     (def (zero) : Nat
+       (private-zero))
      (def (add-one (x : Nat)) : Nat
        (+ x 1))))
 
 (define calc-rust
-  "// Generated by Jerboa's typed Rust backend. Do not edit.\n#![forbid(unsafe_code)]\n#![allow(unused_parens)]\n#![allow(unused_variables)]\n\npub fn zero() -> u64 {\n    0u64\n}\n\npub fn add_one(x: u64) -> u64 {\n    (x + 1u64)\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    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")
 
 (define data-form
   '(typed-library (sample typed data)
@@ -43,7 +45,7 @@
        (Noop))))
 
 (define data-rust
-  "// Generated by Jerboa's typed Rust backend. Do not edit.\n#![forbid(unsafe_code)]\n#![allow(unused_parens)]\n#![allow(unused_variables)]\n\n#[derive(Clone, Debug, PartialEq)]\npub struct Pane {\n    pub id: u64,\n    pub focused_p: bool,\n}\n\n#[derive(Clone, Debug, PartialEq)]\npub enum EditOp {\n    Insert {\n        at: u64,\n        text: String,\n    },\n    Noop,\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\n#[derive(Clone, Debug, PartialEq)]\npub struct Pane {\n    pub id: u64,\n    pub focused_p: bool,\n}\n\n#[derive(Clone, Debug, PartialEq)]\npub enum EditOp {\n    Insert {\n        at: u64,\n        text: String,\n    },\n    Noop,\n}\n\n")
 
 (define ops-form
   '(typed-library (sample typed ops)
@@ -66,7 +68,7 @@
          ((Noop) 0)))))
 
 (define ops-rust
-  "// Generated by Jerboa's typed Rust backend. Do not edit.\n#![forbid(unsafe_code)]\n#![allow(unused_parens)]\n#![allow(unused_variables)]\n\n#[derive(Clone, Debug, PartialEq)]\npub struct Pane {\n    pub id: u64,\n    pub focused_p: bool,\n}\n\n#[derive(Clone, Debug, PartialEq)]\npub enum EditOp {\n    Insert {\n        at: u64,\n        text: String,\n    },\n    Noop,\n}\n\npub fn pane_id(pane: Pane) -> u64 {\n    (pane).id\n}\n\npub fn make_insert(at: u64, text: String) -> EditOp {\n    EditOp::Insert { at: at, text: text }\n}\n\npub fn make_noop() -> EditOp {\n    EditOp::Noop\n}\n\npub fn edit_size(op: EditOp) -> u64 {\n    match op { EditOp::Insert { at, text } => at, EditOp::Noop => 0u64, }\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\n#[derive(Clone, Debug, PartialEq)]\npub struct Pane {\n    pub id: u64,\n    pub focused_p: bool,\n}\n\n#[derive(Clone, Debug, PartialEq)]\npub enum EditOp {\n    Insert {\n        at: u64,\n        text: String,\n    },\n    Noop,\n}\n\npub fn pane_id(pane: Pane) -> u64 {\n    (pane).id\n}\n\npub fn make_insert(at: u64, text: String) -> EditOp {\n    EditOp::Insert { at: at, text: text }\n}\n\npub fn make_noop() -> EditOp {\n    EditOp::Noop\n}\n\npub fn edit_size(op: EditOp) -> u64 {\n    match op { EditOp::Insert { at, text } => at, EditOp::Noop => 0u64, }\n}\n\n")
 
 (printf "--- Typed Jerboa Rust emitter tests ---~%")