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>afb2a6e parent: 0a375d8 modified
DESIGN.md +58 -43 | @@ -2,9 +2,11 @@ | ||
| 2 | 2 | |
| 3 | 3 | ## Status |
| 4 | 4 | |
| 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`. | |
| 8 | 10 | |
| 9 | 11 | Passing today: functions, `impl` methods, trait impls (formatting traits and |
| 10 | 12 | `From`), structs, enums (C-like and data-carrying), `Option`/`Result` with |
| @@ -105,11 +107,22 @@ loop rather than on each iteration. | ||
| 105 | 107 | |
| 106 | 108 | A `Display` impl becomes `proc rsDisplay(self: T): string`. Rust's `Formatter` |
| 107 | 109 | 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. | |
| 113 | 126 | |
| 114 | 127 | `impl From<A> for B` becomes a conversion proc that `.into()` resolves |
| 115 | 128 | 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 | 162 | relied on, because copying into a `seq` would print the right bytes while |
| 150 | 163 | silently changing aliasing. |
| 151 | 164 | |
| 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. | |
| 157 | 183 | |
| 158 | 184 | ### Closures and `unsafe` |
| 159 | 185 | |
| @@ -257,6 +283,9 @@ runner) rather than a wrong answer. | ||
| 257 | 283 | modules declaring the same type name would collide. Relatedly, a crate's |
| 258 | 284 | own `type Result<T>` is told apart from the builtin `Result<T, E>` by |
| 259 | 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 | 290 | ## Testing: differential, not golden |
| 262 | 291 | |
| @@ -312,43 +341,29 @@ or any parent, or via `RUSTNIM_NIM`. | ||
| 312 | 341 | Transpile `base16ct` 1.0.0 — the crate the other transpiler failed on — and |
| 313 | 342 | have its decoder produce byte-identical output to the Rust original. |
| 314 | 343 | |
| 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: | |
| 321 | 350 | |
| 322 | 351 | ``` |
| 323 | 352 | lower ok abcd1234 len=4 decode: lower, upper, mixed |
| 324 | -mixed-m ok abcd1234 len=4 | |
| 325 | -upper ok abcd1234 len=4 | |
| 326 | 353 | upper-rej err InvalidEncoding ... upper correctly rejects lowercase |
| 327 | 354 | oddlen err InvalidLength / invalid Base16 length <- Debug and Display |
| 328 | 355 | encode ok 6162636431323334 len=8 encode, both cases |
| 329 | -encode-up ok 4142434431323334 len=8 | |
| 330 | 356 | 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)?)`, | |
| 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. | |
| 337 | 367 | |
| 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 | ## Status | 3 | ## Status |
| 4 | 4 | ||
| 5 | -**Milestone 1 is reached, and then some.** Four of base16ct's six modules go | 5 | +**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 and | 11 | Passing today: functions, `impl` methods, trait impls (formatting traits and |
| 10 | `From`), structs, enums (C-like and data-carrying), `Option`/`Result` with | 12 | `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 into | 109 | 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 | 110 | +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 does | 111 | +may write repeatedly, and `UpperHex` writes once per byte in a loop. A body |
| 110 | -anything else with the formatter — padding, precision, `debug_struct` — is | 112 | +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()` resolves | 127 | `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 trait | 128 | 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 while | 162 | 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 Nim | 165 | +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. Instead | 166 | +probed, both preserving aliasing — so `Result<&[u8], E>` and |
| 154 | -the view and its validity condition travel together through `ok_or` until a | 167 | +`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 such | 168 | +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's | 283 | 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>` by | 284 | 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 golden | 290 | ## 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 — and | 341 | 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 on | 346 | +`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`'s | 347 | +`cmp` rather than by eye — together with `lib.rs`'s `decoded_len`, |
| 319 | -`decoded_len`, `encoded_len` and `decode_inner` verbatim. Output is | 348 | +`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, mixed | 352 | 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 lowercase | 353 | upper-rej err InvalidEncoding ... upper correctly rejects lowercase |
| 327 | oddlen err InvalidLength / invalid Base16 length <- Debug and Display | 354 | oddlen err InvalidLength / invalid Base16 length <- Debug and Display |
| 328 | encode ok 6162636431323334 len=8 encode, both cases | 355 | 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 &str | 356 | 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 the | 363 | +`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 that | 364 | +`&'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 bytes | 369 | +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 | ||
| 44 | 44 | container, so `*d = v` through `iter_mut()` reaches the caller's slice. |
| 45 | 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 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, | |
| 49 | 50 | and items are scoped by module, so `lower::decode` and `mixed::decode` stay |
| 50 | 51 | distinct. |
| 51 | 52 | |
| @@ -56,16 +57,17 @@ any standard-library method that isn't mapped. | ||
| 56 | 57 | |
| 57 | 58 | ## base16ct |
| 58 | 59 | |
| 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. | |
| 69 | 71 | |
| 70 | 72 | ## Tests |
| 71 | 73 | |
| @@ -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 an | 47 | +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` stay | 50 | 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 | ## base16ct | 58 | ## base16ct |
| 58 | 59 | ||
| 59 | -`tests/cases/026-base16ct-crate/` transpiles four of base16ct 1.0.0's six | 60 | +**It goes through.** `tests/cases/026-base16ct-crate/` transpiles every source |
| 60 | -modules — `error.rs`, `lower.rs`, `upper.rs`, `mixed.rs` — byte-for-byte as | 61 | +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` and | 62 | +`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 a | 64 | +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 the | 65 | +`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 | ## Tests | 72 | ## Tests |
| 71 | 73 | ||
modified
src/fmt.rs +17 -2 | @@ -116,9 +116,14 @@ fn parse_spec(s: &str) -> Result<Spec, String> { | ||
| 116 | 116 | } |
| 117 | 117 | |
| 118 | 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 | 125 | let core = match spec.radix { |
| 121 | - Some(r) => format!( | |
| 126 | + Some(r) if integer => format!( | |
| 122 | 127 | "rsRadix({}, {}, {})", |
| 123 | 128 | value, |
| 124 | 129 | match r { |
| @@ -128,6 +133,16 @@ pub fn render_arg(value: &str, spec: &Spec) -> String { | ||
| 128 | 133 | }, |
| 129 | 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 | 146 | None if spec.debug => format!("rsDebug({value})"), |
| 132 | 147 | None => format!("rsDisplay({value})"), |
| 133 | 148 | }; |
| @@ -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 { | ||
| 474 | 474 | Some(id) => id.to_string(), |
| 475 | 475 | 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 | 484 | self.structs.insert(s.ident.to_string(), fields); |
| 480 | 485 | } |
| @@ -520,7 +525,9 @@ impl Lowerer { | ||
| 520 | 525 | Some(id) => format!("{vname}_{id}"), |
| 521 | 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 | 532 | variants.push(Variant { name: vname, fields }); |
| 526 | 533 | } |
| @@ -1129,9 +1136,10 @@ impl Lowerer { | ||
| 1129 | 1136 | self.bind("self", self_ty.clone()); |
| 1130 | 1137 | let saved = self.fmt_param.replace(f); |
| 1131 | 1138 | 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(); | |
| 1135 | 1143 | |
| 1136 | 1144 | self.line(&format!( |
| 1137 | 1145 | "proc {}*(self: {}): string =", |
| @@ -1140,8 +1148,7 @@ impl Lowerer { | ||
| 1140 | 1148 | )); |
| 1141 | 1149 | self.indent += 1; |
| 1142 | 1150 | 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)?; | |
| 1145 | 1152 | self.emit_tail(tail); |
| 1146 | 1153 | if self.out.len() == before { |
| 1147 | 1154 | self.line("discard"); |
| @@ -1406,9 +1413,12 @@ impl Lowerer { | ||
| 1406 | 1413 | self.bind_alias(&name, w); |
| 1407 | 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 | 1419 | 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(), | |
| 1412 | 1422 | (None, None) => { |
| 1413 | 1423 | return Err(format!( |
| 1414 | 1424 | "cannot infer the type of `let {name}`; annotate it — \ |
| @@ -2347,8 +2357,11 @@ impl Lowerer { | ||
| 2347 | 2357 | Ok(Val::new(code, ty)) |
| 2348 | 2358 | } |
| 2349 | 2359 | Expr::Macro(m) => { |
| 2360 | + let is_write = matches!(path_name(&m.mac.path).as_str(), "write" | "writeln"); | |
| 2350 | 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 | 2366 | Expr::Struct(s) => { |
| 2354 | 2367 | if s.rest.is_some() { |
| @@ -2882,6 +2895,31 @@ impl Lowerer { | ||
| 2882 | 2895 | .into()); |
| 2883 | 2896 | } |
| 2884 | 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 | 2923 | if let (Some(guard), Some(w)) = (v.guard.clone(), v.window.clone()) { |
| 2886 | 2924 | // An `Option`/`Result` of a view: the check is emitted here and the |
| 2887 | 2925 | // view itself survives as an alias, since it has no value form. |
| @@ -2998,6 +3036,12 @@ impl Lowerer { | ||
| 2998 | 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 | 3045 | "Ok" | "Err" => { |
| 3002 | 3046 | let (t, e) = match expect { |
| 3003 | 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 | 3083 | // `core::str::from_utf8_unchecked(b)` reinterprets a byte view as a |
| 3024 | 3084 | // string view; no copy, no validation, same memory. |
| 3025 | 3085 | 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 | + }; | |
| 3030 | 3113 | } |
| 3031 | 3114 | |
| 3032 | 3115 | // A tuple enum variant applied to arguments: `Shape::Circle(1.0)`. |
| @@ -3170,13 +3253,57 @@ impl Lowerer { | ||
| 3170 | 3253 | "clone" | "to_vec" | "to_owned" | "as_slice" | "as_ref" | "as_mut" | "iter" |
| 3171 | 3254 | | "into_iter" => (recv.code.clone(), rt.clone()), |
| 3172 | 3255 | "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()) | |
| 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 | 3308 | "ok_or" if recv.guard.is_some() => { |
| 3182 | 3309 | let e = args.first().ok_or("`ok_or` takes one argument")?; |
| @@ -3262,10 +3389,15 @@ impl Lowerer { | ||
| 3262 | 3389 | } |
| 3263 | 3390 | // Inside a formatting impl, a write through the `Formatter` *is* |
| 3264 | 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) => ( | |
| 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 | + } | |
| 3269 | 3401 | "abs" => (format!("abs({})", recv.code), rt.clone()), |
| 3270 | 3402 | "min" => (format!("min({}, {})", recv.code, a0.unwrap_or_default()), rt.clone()), |
| 3271 | 3403 | "max" => (format!("max({}, {})", recv.code, a0.unwrap_or_default()), rt.clone()), |
| @@ -3374,22 +3506,57 @@ impl Lowerer { | ||
| 3374 | 3506 | .into()); |
| 3375 | 3507 | } |
| 3376 | 3508 | let s = self.format_pieces(&args[1..])?; |
| 3377 | - Ok(if name == "writeln" { | |
| 3509 | + let s = if name == "writeln" { | |
| 3378 | 3510 | format!("({} & \"\\n\")", s) |
| 3379 | 3511 | } else { |
| 3380 | 3512 | s |
| 3381 | - }) | |
| 3513 | + }; | |
| 3514 | + Ok(format!("result.add({})", s)) | |
| 3382 | 3515 | } |
| 3383 | 3516 | "panic" => { |
| 3384 | 3517 | let s = self.format_args(mac)?; |
| 3385 | 3518 | Ok(format!("rsPanic({s})")) |
| 3386 | 3519 | } |
| 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)`" }; | |
| 3390 | 3557 | 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 | |
| 3393 | 3560 | )) |
| 3394 | 3561 | } |
| 3395 | 3562 | "vec" => { |
| @@ -3477,7 +3644,17 @@ impl Lowerer { | ||
| 3477 | 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 | } |
| @@ -474,7 +474,12 @@ impl Lowerer { | |||
| 474 | Some(id) => id.to_string(), | 474 | Some(id) => id.to_string(), |
| 475 | None => format!("f{i}"), // tuple struct | 475 | 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 = self | 1139 | + // No assignment target: a formatter write *appends*, because a `fmt` |
| 1133 | - .target | 1140 | + // 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 the | 2924 | // 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 a | 3083 | // `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 | s | 3512 | 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.code | 3559 | + 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] = | ||
| 162 | 162 | else: |
| 163 | 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 | 172 | proc rsDisplay*(x: openArray[char]): string = |
| 166 | 173 | result = newStringOfCap(x.len) |
| 167 | 174 | 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 { | ||
| 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 | 69 | /// Owned form: a borrowed slice parameter is `openArray[T]`, but the same |
| 61 | 70 | /// type in an owned position (a field, a return value) must be `seq[T]`. |
| 62 | 71 | 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 same | 69 | /// 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 @@ | ||
| 1 | 1 | //@ args: run |
| 2 | +//@ cfg: feature=alloc | |
| 2 | 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 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. | |
| 6 | 7 | // This file carries `lib.rs`'s core -- `decoded_len`, `encoded_len` and |
| 7 | 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 | 12 | mod error; |
| 11 | 13 | mod lower; |
| 12 | 14 | mod mixed; |
| 13 | 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 | 23 | pub use crate::error::{Error, Result}; |
| 16 | 24 | |
| 17 | 25 | /// Compute decoded length of the given hex-encoded input. |
| @@ -102,4 +110,19 @@ fn main() { | ||
| 102 | 110 | } |
| 103 | 111 | |
| 104 | 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 | } |
| @@ -1,17 +1,25 @@ | |||
| 1 | //@ args: run | 1 | //@ 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 own | 5 | +// `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` and | 7 | // 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 { | ||
| 111 | 111 | skip: Option<String>, |
| 112 | 112 | args: Vec<String>, |
| 113 | 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 | 118 | fn directives(src: &str) -> Directives { |
| @@ -134,6 +136,7 @@ fn directives(src: &str) -> Directives { | ||
| 134 | 136 | "reject" => d.reject = Some(val), |
| 135 | 137 | "skip" => d.skip = Some(val), |
| 136 | 138 | "args" => d.args = val.split_whitespace().map(str::to_string).collect(), |
| 139 | + "cfg" => d.cfg.push(val), | |
| 137 | 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 | 231 | for e in &extra { |
| 229 | 232 | cmd.arg(e); |
| 230 | 233 | } |
| 234 | + for c in &d.cfg { | |
| 235 | + cmd.arg("--cfg").arg(c); | |
| 236 | + } | |
| 231 | 237 | cmd.arg("-o").arg(&nim_src).env("TMPDIR", &dir); |
| 232 | 238 | let transpile = match run(&mut cmd, None) { |
| 233 | 239 | Ok(r) => r, |
| @@ -273,6 +279,15 @@ fn run_case(case: &Path, work: &Path, nim: &Path) -> Outcome { | ||
| 273 | 279 | Command::new("rustc") |
| 274 | 280 | .arg("--edition=2021") |
| 275 | 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 | 291 | .arg(&root) |
| 277 | 292 | .arg("-o").arg(&rs_bin) |
| 278 | 293 | .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), |