typed-rust: lower string->utf8 and utf8->string
ober
af5e095d662ec9864396445c9ff64c99dde18b9a
--- a/docs/jerboa-to-rust.md +++ b/docs/jerboa-to-rust.md @@ -87,6 +87,11 @@ Landed: (`make-bytevector`) and in-place mutation (`bytevector-u8-set!`, `bytevector-copy!`) are not yet lowered -- they need the mutable-ownership model that the current Clone-heavy lowering does not yet provide. +- `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 + `String::from_utf8_lossy(&(buf)).into_owned()` (matches the String input ABI, + never panics). Both borrow rather than move their source. - 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 @@ -234,7 +234,11 @@ Current landing: `(Bytes Nat -> Nat)` and lowers to indexed access `buf[(i) as usize] as u64`; combined with the bitwise primitives and same-module recursion, this is enough to express a constant-time `equal?` (XOR-accumulate fold) entirely in Typed - Jerboa — see `tests/fixtures/typed/rust-ct-equal.ss`. `debug-string` checks one typed + Jerboa — see `tests/fixtures/typed/rust-ct-equal.ss`. `string->utf8` + (`String -> Bytes`) and `utf8->string` (`Bytes -> String`) cross the + text/binary boundary, lowering to `as_bytes().to_vec()` and a lossy + `from_utf8_lossy` decode, so a constant-time auth-token comparison ports too + (`tests/fixtures/typed/rust-utf8.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 @@ -365,7 +365,13 @@ 'bytevector-length '())) (cons 'bytevector-u8-ref (make-typed-call-sig (list 'Bytes 'Nat) 'Nat '() - 'bytevector-u8-ref '())))) + 'bytevector-u8-ref '())) + (cons 'string->utf8 + (make-typed-call-sig (list 'String) 'Bytes '() + 'string->utf8 '())) + (cons 'utf8->string + (make-typed-call-sig (list 'Bytes) 'String '() + 'utf8->string '())))) (def (field-types fields) (map typed-field-type fields)) --- a/lib/jerboa/typed/rust.ss +++ b/lib/jerboa/typed/rust.ss @@ -1302,6 +1302,26 @@ (emit-expression (cadr args)) ") as usize] as u64)")) + ;; borrow the String's bytes and copy into an owned Vec<u8> (no move, so the + ;; source String stays usable under the Clone-heavy ownership model) + (def (emit-string->utf8 args) + (unless (= (length args) 1) + (error 'typed-rust "string->utf8 expects one operand" args)) + (string-append + "(" + (emit-expression (car args)) + ").as_bytes().to_vec()")) + + ;; lossy decode (never panics; invalid sequences become U+FFFD), matching the + ;; String input ABI's from_utf8_lossy and avoiding a move of the buffer + (def (emit-utf8->string args) + (unless (= (length args) 1) + (error 'typed-rust "utf8->string expects one operand" args)) + (string-append + "String::from_utf8_lossy(&(" + (emit-expression (car args)) + ")).into_owned()")) + (def (emit-equality args) (unless (= (length args) 2) (error 'typed-rust "equal? expects two operands" args)) @@ -1505,6 +1525,8 @@ [(string-append) (emit-string-append args)] [(bytevector-length) (emit-bytevector-length args)] [(bytevector-u8-ref) (emit-bytevector-u8-ref args)] + [(string->utf8) (emit-string->utf8 args)] + [(utf8->string) (emit-utf8->string args)] [(debug-string) (emit-debug-string args)] [(record-ctor) (let ([record (lookup-name (ir-call-info-ref ir 'record) new file mode 100644 --- /dev/null +++ b/tests/fixtures/typed/rust-utf8.ss @@ -0,0 +1,26 @@ +(typed-library (sample typed utf8-auth) + (export encode decode round-trip + fold-diff token-bytes-equal?) + + ;; UTF-8 codec at the text/binary boundary + (def (encode (s : String)) : Bytes + (string->utf8 s)) + + (def (decode (data : Bytes)) : String + (utf8->string data)) + + (def (round-trip (s : String)) : String + (utf8->string (string->utf8 s))) + + ;; constant-time XOR-accumulate over two byte buffers + (def (fold-diff (a : Bytes) (b : Bytes) (i : Nat) (n : Nat) (acc : Nat)) : Nat + (if (>= i n) + acc + (fold-diff a b (+ i 1) n + (bitwise-ior acc (bitwise-xor (bytevector-u8-ref a i) + (bytevector-u8-ref b i)))))) + + ;; compare a presented token string against expected bytes in constant time: + ;; encode the text to UTF-8, then fold the byte differences (0 iff equal). + (def (token-bytes-equal? (presented : String) (expected : Bytes) (n : Nat)) : Bool + (= (fold-diff (string->utf8 presented) expected 0 n 0) 0))) --- a/tests/test-typed-checker.ss +++ b/tests/test-typed-checker.ss @@ -621,6 +621,32 @@ (bytevector-u8-ref data)))) '(bad-call-arity)) +(test "builtin string->utf8 returns Bytes, utf8->string returns String" + (error-kinds + '(typed-library (body utf8-ok) + (export enc dec) + (def (enc (s : String)) : Bytes + (string->utf8 s)) + (def (dec (b : Bytes)) : String + (utf8->string b)))) + '()) + +(test "builtin string->utf8 rejects non-String" + (error-kinds + '(typed-library (body utf8-enc-bad) + (export f) + (def (f (b : Bytes)) : Bytes + (string->utf8 b)))) + '(argument-type-mismatch)) + +(test "builtin utf8->string rejects non-Bytes" + (error-kinds + '(typed-library (body utf8-dec-bad) + (export f) + (def (f (s : String)) : String + (utf8->string s)))) + '(argument-type-mismatch)) + (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 @@ -184,6 +184,21 @@ (define bvref-rust (typed-library-form->rust-string bvref-form)) +;; string <-> bytes conversion: the text/binary boundary crypto code needs +;; (hash a message, decode a token) without moving the source value. +(define utf8-form + '(typed-library (sample typed utf8) + (export encode decode round-trip) + (def (encode (s : String)) : Bytes + (string->utf8 s)) + (def (decode (data : Bytes)) : String + (utf8->string data)) + (def (round-trip (s : String)) : String + (utf8->string (string->utf8 s))))) + +(define utf8-rust + (typed-library-form->rust-string utf8-form)) + (define return-form '(typed-library (sample typed return-values) (export greeting echo-text echo-bytes) @@ -451,6 +466,16 @@ "(acc | ((a[(i) as usize] as u64) ^ (b[(i) as usize] as u64)))")) #t) +(test "rust lowers string<->bytes utf8 conversions" + (and (substring? utf8-rust "pub fn encode(s: String) -> Vec<u8>") + (substring? utf8-rust "(s).as_bytes().to_vec()") + (substring? utf8-rust "pub fn decode(data: Vec<u8>) -> String") + (substring? utf8-rust "String::from_utf8_lossy(&(data)).into_owned()") + ;; nested round-trip: decode(encode(s)) + (substring? utf8-rust + "String::from_utf8_lossy(&((s).as_bytes().to_vec())).into_owned()")) + #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>")