nandi/rustnimpublic Fork 0
afb2a6e
Commits
Clone
git clone https://git.rickub.com/nandi/rustnim.git
git clone ssh://git@rickub.com/nandi/rustnim.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Add display.rs and the alloc half: all of base16ct now goes through

Formatting impls now accumulate. A `fmt` body may write more than once --
base16ct's UpperHex writes one byte per iteration -- so a formatter write
appends to the proc's string result rather than assigning it, which is what
the previous model did and what limited it to a single write or a `match`.
Writing into a string cannot fail, so `?` on a formatter write is a no-op;
`?` on anything else inside a `fmt` body can fail, and since `format!` panics
when a formatting impl returns an error, that is what the error branch does,
with std's own message.

`{:x}` on an integer formats its two's-complement bit pattern; on any other
type it calls that type's own LowerHex impl. Those are different operations,
so the argument's type now selects between them and a radix format on an
argument of unknown type is rejected rather than guessed.

The assert family -- assert!, assert_eq!, assert_ne! and the debug_assert*
forms -- lowers to a check and a panic carrying both operands, as Rust's
message does. debug_assert* fires in debug builds, which is the profile this
project models, so it lowers the same as assert.

Three bugs, all of the same kind: something borrowed was being copied.
A `let` binding a borrow was calling `owned()`, turning a view of the
caller's buffer into a `seq` -- right bytes, broken aliasing. Struct fields
of `&[T]` type were doing the same. And `str::from_utf8_unchecked` (borrows,
yields a view) had been conflated with `String::from_utf8_unchecked`
(consumes, yields an owned string); they share a name and are different
operations, so the qualifier now decides and an unqualified call is rejected.

A correction to DESIGN.md: it claimed Nim cannot put a view inside an object.
It can, including as an object field, with aliasing preserved -- both probed.
The real constraint is narrower: Nim will not let a `let` borrow out of a
local, so `.unwrap()`/`.expect()` on a Result holding a view is expanded
inline and binds an alias instead of materialising anything.

Milestone 1 is reached. Every source file of base16ct 1.0.0 -- error.rs,
lower.rs, upper.rs, mixed.rs, display.rs -- transpiles byte-for-byte as
published on crates.io, with lib.rs's decoded_len, encoded_len and
decode_inner verbatim and the alloc half enabled, and the output is
byte-identical to rustc's across decode, encode, encode_str, decode_vec,
encode_string and HexDisplay. That is the crate the transpiler in findings/
emitted empty files for while exiting 0.

33 differential cases and 6 integration tests, all green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandithebull committed 2026-09-18T20:37:24-07:00 Browse files
afb2a6e parent: 0a375d8
modified DESIGN.md +58 -43
@@ -2,9 +2,11 @@
22
33 ## Status
44
5-**Milestone 1 is reached, and then some.** Four of base16ct's six modules go
6-through byte-for-byte. 32 differential cases, 28 behavioural and 4 rejections,
7-plus 6 unit/integration tests. All green. Run `cargo test`.
5+**Milestone 1 is reached: all of `base16ct` goes through.** Every one of its
6+source files transpiles byte-for-byte as published, `alloc` half included, and
7+its decode and encode output is byte-identical to rustc's. 33 differential
8+cases, 29 behavioural and 4 rejections, plus 6 unit/integration tests. All
9+green. Run `cargo test`.
810
911 Passing today: functions, `impl` methods, trait impls (formatting traits and
1012 `From`), structs, enums (C-like and data-carrying), `Option`/`Result` with
@@ -105,11 +107,22 @@ loop rather than on each iteration.
105107
106108 A `Display` impl becomes `proc rsDisplay(self: T): string`. Rust's `Formatter`
107109 is a sink and the observable result of `{}` is exactly the bytes written into
108-it, so every write through the formatter produces that string and the existing
109-`match`/trailing-expression machinery assembles it. A `fmt` body that does
110-anything else with the formatter — padding, precision, `debug_struct` — is
111-rejected, because those change the output and this model does not carry them.
112-`Debug`, `LowerHex`, `UpperHex`, `Binary` and `Octal` work the same way.
110+it, so a write through the formatter **appends** to that string — a `fmt` body
111+may write repeatedly, and `UpperHex` writes once per byte in a loop. A body
112+that does anything else with the formatter — padding, precision,
113+`debug_struct` — is rejected, because those change the output and this model
114+does not carry them. `Debug`, `LowerHex`, `UpperHex`, `Binary` and `Octal`
115+work the same way.
116+
117+Writing into a string cannot fail, so `?` on a formatter write is a no-op. `?`
118+on anything else inside a `fmt` body *can* fail, and `format!` panics when a
119+formatting impl returns an error — so that is what the error branch does, with
120+std's own message.
121+
122+`{:x}` on an integer formats its two's-complement bit pattern; on any other
123+type it calls that type's own `LowerHex` impl. Those are different operations,
124+so a radix format on an argument of unknown type is rejected rather than
125+guessed.
113126
114127 `impl From<A> for B` becomes a conversion proc that `.into()` resolves
115128 through. A marker trait with no items generates nothing: we do not model trait
@@ -149,11 +162,24 @@ visible in the original buffer. That was probed against Nim 2.2.4 before being
149162 relied on, because copying into a `seq` would print the right bytes while
150163 silently changing aliasing.
151164
152-`s.get(a..b)` is the one place this leaks. It is an `Option<&[T]>`, and Nim
153-cannot put a view inside an object, so there is no value to hand back. Instead
154-the view and its validity condition travel together through `ok_or` until a
155-`?` or `unwrap` resolves them into a bounds check plus a binding. Keeping such
156-an `Option` in a variable is rejected with a message saying so.
165+Nim does allow a view inside an object and inside an object *field* — both
166+probed, both preserving aliasing — so `Result<&[u8], E>` and
167+`HexDisplay<'a>(&'a [u8])` both work. (An earlier version of this document
168+claimed otherwise; that was wrong.)
169+
170+Two real constraints remain. Nim will not let a `let` borrow out of a local,
171+so `.unwrap()`/`.expect()` on a `Result` holding a view is expanded inline and
172+the binding becomes an alias — a view is a reference, so there is nothing to
173+materialise, and the substituted expression is a plain field access that
174+re-evaluates nothing. And `s.get(a..b)` is an `Option` of a view whose
175+*validity* is what matters: the view and its condition travel together through
176+`ok_or` until a `?` or `unwrap` resolves them into a bounds check plus a
177+binding. Keeping such an `Option` in a variable is rejected with a message
178+saying so.
179+
180+A `let` binding a borrow keeps the view rather than copying into a `seq`:
181+`let res = encode(..)?` names the caller's buffer, and copying would print the
182+right bytes while silently breaking the aliasing.
157183
158184 ### Closures and `unsafe`
159185
@@ -257,6 +283,9 @@ runner) rather than a wrong answer.
257283 modules declaring the same type name would collide. Relatedly, a crate's
258284 own `type Result<T>` is told apart from the builtin `Result<T, E>` by
259285 arity, which is not how Rust resolves it.
286+9. `String::from_utf8_unchecked` copies, because Nim's `string` is an owned
287+ value. Rust's consumes the `Vec` without copying. Observably the same from
288+ the caller, but it is a copy where Rust has none.
260289
261290 ## Testing: differential, not golden
262291
@@ -312,43 +341,29 @@ or any parent, or via `RUSTNIM_NIM`.
312341 Transpile `base16ct` 1.0.0 — the crate the other transpiler failed on — and
313342 have its decoder produce byte-identical output to the Rust original.
314343
315-**Reached, for four of the crate's six modules.**
316-`tests/cases/026-base16ct-crate/` transpiles base16ct's `error.rs`,
317-`lower.rs`, `upper.rs` and `mixed.rs` **byte-for-byte as published on
318-crates.io** — verified with `cmp`, not by eye — together with `lib.rs`'s
319-`decoded_len`, `encoded_len` and `decode_inner` verbatim. Output is
320-byte-identical to rustc's:
344+**Reached.** `tests/cases/026-base16ct-crate/` transpiles **every source file
345+of base16ct 1.0.0** — `error.rs`, `lower.rs`, `upper.rs`, `mixed.rs` and
346+`display.rs`, each byte-for-byte as published on crates.io, verified with
347+`cmp` rather than by eye — together with `lib.rs`'s `decoded_len`,
348+`encoded_len` and `decode_inner` verbatim. The `alloc` half is on, via
349+`--cfg feature=alloc`. Output is byte-identical to rustc's:
321350
322351 ```
323352 lower ok abcd1234 len=4 decode: lower, upper, mixed
324-mixed-m ok abcd1234 len=4
325-upper ok abcd1234 len=4
326353 upper-rej err InvalidEncoding ... upper correctly rejects lowercase
327354 oddlen err InvalidLength / invalid Base16 length <- Debug and Display
328355 encode ok 6162636431323334 len=8 encode, both cases
329-encode-up ok 4142434431323334 len=8
330356 encode_str ok abcd1234 len=8 closure over unsafe, borrowed &str
357+Ok([171, 205, 18, 52]) decode_vec \
358+abcd1234 encode_string > the alloc half
359+ABCD1234 abcd1234 HexDisplay {:X} {:x}
331360 ```
332361
333-`decode_inner` goes through as written: `dst.get_mut(..decoded_len(src)?)`,
334-`src.chunks_exact(2).zip(dst.iter_mut())`, `*dst = byte as u8`, and the
335-returned `&'a [u8]` view into the caller's buffer. The `Display` line in that
336-output comes from the crate's own `impl fmt::Display for Error`.
362+Everything lowers as written: `dst.get_mut(..decoded_len(src)?)`,
363+`src.chunks_exact(2).zip(dst.iter_mut())`, `*dst = byte as u8`, the returned
364+`&'a [u8]` view into the caller's buffer, `encode(src, dst).map(|r| unsafe {
365+core::str::from_utf8_unchecked(r) })`, and `HexDisplay`'s `UpperHex` impl
366+writing once per byte into the formatter.
337367
338-`encode_str` goes through as written — `encode(src, dst).map(|r| unsafe {
339-core::str::from_utf8_unchecked(r) })` — returning a `&str` view of the bytes
340-just written, not a copy.
341-
342-### Still to do for the whole crate
343-
344-Two modules remain, and both are close:
345-
346-- `display.rs` needs the formatter to *accumulate* writes. Our model maps a
347- formatting impl to a proc returning the string it wrote, which handles one
348- write or a `match` over several; `UpperHex` here calls `f.write_str(..)?` in
349- a loop, so the writes have to append instead of being the value. It also
350- needs `HexDisplay<'a>(pub &'a [u8])` — a struct *field* of view type, the
351- same wall as `Option<&[T]>`.
352-- The `alloc` half (`--cfg feature=alloc`) now gets as far as
353- `debug_assert_eq!`, then needs `String::from_utf8_unchecked` to produce an
354- owned `String` from a `Vec<u8>`.
368+This is the crate whose six files the transpiler in `findings/` emitted empty
369+output for, while exiting 0.
@@ -2,9 +2,11 @@
2 2
3 ## Status3 ## Status
4 4
5-**Milestone 1 is reached, and then some.** Four of base16ct's six modules go5+**Milestone 1 is reached: all of `base16ct` goes through.** Every one of its
6-through byte-for-byte. 32 differential cases, 28 behavioural and 4 rejections,6+source files transpiles byte-for-byte as published, `alloc` half included, and
7-plus 6 unit/integration tests. All green. Run `cargo test`.7+its decode and encode output is byte-identical to rustc's. 33 differential
8+cases, 29 behavioural and 4 rejections, plus 6 unit/integration tests. All
9+green. Run `cargo test`.
8 10
9 Passing today: functions, `impl` methods, trait impls (formatting traits and11 Passing today: functions, `impl` methods, trait impls (formatting traits and
10 `From`), structs, enums (C-like and data-carrying), `Option`/`Result` with12 `From`), structs, enums (C-like and data-carrying), `Option`/`Result` with
@@ -105,11 +107,22 @@ loop rather than on each iteration.
105 107
106 A `Display` impl becomes `proc rsDisplay(self: T): string`. Rust's `Formatter`108 A `Display` impl becomes `proc rsDisplay(self: T): string`. Rust's `Formatter`
107 is a sink and the observable result of `{}` is exactly the bytes written into109 is a sink and the observable result of `{}` is exactly the bytes written into
108-it, so every write through the formatter produces that string and the existing110+it, so a write through the formatter **appends** to that string — a `fmt` body
109-`match`/trailing-expression machinery assembles it. A `fmt` body that does111+may write repeatedly, and `UpperHex` writes once per byte in a loop. A body
110-anything else with the formatter — padding, precision, `debug_struct` — is112+that does anything else with the formatter — padding, precision,
111-rejected, because those change the output and this model does not carry them.113+`debug_struct` — is rejected, because those change the output and this model
112-`Debug`, `LowerHex`, `UpperHex`, `Binary` and `Octal` work the same way.114+does not carry them. `Debug`, `LowerHex`, `UpperHex`, `Binary` and `Octal`
115+work the same way.
116+
117+Writing into a string cannot fail, so `?` on a formatter write is a no-op. `?`
118+on anything else inside a `fmt` body *can* fail, and `format!` panics when a
119+formatting impl returns an error — so that is what the error branch does, with
120+std's own message.
121+
122+`{:x}` on an integer formats its two's-complement bit pattern; on any other
123+type it calls that type's own `LowerHex` impl. Those are different operations,
124+so a radix format on an argument of unknown type is rejected rather than
125+guessed.
113 126
114 `impl From<A> for B` becomes a conversion proc that `.into()` resolves127 `impl From<A> for B` becomes a conversion proc that `.into()` resolves
115 through. A marker trait with no items generates nothing: we do not model trait128 through. A marker trait with no items generates nothing: we do not model trait
@@ -149,11 +162,24 @@ visible in the original buffer. That was probed against Nim 2.2.4 before being
149 relied on, because copying into a `seq` would print the right bytes while162 relied on, because copying into a `seq` would print the right bytes while
150 silently changing aliasing.163 silently changing aliasing.
151 164
152-`s.get(a..b)` is the one place this leaks. It is an `Option<&[T]>`, and Nim165+Nim does allow a view inside an object and inside an object *field* — both
153-cannot put a view inside an object, so there is no value to hand back. Instead166+probed, both preserving aliasing — so `Result<&[u8], E>` and
154-the view and its validity condition travel together through `ok_or` until a167+`HexDisplay<'a>(&'a [u8])` both work. (An earlier version of this document
155-`?` or `unwrap` resolves them into a bounds check plus a binding. Keeping such168+claimed otherwise; that was wrong.)
156-an `Option` in a variable is rejected with a message saying so.169+
170+Two real constraints remain. Nim will not let a `let` borrow out of a local,
171+so `.unwrap()`/`.expect()` on a `Result` holding a view is expanded inline and
172+the binding becomes an alias — a view is a reference, so there is nothing to
173+materialise, and the substituted expression is a plain field access that
174+re-evaluates nothing. And `s.get(a..b)` is an `Option` of a view whose
175+*validity* is what matters: the view and its condition travel together through
176+`ok_or` until a `?` or `unwrap` resolves them into a bounds check plus a
177+binding. Keeping such an `Option` in a variable is rejected with a message
178+saying so.
179+
180+A `let` binding a borrow keeps the view rather than copying into a `seq`:
181+`let res = encode(..)?` names the caller's buffer, and copying would print the
182+right bytes while silently breaking the aliasing.
157 183
158 ### Closures and `unsafe`184 ### Closures and `unsafe`
159 185
@@ -257,6 +283,9 @@ runner) rather than a wrong answer.
257 modules declaring the same type name would collide. Relatedly, a crate's283 modules declaring the same type name would collide. Relatedly, a crate's
258 own `type Result<T>` is told apart from the builtin `Result<T, E>` by284 own `type Result<T>` is told apart from the builtin `Result<T, E>` by
259 arity, which is not how Rust resolves it.285 arity, which is not how Rust resolves it.
286+9. `String::from_utf8_unchecked` copies, because Nim's `string` is an owned
287+ value. Rust's consumes the `Vec` without copying. Observably the same from
288+ the caller, but it is a copy where Rust has none.
260 289
261 ## Testing: differential, not golden290 ## Testing: differential, not golden
262 291
@@ -312,43 +341,29 @@ or any parent, or via `RUSTNIM_NIM`.
312 Transpile `base16ct` 1.0.0 — the crate the other transpiler failed on — and341 Transpile `base16ct` 1.0.0 — the crate the other transpiler failed on — and
313 have its decoder produce byte-identical output to the Rust original.342 have its decoder produce byte-identical output to the Rust original.
314 343
315-**Reached, for four of the crate's six modules.**344+**Reached.** `tests/cases/026-base16ct-crate/` transpiles **every source file
316-`tests/cases/026-base16ct-crate/` transpiles base16ct's `error.rs`,345+of base16ct 1.0.0** — `error.rs`, `lower.rs`, `upper.rs`, `mixed.rs` and
317-`lower.rs`, `upper.rs` and `mixed.rs` **byte-for-byte as published on346+`display.rs`, each byte-for-byte as published on crates.io, verified with
318-crates.io** — verified with `cmp`, not by eye — together with `lib.rs`'s347+`cmp` rather than by eye — together with `lib.rs`'s `decoded_len`,
319-`decoded_len`, `encoded_len` and `decode_inner` verbatim. Output is348+`encoded_len` and `decode_inner` verbatim. The `alloc` half is on, via
320-byte-identical to rustc's:349+`--cfg feature=alloc`. Output is byte-identical to rustc's:
321 350
322 ```351 ```
323 lower ok abcd1234 len=4 decode: lower, upper, mixed352 lower ok abcd1234 len=4 decode: lower, upper, mixed
324-mixed-m ok abcd1234 len=4
325-upper ok abcd1234 len=4
326 upper-rej err InvalidEncoding ... upper correctly rejects lowercase353 upper-rej err InvalidEncoding ... upper correctly rejects lowercase
327 oddlen err InvalidLength / invalid Base16 length <- Debug and Display354 oddlen err InvalidLength / invalid Base16 length <- Debug and Display
328 encode ok 6162636431323334 len=8 encode, both cases355 encode ok 6162636431323334 len=8 encode, both cases
329-encode-up ok 4142434431323334 len=8
330 encode_str ok abcd1234 len=8 closure over unsafe, borrowed &str356 encode_str ok abcd1234 len=8 closure over unsafe, borrowed &str
357+Ok([171, 205, 18, 52]) decode_vec \
358+abcd1234 encode_string > the alloc half
359+ABCD1234 abcd1234 HexDisplay {:X} {:x}
331 ```360 ```
332 361
333-`decode_inner` goes through as written: `dst.get_mut(..decoded_len(src)?)`,362+Everything lowers as written: `dst.get_mut(..decoded_len(src)?)`,
334-`src.chunks_exact(2).zip(dst.iter_mut())`, `*dst = byte as u8`, and the363+`src.chunks_exact(2).zip(dst.iter_mut())`, `*dst = byte as u8`, the returned
335-returned `&'a [u8]` view into the caller's buffer. The `Display` line in that364+`&'a [u8]` view into the caller's buffer, `encode(src, dst).map(|r| unsafe {
336-output comes from the crate's own `impl fmt::Display for Error`.365+core::str::from_utf8_unchecked(r) })`, and `HexDisplay`'s `UpperHex` impl
366+writing once per byte into the formatter.
337 367
338-`encode_str` goes through as written — `encode(src, dst).map(|r| unsafe {368+This is the crate whose six files the transpiler in `findings/` emitted empty
339-core::str::from_utf8_unchecked(r) })` — returning a `&str` view of the bytes369+output for, while exiting 0.
340-just written, not a copy.
341-
342-### Still to do for the whole crate
343-
344-Two modules remain, and both are close:
345-
346-- `display.rs` needs the formatter to *accumulate* writes. Our model maps a
347- formatting impl to a proc returning the string it wrote, which handles one
348- write or a `match` over several; `UpperHex` here calls `f.write_str(..)?` in
349- a loop, so the writes have to append instead of being the value. It also
350- needs `HexDisplay<'a>(pub &'a [u8])` — a struct *field* of view type, the
351- same wall as `Option<&[T]>`.
352-- The `alloc` half (`--cfg feature=alloc`) now gets as far as
353- `debug_assert_eq!`, then needs `String::from_utf8_unchecked` to produce an
354- owned `String` from a `Vec<u8>`.
modified README.md +14 -12
@@ -44,8 +44,9 @@ into a single index loop where each binding is an lvalue into the original
4444 container, so `*d = v` through `iter_mut()` reaches the caller's slice.
4545 Borrowed slices are views, not copies, including as return types.
4646
47-Closures and `unsafe` blocks, and `&str` as a borrowed view rather than an
48-owned copy. Modules: pass the crate root first and each further file after it,
47+Closures and `unsafe` blocks, `&str` as a borrowed view rather than an owned
48+copy, formatting impls whose `fmt` body writes repeatedly, and the
49+`assert!`/`assert_eq!`/`debug_assert*` family. Modules: pass the crate root first and each further file after it,
4950 and items are scoped by module, so `lower::decode` and `mixed::decode` stay
5051 distinct.
5152
@@ -56,16 +57,17 @@ any standard-library method that isn't mapped.
5657
5758 ## base16ct
5859
59-`tests/cases/026-base16ct-crate/` transpiles four of base16ct 1.0.0's six
60-modules — `error.rs`, `lower.rs`, `upper.rs`, `mixed.rs` — byte-for-byte as
61-published on crates.io, plus `lib.rs`'s `decoded_len`, `encoded_len` and
62-`decode_inner` verbatim. Decoding and encoding are byte-identical to rustc's,
63-including `encode_str`, which is a closure over an `unsafe` block returning a
64-borrowed `&str` view of the bytes just written. That is the crate the
65-transpiler in [`findings/`](findings/) emitted an empty file for.
66-
67-`display.rs` and the `alloc` half are not through yet;
68-[`DESIGN.md`](DESIGN.md) says exactly what each still needs.
60+**It goes through.** `tests/cases/026-base16ct-crate/` transpiles every source
61+file of base16ct 1.0.0 — `error.rs`, `lower.rs`, `upper.rs`, `mixed.rs`,
62+`display.rs` — byte-for-byte as published on crates.io, plus `lib.rs`'s
63+`decoded_len`, `encoded_len` and `decode_inner` verbatim, with the `alloc`
64+half enabled. Decoding and encoding are byte-identical to rustc's, including
65+`encode_str` (a closure over an `unsafe` block returning a borrowed `&str`
66+view of the bytes just written) and `HexDisplay`, whose `UpperHex` impl writes
67+once per byte into the formatter.
68+
69+That is the crate the transpiler in [`findings/`](findings/) emitted an empty
70+file for, while exiting 0.
6971
7072 ## Tests
7173
@@ -44,8 +44,9 @@ into a single index loop where each binding is an lvalue into the original
44 container, so `*d = v` through `iter_mut()` reaches the caller's slice.44 container, so `*d = v` through `iter_mut()` reaches the caller's slice.
45 Borrowed slices are views, not copies, including as return types.45 Borrowed slices are views, not copies, including as return types.
46 46
47-Closures and `unsafe` blocks, and `&str` as a borrowed view rather than an47+Closures and `unsafe` blocks, `&str` as a borrowed view rather than an owned
48-owned copy. Modules: pass the crate root first and each further file after it,48+copy, formatting impls whose `fmt` body writes repeatedly, and the
49+`assert!`/`assert_eq!`/`debug_assert*` family. Modules: pass the crate root first and each further file after it,
49 and items are scoped by module, so `lower::decode` and `mixed::decode` stay50 and items are scoped by module, so `lower::decode` and `mixed::decode` stay
50 distinct.51 distinct.
51 52
@@ -56,16 +57,17 @@ any standard-library method that isn't mapped.
56 57
57 ## base16ct58 ## base16ct
58 59
59-`tests/cases/026-base16ct-crate/` transpiles four of base16ct 1.0.0's six60+**It goes through.** `tests/cases/026-base16ct-crate/` transpiles every source
60-modules — `error.rs`, `lower.rs`, `upper.rs`, `mixed.rs` — byte-for-byte as61+file of base16ct 1.0.0 — `error.rs`, `lower.rs`, `upper.rs`, `mixed.rs`,
61-published on crates.io, plus `lib.rs`'s `decoded_len`, `encoded_len` and62+`display.rs` — byte-for-byte as published on crates.io, plus `lib.rs`'s
62-`decode_inner` verbatim. Decoding and encoding are byte-identical to rustc's,63+`decoded_len`, `encoded_len` and `decode_inner` verbatim, with the `alloc`
63-including `encode_str`, which is a closure over an `unsafe` block returning a64+half enabled. Decoding and encoding are byte-identical to rustc's, including
64-borrowed `&str` view of the bytes just written. That is the crate the65+`encode_str` (a closure over an `unsafe` block returning a borrowed `&str`
65-transpiler in [`findings/`](findings/) emitted an empty file for.66+view of the bytes just written) and `HexDisplay`, whose `UpperHex` impl writes
66-67+once per byte into the formatter.
67-`display.rs` and the `alloc` half are not through yet;68+
68-[`DESIGN.md`](DESIGN.md) says exactly what each still needs.69+That is the crate the transpiler in [`findings/`](findings/) emitted an empty
70+file for, while exiting 0.
69 71
70 ## Tests72 ## Tests
71 73
modified src/fmt.rs +17 -2
@@ -116,9 +116,14 @@ fn parse_spec(s: &str) -> Result<Spec, String> {
116116 }
117117
118118 /// Build the Nim expression for one argument, given its already-lowered value.
119-pub fn render_arg(value: &str, spec: &Spec) -> String {
119+///
120+/// `integer` says whether the value is one of Nim's integer types. `{:x}` on
121+/// an integer formats its two's-complement bit pattern; on anything else it is
122+/// a call to that type's own `LowerHex`/`UpperHex` impl, which is a different
123+/// operation and a different proc.
124+pub fn render_arg(value: &str, spec: &Spec, integer: bool) -> String {
120125 let core = match spec.radix {
121- Some(r) => format!(
126+ Some(r) if integer => format!(
122127 "rsRadix({}, {}, {})",
123128 value,
124129 match r {
@@ -128,6 +133,16 @@ pub fn render_arg(value: &str, spec: &Spec) -> String {
128133 },
129134 r == 'X'
130135 ),
136+ Some(r) => format!(
137+ "{}({})",
138+ match r {
139+ 'x' => "rsLowerHex",
140+ 'X' => "rsUpperHex",
141+ 'b' => "rsBinary",
142+ _ => "rsOctal",
143+ },
144+ value
145+ ),
131146 None if spec.debug => format!("rsDebug({value})"),
132147 None => format!("rsDisplay({value})"),
133148 };
@@ -116,9 +116,14 @@ fn parse_spec(s: &str) -> Result<Spec, String> {
116 }116 }
117 117
118 /// Build the Nim expression for one argument, given its already-lowered value.118 /// Build the Nim expression for one argument, given its already-lowered value.
119-pub fn render_arg(value: &str, spec: &Spec) -> String {119+///
120+/// `integer` says whether the value is one of Nim's integer types. `{:x}` on
121+/// an integer formats its two's-complement bit pattern; on anything else it is
122+/// a call to that type's own `LowerHex`/`UpperHex` impl, which is a different
123+/// operation and a different proc.
124+pub fn render_arg(value: &str, spec: &Spec, integer: bool) -> String {
120 let core = match spec.radix {125 let core = match spec.radix {
121- Some(r) => format!(126+ Some(r) if integer => format!(
122 "rsRadix({}, {}, {})",127 "rsRadix({}, {}, {})",
123 value,128 value,
124 match r {129 match r {
@@ -128,6 +133,16 @@ pub fn render_arg(value: &str, spec: &Spec) -> String {
128 },133 },
129 r == 'X'134 r == 'X'
130 ),135 ),
136+ Some(r) => format!(
137+ "{}({})",
138+ match r {
139+ 'x' => "rsLowerHex",
140+ 'X' => "rsUpperHex",
141+ 'b' => "rsBinary",
142+ _ => "rsOctal",
143+ },
144+ value
145+ ),
131 None if spec.debug => format!("rsDebug({value})"),146 None if spec.debug => format!("rsDebug({value})"),
132 None => format!("rsDisplay({value})"),147 None => format!("rsDisplay({value})"),
133 };148 };
modified src/lower.rs +208 -31
@@ -474,7 +474,12 @@ impl Lowerer {
474474 Some(id) => id.to_string(),
475475 None => format!("f{i}"), // tuple struct
476476 };
477- fields.push((name, self.map_ty(&f.ty)?.owned()));
477+ // A field of `&[T]` / `&str` type is a borrow, and Nim's
478+ // view types allow it as an object field, so it stays a
479+ // view rather than being copied into a `seq`.
480+ let t = self.map_ty(&f.ty)?;
481+ let t = if returns_borrow(&f.ty) { t.unvar() } else { t.owned() };
482+ fields.push((name, t));
478483 }
479484 self.structs.insert(s.ident.to_string(), fields);
480485 }
@@ -520,7 +525,9 @@ impl Lowerer {
520525 Some(id) => format!("{vname}_{id}"),
521526 None => format!("{vname}_f{i}"),
522527 };
523- fields.push((fname, self.map_ty(&f.ty)?.owned()));
528+ let t = self.map_ty(&f.ty)?;
529+ let t = if returns_borrow(&f.ty) { t.unvar() } else { t.owned() };
530+ fields.push((fname, t));
524531 }
525532 variants.push(Variant { name: vname, fields });
526533 }
@@ -1129,9 +1136,10 @@ impl Lowerer {
11291136 self.bind("self", self_ty.clone());
11301137 let saved = self.fmt_param.replace(f);
11311138 let outer_ret = self.ret.replace(Nim::Prim("string".into()));
1132- let outer_target = self
1133- .target
1134- .replace(("result".to_string(), Some(Nim::Prim("string".into()))));
1139+ // No assignment target: a formatter write *appends*, because a `fmt`
1140+ // body may write repeatedly -- `UpperHex` writes once per byte in a
1141+ // loop -- and assigning would keep only the last one.
1142+ let outer_target = self.target.take();
11351143
11361144 self.line(&format!(
11371145 "proc {}*(self: {}): string =",
@@ -1140,8 +1148,7 @@ impl Lowerer {
11401148 ));
11411149 self.indent += 1;
11421150 let before = self.out.len();
1143- let want = Nim::Prim("string".into());
1144- let tail = self.block_body_at(body, Some(&want))?;
1151+ let tail = self.block_body(body)?;
11451152 self.emit_tail(tail);
11461153 if self.out.len() == before {
11471154 self.line("discard");
@@ -1406,9 +1413,12 @@ impl Lowerer {
14061413 self.bind_alias(&name, w);
14071414 return Ok(());
14081415 }
1416+ // A `let` binding a borrow keeps the view: `let res = encode(..)?`
1417+ // names the caller's buffer, and copying it into a `seq` would still
1418+ // print the right bytes while silently breaking the aliasing.
14091419 let t = match (ann, &v.ty) {
1410- (Some(a), _) => a.owned(),
1411- (None, Some(t)) => t.clone().owned(),
1420+ (Some(a), _) => a.unvar(),
1421+ (None, Some(t)) => t.clone().unvar(),
14121422 (None, None) => {
14131423 return Err(format!(
14141424 "cannot infer the type of `let {name}`; annotate it — \
@@ -2347,8 +2357,11 @@ impl Lowerer {
23472357 Ok(Val::new(code, ty))
23482358 }
23492359 Expr::Macro(m) => {
2360+ let is_write = matches!(path_name(&m.mac.path).as_str(), "write" | "writeln");
23502361 let code = self.macro_call(&m.mac)?;
2351- Ok(Val::new(code, None))
2362+ // A formatter write is a statement that appends, not a value.
2363+ let ty = if is_write { Some(Nim::Unit) } else { None };
2364+ Ok(Val::new(code, ty))
23522365 }
23532366 Expr::Struct(s) => {
23542367 if s.rest.is_some() {
@@ -2882,6 +2895,31 @@ impl Lowerer {
28822895 .into());
28832896 }
28842897 let v = self.expr(&t.expr)?;
2898+ if self.fmt_param.is_some() {
2899+ // Writing into a string cannot fail, so `?` on a formatter write
2900+ // is a no-op. `?` on anything else can fail, and `format!` panics
2901+ // when a formatting impl returns an error -- so that is what the
2902+ // error branch does here, with std's own message.
2903+ if v.ty.as_ref() == Some(&Nim::Unit) {
2904+ return Ok(v);
2905+ }
2906+ if let Some(Nim::Named(n, a)) = v.ty.clone() {
2907+ if n == "Result" && a.len() == 2 {
2908+ let tmp = self.fresh("Fmt");
2909+ self.line(&format!(
2910+ "let {}: {} = {}",
2911+ tmp,
2912+ Nim::Named(n, a.clone()).render(),
2913+ v.code
2914+ ));
2915+ self.line(&format!("if not {}.ok:", tmp));
2916+ self.line(
2917+ " rsPanic(\"a formatting trait implementation returned an error\")",
2918+ );
2919+ return Ok(Val::new(format!("{}.val", tmp), Some(a[0].clone())));
2920+ }
2921+ }
2922+ }
28852923 if let (Some(guard), Some(w)) = (v.guard.clone(), v.window.clone()) {
28862924 // An `Option`/`Result` of a view: the check is emitted here and the
28872925 // view itself survives as an alias, since it has no value form.
@@ -2998,6 +3036,12 @@ impl Lowerer {
29983036 expect.cloned(),
29993037 ));
30003038 }
3039+ "Ok" if self.fmt_param.is_some()
3040+ && matches!(c.args.first(), Some(Expr::Tuple(t)) if t.elems.is_empty()) =>
3041+ {
3042+ // `Ok(())` ends a `fmt` body: nothing more is written.
3043+ return Ok(Val::new(String::new(), Some(Nim::Unit)));
3044+ }
30013045 "Ok" | "Err" => {
30023046 let (t, e) = match expect {
30033047 Some(Nim::Named(n, a)) if n == "Result" && a.len() == 2 => {
@@ -3020,13 +3064,52 @@ impl Lowerer {
30203064 _ => {}
30213065 }
30223066
3067+ // A tuple struct applied to arguments: `HexDisplay(bytes)`. Nim's
3068+ // object constructor names its fields even when Rust's does not.
3069+ if let Some(fields) = self.structs.get(&name).cloned() {
3070+ if fields.len() == c.args.len() {
3071+ let mut parts = Vec::new();
3072+ for (i, a) in c.args.iter().enumerate() {
3073+ let v = self.expr_at(a, Some(&fields[i].1))?;
3074+ parts.push(format!("{}: {}", ident(&fields[i].0), v.code));
3075+ }
3076+ return Ok(Val::new(
3077+ format!("{}({})", ident(&name), parts.join(", ")),
3078+ Some(Nim::Named(name.clone(), vec![])),
3079+ ));
3080+ }
3081+ }
3082+
30233083 // `core::str::from_utf8_unchecked(b)` reinterprets a byte view as a
30243084 // string view; no copy, no validation, same memory.
30253085 if name == "from_utf8_unchecked" && codes.len() == 1 {
3026- return Ok(Val::new(
3027- format!("rsStrView({})", codes[0]),
3028- Some(Nim::OpenArray(Box::new(Nim::Prim("char".into())))),
3029- ));
3086+ // `String::from_utf8_unchecked(v)` takes ownership and yields an
3087+ // owned `String`; `str::from_utf8_unchecked(b)` borrows and yields
3088+ // a view. Same name, different operations -- the qualifier says
3089+ // which, and an unqualified call is ambiguous.
3090+ let q = p
3091+ .path
3092+ .segments
3093+ .iter()
3094+ .rev()
3095+ .nth(1)
3096+ .map(|s| s.ident.to_string());
3097+ return match q.as_deref() {
3098+ Some("String") => Ok(Val::new(
3099+ format!("rsStringOf({})", codes[0]),
3100+ Some(Nim::Prim("string".into())),
3101+ )),
3102+ Some("str") => Ok(Val::new(
3103+ format!("rsStrView({})", codes[0]),
3104+ Some(Nim::OpenArray(Box::new(Nim::Prim("char".into())))),
3105+ )),
3106+ _ => Err(
3107+ "`from_utf8_unchecked` must be written as `str::..` (a \
3108+ borrowed view) or `String::..` (an owned string); the two \
3109+ are different operations"
3110+ .into(),
3111+ ),
3112+ };
30303113 }
30313114
30323115 // A tuple enum variant applied to arguments: `Shape::Circle(1.0)`.
@@ -3170,13 +3253,57 @@ impl Lowerer {
31703253 "clone" | "to_vec" | "to_owned" | "as_slice" | "as_ref" | "as_mut" | "iter"
31713254 | "into_iter" => (recv.code.clone(), rt.clone()),
31723255 "unwrap" | "expect" => {
3173- let inner = match &rt {
3174- Some(Nim::Named(n, a)) if (n == "Option" && a.len() == 1) || (n == "Result" && a.len() == 2) => {
3175- Some(a[0].clone())
3256+ // Expanded inline rather than called as a generic proc: when
3257+ // the payload is a view, Nim can only borrow from a path
3258+ // expression, which a proc body containing the panic is not.
3259+ let (kind, inner) = match &rt {
3260+ Some(Nim::Named(n, a)) if n == "Option" && a.len() == 1 => {
3261+ ("Option", a[0].clone())
31763262 }
3177- _ => None,
3263+ Some(Nim::Named(n, a)) if n == "Result" && a.len() == 2 => {
3264+ ("Result", a[0].clone())
3265+ }
3266+ _ => {
3267+ return Err(format!(
3268+ "`.{name}()` needs a known `Option`/`Result` receiver type"
3269+ ))
3270+ }
3271+ };
3272+ if self.in_loop_cond {
3273+ return Err(format!(
3274+ "`.{name}()` in a loop condition is not implemented yet: the \
3275+ check it expands to would run once, before the loop"
3276+ ));
3277+ }
3278+ let tmp = self.fresh("Unwrap");
3279+ let rty = rt.clone().unwrap();
3280+ self.line(&format!("let {}: {} = {}", tmp, rty.render(), recv.code));
3281+ let (test, msg) = if kind == "Option" {
3282+ (format!("{}.has", tmp), "called `Option::unwrap()` on a `None` value")
3283+ } else {
3284+ (format!("{}.ok", tmp), "called `Result::unwrap()` on an `Err` value")
31783285 };
3179- (format!("unwrap({})", recv.code), inner)
3286+ let msg = if name == "expect" {
3287+ args.first().map(|a| a.code.clone()).unwrap_or_else(|| fmt::nim_str(msg))
3288+ } else {
3289+ fmt::nim_str(msg)
3290+ };
3291+ self.line(&format!("if not {}:", test));
3292+ self.line(&format!(" rsPanic({})", msg));
3293+ // If the payload is a view, hand back an alias rather than a
3294+ // value: Nim will not let a `let` borrow out of a local, and a
3295+ // view is a reference anyway, so there is nothing to bind.
3296+ // `{tmp}.val` is a plain field access, so substituting it at
3297+ // each use re-evaluates nothing.
3298+ if matches!(inner, Nim::OpenArray(_)) {
3299+ let mut v = Val::new(format!("{}.val", tmp), Some(inner.clone()));
3300+ v.window = Some(Alias::Value {
3301+ code: format!("{}.val", tmp),
3302+ ty: Some(inner),
3303+ });
3304+ return Ok(v);
3305+ }
3306+ (format!("{}.val", tmp), Some(inner))
31803307 }
31813308 "ok_or" if recv.guard.is_some() => {
31823309 let e = args.first().ok_or("`ok_or` takes one argument")?;
@@ -3262,10 +3389,15 @@ impl Lowerer {
32623389 }
32633390 // Inside a formatting impl, a write through the `Formatter` *is*
32643391 // the value the proc returns, so it lowers to the string written.
3265- "write_str" | "write_char" if self.is_fmt_param(&m.receiver) => (
3266- a0.ok_or("`write_str` takes one argument")?,
3267- Some(Nim::Prim("string".into())),
3268- ),
3392+ "write_str" | "write_char" if self.is_fmt_param(&m.receiver) => {
3393+ let a = args.first().ok_or("`write_str` takes one argument")?;
3394+ // A `&str` argument is a character view, not a Nim string.
3395+ let text = match &a.ty {
3396+ Some(Nim::Prim(p)) if p == "string" => a.code.clone(),
3397+ _ => format!("rsDisplay({})", a.code),
3398+ };
3399+ (format!("result.add({})", text), Some(Nim::Unit))
3400+ }
32693401 "abs" => (format!("abs({})", recv.code), rt.clone()),
32703402 "min" => (format!("min({}, {})", recv.code, a0.unwrap_or_default()), rt.clone()),
32713403 "max" => (format!("max({}, {})", recv.code, a0.unwrap_or_default()), rt.clone()),
@@ -3374,22 +3506,57 @@ impl Lowerer {
33743506 .into());
33753507 }
33763508 let s = self.format_pieces(&args[1..])?;
3377- Ok(if name == "writeln" {
3509+ let s = if name == "writeln" {
33783510 format!("({} & \"\\n\")", s)
33793511 } else {
33803512 s
3381- })
3513+ };
3514+ Ok(format!("result.add({})", s))
33823515 }
33833516 "panic" => {
33843517 let s = self.format_args(mac)?;
33853518 Ok(format!("rsPanic({s})"))
33863519 }
3387- "assert" => {
3388- let e: Expr = mac.parse_body().map_err(|e| format!("assert!: {e}"))?;
3389- let v = self.expr(&e)?;
3520+ // `debug_assert*` fires in debug builds, which is the profile
3521+ // this project models, so it lowers the same as `assert*`.
3522+ "assert" | "debug_assert" => {
3523+ let args: Vec<Expr> = mac
3524+ .parse_body_with(
3525+ syn::punctuated::Punctuated::<Expr, syn::Token![,]>::parse_terminated,
3526+ )
3527+ .map_err(|e| format!("{name}!: {e}"))?
3528+ .into_iter()
3529+ .collect();
3530+ let cond = args.first().ok_or("`assert!` needs a condition")?;
3531+ let v = self.expr(cond)?;
3532+ let msg = if args.len() > 1 {
3533+ self.format_pieces(&args[1..])?
3534+ } else {
3535+ fmt::nim_str("assertion failed")
3536+ };
3537+ Ok(format!("(if not ({}): rsPanic({}))", v.code, msg))
3538+ }
3539+ "assert_eq" | "assert_ne" | "debug_assert_eq" | "debug_assert_ne" => {
3540+ let args: Vec<Expr> = mac
3541+ .parse_body_with(
3542+ syn::punctuated::Punctuated::<Expr, syn::Token![,]>::parse_terminated,
3543+ )
3544+ .map_err(|e| format!("{name}!: {e}"))?
3545+ .into_iter()
3546+ .collect();
3547+ if args.len() < 2 {
3548+ return Err(format!("`{name}!` takes two operands"));
3549+ }
3550+ let a = self.expr(&args[0])?;
3551+ let b = self.expr_at(&args[1], a.ty.as_ref())?;
3552+ let ne = name.ends_with("_ne");
3553+ let op = if ne { "!=" } else { "==" };
3554+ // Rust's message shows both sides; reproducing it keeps a
3555+ // failing assertion as informative as the original.
3556+ let label = if ne { "assertion failed: `(left != right)`" } else { "assertion failed: `(left == right)`" };
33903557 Ok(format!(
3391- "(if not ({}): rsPanic(\"assertion failed\"))",
3392- v.code
3558+ "(if not (({}) {} ({})): rsPanic({} & \"\\n left: \" & rsDebug({}) & \"\\n right: \" & rsDebug({})))",
3559+ a.code, op, b.code, fmt::nim_str(label), a.code, b.code
33933560 ))
33943561 }
33953562 "vec" => {
@@ -3477,7 +3644,17 @@ impl Lowerer {
34773644 Val::new(ident(n), Some(t))
34783645 }
34793646 };
3480- parts.push(fmt::render_arg(&v.code, spec));
3647+ let integer = v.ty.as_ref().is_some_and(|t| t.is_integer());
3648+ if spec.radix.is_some() && !integer && v.ty.is_none() {
3649+ return Err(
3650+ "a radix format (`{:x}`, `{:b}`, ...) needs a known \
3651+ argument type: on an integer it formats the bit \
3652+ pattern, on anything else it calls that type's own \
3653+ impl"
3654+ .into(),
3655+ );
3656+ }
3657+ parts.push(fmt::render_arg(&v.code, spec, integer));
34813658 }
34823659 }
34833660 }
@@ -474,7 +474,12 @@ impl Lowerer {
474 Some(id) => id.to_string(),474 Some(id) => id.to_string(),
475 None => format!("f{i}"), // tuple struct475 None => format!("f{i}"), // tuple struct
476 };476 };
477- fields.push((name, self.map_ty(&f.ty)?.owned()));477+ // A field of `&[T]` / `&str` type is a borrow, and Nim's
478+ // view types allow it as an object field, so it stays a
479+ // view rather than being copied into a `seq`.
480+ let t = self.map_ty(&f.ty)?;
481+ let t = if returns_borrow(&f.ty) { t.unvar() } else { t.owned() };
482+ fields.push((name, t));
478 }483 }
479 self.structs.insert(s.ident.to_string(), fields);484 self.structs.insert(s.ident.to_string(), fields);
480 }485 }
@@ -520,7 +525,9 @@ impl Lowerer {
520 Some(id) => format!("{vname}_{id}"),525 Some(id) => format!("{vname}_{id}"),
521 None => format!("{vname}_f{i}"),526 None => format!("{vname}_f{i}"),
522 };527 };
523- fields.push((fname, self.map_ty(&f.ty)?.owned()));528+ let t = self.map_ty(&f.ty)?;
529+ let t = if returns_borrow(&f.ty) { t.unvar() } else { t.owned() };
530+ fields.push((fname, t));
524 }531 }
525 variants.push(Variant { name: vname, fields });532 variants.push(Variant { name: vname, fields });
526 }533 }
@@ -1129,9 +1136,10 @@ impl Lowerer {
1129 self.bind("self", self_ty.clone());1136 self.bind("self", self_ty.clone());
1130 let saved = self.fmt_param.replace(f);1137 let saved = self.fmt_param.replace(f);
1131 let outer_ret = self.ret.replace(Nim::Prim("string".into()));1138 let outer_ret = self.ret.replace(Nim::Prim("string".into()));
1132- let outer_target = self1139+ // No assignment target: a formatter write *appends*, because a `fmt`
1133- .target1140+ // body may write repeatedly -- `UpperHex` writes once per byte in a
1134- .replace(("result".to_string(), Some(Nim::Prim("string".into()))));1141+ // loop -- and assigning would keep only the last one.
1142+ let outer_target = self.target.take();
1135 1143
1136 self.line(&format!(1144 self.line(&format!(
1137 "proc {}*(self: {}): string =",1145 "proc {}*(self: {}): string =",
@@ -1140,8 +1148,7 @@ impl Lowerer {
1140 ));1148 ));
1141 self.indent += 1;1149 self.indent += 1;
1142 let before = self.out.len();1150 let before = self.out.len();
1143- let want = Nim::Prim("string".into());1151+ let tail = self.block_body(body)?;
1144- let tail = self.block_body_at(body, Some(&want))?;
1145 self.emit_tail(tail);1152 self.emit_tail(tail);
1146 if self.out.len() == before {1153 if self.out.len() == before {
1147 self.line("discard");1154 self.line("discard");
@@ -1406,9 +1413,12 @@ impl Lowerer {
1406 self.bind_alias(&name, w);1413 self.bind_alias(&name, w);
1407 return Ok(());1414 return Ok(());
1408 }1415 }
1416+ // A `let` binding a borrow keeps the view: `let res = encode(..)?`
1417+ // names the caller's buffer, and copying it into a `seq` would still
1418+ // print the right bytes while silently breaking the aliasing.
1409 let t = match (ann, &v.ty) {1419 let t = match (ann, &v.ty) {
1410- (Some(a), _) => a.owned(),1420+ (Some(a), _) => a.unvar(),
1411- (None, Some(t)) => t.clone().owned(),1421+ (None, Some(t)) => t.clone().unvar(),
1412 (None, None) => {1422 (None, None) => {
1413 return Err(format!(1423 return Err(format!(
1414 "cannot infer the type of `let {name}`; annotate it — \1424 "cannot infer the type of `let {name}`; annotate it — \
@@ -2347,8 +2357,11 @@ impl Lowerer {
2347 Ok(Val::new(code, ty))2357 Ok(Val::new(code, ty))
2348 }2358 }
2349 Expr::Macro(m) => {2359 Expr::Macro(m) => {
2360+ let is_write = matches!(path_name(&m.mac.path).as_str(), "write" | "writeln");
2350 let code = self.macro_call(&m.mac)?;2361 let code = self.macro_call(&m.mac)?;
2351- Ok(Val::new(code, None))2362+ // A formatter write is a statement that appends, not a value.
2363+ let ty = if is_write { Some(Nim::Unit) } else { None };
2364+ Ok(Val::new(code, ty))
2352 }2365 }
2353 Expr::Struct(s) => {2366 Expr::Struct(s) => {
2354 if s.rest.is_some() {2367 if s.rest.is_some() {
@@ -2882,6 +2895,31 @@ impl Lowerer {
2882 .into());2895 .into());
2883 }2896 }
2884 let v = self.expr(&t.expr)?;2897 let v = self.expr(&t.expr)?;
2898+ if self.fmt_param.is_some() {
2899+ // Writing into a string cannot fail, so `?` on a formatter write
2900+ // is a no-op. `?` on anything else can fail, and `format!` panics
2901+ // when a formatting impl returns an error -- so that is what the
2902+ // error branch does here, with std's own message.
2903+ if v.ty.as_ref() == Some(&Nim::Unit) {
2904+ return Ok(v);
2905+ }
2906+ if let Some(Nim::Named(n, a)) = v.ty.clone() {
2907+ if n == "Result" && a.len() == 2 {
2908+ let tmp = self.fresh("Fmt");
2909+ self.line(&format!(
2910+ "let {}: {} = {}",
2911+ tmp,
2912+ Nim::Named(n, a.clone()).render(),
2913+ v.code
2914+ ));
2915+ self.line(&format!("if not {}.ok:", tmp));
2916+ self.line(
2917+ " rsPanic(\"a formatting trait implementation returned an error\")",
2918+ );
2919+ return Ok(Val::new(format!("{}.val", tmp), Some(a[0].clone())));
2920+ }
2921+ }
2922+ }
2885 if let (Some(guard), Some(w)) = (v.guard.clone(), v.window.clone()) {2923 if let (Some(guard), Some(w)) = (v.guard.clone(), v.window.clone()) {
2886 // An `Option`/`Result` of a view: the check is emitted here and the2924 // An `Option`/`Result` of a view: the check is emitted here and the
2887 // view itself survives as an alias, since it has no value form.2925 // view itself survives as an alias, since it has no value form.
@@ -2998,6 +3036,12 @@ impl Lowerer {
2998 expect.cloned(),3036 expect.cloned(),
2999 ));3037 ));
3000 }3038 }
3039+ "Ok" if self.fmt_param.is_some()
3040+ && matches!(c.args.first(), Some(Expr::Tuple(t)) if t.elems.is_empty()) =>
3041+ {
3042+ // `Ok(())` ends a `fmt` body: nothing more is written.
3043+ return Ok(Val::new(String::new(), Some(Nim::Unit)));
3044+ }
3001 "Ok" | "Err" => {3045 "Ok" | "Err" => {
3002 let (t, e) = match expect {3046 let (t, e) = match expect {
3003 Some(Nim::Named(n, a)) if n == "Result" && a.len() == 2 => {3047 Some(Nim::Named(n, a)) if n == "Result" && a.len() == 2 => {
@@ -3020,13 +3064,52 @@ impl Lowerer {
3020 _ => {}3064 _ => {}
3021 }3065 }
3022 3066
3067+ // A tuple struct applied to arguments: `HexDisplay(bytes)`. Nim's
3068+ // object constructor names its fields even when Rust's does not.
3069+ if let Some(fields) = self.structs.get(&name).cloned() {
3070+ if fields.len() == c.args.len() {
3071+ let mut parts = Vec::new();
3072+ for (i, a) in c.args.iter().enumerate() {
3073+ let v = self.expr_at(a, Some(&fields[i].1))?;
3074+ parts.push(format!("{}: {}", ident(&fields[i].0), v.code));
3075+ }
3076+ return Ok(Val::new(
3077+ format!("{}({})", ident(&name), parts.join(", ")),
3078+ Some(Nim::Named(name.clone(), vec![])),
3079+ ));
3080+ }
3081+ }
3082+
3023 // `core::str::from_utf8_unchecked(b)` reinterprets a byte view as a3083 // `core::str::from_utf8_unchecked(b)` reinterprets a byte view as a
3024 // string view; no copy, no validation, same memory.3084 // string view; no copy, no validation, same memory.
3025 if name == "from_utf8_unchecked" && codes.len() == 1 {3085 if name == "from_utf8_unchecked" && codes.len() == 1 {
3026- return Ok(Val::new(3086+ // `String::from_utf8_unchecked(v)` takes ownership and yields an
3027- format!("rsStrView({})", codes[0]),3087+ // owned `String`; `str::from_utf8_unchecked(b)` borrows and yields
3028- Some(Nim::OpenArray(Box::new(Nim::Prim("char".into())))),3088+ // a view. Same name, different operations -- the qualifier says
3029- ));3089+ // which, and an unqualified call is ambiguous.
3090+ let q = p
3091+ .path
3092+ .segments
3093+ .iter()
3094+ .rev()
3095+ .nth(1)
3096+ .map(|s| s.ident.to_string());
3097+ return match q.as_deref() {
3098+ Some("String") => Ok(Val::new(
3099+ format!("rsStringOf({})", codes[0]),
3100+ Some(Nim::Prim("string".into())),
3101+ )),
3102+ Some("str") => Ok(Val::new(
3103+ format!("rsStrView({})", codes[0]),
3104+ Some(Nim::OpenArray(Box::new(Nim::Prim("char".into())))),
3105+ )),
3106+ _ => Err(
3107+ "`from_utf8_unchecked` must be written as `str::..` (a \
3108+ borrowed view) or `String::..` (an owned string); the two \
3109+ are different operations"
3110+ .into(),
3111+ ),
3112+ };
3030 }3113 }
3031 3114
3032 // A tuple enum variant applied to arguments: `Shape::Circle(1.0)`.3115 // A tuple enum variant applied to arguments: `Shape::Circle(1.0)`.
@@ -3170,13 +3253,57 @@ impl Lowerer {
3170 "clone" | "to_vec" | "to_owned" | "as_slice" | "as_ref" | "as_mut" | "iter"3253 "clone" | "to_vec" | "to_owned" | "as_slice" | "as_ref" | "as_mut" | "iter"
3171 | "into_iter" => (recv.code.clone(), rt.clone()),3254 | "into_iter" => (recv.code.clone(), rt.clone()),
3172 "unwrap" | "expect" => {3255 "unwrap" | "expect" => {
3173- let inner = match &rt {3256+ // Expanded inline rather than called as a generic proc: when
3174- Some(Nim::Named(n, a)) if (n == "Option" && a.len() == 1) || (n == "Result" && a.len() == 2) => {3257+ // the payload is a view, Nim can only borrow from a path
3175- Some(a[0].clone())3258+ // expression, which a proc body containing the panic is not.
3259+ let (kind, inner) = match &rt {
3260+ Some(Nim::Named(n, a)) if n == "Option" && a.len() == 1 => {
3261+ ("Option", a[0].clone())
3176 }3262 }
3177- _ => None,3263+ Some(Nim::Named(n, a)) if n == "Result" && a.len() == 2 => {
3264+ ("Result", a[0].clone())
3265+ }
3266+ _ => {
3267+ return Err(format!(
3268+ "`.{name}()` needs a known `Option`/`Result` receiver type"
3269+ ))
3270+ }
3271+ };
3272+ if self.in_loop_cond {
3273+ return Err(format!(
3274+ "`.{name}()` in a loop condition is not implemented yet: the \
3275+ check it expands to would run once, before the loop"
3276+ ));
3277+ }
3278+ let tmp = self.fresh("Unwrap");
3279+ let rty = rt.clone().unwrap();
3280+ self.line(&format!("let {}: {} = {}", tmp, rty.render(), recv.code));
3281+ let (test, msg) = if kind == "Option" {
3282+ (format!("{}.has", tmp), "called `Option::unwrap()` on a `None` value")
3283+ } else {
3284+ (format!("{}.ok", tmp), "called `Result::unwrap()` on an `Err` value")
3178 };3285 };
3179- (format!("unwrap({})", recv.code), inner)3286+ let msg = if name == "expect" {
3287+ args.first().map(|a| a.code.clone()).unwrap_or_else(|| fmt::nim_str(msg))
3288+ } else {
3289+ fmt::nim_str(msg)
3290+ };
3291+ self.line(&format!("if not {}:", test));
3292+ self.line(&format!(" rsPanic({})", msg));
3293+ // If the payload is a view, hand back an alias rather than a
3294+ // value: Nim will not let a `let` borrow out of a local, and a
3295+ // view is a reference anyway, so there is nothing to bind.
3296+ // `{tmp}.val` is a plain field access, so substituting it at
3297+ // each use re-evaluates nothing.
3298+ if matches!(inner, Nim::OpenArray(_)) {
3299+ let mut v = Val::new(format!("{}.val", tmp), Some(inner.clone()));
3300+ v.window = Some(Alias::Value {
3301+ code: format!("{}.val", tmp),
3302+ ty: Some(inner),
3303+ });
3304+ return Ok(v);
3305+ }
3306+ (format!("{}.val", tmp), Some(inner))
3180 }3307 }
3181 "ok_or" if recv.guard.is_some() => {3308 "ok_or" if recv.guard.is_some() => {
3182 let e = args.first().ok_or("`ok_or` takes one argument")?;3309 let e = args.first().ok_or("`ok_or` takes one argument")?;
@@ -3262,10 +3389,15 @@ impl Lowerer {
3262 }3389 }
3263 // Inside a formatting impl, a write through the `Formatter` *is*3390 // Inside a formatting impl, a write through the `Formatter` *is*
3264 // the value the proc returns, so it lowers to the string written.3391 // the value the proc returns, so it lowers to the string written.
3265- "write_str" | "write_char" if self.is_fmt_param(&m.receiver) => (3392+ "write_str" | "write_char" if self.is_fmt_param(&m.receiver) => {
3266- a0.ok_or("`write_str` takes one argument")?,3393+ let a = args.first().ok_or("`write_str` takes one argument")?;
3267- Some(Nim::Prim("string".into())),3394+ // A `&str` argument is a character view, not a Nim string.
3268- ),3395+ let text = match &a.ty {
3396+ Some(Nim::Prim(p)) if p == "string" => a.code.clone(),
3397+ _ => format!("rsDisplay({})", a.code),
3398+ };
3399+ (format!("result.add({})", text), Some(Nim::Unit))
3400+ }
3269 "abs" => (format!("abs({})", recv.code), rt.clone()),3401 "abs" => (format!("abs({})", recv.code), rt.clone()),
3270 "min" => (format!("min({}, {})", recv.code, a0.unwrap_or_default()), rt.clone()),3402 "min" => (format!("min({}, {})", recv.code, a0.unwrap_or_default()), rt.clone()),
3271 "max" => (format!("max({}, {})", recv.code, a0.unwrap_or_default()), rt.clone()),3403 "max" => (format!("max({}, {})", recv.code, a0.unwrap_or_default()), rt.clone()),
@@ -3374,22 +3506,57 @@ impl Lowerer {
3374 .into());3506 .into());
3375 }3507 }
3376 let s = self.format_pieces(&args[1..])?;3508 let s = self.format_pieces(&args[1..])?;
3377- Ok(if name == "writeln" {3509+ let s = if name == "writeln" {
3378 format!("({} & \"\\n\")", s)3510 format!("({} & \"\\n\")", s)
3379 } else {3511 } else {
3380 s3512 s
3381- })3513+ };
3514+ Ok(format!("result.add({})", s))
3382 }3515 }
3383 "panic" => {3516 "panic" => {
3384 let s = self.format_args(mac)?;3517 let s = self.format_args(mac)?;
3385 Ok(format!("rsPanic({s})"))3518 Ok(format!("rsPanic({s})"))
3386 }3519 }
3387- "assert" => {3520+ // `debug_assert*` fires in debug builds, which is the profile
3388- let e: Expr = mac.parse_body().map_err(|e| format!("assert!: {e}"))?;3521+ // this project models, so it lowers the same as `assert*`.
3389- let v = self.expr(&e)?;3522+ "assert" | "debug_assert" => {
3523+ let args: Vec<Expr> = mac
3524+ .parse_body_with(
3525+ syn::punctuated::Punctuated::<Expr, syn::Token![,]>::parse_terminated,
3526+ )
3527+ .map_err(|e| format!("{name}!: {e}"))?
3528+ .into_iter()
3529+ .collect();
3530+ let cond = args.first().ok_or("`assert!` needs a condition")?;
3531+ let v = self.expr(cond)?;
3532+ let msg = if args.len() > 1 {
3533+ self.format_pieces(&args[1..])?
3534+ } else {
3535+ fmt::nim_str("assertion failed")
3536+ };
3537+ Ok(format!("(if not ({}): rsPanic({}))", v.code, msg))
3538+ }
3539+ "assert_eq" | "assert_ne" | "debug_assert_eq" | "debug_assert_ne" => {
3540+ let args: Vec<Expr> = mac
3541+ .parse_body_with(
3542+ syn::punctuated::Punctuated::<Expr, syn::Token![,]>::parse_terminated,
3543+ )
3544+ .map_err(|e| format!("{name}!: {e}"))?
3545+ .into_iter()
3546+ .collect();
3547+ if args.len() < 2 {
3548+ return Err(format!("`{name}!` takes two operands"));
3549+ }
3550+ let a = self.expr(&args[0])?;
3551+ let b = self.expr_at(&args[1], a.ty.as_ref())?;
3552+ let ne = name.ends_with("_ne");
3553+ let op = if ne { "!=" } else { "==" };
3554+ // Rust's message shows both sides; reproducing it keeps a
3555+ // failing assertion as informative as the original.
3556+ let label = if ne { "assertion failed: `(left != right)`" } else { "assertion failed: `(left == right)`" };
3390 Ok(format!(3557 Ok(format!(
3391- "(if not ({}): rsPanic(\"assertion failed\"))",3558+ "(if not (({}) {} ({})): rsPanic({} & \"\\n left: \" & rsDebug({}) & \"\\n right: \" & rsDebug({})))",
3392- v.code3559+ a.code, op, b.code, fmt::nim_str(label), a.code, b.code
3393 ))3560 ))
3394 }3561 }
3395 "vec" => {3562 "vec" => {
@@ -3477,7 +3644,17 @@ impl Lowerer {
3477 Val::new(ident(n), Some(t))3644 Val::new(ident(n), Some(t))
3478 }3645 }
3479 };3646 };
3480- parts.push(fmt::render_arg(&v.code, spec));3647+ let integer = v.ty.as_ref().is_some_and(|t| t.is_integer());
3648+ if spec.radix.is_some() && !integer && v.ty.is_none() {
3649+ return Err(
3650+ "a radix format (`{:x}`, `{:b}`, ...) needs a known \
3651+ argument type: on an integer it formats the bit \
3652+ pattern, on anything else it calls that type's own \
3653+ impl"
3654+ .into(),
3655+ );
3656+ }
3657+ parts.push(fmt::render_arg(&v.code, spec, integer));
3481 }3658 }
3482 }3659 }
3483 }3660 }
modified src/prelude.nim +7 -0
@@ -162,6 +162,13 @@ proc rsStrView*(b: openArray[uint8]): openArray[char] =
162162 else:
163163 result = toOpenArray(cast[ptr UncheckedArray[char]](unsafeAddr b[0]), 0, b.len - 1)
164164
165+proc rsStringOf*(b: openArray[uint8]): string =
166+ ## `String::from_utf8_unchecked` takes ownership of the bytes. Nim's `string`
167+ ## is an owned value, so this copies -- which is what the Rust call does to
168+ ## the `Vec` it consumes, from the caller's point of view.
169+ result = newStringOfCap(b.len)
170+ for v in b: result.add(char(v))
171+
165172 proc rsDisplay*(x: openArray[char]): string =
166173 result = newStringOfCap(x.len)
167174 for c in x: result.add(c)
@@ -162,6 +162,13 @@ proc rsStrView*(b: openArray[uint8]): openArray[char] =
162 else:162 else:
163 result = toOpenArray(cast[ptr UncheckedArray[char]](unsafeAddr b[0]), 0, b.len - 1)163 result = toOpenArray(cast[ptr UncheckedArray[char]](unsafeAddr b[0]), 0, b.len - 1)
164 164
165+proc rsStringOf*(b: openArray[uint8]): string =
166+ ## `String::from_utf8_unchecked` takes ownership of the bytes. Nim's `string`
167+ ## is an owned value, so this copies -- which is what the Rust call does to
168+ ## the `Vec` it consumes, from the caller's point of view.
169+ result = newStringOfCap(b.len)
170+ for v in b: result.add(char(v))
171+
165 proc rsDisplay*(x: openArray[char]): string =172 proc rsDisplay*(x: openArray[char]): string =
166 result = newStringOfCap(x.len)173 result = newStringOfCap(x.len)
167 for c in x: result.add(c)174 for c in x: result.add(c)
modified src/ty.rs +9 -0
@@ -57,6 +57,15 @@ impl Nim {
5757 }
5858 }
5959
60+ /// Strip a `var`, which is a parameter-passing mode rather than a type.
61+ /// Unlike `owned`, this keeps a view a view.
62+ pub fn unvar(self) -> Nim {
63+ match self {
64+ Nim::Var(t) => t.unvar(),
65+ other => other,
66+ }
67+ }
68+
6069 /// Owned form: a borrowed slice parameter is `openArray[T]`, but the same
6170 /// type in an owned position (a field, a return value) must be `seq[T]`.
6271 pub fn owned(self) -> Nim {
@@ -57,6 +57,15 @@ impl Nim {
57 }57 }
58 }58 }
59 59
60+ /// Strip a `var`, which is a parameter-passing mode rather than a type.
61+ /// Unlike `owned`, this keeps a view a view.
62+ pub fn unvar(self) -> Nim {
63+ match self {
64+ Nim::Var(t) => t.unvar(),
65+ other => other,
66+ }
67+ }
68+
60 /// Owned form: a borrowed slice parameter is `openArray[T]`, but the same69 /// Owned form: a borrowed slice parameter is `openArray[T]`, but the same
61 /// type in an owned position (a field, a return value) must be `seq[T]`.70 /// type in an owned position (a field, a return value) must be `seq[T]`.
62 pub fn owned(self) -> Nim {71 pub fn owned(self) -> Nim {
added tests/cases/026-base16ct-crate/display.rs +35 -0
new file mode 100644
@@ -0,0 +1,35 @@
1+use core::fmt;
2+
3+/// `core::fmt` presenter for binary data encoded as hexadecimal (Base16).
4+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
5+pub struct HexDisplay<'a>(pub &'a [u8]);
6+
7+impl fmt::Display for HexDisplay<'_> {
8+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9+ write!(f, "{self:X}")
10+ }
11+}
12+
13+impl fmt::UpperHex for HexDisplay<'_> {
14+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
15+ let mut hex = [0u8; 2];
16+
17+ for &byte in self.0 {
18+ f.write_str(crate::upper::encode_str(&[byte], &mut hex)?)?;
19+ }
20+
21+ Ok(())
22+ }
23+}
24+
25+impl fmt::LowerHex for HexDisplay<'_> {
26+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27+ let mut hex = [0u8; 2];
28+
29+ for &byte in self.0 {
30+ f.write_str(crate::lower::encode_str(&[byte], &mut hex)?)?;
31+ }
32+
33+ Ok(())
34+ }
35+}
new file mode 100644
@@ -0,0 +1,35 @@
1+use core::fmt;
2+
3+/// `core::fmt` presenter for binary data encoded as hexadecimal (Base16).
4+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
5+pub struct HexDisplay<'a>(pub &'a [u8]);
6+
7+impl fmt::Display for HexDisplay<'_> {
8+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9+ write!(f, "{self:X}")
10+ }
11+}
12+
13+impl fmt::UpperHex for HexDisplay<'_> {
14+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
15+ let mut hex = [0u8; 2];
16+
17+ for &byte in self.0 {
18+ f.write_str(crate::upper::encode_str(&[byte], &mut hex)?)?;
19+ }
20+
21+ Ok(())
22+ }
23+}
24+
25+impl fmt::LowerHex for HexDisplay<'_> {
26+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27+ let mut hex = [0u8; 2];
28+
29+ for &byte in self.0 {
30+ f.write_str(crate::lower::encode_str(&[byte], &mut hex)?)?;
31+ }
32+
33+ Ok(())
34+ }
35+}
modified tests/cases/026-base16ct-crate/main.rs +26 -3
@@ -1,17 +1,25 @@
11 //@ args: run
2+//@ cfg: feature=alloc
23 // base16ct 1.0.0, transpiled as a multi-file crate.
34 //
4-// `error.rs`, `lower.rs`, `upper.rs` and `mixed.rs` are the crate's own
5-// files, byte-for-byte.
5+// `error.rs`, `lower.rs`, `upper.rs`, `mixed.rs` and `display.rs` are the
6+// crate's own files, byte-for-byte.
67 // This file carries `lib.rs`'s core -- `decoded_len`, `encoded_len` and
78 // `decode_inner` verbatim -- plus a driver, because the runner needs a `main`.
8-// The `alloc`-gated items are off, as they are by default in the crate.
9+// The `alloc`-gated items are on, via `--cfg feature=alloc`.
910
11+mod display;
1012 mod error;
1113 mod lower;
1214 mod mixed;
1315 mod upper;
1416
17+// `lib.rs` re-exports these from `alloc` for its modules to `use crate::..`;
18+// this crate root stands in for it.
19+#[cfg(feature = "alloc")]
20+pub use std::{string::String, vec::Vec};
21+
22+pub use crate::display::HexDisplay;
1523 pub use crate::error::{Error, Result};
1624
1725 /// Compute decoded length of the given hex-encoded input.
@@ -102,4 +110,19 @@ fn main() {
102110 }
103111
104112 println!("{} {}", decoded_len(b"abcd").unwrap(), encoded_len(b"\xab\xcd"));
113+
114+ // The `alloc` half: `decode_vec` and `encode_string`.
115+ println!("{:?}", lower::decode_vec(b"abcd1234"));
116+ println!("{:?}", lower::decode_vec(b"abc"));
117+ println!("{:?}", mixed::decode_vec(b"ABcd1234"));
118+ println!("{}", lower::encode_string(b"\xab\xcd\x12\x34"));
119+ println!("{}", upper::encode_string(b"\xab\xcd\x12\x34"));
120+ println!("[{}]", lower::encode_string(b""));
121+
122+ // `HexDisplay` is a tuple struct holding a borrowed slice, and its
123+ // `UpperHex`/`LowerHex` impls write once per byte into the formatter.
124+ let raw = b"\xab\xcd\x12\x34";
125+ println!("{}", HexDisplay(raw));
126+ println!("{:X} {:x}", HexDisplay(raw), HexDisplay(raw));
127+ println!("{}", HexDisplay(b""));
105128 }
@@ -1,17 +1,25 @@
1 //@ args: run1 //@ args: run
2+//@ cfg: feature=alloc
2 // base16ct 1.0.0, transpiled as a multi-file crate.3 // base16ct 1.0.0, transpiled as a multi-file crate.
3 //4 //
4-// `error.rs`, `lower.rs`, `upper.rs` and `mixed.rs` are the crate's own5+// `error.rs`, `lower.rs`, `upper.rs`, `mixed.rs` and `display.rs` are the
5-// files, byte-for-byte.6+// crate's own files, byte-for-byte.
6 // This file carries `lib.rs`'s core -- `decoded_len`, `encoded_len` and7 // This file carries `lib.rs`'s core -- `decoded_len`, `encoded_len` and
7 // `decode_inner` verbatim -- plus a driver, because the runner needs a `main`.8 // `decode_inner` verbatim -- plus a driver, because the runner needs a `main`.
8-// The `alloc`-gated items are off, as they are by default in the crate.9+// The `alloc`-gated items are on, via `--cfg feature=alloc`.
9 10
11+mod display;
10 mod error;12 mod error;
11 mod lower;13 mod lower;
12 mod mixed;14 mod mixed;
13 mod upper;15 mod upper;
14 16
17+// `lib.rs` re-exports these from `alloc` for its modules to `use crate::..`;
18+// this crate root stands in for it.
19+#[cfg(feature = "alloc")]
20+pub use std::{string::String, vec::Vec};
21+
22+pub use crate::display::HexDisplay;
15 pub use crate::error::{Error, Result};23 pub use crate::error::{Error, Result};
16 24
17 /// Compute decoded length of the given hex-encoded input.25 /// Compute decoded length of the given hex-encoded input.
@@ -102,4 +110,19 @@ fn main() {
102 }110 }
103 111
104 println!("{} {}", decoded_len(b"abcd").unwrap(), encoded_len(b"\xab\xcd"));112 println!("{} {}", decoded_len(b"abcd").unwrap(), encoded_len(b"\xab\xcd"));
113+
114+ // The `alloc` half: `decode_vec` and `encode_string`.
115+ println!("{:?}", lower::decode_vec(b"abcd1234"));
116+ println!("{:?}", lower::decode_vec(b"abc"));
117+ println!("{:?}", mixed::decode_vec(b"ABcd1234"));
118+ println!("{}", lower::encode_string(b"\xab\xcd\x12\x34"));
119+ println!("{}", upper::encode_string(b"\xab\xcd\x12\x34"));
120+ println!("[{}]", lower::encode_string(b""));
121+
122+ // `HexDisplay` is a tuple struct holding a borrowed slice, and its
123+ // `UpperHex`/`LowerHex` impls write once per byte into the formatter.
124+ let raw = b"\xab\xcd\x12\x34";
125+ println!("{}", HexDisplay(raw));
126+ println!("{:X} {:x}", HexDisplay(raw), HexDisplay(raw));
127+ println!("{}", HexDisplay(b""));
105 }128 }
added tests/cases/028-formatter-and-asserts.rs +46 -0
new file mode 100644
@@ -0,0 +1,46 @@
1+// A `fmt` body may write repeatedly, so a formatter write appends rather than
2+// assigns. `UpperHex` here writes once per element.
3+use core::fmt;
4+
5+struct Bytes<'a>(&'a [u8]);
6+
7+impl fmt::UpperHex for Bytes<'_> {
8+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9+ for &b in self.0 {
10+ write!(f, "{:02X}", b)?;
11+ }
12+ Ok(())
13+ }
14+}
15+
16+impl fmt::LowerHex for Bytes<'_> {
17+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18+ for &b in self.0 {
19+ write!(f, "{:02x}", b)?;
20+ }
21+ Ok(())
22+ }
23+}
24+
25+impl fmt::Display for Bytes<'_> {
26+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27+ f.write_str("<")?;
28+ write!(f, "{self:x}")?;
29+ f.write_str(">")
30+ }
31+}
32+
33+fn main() {
34+ let raw: Vec<u8> = vec![0xab, 0xcd, 0x01, 0x00];
35+ println!("{:X}", Bytes(&raw));
36+ println!("{:x}", Bytes(&raw));
37+ println!("{}", Bytes(&raw));
38+ println!("[{}]", Bytes(&[]));
39+
40+ // `debug_assert*` fires in debug builds, which is the profile modelled.
41+ assert!(raw.len() == 4);
42+ assert_eq!(raw.len(), 4);
43+ assert_ne!(raw.len(), 5);
44+ debug_assert_eq!(raw[0], 0xab);
45+ println!("asserts passed");
46+}
new file mode 100644
@@ -0,0 +1,46 @@
1+// A `fmt` body may write repeatedly, so a formatter write appends rather than
2+// assigns. `UpperHex` here writes once per element.
3+use core::fmt;
4+
5+struct Bytes<'a>(&'a [u8]);
6+
7+impl fmt::UpperHex for Bytes<'_> {
8+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9+ for &b in self.0 {
10+ write!(f, "{:02X}", b)?;
11+ }
12+ Ok(())
13+ }
14+}
15+
16+impl fmt::LowerHex for Bytes<'_> {
17+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18+ for &b in self.0 {
19+ write!(f, "{:02x}", b)?;
20+ }
21+ Ok(())
22+ }
23+}
24+
25+impl fmt::Display for Bytes<'_> {
26+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27+ f.write_str("<")?;
28+ write!(f, "{self:x}")?;
29+ f.write_str(">")
30+ }
31+}
32+
33+fn main() {
34+ let raw: Vec<u8> = vec![0xab, 0xcd, 0x01, 0x00];
35+ println!("{:X}", Bytes(&raw));
36+ println!("{:x}", Bytes(&raw));
37+ println!("{}", Bytes(&raw));
38+ println!("[{}]", Bytes(&[]));
39+
40+ // `debug_assert*` fires in debug builds, which is the profile modelled.
41+ assert!(raw.len() == 4);
42+ assert_eq!(raw.len(), 4);
43+ assert_ne!(raw.len(), 5);
44+ debug_assert_eq!(raw[0], 0xab);
45+ println!("asserts passed");
46+}
modified tests/differential.rs +15 -0
@@ -111,6 +111,8 @@ struct Directives {
111111 skip: Option<String>,
112112 args: Vec<String>,
113113 stdin: Option<String>,
114+ /// `--cfg` flags for rustnim. rustc gets `--cfg feature="x"` to match.
115+ cfg: Vec<String>,
114116 }
115117
116118 fn directives(src: &str) -> Directives {
@@ -134,6 +136,7 @@ fn directives(src: &str) -> Directives {
134136 "reject" => d.reject = Some(val),
135137 "skip" => d.skip = Some(val),
136138 "args" => d.args = val.split_whitespace().map(str::to_string).collect(),
139+ "cfg" => d.cfg.push(val),
137140 "stdin" => d.stdin = Some(format!("{val}\n")),
138141 _ => {}
139142 }
@@ -228,6 +231,9 @@ fn run_case(case: &Path, work: &Path, nim: &Path) -> Outcome {
228231 for e in &extra {
229232 cmd.arg(e);
230233 }
234+ for c in &d.cfg {
235+ cmd.arg("--cfg").arg(c);
236+ }
231237 cmd.arg("-o").arg(&nim_src).env("TMPDIR", &dir);
232238 let transpile = match run(&mut cmd, None) {
233239 Ok(r) => r,
@@ -273,6 +279,15 @@ fn run_case(case: &Path, work: &Path, nim: &Path) -> Outcome {
273279 Command::new("rustc")
274280 .arg("--edition=2021")
275281 .arg("-A").arg("warnings")
282+ .args(d.cfg.iter().flat_map(|c| {
283+ // rustc spells it `feature="x"`; the directive uses the
284+ // rustnim form, so it is rewritten here.
285+ let c = match c.split_once('=') {
286+ Some((k, v)) => format!("{k}=\"{v}\""),
287+ None => c.clone(),
288+ };
289+ ["--cfg".to_string(), c]
290+ }))
276291 .arg(&root)
277292 .arg("-o").arg(&rs_bin)
278293 .env("TMPDIR", &dir),
@@ -111,6 +111,8 @@ struct Directives {
111 skip: Option<String>,111 skip: Option<String>,
112 args: Vec<String>,112 args: Vec<String>,
113 stdin: Option<String>,113 stdin: Option<String>,
114+ /// `--cfg` flags for rustnim. rustc gets `--cfg feature="x"` to match.
115+ cfg: Vec<String>,
114 }116 }
115 117
116 fn directives(src: &str) -> Directives {118 fn directives(src: &str) -> Directives {
@@ -134,6 +136,7 @@ fn directives(src: &str) -> Directives {
134 "reject" => d.reject = Some(val),136 "reject" => d.reject = Some(val),
135 "skip" => d.skip = Some(val),137 "skip" => d.skip = Some(val),
136 "args" => d.args = val.split_whitespace().map(str::to_string).collect(),138 "args" => d.args = val.split_whitespace().map(str::to_string).collect(),
139+ "cfg" => d.cfg.push(val),
137 "stdin" => d.stdin = Some(format!("{val}\n")),140 "stdin" => d.stdin = Some(format!("{val}\n")),
138 _ => {}141 _ => {}
139 }142 }
@@ -228,6 +231,9 @@ fn run_case(case: &Path, work: &Path, nim: &Path) -> Outcome {
228 for e in &extra {231 for e in &extra {
229 cmd.arg(e);232 cmd.arg(e);
230 }233 }
234+ for c in &d.cfg {
235+ cmd.arg("--cfg").arg(c);
236+ }
231 cmd.arg("-o").arg(&nim_src).env("TMPDIR", &dir);237 cmd.arg("-o").arg(&nim_src).env("TMPDIR", &dir);
232 let transpile = match run(&mut cmd, None) {238 let transpile = match run(&mut cmd, None) {
233 Ok(r) => r,239 Ok(r) => r,
@@ -273,6 +279,15 @@ fn run_case(case: &Path, work: &Path, nim: &Path) -> Outcome {
273 Command::new("rustc")279 Command::new("rustc")
274 .arg("--edition=2021")280 .arg("--edition=2021")
275 .arg("-A").arg("warnings")281 .arg("-A").arg("warnings")
282+ .args(d.cfg.iter().flat_map(|c| {
283+ // rustc spells it `feature="x"`; the directive uses the
284+ // rustnim form, so it is rewritten here.
285+ let c = match c.split_once('=') {
286+ Some((k, v)) => format!("{k}=\"{v}\""),
287+ None => c.clone(),
288+ };
289+ ["--cfg".to_string(), c]
290+ }))
276 .arg(&root)291 .arg(&root)
277 .arg("-o").arg(&rs_bin)292 .arg("-o").arg(&rs_bin)
278 .env("TMPDIR", &dir),293 .env("TMPDIR", &dir),