typed-rust: add bytes-build for functional buffer production

ober

cca2b094b62e27317f69fe2ee16f522f012cc354

diff --git a/docs/jerboa-to-rust.md b/docs/jerboa-to-rust.md
index 4d041b5..0a89380 100644
--- a/docs/jerboa-to-rust.md
+++ b/docs/jerboa-to-rust.md
@@ -87,10 +87,10 @@ Landed:
 - `make-bytevector` is checked as `(Nat -> Bytes)` / `(Nat Nat -> Bytes)` (the
   fill defaults to a literal `0`) and lowers to a fresh owned buffer `vec![(fill)
   as u8; (size) as usize]`; see `tests/fixtures/typed/rust-make-bytevector.ss`.
-  In-place mutation (`bytevector-u8-set!`, `bytevector-copy!`) is the next step
-  and needs the mutable-ownership model that the current Clone-heavy lowering
-  does not yet provide -- the plan is a `let mut` buffer mutated inside an
-  effectful `for` loop (the construct half is now in place).
+  In-place mutation (`bytevector-u8-set!`, `bytevector-copy!`) would need a
+  mutable-ownership model the current Clone-heavy lowering does not provide, but
+  `bytes-build` (below) now covers the buffer-*producing* cases functionally, so
+  true in-place mutation is no longer on the critical path.
 - `string->utf8` / `utf8->string` cross the text/binary boundary. `string->utf8`
   is `(String -> Bytes)` and lowers to `(s).as_bytes().to_vec()`; `utf8->string`
   is `(Bytes -> String)` and lowers to lossy decode
@@ -113,6 +113,15 @@ Landed:
   the per-call clone storms tail recursion would incur. It is enough to write a
   constant-time difference fold and a rolling checksum directly; see
   `tests/fixtures/typed/rust-for-fold.ss`.
+- `bytes-build` produces a `Bytes` value functionally by index: `(bytes-build
+  size (i body))` binds index `i : Nat` over `[0, size)` and lowers to
+  `(0u64..(size)).map(|i| ((body) as u8)).collect::<Vec<u8>>()`. This sidesteps
+  the mutable-ownership problem entirely for buffer-*producing* steps -- the byte
+  at each index is computed independently, so the common crypto kernels (XOR
+  combine / one-time-pad, range copy, and lowercase hex encode) are expressible
+  without `bytevector-u8-set!`. The body type-checks as a numeric value
+  (truncated to `u8`); see `tests/fixtures/typed/rust-bytes-build.ss`, whose
+  `hex-encode` matches the `hex` crate byte-for-byte.
 - Rust `struct`/`enum` generation for Typed Jerboa records and variants,
   including `Clone`, `Debug`, and `PartialEq` derives.
 - Recursive variant field boxing and match rebinding for owned child values.
diff --git a/docs/typed-jerboa.md b/docs/typed-jerboa.md
index e203032..48e323f 100644
--- a/docs/typed-jerboa.md
+++ b/docs/typed-jerboa.md
@@ -250,7 +250,11 @@ Current landing:
   `for` loop (`{ let mut acc = init; for i in (start)..(end) { acc = body; } acc
   }`), the idiomatic typed stand-in for a counting named-let that needs neither
   type variables nor clone-heavy tail recursion (`tests/fixtures/typed/rust-for-fold.ss`).
-  `debug-string` checks one typed
+  `bytes-build` builds a `Bytes` value functionally by index — `(bytes-build
+  size (i body))` binds `i : Nat` over `[0, size)` and lowers to
+  `(0u64..(size)).map(|i| ((body) as u8)).collect::<Vec<u8>>()`, covering XOR
+  combine, range copy, and lowercase hex encode without in-place mutation
+  (`tests/fixtures/typed/rust-bytes-build.ss`). `debug-string` checks one typed
   operand and lowers to Rust `format!("{:?}", ...)`, using the Debug derives on
   generated records and variants. Imported calls and richer forms are reported
   as unsupported. Typed core IR is now produced by the checker and consumed by
diff --git a/lib/jerboa/typed/checker.ss b/lib/jerboa/typed/checker.ss
index 63f5f54..bffdc73 100644
--- a/lib/jerboa/typed/checker.ss
+++ b/lib/jerboa/typed/checker.ss
@@ -1330,6 +1330,46 @@
                    (list size-ir fill-ir) '()))
             (append size-errors fill-errors operand-errors))))))
 
+  (def (infer-bytes-build args env type-names expr)
+    ;; (bytes-build size (idx body)): a fresh Bytes of length `size` whose i-th
+    ;; byte is `body` (numeric, truncated to u8). idx runs [0, size). A
+    ;; functional build-by-index -- no mutable buffer -- so it lowers to a Rust
+    ;; iterator map+collect and fits the value-oriented type system cleanly.
+    (cond
+      [(not (= (length args) 2))
+       (values #f
+         (list (error-at expr 'bad-bytes-build
+                 "bytes-build expects a size and an (index body) clause"
+                 expr)))]
+      [(not (and (expr-list? (cadr args))
+                 (= (expr-length (cadr args)) 2)
+                 (expr-symbol? (expr-car (cadr args)))))
+       (values #f
+         (list (error-at expr 'bad-bytes-build
+                 "bytes-build clause must be (index body)"
+                 expr)))]
+      [else
+       (let ([size-expr (car args)]
+             [idx-name (expr-value (expr-car (cadr args)))]
+             [body-expr (expr-cadr (cadr args))])
+         (let*-values
+           ([(size-ir size-errors) (infer-expression size-expr env type-names)]
+            [(body-ir body-errors)
+             (infer-expression body-expr
+               (extend-env idx-name 'Nat env) type-names)])
+           (let* ([size-errs (operand-type-errors 'numeric
+                               (list (ir-type size-ir)) (list size-expr) size-expr)]
+                  [body-errs (operand-type-errors 'numeric
+                               (list (ir-type body-ir)) (list body-expr) body-expr)]
+                  [ok? (and size-ir body-ir
+                            (null? size-errors) (null? body-errors)
+                            (null? size-errs) (null? body-errs))])
+             (values
+               (and ok?
+                    (make-typed-ir-bytes-build 'Bytes (expr-source expr)
+                      idx-name size-ir body-ir))
+               (append size-errors body-errors size-errs body-errs)))))]))
+
   (def (bad-constructor-arity expr name expected args)
     (list (error-at expr 'bad-call-arity
             "typed constructor arity does not match"
@@ -1668,6 +1708,8 @@
               (infer-make-bytevector args env type-names expr)]
              [(exact->inexact)
               (infer-to-float args env type-names expr)]
+             [(bytes-build)
+              (infer-bytes-build args env type-names expr)]
              [(option-some)
               (infer-option-some args env type-names expr)]
              [(option-none)
diff --git a/lib/jerboa/typed/core.ss b/lib/jerboa/typed/core.ss
index bff8c59..0f8c7fa 100644
--- a/lib/jerboa/typed/core.ss
+++ b/lib/jerboa/typed/core.ss
@@ -47,6 +47,12 @@
     typed-ir-for-fold-range-start typed-ir-for-fold-range-end
     typed-ir-for-fold-body
 
+    typed-ir-bytes-build?
+    make-typed-ir-bytes-build
+    typed-ir-bytes-build-type typed-ir-bytes-build-source
+    typed-ir-bytes-build-var-name typed-ir-bytes-build-size
+    typed-ir-bytes-build-body
+
     typed-ir-match?
     make-typed-ir-match
     typed-ir-match-type typed-ir-match-source
@@ -86,6 +92,11 @@
   ;; [range-start, range-end); body (of acc's type) becomes acc's next value.
   (defstruct typed-ir-for-fold
     (type source acc-name acc-init var-name range-start range-end body))
+  ;; functional buffer build-by-index: produce a Bytes of length `size` whose
+  ;; byte at index var-name (running 0..size) is `body` (a numeric value,
+  ;; truncated to u8). Lowers to (0..size).map(|i| body as u8).collect().
+  (defstruct typed-ir-bytes-build
+    (type source var-name size body))
   (defstruct typed-ir-match
     (type source scrutinee scrutinee-type clauses default))
   (defstruct typed-ir-match-clause (case bindings field-types body))
@@ -138,6 +149,7 @@
         (typed-ir-let? x)
         (typed-ir-if? x)
         (typed-ir-for-fold? x)
+        (typed-ir-bytes-build? x)
         (typed-ir-match? x)
         (typed-ir-call? x)))
 
@@ -149,6 +161,7 @@
       [(typed-ir-let? node) (typed-ir-let-type node)]
       [(typed-ir-if? node) (typed-ir-if-type node)]
       [(typed-ir-for-fold? node) (typed-ir-for-fold-type node)]
+      [(typed-ir-bytes-build? node) (typed-ir-bytes-build-type node)]
       [(typed-ir-match? node) (typed-ir-match-type node)]
       [(typed-ir-call? node) (typed-ir-call-type node)]
       [else (error 'typed-ir-node-type "not a typed IR node" node)]))
@@ -161,6 +174,7 @@
       [(typed-ir-let? node) (typed-ir-let-source node)]
       [(typed-ir-if? node) (typed-ir-if-source node)]
       [(typed-ir-for-fold? node) (typed-ir-for-fold-source node)]
+      [(typed-ir-bytes-build? node) (typed-ir-bytes-build-source node)]
       [(typed-ir-match? node) (typed-ir-match-source node)]
       [(typed-ir-call? node) (typed-ir-call-source node)]
       [else (error 'typed-ir-node-source "not a typed IR node" node)]))
diff --git a/lib/jerboa/typed/rust.ss b/lib/jerboa/typed/rust.ss
index 5f4d78b..d82ebea 100644
--- a/lib/jerboa/typed/rust.ss
+++ b/lib/jerboa/typed/rust.ss
@@ -1432,6 +1432,7 @@
       [(typed-ir-let? ir) (emit-ir-let ir)]
       [(typed-ir-if? ir) (emit-ir-if ir)]
       [(typed-ir-for-fold? ir) (emit-ir-for-fold ir)]
+      [(typed-ir-bytes-build? ir) (emit-ir-bytes-build ir)]
       [(typed-ir-match? ir) (emit-ir-match ir)]
       [(typed-ir-call? ir) (emit-ir-call ir)]
       [else (error 'typed-rust "unsupported IR node" ir)]))
@@ -1482,6 +1483,18 @@
         acc " = " (emit-expression (typed-ir-for-fold-body ir)) "; } "
         acc " }")))
 
+  ;; (bytes-build size (i body)) lowers to a functional iterator map+collect:
+  ;; (0u64..size).map(|i| (body) as u8).collect::<Vec<u8>>(). The 0u64 fixes
+  ;; the index type to u64 so body arithmetic over i type-checks as Nat.
+  (def (emit-ir-bytes-build ir)
+    (let ([var (rust-symbol-name (typed-ir-bytes-build-var-name ir))])
+      (string-append
+        (rust-inline-source-comment (typed-ir-node-source ir))
+        "(0u64..("
+        (emit-expression (typed-ir-bytes-build-size ir)) ")).map(|" var "| (("
+        (emit-expression (typed-ir-bytes-build-body ir))
+        ") as u8)).collect::<Vec<u8>>()")))
+
   (def (emit-ir-match-clause clause)
     (let* ([case-name (typed-ir-match-clause-case clause)]
            [bindings (typed-ir-match-clause-bindings clause)]
diff --git a/tests/fixtures/typed/rust-bytes-build.ss b/tests/fixtures/typed/rust-bytes-build.ss
new file mode 100644
index 0000000..0a3939c
--- /dev/null
+++ b/tests/fixtures/typed/rust-bytes-build.ss
@@ -0,0 +1,26 @@
+(typed-library (sample typed bytes-build)
+  (export repeat-byte xor-bytes hex-encode)
+
+  ;; a buffer of length n with every byte = v (build-by-index, no mutation)
+  (def (repeat-byte (n : Nat) (v : Nat)) : Bytes
+    (bytes-build n (i v)))
+
+  ;; XOR two equal-length buffers -- keystream/one-time-pad combine, the
+  ;; archetypal buffer-producing crypto step.
+  (def (xor-bytes (a : Bytes) (b : Bytes) (n : Nat)) : Bytes
+    (bytes-build n (i (bitwise-xor (bytevector-u8-ref a i)
+                                   (bytevector-u8-ref b i)))))
+
+  ;; map a 0..15 nibble to its lowercase-hex ASCII byte: 0-9 -> '0'..'9' (48),
+  ;; 10-15 -> 'a'..'f' (87 + n).
+  (def (nibble-hex (x : Nat)) : Nat
+    (if (< x 10) (+ 48 x) (+ 87 x)))
+
+  ;; lowercase hex encoding: two output bytes per input byte. Output index j
+  ;; maps to input byte j/2; even j is the high nibble, odd j the low nibble.
+  (def (hex-encode (data : Bytes) (n : Nat)) : Bytes
+    (bytes-build (* 2 n)
+      (j (let ((b (bytevector-u8-ref data (bitwise-arithmetic-shift-right j 1))))
+           (if (= (bitwise-and j 1) 0)
+               (nibble-hex (bitwise-and (bitwise-arithmetic-shift-right b 4) 15))
+               (nibble-hex (bitwise-and b 15))))))))
diff --git a/tests/test-typed-checker.ss b/tests/test-typed-checker.ss
index 01ea603..f0f0ea6 100644
--- a/tests/test-typed-checker.ss
+++ b/tests/test-typed-checker.ss
@@ -761,6 +761,39 @@
          (/ (exact->inexact c) (exact->inexact n)))))
   '())
 
+(test "bytes-build returns Bytes from a numeric body"
+  (error-kinds
+    '(typed-library (body bb-ok)
+       (export f)
+       (def (f (n : Nat) (v : Nat)) : Bytes
+         (bytes-build n (i v)))))
+  '())
+
+(test "bytes-build body may read the index and other buffers"
+  (error-kinds
+    '(typed-library (body bb-xor)
+       (export f)
+       (def (f (a : Bytes) (b : Bytes) (n : Nat)) : Bytes
+         (bytes-build n (i (bitwise-xor (bytevector-u8-ref a i)
+                                        (bytevector-u8-ref b i)))))))
+  '())
+
+(test "bytes-build rejects a non-numeric body"
+  (error-kinds
+    '(typed-library (body bb-bad-body)
+       (export f)
+       (def (f (n : Nat) (s : String)) : Bytes
+         (bytes-build n (i s)))))
+  '(operand-type-mismatch))
+
+(test "bytes-build rejects a malformed clause"
+  (error-kinds
+    '(typed-library (body bb-bad-clause)
+       (export f)
+       (def (f (n : Nat)) : Bytes
+         (bytes-build n n))))
+  '(bad-bytes-build))
+
 (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 17c3bb4..b98aa25 100644
--- a/tests/test-typed-rust.ss
+++ b/tests/test-typed-rust.ss
@@ -248,6 +248,20 @@
 (define float-rust
   (typed-library-form->rust-string float-form))
 
+;; bytes-build: functional buffer build-by-index, lowering to an iterator
+;; map+collect -- the buffer-producing crypto primitive (xor, hex, copy).
+(define bytes-build-form
+  '(typed-library (sample typed bytes-build)
+     (export repeat-byte xor-bytes)
+     (def (repeat-byte (n : Nat) (v : Nat)) : Bytes
+       (bytes-build n (i v)))
+     (def (xor-bytes (a : Bytes) (b : Bytes) (n : Nat)) : Bytes
+       (bytes-build n (i (bitwise-xor (bytevector-u8-ref a i)
+                                      (bytevector-u8-ref b i)))))))
+
+(define bytes-build-rust
+  (typed-library-form->rust-string bytes-build-form))
+
 (define return-form
   '(typed-library (sample typed return-values)
      (export greeting echo-text echo-bytes)
@@ -555,6 +569,15 @@
        (substring? float-rust "(0.0f64 - (p * ((p).log2())))"))
   #t)
 
+(test "rust lowers bytes-build to an iterator map+collect"
+  (and (substring? bytes-build-rust "pub fn repeat_byte(n: u64, v: u64) -> Vec<u8>")
+       (substring? bytes-build-rust
+         "(0u64..(n)).map(|i| ((v) as u8)).collect::<Vec<u8>>()")
+       ;; the xor combine: each output byte is a function of the index
+       (substring? bytes-build-rust
+         "(0u64..(n)).map(|i| ((((a[(i) as usize] as u64) ^ (b[(i) as usize] as u64))) as u8)).collect::<Vec<u8>>()"))
+  #t)
+
 (test "rust emits handle registry for record and variant ABI"
   (and (substring? handle-rust "fn jt_store_handle<T: Any + Send>(value: T) -> u64")
        (substring? handle-rust "fn jt_clone_handle<T: Any + Clone>(id: u64) -> Option<T>")