| Scaffold rustnim: Rust->Nim transpiler, type mapping 1a218c2 nandi 7h ago | 1 | # rustnim — a Rust → Nim transpiler |
| 2 | |
| 3 | ## Status |
| 4 | |
| Add display.rs and the alloc half: all of base16ct now goes through afb2a6e nandithebull 5h ago | 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`. |
| Add trait impls and slice iterators; base16ct's decoder now goes through ae9f986 nandithebull 6h ago | 10 | |
| 11 | Passing today: functions, `impl` methods, trait impls (formatting traits and |
| 12 | `From`), structs, enums (C-like and data-carrying), `Option`/`Result` with |
| Add closures and unsafe; base16ct's lower.rs and upper.rs go through 0a375d8 nandithebull 5h ago | 13 | `?`, closures, `unsafe`, slice iterators (`iter`/`iter_mut`/`enumerate`/`zip`/`chunks_exact`/ |
| Add trait impls and slice iterators; base16ct's decoder now goes through ae9f986 nandithebull 6h ago | 14 | `chunks_exact_mut`/`windows`), borrowed slices as values and return types, |
| 15 | `let`/`let mut`, the full integer |
| Add enums, Option/Result, `?`, and the machinery base16ct needs around them b0ccd80 nandithebull 6h ago | 16 | and float operator set at exact widths, `as` casts, `if`/`while`/`loop`/`for`, |
| 17 | `match` including patterns that bind, `Vec`/slices/arrays, type aliases |
| 18 | (including generic ones), function-typed parameters (`impl Fn(A) -> B`), |
| 19 | multi-file input, `#[cfg]` evaluation, and `println!`/`format!` with `{}`, |
| 20 | `{:?}`, `{:x}`, `{:b}`, positional and inline-named arguments, and |
| Add the differential test runner, and a lowering to measure with it 8ac32af nandithebull 7h ago | 21 | zero/space padding. |
| Scaffold rustnim: Rust->Nim transpiler, type mapping 1a218c2 nandi 7h ago | 22 | |
| 23 | ## Why this exists |
| 24 | |
| 25 | We tried [tarekwasfy01/Code-Transpiler](https://github.com/tarekwasfy01/Code-Transpiler), |
| 26 | which advertises `rust` as a source language, on the `base16ct` crate. It emits |
| 27 | empty files and exits 0. The full investigation is in [`findings/`](findings/) |
| 28 | and is published at |
| 29 | https://rickub.com/nandi/code-transpiler-rust-frontend-findings |
| 30 | |
| 31 | The decisive finding, and the reason this is a new project rather than a patch: |
| 32 | its Universal AST cannot represent Rust. `defaultSemanticTypeContract()` in |
| 33 | `internal/backend/semantic_program.go:85` is hardcoded to |
| 34 | |
| 35 | ``` |
| 36 | numeric: binary64, integer_width: unknown, truth: r_compatible, |
| 37 | ownership: unknown, index_base: 1 |
| 38 | ``` |
| 39 | |
| 40 | and `semantic_document.go:1014` *validates* that every contract equals exactly |
| 41 | that, while `typed_operation.go:46` rejects any value model that is not |
| 42 | `tagged_dynamic_binary64`. There is no integer width and no ownership in the |
| 43 | model at all. Code like `base16ct`'s constant-time decoder — |
| 44 | |
| 45 | ```rust |
| 46 | ret += (((0x2fi16 - byte) & (byte - 0x3a)) >> 8) & (byte - 47); |
| 47 | ``` |
| 48 | |
| 49 | — depends on exact 16-bit signed wrapping and arithmetic shift. Lowering that |
| 50 | into a 1-indexed dynamic float64 model produces silently wrong answers. So the |
| 51 | first rule of this project is the one that codebase broke: |
| 52 | |
| 53 | > **Never approximate a semantic you cannot represent. Fail loudly instead.** |
| 54 | |
| 55 | `src/ty.rs` already does this: `i128`/`u128` are rejected with a reason rather |
| 56 | than widened or truncated. |
| 57 | |
| 58 | ## Architecture |
| 59 | |
| 60 | ``` |
| 61 | Rust source ──syn──> syn AST ──lower──> Nim source ──nim c──> binary |
| 62 | ``` |
| 63 | |
| 64 | **The frontend is `syn`, deliberately.** Hand-rolling a Rust grammar is how the |
| 65 | other project went wrong; a correct parser is not the interesting part of this |
| 66 | problem. The interesting part is the lowering, which is where all the work goes. |
| 67 | |
| 68 | Planned modules: |
| 69 | |
| 70 | | file | role | state | |
| 71 | |---|---|---| |
| 72 | | `src/ty.rs` | Rust type → Nim type, exact widths, explicit rejections | written | |
| Add the differential test runner, and a lowering to measure with it 8ac32af nandithebull 7h ago | 73 | | `src/lower.rs` | items, statements, expressions → Nim | written | |
| 74 | | `src/fmt.rs` | `println!`/`format!` format-string handling | written | |
| 75 | | `src/prelude.nim` | `Option`/`Result`/panic/`Display`/`Debug` runtime | written | |
| 76 | | `src/main.rs` | CLI: `rustnim <in.rs> -o <out.nim>` | written | |
| 77 | | `tests/differential.rs` | the runner described below | written | |
| 78 | |
| Add enums, Option/Result, `?`, and the machinery base16ct needs around them b0ccd80 nandithebull 6h ago | 79 | ### Enums, `Option` and `Result` |
| 80 | |
| 81 | A C-like enum becomes a plain Nim `enum`, which compares, orders and |
| 82 | `case`-checks the way Rust's does. A data-carrying enum becomes a Nim object |
| 83 | variant — a discriminant enum plus one branch per variant — which is the same |
| 84 | shape the prelude already uses for `Option` and `Result`. Nim requires the |
| 85 | branches of a variant object to have distinct field names, so each payload |
| 86 | field is prefixed with its variant. |
| 87 | |
| 88 | `match` takes one of two forms. Arms that neither bind nor destructure become |
| 89 | a Nim `case`, which is exhaustiveness-checked the way Rust's is. Arms that do |
| 90 | bind become an `if`/`elif` chain with the bindings emitted as `let`s, because |
| 91 | Nim's `case` cannot destructure. The chain always ends in an arm that panics: |
| 92 | Rust proved it unreachable, but Nim cannot see that, and leaving the chain |
| 93 | open would silently fall through instead. |
| 94 | |
| 95 | `Ok`, `Err` and `Some` are emitted with their full type arguments |
| 96 | (`rsOk[T, E](v)`), because Nim cannot infer `E` from an `Ok(v)` alone. That is |
| 97 | why the expected type has to reach a `match` arm as well as a `let`. |
| 98 | |
| 99 | `?` expands to statements — a temporary, a discriminant test, and an early |
| 100 | `return` — which are emitted ahead of the line being built. Rust inserts a |
| 101 | `From::from` on the error there; we accept only the case where the two error |
| 102 | types already agree, rather than assume a conversion is the identity. `?` in a |
| 103 | `while` condition is rejected: the early return would run once before the |
| 104 | loop rather than on each iteration. |
| 105 | |
| Add trait impls and slice iterators; base16ct's decoder now goes through ae9f986 nandithebull 6h ago | 106 | ### Trait impls |
| 107 | |
| 108 | A `Display` impl becomes `proc rsDisplay(self: T): string`. Rust's `Formatter` |
| 109 | is a sink and the observable result of `{}` is exactly the bytes written into |
| Add display.rs and the alloc half: all of base16ct now goes through afb2a6e nandithebull 5h ago | 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. |
| Add trait impls and slice iterators; base16ct's decoder now goes through ae9f986 nandithebull 6h ago | 126 | |
| 127 | `impl From<A> for B` becomes a conversion proc that `.into()` resolves |
| 128 | through. A marker trait with no items generates nothing: we do not model trait |
| 129 | resolution anywhere, so there is nothing for it to affect; a use that actually |
| 130 | needed the trait (a `dyn`, a bound) is rejected where it appears. Any other |
| 131 | trait impl is rejected. |
| 132 | |
| 133 | Methods are keyed by `(receiver type, name)`, not by name alone — two types |
| 134 | may define the same method, and Nim tells them apart by overload resolution on |
| 135 | the first parameter. |
| 136 | |
| 137 | `fmt::Error` is *not* the same type as a crate's own `Error`. Collapsing a |
| 138 | qualified path to its last segment merged them, which was a real soundness |
| 139 | bug; `core::fmt`'s types are now recognised by their qualified name. |
| 140 | |
| 141 | ### Slice iterators are resolved to one index loop |
| 142 | |
| 143 | Rust's slice iterators are lazy and compose. Nim's `for` is over one sequence, |
| 144 | so a chain of adaptors is resolved into a small IR and emitted as a single |
| 145 | index loop in which **each binding is an lvalue into the original container**. |
| 146 | That is what makes `*d = v` through `iter_mut()` write back to the caller's |
| 147 | slice instead of to a copy, and what lets `chunks_exact(2)` hand out a window |
| 148 | that indexes straight into the source with an offset. |
| 149 | |
| 150 | Only adaptors with an exact index-loop equivalent are accepted. `map`, |
| 151 | `filter` and `take_while` are rejected rather than partially honoured: |
| 152 | silently dropping an adaptor would change which elements the loop visits. |
| 153 | |
| 154 | `zip` stops at the shorter side, as Rust's does — that is a test, not an |
| 155 | assumption (`tests/cases/023`). |
| 156 | |
| 157 | ### Borrowed slices are views, not copies |
| 158 | |
| 159 | `&[T]` is a borrow. Nim's experimental view types model exactly that, |
| 160 | including returning one from a proc: writing through the returned view is |
| 161 | visible in the original buffer. That was probed against Nim 2.2.4 before being |
| 162 | relied on, because copying into a `seq` would print the right bytes while |
| 163 | silently changing aliasing. |
| 164 | |
| Add display.rs and the alloc half: all of base16ct now goes through afb2a6e nandithebull 5h ago | 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. |
| Add trait impls and slice iterators; base16ct's decoder now goes through ae9f986 nandithebull 6h ago | 183 | |
| Add closures and unsafe; base16ct's lower.rs and upper.rs go through 0a375d8 nandithebull 5h ago | 184 | ### Closures and `unsafe` |
| 185 | |
| 186 | `unsafe` is a permission marker, not a semantic change: it does not alter what |
| 187 | the enclosed operations mean. So the block is transparent, and every operation |
| 188 | inside still goes through the ordinary lowering and is still rejected if it has |
| 189 | no faithful mapping. `unsafe fn` lowers like any other proc. |
| 190 | |
| 191 | A closure becomes a Nim anonymous proc. Nim's closures capture by reference, as |
| 192 | Rust's non-`move` closures do; a `move` closure captures by value, which is a |
| 193 | different thing, so it is rejected rather than lowered to the same construct. |
| 194 | `impl Fn(A) -> B` is left at Nim's default calling convention, which accepts |
| 195 | both a plain top-level proc and a capturing closure — as Rust's `impl Fn` does. |
| 196 | |
| 197 | `.map`/`.and_then` over an `Option`/`Result` are expanded inline with the |
| 198 | closure's parameter aliased to the payload, rather than handed to a generic |
| 199 | proc. That keeps the whole thing an expression and keeps a view a view. |
| 200 | |
| 201 | `&str` is a borrowed view of someone else's bytes, so it maps to |
| 202 | `openArray[char]`, not to an owned `string`. Nim accepts a `string` argument |
| 203 | for an `openArray[char]` parameter, so a literal still passes straight through. |
| 204 | `from_utf8_unchecked` reinterprets a byte view as a character view over the |
| 205 | same memory — no copy, no validation, and writes through the original are |
| 206 | visible, as in Rust. |
| 207 | |
| 208 | ### Modules |
| 209 | |
| 210 | Rust keeps `lower::decode` and `mixed::decode` apart by module; flattening into |
| 211 | one Nim module would merge them — they are *different functions*. So the first |
| 212 | input is the crate root and each later one is a module named by its file stem, |
| 213 | items are emitted as `<module>_<name>`, and a call resolves through an explicit |
| 214 | qualifier, then the current module, then what `use` brought into scope, then |
| 215 | the root. |
| 216 | |
| Add trait impls and slice iterators; base16ct's decoder now goes through ae9f986 nandithebull 6h ago | 217 | ### Declaration order |
| 218 | |
| 219 | Rust has no declaration-before-use rule and Nim does, so every proc is |
| 220 | forward-declared between the type definitions and the bodies. Reordering the |
| 221 | input instead would not handle mutual recursion. |
| 222 | |
| Add the differential test runner, and a lowering to measure with it 8ac32af nandithebull 7h ago | 223 | ### Type propagation is load-bearing |
| 224 | |
| 225 | Rust infers an unsuffixed integer literal's type from context and falls back |
| 226 | to `i32`; Nim falls back to 64-bit `int`. So `lower.rs` threads an *expected |
| 227 | type* down through every expression — into `let` annotations, call arguments, |
| 228 | `match` patterns, compound assignments and both operands of a binary — and |
| 229 | annotates every binding it emits. Without that, `let x: u8 = 200; x + 100` |
| 230 | means two different things in the two languages. With it, a width the lowering |
| 231 | gets wrong becomes a Nim compile error (a loud failure, reported by the |
| 232 | runner) rather than a wrong answer. |
| Scaffold rustnim: Rust->Nim transpiler, type mapping 1a218c2 nandi 7h ago | 233 | |
| 234 | ## Mapping decisions made so far |
| 235 | |
| 236 | - **Integers**: exact width. `i32`→`int32`, `usize`→`uint`, etc. `i128`/`u128` |
| 237 | rejected. |
| 238 | - **Indexing**: both 0-based. Direct. |
| 239 | - **`&T`** → plain value. **`&mut T`** → `var T` parameter. |
| 240 | - **`&[T]`** → `openArray[T]` in parameter position, `seq[T]` when owned. |
| 241 | `Nim::owned()` performs that conversion. |
| 242 | - **Ownership/borrowck**: ignored. Nim is GC'd; for safe Rust this is sound. |
| 243 | - **`Option`/`Result`** → object variants in the prelude. |
| 244 | - **`match`** → Nim `case` where the arms are simple, `if`/`elif` when arms have |
| 245 | guards or bindings. |
| 246 | - **Rust's expression-orientation** maps well: Nim `if`/`case` are expressions |
| 247 | too, and a proc's trailing expression is its return value. |
| 248 | |
| Settle signed-shr and unsigned-wrap semantics against both compilers 87c9cc8 nandi 7h ago | 249 | ### Settled empirically (Nim 2.2.4 vs rustc 1.98.1, both run) |
| 250 | |
| 251 | 1. **Nim's `shr` on a signed integer is arithmetic**, matching Rust. |
| 252 | `int16(-256) shr 8` = `-1` in Nim; `(-256i16) >> 8` = `-1` in Rust. |
| 253 | `base16ct`'s decoder depends on this, so it maps directly with no helper. |
| 254 | 2. **Nim's fixed-width unsigned arithmetic wraps silently**, matching Rust's |
| 255 | `wrapping_*`. `uint8(200) + 100` = `44` in Nim; `200u8.wrapping_add(100)` |
| 256 | = `44` in Rust. So `wrapping_add` on an unsigned type is just `+`. |
| 257 | |
| Add the differential test runner, and a lowering to measure with it 8ac32af nandithebull 7h ago | 258 | 3. **We model rustc's debug profile.** Rust debug builds panic on signed |
| 259 | integer overflow; Nim's default build raises `OverflowDefect` on it. Those |
| 260 | are the matching pair, so the runner invokes `rustc` without `-O` and `nim |
| 261 | c` with its defaults, and `tests/cases/016` pins the behaviour. A Rust |
| 262 | panic exits 101 where a Nim Defect exits 1, so every generated module ends |
| 263 | with a handler that maps one to the other — otherwise the runner's |
| 264 | exit-status comparison would be vacuous. `wrapping_*` is therefore an |
| 265 | explicit operation on both sides: unsigned maps to the bare operator (item |
| 266 | 2), signed is routed through the unsigned view of the same width. |
| 267 | 4. **`char` round-trips.** Rust `char` → Nim `Rune`, confirmed for ASCII and |
| 268 | non-ASCII scalars in both `{}` and `{:?}`, and across `as u32` |
| 269 | (`tests/cases/014`). |
| Prove byte-identity for base16ct by enumerating whole input domains 99b8376 nandithebull 5h ago | 270 | 5. **Nim's integer conversion `T(x)` truncates; it does not range-check.** |
| 271 | `uint8(511'u16)` is `255`, `uint8(300'i32)` is `44`, `uint8(-1'i32)` is |
| 272 | `255` — the same answers as `cast[uint8]`. An earlier version of this |
| 273 | document asserted that `T(x)` range-checks, and used that to justify |
| 274 | `cast`. The conclusion stands — `cast` is the clearer spelling of |
| 275 | "truncate" — but the stated reason was wrong, and it had been assumed |
| 276 | rather than probed. Found by sabotaging the cast lowering and watching the |
| 277 | exhaustive proof *not* fail, which is what a sabotage test is for. |
| Add the differential test runner, and a lowering to measure with it 8ac32af nandithebull 7h ago | 278 | |
| Settle signed-shr and unsigned-wrap semantics against both compilers 87c9cc8 nandi 7h ago | 279 | ### Still open |
| 280 | |
| Prove byte-identity for base16ct by enumerating whole input domains 99b8376 nandithebull 5h ago | 281 | 6. `checked_*` and `saturating_*` are not mapped yet; they are currently |
| Add the differential test runner, and a lowering to measure with it 8ac32af nandithebull 7h ago | 282 | rejected as unsupported methods rather than approximated. |
| Prove byte-identity for base16ct by enumerating whole input domains 99b8376 nandithebull 5h ago | 283 | 7. Generics (type and const parameters), `move` closures, closure bodies with |
| Add closures and unsafe; base16ct's lower.rs and upper.rs go through 0a375d8 nandithebull 5h ago | 284 | statements, and trait impls other than the formatting traits and `From` are |
| 285 | rejected with a reason. |
| Add trait impls and slice iterators; base16ct's decoder now goes through ae9f986 nandithebull 6h ago | 286 | Lifetime parameters are *not* a rejection: they carry no runtime meaning |
| 287 | and Nim is GC'd, so `fn encode<'a>(..)` lowers fine. |
| Prove byte-identity for base16ct by enumerating whole input domains 99b8376 nandithebull 5h ago | 288 | 8. Float formatting matches Rust for ordinary values and for `inf`/`NaN`, but |
| Add the differential test runner, and a lowering to measure with it 8ac32af nandithebull 7h ago | 289 | the exponent-form thresholds have only been checked at `1e21`. |
| Prove byte-identity for base16ct by enumerating whole input domains 99b8376 nandithebull 5h ago | 290 | 9. Functions are scoped by module now, but *types* are still global: two |
| Add closures and unsafe; base16ct's lower.rs and upper.rs go through 0a375d8 nandithebull 5h ago | 291 | modules declaring the same type name would collide. Relatedly, a crate's |
| 292 | own `type Result<T>` is told apart from the builtin `Result<T, E>` by |
| 293 | arity, which is not how Rust resolves it. |
| Transpile a second crate, adler2, to test whether any of this generalises 5fdd5be nandithebull 5h ago | 294 | 10. `#[cfg(target_pointer_width)]` and `#[cfg(target_endian)]` are evaluated |
| 295 | against the *host*, since the generated Nim is compiled for it. That makes |
| 296 | the output host-shaped: a crate branching on pointer width has had that |
| 297 | branch decided at transpile time. |
| 298 | 11. Associated types (`impl Iterator { type Item = .. }`) and `mod` |
| 299 | directories (`specialized/mod.rs`) are not implemented. |
| 300 | 12. `String::from_utf8_unchecked` copies, because Nim's `string` is an owned |
| Add display.rs and the alloc half: all of base16ct now goes through afb2a6e nandithebull 5h ago | 301 | value. Rust's consumes the `Vec` without copying. Observably the same from |
| 302 | the caller, but it is a copy where Rust has none. |
| Scaffold rustnim: Rust->Nim transpiler, type mapping 1a218c2 nandi 7h ago | 303 | |
| Transpile a second crate, adler2, to test whether any of this generalises 5fdd5be nandithebull 5h ago | 304 | ## A second crate: does this generalise, or is it fitted to `base16ct`? |
| 305 | |
| 306 | `base16ct` is the crate this was built toward, so passing it proves less than |
| 307 | it looks. `adler2` 2.0.1 was picked as a deliberately different shape — |
| 308 | a stateful struct with methods, operator-overload trait impls, a hand-unrolled |
| 309 | four-lane inner loop — and it now works: `tests/cases/029-adler2-crate/` |
| 310 | transpiles `algo.rs` byte-for-byte as published, with `lib.rs`'s items and a |
| 311 | driver, and its checksums are byte-identical to rustc's across every single |
| 312 | byte, every length to 600 (crossing the 4-byte unrolling boundary and the |
| 313 | 5552-chunk path), and 144 incremental-write splits. |
| 314 | |
| 315 | It needed real work, which is the honest part of the answer. Ten features: |
| 316 | trait impls generalised beyond formatting and `From` (any trait's methods |
| 317 | become procs on the type, with the operator traits wired into `+=`/`+` |
| 318 | dispatch), `Self`, `Type::method()` static calls, `u32::from` between |
| 319 | primitives, tuple-destructuring `let`, `split_at`, iterators bound to |
| 320 | variables and `.remainder()`, `[0; 4]` as an array rather than a `seq`, and |
| 321 | the bare `#[cfg]` flags. |
| 322 | |
| 323 | It also caught a **regression I had introduced**: the three-phase emission |
| 324 | added for forward declarations was silently dropping `const` items declared |
| 325 | *inside* a function body. `base16ct` has none, so 33 passing cases said |
| 326 | nothing about it. |
| 327 | |
| 328 | ### What the other crates did |
| 329 | |
| 330 | Run without fixing anything, to see where the wall is rather than to move it: |
| 331 | |
| 332 | | crate | outcome | |
| 333 | |---|---| |
| 334 | | `adler2` 2.0.1 | **works**, byte-identical | |
| 335 | | `siphasher` 1.0.1 | rejected: `u128` | |
| 336 | | `rustc-hash` 2.1.1 | rejected: `u128` | |
| 337 | | `hex` 0.4.3 | rejected: `impl Iterator` needs an associated type | |
| 338 | | `crc32fast` 1.5.0 | rejected: directory modules (`specialized/mod.rs`), then SIMD intrinsics | |
| 339 | |
| 340 | Two of the five stop at `u128`, which is the founding rule doing its job |
| 341 | rather than a gap: they are told they cannot be translated instead of being |
| 342 | handed a silently truncated hasher. The other two are honest missing |
| 343 | features — associated types, and `mod` directories. |
| 344 | |
| Prove byte-identity for base16ct by enumerating whole input domains 99b8376 nandithebull 5h ago | 345 | ## Proof of byte-identity for `base16ct` |
| 346 | |
| 347 | [`PROOF.md`](PROOF.md) sets out what is actually established: exhaustive |
| 348 | agreement over every two-byte decode input (65,536), every two-byte encode |
| 349 | input (65,536), every single byte through `encode_str` and `HexDisplay`, and |
| 350 | every length to 128 — plus a compositional argument extending those to inputs |
| 351 | of any length, and 20,000 pseudorandom multi-chunk cases attacking the one |
| 352 | step in that argument that is inspection rather than enumeration. Run it with |
| 353 | `cargo test --test proof`. It is explicit about the difference between the |
| 354 | exhaustive parts and the sampled ones. |
| 355 | |
| Scaffold rustnim: Rust->Nim transpiler, type mapping 1a218c2 nandi 7h ago | 356 | ## Testing: differential, not golden |
| 357 | |
| 358 | The bar is **behavioural equivalence with rustc**, not that the output looks |
| 359 | plausible. For each case in `tests/cases/`: |
| 360 | |
| 361 | ``` |
| 362 | rustc case.rs && ./case > expected |
| 363 | rustnim case.rs -o case.nim && nim c -r case.nim > actual |
| 364 | diff expected actual |
| 365 | ``` |
| 366 | |
| 367 | A case only counts as passing when both binaries build *and* produce identical |
| Add the differential test runner, and a lowering to measure with it 8ac32af nandithebull 7h ago | 368 | stdout *and* exit with the same status. |
| 369 | |
| 370 | `tests/differential.rs` implements this, and checks each stage separately so a |
| 371 | failure says where it went wrong: `rustnim`, `rustc`, `nim`, or `diff`. Three |
| 372 | guards exist specifically because of how the other transpiler failed: |
| 373 | |
| 374 | - `rustnim` exiting 0 while writing **no output file** is a failure. |
| 375 | - `rustnim` exiting 0 while writing an **empty output file** is a failure. |
| 376 | - An **empty corpus** is a failure, so the runner cannot pass by finding |
| 377 | nothing to do. |
| 378 | |
| 379 | All three have been verified by deliberately breaking the transpiler and |
| 380 | confirming the runner goes red. |
| 381 | |
| 382 | Cases carry directives in leading `//@` comments: |
| 383 | |
| 384 | | directive | meaning | |
| 385 | |---|---| |
| 386 | | `//@ reject: <substring>` | `rustnim` must *fail*, with this in its message | |
| 387 | | `//@ skip: <reason>` | not run; reported as skipped | |
| 388 | | `//@ args: <argv>` | passed to both binaries | |
| 389 | | `//@ stdin: <line>` | fed to both binaries | |
| 390 | |
| 391 | `reject` cases are how the "fail loudly" rule is tested rather than merely |
| 392 | stated: `900`–`904` pin the rejections of `i128`, an unmapped standard-library |
| 393 | method, a float→int cast, an unimplemented format spec, and a closure. |
| 394 | |
| 395 | Run one case with `RUSTNIM_CASE=005 cargo test --test differential -- |
| 396 | --nocapture`. Nim is found at `.nim-toolchain/bin/nim` in the repository root |
| 397 | or any parent, or via `RUSTNIM_NIM`. |
| Scaffold rustnim: Rust->Nim transpiler, type mapping 1a218c2 nandi 7h ago | 398 | |
| 399 | ## Toolchain |
| 400 | |
| 401 | - `rustc` / `cargo` 1.98.1 — system. |
| 402 | - Nim 2.2.4 — vendored at `.nim-toolchain/` (gitignored; downloaded from |
| 403 | nim-lang.org, not installed system-wide). Binary: `.nim-toolchain/bin/nim`. |
| 404 | |
| 405 | ## Milestone 1 |
| 406 | |
| 407 | Transpile `base16ct` 1.0.0 — the crate the other transpiler failed on — and |
| Add enums, Option/Result, `?`, and the machinery base16ct needs around them b0ccd80 nandithebull 6h ago | 408 | have its decoder produce byte-identical output to the Rust original. |
| 409 | |
| Add display.rs and the alloc half: all of base16ct now goes through afb2a6e nandithebull 5h ago | 410 | **Reached.** `tests/cases/026-base16ct-crate/` transpiles **every source file |
| 411 | of base16ct 1.0.0** — `error.rs`, `lower.rs`, `upper.rs`, `mixed.rs` and |
| 412 | `display.rs`, each byte-for-byte as published on crates.io, verified with |
| 413 | `cmp` rather than by eye — together with `lib.rs`'s `decoded_len`, |
| 414 | `encoded_len` and `decode_inner` verbatim. The `alloc` half is on, via |
| 415 | `--cfg feature=alloc`. Output is byte-identical to rustc's: |
| Add enums, Option/Result, `?`, and the machinery base16ct needs around them b0ccd80 nandithebull 6h ago | 416 | |
| Add trait impls and slice iterators; base16ct's decoder now goes through ae9f986 nandithebull 6h ago | 417 | ``` |
| Add closures and unsafe; base16ct's lower.rs and upper.rs go through 0a375d8 nandithebull 5h ago | 418 | lower ok abcd1234 len=4 decode: lower, upper, mixed |
| 419 | upper-rej err InvalidEncoding ... upper correctly rejects lowercase |
| 420 | oddlen err InvalidLength / invalid Base16 length <- Debug and Display |
| 421 | encode ok 6162636431323334 len=8 encode, both cases |
| 422 | encode_str ok abcd1234 len=8 closure over unsafe, borrowed &str |
| Add display.rs and the alloc half: all of base16ct now goes through afb2a6e nandithebull 5h ago | 423 | Ok([171, 205, 18, 52]) decode_vec \ |
| 424 | abcd1234 encode_string > the alloc half |
| 425 | ABCD1234 abcd1234 HexDisplay {:X} {:x} |
| Add trait impls and slice iterators; base16ct's decoder now goes through ae9f986 nandithebull 6h ago | 426 | ``` |
| Add enums, Option/Result, `?`, and the machinery base16ct needs around them b0ccd80 nandithebull 6h ago | 427 | |
| Add display.rs and the alloc half: all of base16ct now goes through afb2a6e nandithebull 5h ago | 428 | Everything lowers as written: `dst.get_mut(..decoded_len(src)?)`, |
| 429 | `src.chunks_exact(2).zip(dst.iter_mut())`, `*dst = byte as u8`, the returned |
| 430 | `&'a [u8]` view into the caller's buffer, `encode(src, dst).map(|r| unsafe { |
| 431 | core::str::from_utf8_unchecked(r) })`, and `HexDisplay`'s `UpperHex` impl |
| 432 | writing once per byte into the formatter. |
| Add trait impls and slice iterators; base16ct's decoder now goes through ae9f986 nandithebull 6h ago | 433 | |
| Add display.rs and the alloc half: all of base16ct now goes through afb2a6e nandithebull 5h ago | 434 | This is the crate whose six files the transpiler in `findings/` emitted empty |
| 435 | output for, while exiting 0. |