Support Typed Jerboa bytes inputs

ober

7c3b0176f4a173c7ccb56f2ff5d6874b1bf30a03

diff --git a/docs/jerboa-to-rust.md b/docs/jerboa-to-rust.md
index c5d3ab6..8a83233 100644
--- a/docs/jerboa-to-rust.md
+++ b/docs/jerboa-to-rust.md
@@ -232,9 +232,10 @@ should get separate ABI wrappers.
 
 Current landing: scalar ABI-safe exported functions get generated Rust
 `extern "C"` wrappers, and `.ss` Jerboa wrapper files call those symbols through
-Chez `foreign-procedure`. `String` arguments can cross this boundary for
-scalar-return functions as UTF-8 bytevector-plus-length pairs. Non-scalar
-returns and owned values still wait for opaque handles and conversion records.
+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.
 
 ## Generics
 
@@ -569,11 +570,12 @@ Second module: typed `rope`.
 
 - Support `Bool`, `Int`, `Nat`, `String`, `Bytes`.
 - Support function calls.
-- Support `if`, `let`, arithmetic, comparisons. Initial `string-length`
-  support landed as a checked `(String -> Nat)` builtin.
-- Generate wrappers. Initial scalar `.ss` wrappers and `String` argument
-  wrappers landed; record, variant, string return, bytes, option, result, and
-  handle conversions remain future work.
+- Support `if`, `let`, arithmetic, comparisons. Initial `string-length` and
+  `bytevector-length` support landed as checked `(String -> Nat)` and
+  `(Bytes -> Nat)` builtins.
+- Generate wrappers. Initial scalar `.ss` wrappers plus `String` and `Bytes`
+  argument wrappers landed; record, variant, string/bytes return, option,
+  result, and handle conversions remain future work.
 
 ### Milestone 3: Records and Variants
 
diff --git a/docs/typed-jerboa.md b/docs/typed-jerboa.md
index 66f69db..54c51d4 100644
--- a/docs/typed-jerboa.md
+++ b/docs/typed-jerboa.md
@@ -160,7 +160,8 @@ Current landing:
   The wrappers load the compiled Rust cdylib from `JERBOA_TYPED_RUST_LIB`,
   validate dynamic arguments, call `foreign-procedure`, convert `Char` values
   through unsigned code points, and pass `String` arguments as UTF-8
-  bytevector-plus-length pairs for scalar-return functions.
+  bytevector-plus-length pairs and `Bytes` arguments as bytevector-plus-length
+  pairs for scalar-return functions.
 - `make typed-wrapper-smoke` builds the primitive Rust fixture, generates its
   wrapper, loads the cdylib, and calls the generated Jerboa functions through
   Chez FFI.
@@ -177,7 +178,8 @@ Current landing:
   the same module, generated record/variant operations, and exhaustive
   `match` over same-module variants. The first builtin string primitive,
   `string-length`, is checked as `(String -> Nat)` and lowers to Rust
-  `.len()`. Imported calls and richer forms are reported as unsupported. It
+  `.len()`; `bytevector-length` is checked as `(Bytes -> Nat)` and lowers the
+  same way. Imported calls and richer forms are reported as unsupported. It
   does not yet resolve imports, lower to typed core IR, or emit LLVM.
 
 ## Surface Syntax
@@ -812,11 +814,11 @@ Minimum excluded features:
 - Compile generated Rust as a static or dynamic library. Initial disposable
   `rlib`/`staticlib`/`cdylib` builds landed.
 - Generate conversion functions. Initial scalar argument checks, `Char`
-  code-point conversions, and `String` argument UTF-8 bytevector conversions
-  landed at the wrapper boundary.
+  code-point conversions, `String` argument UTF-8 bytevector conversions, and
+  `Bytes` argument bytevector conversions landed at the wrapper boundary.
 - Generate Jerboa wrappers. Initial `.ss` wrapper generation landed for
-  scalar ABI-safe exported functions and `String` arguments with scalar
-  returns.
+  scalar ABI-safe exported functions, plus `String` and `Bytes` arguments with
+  scalar returns.
 
 ### Milestone 4: First Real Module
 
diff --git a/lib/jerboa/typed/checker.ss b/lib/jerboa/typed/checker.ss
index a1945bd..a4f65e5 100644
--- a/lib/jerboa/typed/checker.ss
+++ b/lib/jerboa/typed/checker.ss
@@ -204,7 +204,9 @@
   (def builtin-call-signatures
     (list
       (cons 'string-length
-            (make-typed-call-sig (list 'String) 'Nat))))
+            (make-typed-call-sig (list 'String) 'Nat))
+      (cons 'bytevector-length
+            (make-typed-call-sig (list 'Bytes) 'Nat))))
 
   (def (field-types fields)
     (map typed-field-type fields))
diff --git a/lib/jerboa/typed/rust.ss b/lib/jerboa/typed/rust.ss
index 1edeb8e..4bbf19f 100644
--- a/lib/jerboa/typed/rust.ss
+++ b/lib/jerboa/typed/rust.ss
@@ -314,7 +314,7 @@
 
   (def (abi-safe-param-type? type)
     (or (abi-safe-type? type)
-        (eq? type 'String)))
+        (memq type '(String Bytes))))
 
   (def (abi-rust-type type)
     (case type
@@ -355,7 +355,7 @@
   (def (emit-abi-param param)
     (let ([name (rust-symbol-name (typed-param-name param))])
       (case (typed-param-type param)
-        [(String)
+        [(String Bytes)
          (list
            (string-append name "_ptr: *const u8")
            (string-append name "_len: usize"))]
@@ -377,7 +377,7 @@
              " = char::from_u32("
              name
              ").unwrap_or('\\u{FFFD}');"))]
-        [(String)
+        [(String Bytes)
          (write-line port 1 (string-append "let " name " = {"))
          (write-line port 2
            (string-append
@@ -396,7 +396,10 @@
              name
              "_len) }"))
          (write-line port 2 "};")
-         (write-line port 2 "String::from_utf8_lossy(bytes).into_owned()")
+         (write-line port 2
+           (if (eq? (typed-param-type param) 'String)
+             "String::from_utf8_lossy(bytes).into_owned()"
+             "bytes.to_vec()"))
          (write-line port 1 "};")]
         [else #f])))
 
@@ -614,6 +617,14 @@
       (emit-expression (car args))
       ").len() as u64"))
 
+  (def (emit-bytevector-length args)
+    (unless (= (length args) 1)
+      (error 'typed-rust "bytevector-length expects one operand" args))
+    (string-append
+      "("
+      (emit-expression (car args))
+      ").len() as u64"))
+
   (def (emit-call name args)
     (let ([record-constructor (lookup-record-constructor name)]
           [record-accessor (lookup-record-accessor name)]
@@ -631,6 +642,8 @@
         [variant-predicate "true"]
         [(eq? name 'string-length)
          (emit-string-length args)]
+        [(eq? name 'bytevector-length)
+         (emit-bytevector-length args)]
         [else
          (string-append
            (rust-symbol-name name)
diff --git a/lib/jerboa/typed/wrapper.ss b/lib/jerboa/typed/wrapper.ss
index 55ef4d2..6962b8e 100644
--- a/lib/jerboa/typed/wrapper.ss
+++ b/lib/jerboa/typed/wrapper.ss
@@ -83,7 +83,7 @@
 
   (def (abi-chez-argument-types type)
     (case type
-      [(String) '(u8* size_t)]
+      [(String Bytes) '(u8* size_t)]
       [else (list (abi-chez-type type))]))
 
   (def (typed-module-wrapper-file-name module)
@@ -121,6 +121,7 @@
         [(Fixnum) (string-append "(fixnum? " name ")")]
         [(Float) (string-append "(real? " name ")")]
         [(String) (string-append "(string? " name ")")]
+        [(Bytes) (string-append "(bytevector? " name ")")]
         [else (error 'typed-wrapper "unsupported ABI parameter type"
                 (typed-param-type param))])))
 
@@ -166,6 +167,10 @@
            (list
              bytes-name
              (string-append "(bytevector-length " bytes-name ")")))]
+        [(Bytes)
+         (list
+           name
+           (string-append "(bytevector-length " name ")"))]
         [else (list name)])))
 
   (def (wrapper-call-expression def)
diff --git a/tests/fixtures/typed/rust-basic.ss b/tests/fixtures/typed/rust-basic.ss
index 9290cb9..cbc32dd 100644
--- a/tests/fixtures/typed/rust-basic.ss
+++ b/tests/fixtures/typed/rust-basic.ss
@@ -1,5 +1,5 @@
 (typed-library (sample typed rust-basic)
-  (export zero add-one positive? choose greeting double-add text-length)
+  (export zero add-one positive? choose greeting double-add text-length bytes-length)
 
   (def (zero) : Nat
     0)
@@ -21,4 +21,7 @@
       (+ y 1)))
 
   (def (text-length (text : String)) : Nat
-    (string-length text)))
+    (string-length text))
+
+  (def (bytes-length (data : Bytes)) : Nat
+    (bytevector-length data)))
diff --git a/tests/test-typed-checker.ss b/tests/test-typed-checker.ss
index 9bac779..3243a68 100644
--- a/tests/test-typed-checker.ss
+++ b/tests/test-typed-checker.ss
@@ -287,6 +287,22 @@
          (string-length x))))
   '(argument-type-mismatch))
 
+(test "builtin bytevector-length returns Nat"
+  (error-kinds
+    '(typed-library (body bytevector-length-ok)
+       (export f)
+       (def (f (x : Bytes)) : Nat
+         (bytevector-length x))))
+  '())
+
+(test "builtin bytevector-length rejects non-Bytes"
+  (error-kinds
+    '(typed-library (body bytevector-length-bad)
+       (export f)
+       (def (f (x : String)) : Nat
+         (bytevector-length x))))
+  '(argument-type-mismatch))
+
 (test "record constructor and accessor calls"
   (error-kinds
     '(typed-library (body record-ok)
diff --git a/tests/test-typed-rust.ss b/tests/test-typed-rust.ss
index e799e3a..e7aa1f5 100644
--- a/tests/test-typed-rust.ss
+++ b/tests/test-typed-rust.ss
@@ -65,6 +65,15 @@
 (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")
 
+(define bytes-form
+  '(typed-library (sample typed bytes)
+     (export bytes-length)
+     (def (bytes-length (data : Bytes)) : Nat
+       (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")
+
 (define ops-form
   '(typed-library (sample typed ops)
      (export pane-id make-insert make-noop edit-size)
@@ -110,6 +119,10 @@
   (typed-library-form->rust-string string-form)
   string-rust)
 
+(test "rust emits bytes input ABI conversions"
+  (typed-library-form->rust-string bytes-form)
+  bytes-rust)
+
 (test "rust lowers record and variant operations"
   (typed-library-form->rust-string ops-form)
   ops-rust)
diff --git a/tests/test-typed-wrapper-e2e.ss b/tests/test-typed-wrapper-e2e.ss
index 64a2b2d..892b12a 100644
--- a/tests/test-typed-wrapper-e2e.ss
+++ b/tests/test-typed-wrapper-e2e.ss
@@ -32,6 +32,7 @@
 (check "choose" (= (choose #t) 1))
 (check "double-add" (= (double-add 20) 41))
 (check "text-length" (= (text-length "hello") 5))
+(check "bytes-length" (= (bytes-length (make-bytevector 7 0)) 7))
 
 (printf "~%Typed wrapper FFI smoke: ~a passed, ~a failed~%" pass fail)
 (when (> fail 0)
diff --git a/tests/test-typed-wrappers.ss b/tests/test-typed-wrappers.ss
index 8f129c2..76cf8a2 100644
--- a/tests/test-typed-wrappers.ss
+++ b/tests/test-typed-wrappers.ss
@@ -51,6 +51,15 @@
 (define string-wrapper
   (typed-library-form->jerboa-wrapper-string string-form))
 
+(define bytes-form
+  '(typed-library (sample typed bytes)
+     (export bytes-length)
+     (def (bytes-length (data : Bytes)) : Nat
+       (bytevector-length data))))
+
+(define bytes-wrapper
+  (typed-library-form->jerboa-wrapper-string bytes-form))
+
 (printf "--- Typed Jerboa wrapper tests ---~%")
 
 (test "wrapper maps Unit to void"
@@ -88,6 +97,21 @@
     "(%text_length %s_bytes (bytevector-length %s_bytes))")
   #t)
 
+(test "wrapper binds Bytes as u8* plus size_t"
+  (substring? bytes-wrapper
+    "(foreign-procedure \"jt_sample_typed_bytes_bytes_length\" (u8* size_t) unsigned-64)")
+  #t)
+
+(test "wrapper validates Bytes args as bytevectors"
+  (substring? bytes-wrapper
+    "(unless (bytevector? data)")
+  #t)
+
+(test "wrapper passes Bytes directly with length"
+  (substring? bytes-wrapper
+    "(%bytes_length data (bytevector-length data))")
+  #t)
+
 (printf "~%Typed wrapper: ~a passed, ~a failed~%" pass fail)
 (when (> fail 0)
   (exit 1))