typed-rust: add bytes-build for functional buffer production
ober
cca2b094b62e27317f69fe2ee16f522f012cc354
--- 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. --- 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 --- 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) --- 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)])) --- 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)] new file mode 100644 --- /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)))))))) --- 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) --- 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>")