# rustnim — a Rust → Nim transpiler ## Status **Milestone 1 is reached: all of `base16ct` goes through.** Every one of its source files transpiles byte-for-byte as published, `alloc` half included, and its decode and encode output is byte-identical to rustc's. 33 differential cases, 29 behavioural and 4 rejections, plus 6 unit/integration tests. All green. Run `cargo test`. Passing today: functions, `impl` methods, trait impls (formatting traits and `From`), structs, enums (C-like and data-carrying), `Option`/`Result` with `?`, closures, `unsafe`, slice iterators (`iter`/`iter_mut`/`enumerate`/`zip`/`chunks_exact`/ `chunks_exact_mut`/`windows`), borrowed slices as values and return types, `let`/`let mut`, the full integer and float operator set at exact widths, `as` casts, `if`/`while`/`loop`/`for`, `match` including patterns that bind, `Vec`/slices/arrays, type aliases (including generic ones), function-typed parameters (`impl Fn(A) -> B`), multi-file input, `#[cfg]` evaluation, and `println!`/`format!` with `{}`, `{:?}`, `{:x}`, `{:b}`, positional and inline-named arguments, and zero/space padding. ## Why this exists We tried [tarekwasfy01/Code-Transpiler](https://github.com/tarekwasfy01/Code-Transpiler), which advertises `rust` as a source language, on the `base16ct` crate. It emits empty files and exits 0. The full investigation is in [`findings/`](findings/) and is published at https://rickub.com/nandi/code-transpiler-rust-frontend-findings The decisive finding, and the reason this is a new project rather than a patch: its Universal AST cannot represent Rust. `defaultSemanticTypeContract()` in `internal/backend/semantic_program.go:85` is hardcoded to ``` numeric: binary64, integer_width: unknown, truth: r_compatible, ownership: unknown, index_base: 1 ``` and `semantic_document.go:1014` *validates* that every contract equals exactly that, while `typed_operation.go:46` rejects any value model that is not `tagged_dynamic_binary64`. There is no integer width and no ownership in the model at all. Code like `base16ct`'s constant-time decoder — ```rust ret += (((0x2fi16 - byte) & (byte - 0x3a)) >> 8) & (byte - 47); ``` — depends on exact 16-bit signed wrapping and arithmetic shift. Lowering that into a 1-indexed dynamic float64 model produces silently wrong answers. So the first rule of this project is the one that codebase broke: > **Never approximate a semantic you cannot represent. Fail loudly instead.** `src/ty.rs` already does this: `i128`/`u128` are rejected with a reason rather than widened or truncated. ## Architecture ``` Rust source ──syn──> syn AST ──lower──> Nim source ──nim c──> binary ``` **The frontend is `syn`, deliberately.** Hand-rolling a Rust grammar is how the other project went wrong; a correct parser is not the interesting part of this problem. The interesting part is the lowering, which is where all the work goes. Planned modules: | file | role | state | |---|---|---| | `src/ty.rs` | Rust type → Nim type, exact widths, explicit rejections | written | | `src/lower.rs` | items, statements, expressions → Nim | written | | `src/fmt.rs` | `println!`/`format!` format-string handling | written | | `src/prelude.nim` | `Option`/`Result`/panic/`Display`/`Debug` runtime | written | | `src/main.rs` | CLI: `rustnim -o ` | written | | `tests/differential.rs` | the runner described below | written | ### Enums, `Option` and `Result` A C-like enum becomes a plain Nim `enum`, which compares, orders and `case`-checks the way Rust's does. A data-carrying enum becomes a Nim object variant — a discriminant enum plus one branch per variant — which is the same shape the prelude already uses for `Option` and `Result`. Nim requires the branches of a variant object to have distinct field names, so each payload field is prefixed with its variant. `match` takes one of two forms. Arms that neither bind nor destructure become a Nim `case`, which is exhaustiveness-checked the way Rust's is. Arms that do bind become an `if`/`elif` chain with the bindings emitted as `let`s, because Nim's `case` cannot destructure. The chain always ends in an arm that panics: Rust proved it unreachable, but Nim cannot see that, and leaving the chain open would silently fall through instead. `Ok`, `Err` and `Some` are emitted with their full type arguments (`rsOk[T, E](v)`), because Nim cannot infer `E` from an `Ok(v)` alone. That is why the expected type has to reach a `match` arm as well as a `let`. `?` expands to statements — a temporary, a discriminant test, and an early `return` — which are emitted ahead of the line being built. Rust inserts a `From::from` on the error there; we accept only the case where the two error types already agree, rather than assume a conversion is the identity. `?` in a `while` condition is rejected: the early return would run once before the loop rather than on each iteration. ### Trait impls A `Display` impl becomes `proc rsDisplay(self: T): string`. Rust's `Formatter` is a sink and the observable result of `{}` is exactly the bytes written into it, so a write through the formatter **appends** to that string — a `fmt` body may write repeatedly, and `UpperHex` writes once per byte in a loop. A body that does anything else with the formatter — padding, precision, `debug_struct` — is rejected, because those change the output and this model does not carry them. `Debug`, `LowerHex`, `UpperHex`, `Binary` and `Octal` work the same way. 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 `format!` panics when a formatting impl returns an error — so 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 a radix format on an argument of unknown type is rejected rather than guessed. `impl From for B` becomes a conversion proc that `.into()` resolves through. A marker trait with no items generates nothing: we do not model trait resolution anywhere, so there is nothing for it to affect; a use that actually needed the trait (a `dyn`, a bound) is rejected where it appears. Any other trait impl is rejected. Methods are keyed by `(receiver type, name)`, not by name alone — two types may define the same method, and Nim tells them apart by overload resolution on the first parameter. `fmt::Error` is *not* the same type as a crate's own `Error`. Collapsing a qualified path to its last segment merged them, which was a real soundness bug; `core::fmt`'s types are now recognised by their qualified name. ### Slice iterators are resolved to one index loop Rust's slice iterators are lazy and compose. Nim's `for` is over one sequence, so a chain of adaptors is resolved into a small IR and emitted as a single index loop in which **each binding is an lvalue into the original container**. That is what makes `*d = v` through `iter_mut()` write back to the caller's slice instead of to a copy, and what lets `chunks_exact(2)` hand out a window that indexes straight into the source with an offset. Only adaptors with an exact index-loop equivalent are accepted. `map`, `filter` and `take_while` are rejected rather than partially honoured: silently dropping an adaptor would change which elements the loop visits. `zip` stops at the shorter side, as Rust's does — that is a test, not an assumption (`tests/cases/023`). ### Borrowed slices are views, not copies `&[T]` is a borrow. Nim's experimental view types model exactly that, including returning one from a proc: writing through the returned view is visible in the original buffer. That was probed against Nim 2.2.4 before being relied on, because copying into a `seq` would print the right bytes while silently changing aliasing. Nim does allow a view inside an object and inside an object *field* — both probed, both preserving aliasing — so `Result<&[u8], E>` and `HexDisplay<'a>(&'a [u8])` both work. (An earlier version of this document claimed otherwise; that was wrong.) Two real constraints remain. Nim will not let a `let` borrow out of a local, so `.unwrap()`/`.expect()` on a `Result` holding a view is expanded inline and the binding becomes an alias — a view is a reference, so there is nothing to materialise, and the substituted expression is a plain field access that re-evaluates nothing. And `s.get(a..b)` is an `Option` of a view whose *validity* is what matters: the view and its condition travel together through `ok_or` until a `?` or `unwrap` resolves them into a bounds check plus a binding. Keeping such an `Option` in a variable is rejected with a message saying so. A `let` binding a borrow keeps the view rather than copying into a `seq`: `let res = encode(..)?` names the caller's buffer, and copying would print the right bytes while silently breaking the aliasing. ### Closures and `unsafe` `unsafe` is a permission marker, not a semantic change: it does not alter what the enclosed operations mean. So the block is transparent, and every operation inside still goes through the ordinary lowering and is still rejected if it has no faithful mapping. `unsafe fn` lowers like any other proc. A closure becomes a Nim anonymous proc. Nim's closures capture by reference, as Rust's non-`move` closures do; a `move` closure captures by value, which is a different thing, so it is rejected rather than lowered to the same construct. `impl Fn(A) -> B` is left at Nim's default calling convention, which accepts both a plain top-level proc and a capturing closure — as Rust's `impl Fn` does. `.map`/`.and_then` over an `Option`/`Result` are expanded inline with the closure's parameter aliased to the payload, rather than handed to a generic proc. That keeps the whole thing an expression and keeps a view a view. `&str` is a borrowed view of someone else's bytes, so it maps to `openArray[char]`, not to an owned `string`. Nim accepts a `string` argument for an `openArray[char]` parameter, so a literal still passes straight through. `from_utf8_unchecked` reinterprets a byte view as a character view over the same memory — no copy, no validation, and writes through the original are visible, as in Rust. ### Modules Rust keeps `lower::decode` and `mixed::decode` apart by module; flattening into one Nim module would merge them — they are *different functions*. So the first input is the crate root and each later one is a module named by its file stem, items are emitted as `_`, and a call resolves through an explicit qualifier, then the current module, then what `use` brought into scope, then the root. ### Generics Rust type parameters become Nim's. Nim instantiates a generic structurally at the call site much as Rust does, so `fn f(x: T) -> T` has a direct target in `proc f[T](x: T): T` and no monomorphisation pass is needed. **Trait bounds and `where` clauses are dropped.** That is sound in the direction that matters: an operation the bound permitted either exists for the instantiated type or is a compile error at that instantiation site. Dropping a bound cannot make an accepted program mean something different — it only makes rustnim accept some programs rustc would reject, which does not matter when the input is known-good Rust. (Where it *would* matter is bound-directed method selection, e.g. blanket impls choosing between candidates. We do not model trait resolution at all, so such a program is rejected elsewhere.) Const generic parameters have no Nim equivalent and are still rejected. Two things need more than a rename. Nim cannot infer an object's generic parameters from a constructor's field values, so `Pair { a: 1, b: 2 }` is emitted as `Pair[int32](...)` using the expected type — and a generic enum's unit variant (`Holder::Empty`) likewise. And a binding's annotation cannot name a parameter Nim is still inferring, so call sites run a small unifier: the callee's declared parameter types are matched against the actual argument types to bind `T`, and the result is substituted into the return type. ### Declaration order Rust has no declaration-before-use rule and Nim does, so every proc is forward-declared between the type definitions and the bodies. Reordering the input instead would not handle mutual recursion. ### Type propagation is load-bearing Rust infers an unsuffixed integer literal's type from context and falls back to `i32`; Nim falls back to 64-bit `int`. So `lower.rs` threads an *expected type* down through every expression — into `let` annotations, call arguments, `match` patterns, compound assignments and both operands of a binary — and annotates every binding it emits. Without that, `let x: u8 = 200; x + 100` means two different things in the two languages. With it, a width the lowering gets wrong becomes a Nim compile error (a loud failure, reported by the runner) rather than a wrong answer. ## Mapping decisions made so far - **Integers**: exact width. `i32`→`int32`, `usize`→`uint`, etc. `i128`/`u128` rejected. - **Indexing**: both 0-based. Direct. - **`&T`** → plain value. **`&mut T`** → `var T` parameter. - **`&[T]`** → `openArray[T]` in parameter position, `seq[T]` when owned. `Nim::owned()` performs that conversion. - **Ownership/borrowck**: ignored. Nim is GC'd; for safe Rust this is sound. - **`Option`/`Result`** → object variants in the prelude. - **`match`** → Nim `case` where the arms are simple, `if`/`elif` when arms have guards or bindings. - **Rust's expression-orientation** maps well: Nim `if`/`case` are expressions too, and a proc's trailing expression is its return value. ### Settled empirically (Nim 2.2.4 vs rustc 1.98.1, both run) 1. **Nim's `shr` on a signed integer is arithmetic**, matching Rust. `int16(-256) shr 8` = `-1` in Nim; `(-256i16) >> 8` = `-1` in Rust. `base16ct`'s decoder depends on this, so it maps directly with no helper. 2. **Nim's fixed-width unsigned arithmetic wraps silently**, matching Rust's `wrapping_*`. `uint8(200) + 100` = `44` in Nim; `200u8.wrapping_add(100)` = `44` in Rust. So `wrapping_add` on an unsigned type is just `+`. 3. **We model rustc's debug profile.** Rust debug builds panic on signed integer overflow; Nim's default build raises `OverflowDefect` on it. Those are the matching pair, so the runner invokes `rustc` without `-O` and `nim c` with its defaults, and `tests/cases/016` pins the behaviour. A Rust panic exits 101 where a Nim Defect exits 1, so every generated module ends with a handler that maps one to the other — otherwise the runner's exit-status comparison would be vacuous. `wrapping_*` is therefore an explicit operation on both sides: unsigned maps to the bare operator (item 2), signed is routed through the unsigned view of the same width. 4. **`char` round-trips.** Rust `char` → Nim `Rune`, confirmed for ASCII and non-ASCII scalars in both `{}` and `{:?}`, and across `as u32` (`tests/cases/014`). 5. **Nim's integer conversion `T(x)` truncates; it does not range-check.** `uint8(511'u16)` is `255`, `uint8(300'i32)` is `44`, `uint8(-1'i32)` is `255` — the same answers as `cast[uint8]`. An earlier version of this document asserted that `T(x)` range-checks, and used that to justify `cast`. The conclusion stands — `cast` is the clearer spelling of "truncate" — but the stated reason was wrong, and it had been assumed rather than probed. Found by sabotaging the cast lowering and watching the exhaustive proof *not* fail, which is what a sabotage test is for. ### Still open 6. Const generic parameters, `move` closures, closure bodies with statements, trait objects and `macro_rules!` definitions are rejected with a reason. A `trait` declaration lowers to nothing — trait resolution is not modelled — but one giving a method a *default body* is rejected, since that body is code with no impl to be emitted into. Lifetime parameters are *not* a rejection: they carry no runtime meaning and Nim is GC'd, so `fn encode<'a>(..)` lowers fine. 7. `saturating_*` and `checked_*` are implemented, detecting overflow on the unsigned view of the same width rather than with a range check that would itself trap. `wrapping_*`, `overflowing_*` and `strict_*` are not all covered: only add, sub and mul have the saturating and checked forms. 8. Float formatting matches Rust for ordinary values and for `inf`/`NaN`, but the exponent-form thresholds have only been checked at `1e21`. 9. Functions are scoped by module now, but *types* are still global: two modules declaring the same type name would collide. Relatedly, a crate's own `type Result` is told apart from the builtin `Result` by arity, which is not how Rust resolves it. 10. Host `#[cfg]` predicates — `unix`, `windows`, `target_os`, `target_arch`, `target_family`, `target_pointer_width`, `target_endian`, `target_has_atomic` — are evaluated against the machine, since the generated Nim is compiled for it. That makes the output host-shaped: a crate branching on platform has had that branch decided at transpile time. `doc`/`doctest`/`miri` are false. A custom or build-script `cfg` (`crossbeam_loom`, `target_has_atomic`) has no value we could know and is rejected. 11. Associated types (`impl Iterator { type Item = .. }`) and `mod` directories (`specialized/mod.rs`) are not implemented. 12. `String::from_utf8_unchecked` copies, because Nim's `string` is an owned value. Rust's consumes the `Vec` without copying. Observably the same from the caller, but it is a copy where Rust has none. ## A second crate: does this generalise, or is it fitted to `base16ct`? `base16ct` is the crate this was built toward, so passing it proves less than it looks. `adler2` 2.0.1 was picked as a deliberately different shape — a stateful struct with methods, operator-overload trait impls, a hand-unrolled four-lane inner loop — and it now works: `tests/cases/029-adler2-crate/` transpiles `algo.rs` byte-for-byte as published, with `lib.rs`'s items and a driver, and its checksums are byte-identical to rustc's across every single byte, every length to 600 (crossing the 4-byte unrolling boundary and the 5552-chunk path), and 144 incremental-write splits. It needed real work, which is the honest part of the answer. Ten features: trait impls generalised beyond formatting and `From` (any trait's methods become procs on the type, with the operator traits wired into `+=`/`+` dispatch), `Self`, `Type::method()` static calls, `u32::from` between primitives, tuple-destructuring `let`, `split_at`, iterators bound to variables and `.remainder()`, `[0; 4]` as an array rather than a `seq`, and the bare `#[cfg]` flags. It also caught a **regression I had introduced**: the three-phase emission added for forward declarations was silently dropping `const` items declared *inside* a function body. `base16ct` has none, so 33 passing cases said nothing about it. ### What the other crates did Run without fixing anything, to see where the wall is rather than to move it: | crate | outcome | |---|---| | `adler2` 2.0.1 | **works**, byte-identical | | `siphasher` 1.0.1 | rejected: `u128` | | `rustc-hash` 2.1.1 | rejected: `u128` | | `hex` 0.4.3 | rejected: `impl Iterator` needs an associated type | | `crc32fast` 1.5.0 | rejected: directory modules (`specialized/mod.rs`), then SIMD intrinsics | Two of the five stop at `u128`, which is the founding rule doing its job rather than a gap: they are told they cannot be translated instead of being handed a silently truncated hasher. The other two are honest missing features — associated types, and `mod` directories. ## How far off is a crate like `libcosmic`? Measured, not guessed. Running rustnim over `libcosmic`'s own `src/`: ``` 0 of 164 files produce any translation 50,633 lines, 112 direct dependencies ``` with 66 generic-parameter blockers, 20 trait objects, 51 `async` uses, 235 `where` clauses, 73 associated types and 1,104 lifetime annotations. Those are not features the crate happens to use; they are its architecture. `libcosmic` is a north star, not a next step. ### The blocker survey, and what it says about roadmaps 400 crates from the local registry (under 4,000 lines each), all their module files passed together, first blocker recorded: | blocker | start | +host-`cfg` | +generics | +assoc types | |---|---|---|---|---| | unevaluable `#[cfg]` | 124 | 34 | 34 | 34 | | generic type parameter | 69 | 91 | **1** | 1 | | associated types in an `impl` | 54 | 63 | 87 | **2** | | unsupported type | 20 | 29 | 60 | 71 | | trait object | 17 | 25 | 30 | 35 | | macro definition | 21 | 24 | 25 | 32 | | raw pointer | 12 | 22 | 24 | 28 | | **crates fully transpiled** | **2** | **2** | **2** | **3** | This has now happened twice. Evaluating the host `#[cfg]` predicates cleared 90 of 124 blockers and moved the fully-working count by zero. Generics then cleared 90 of 91 and moved it by zero again. Every crate each unblocked simply hit its next blocker. That is the shape of the problem: blockers are **deep, not wide**. A frequency ranking of *first* blockers is not a roadmap — it says which feature is most often first, not which one finishes a crate. `base16ct`, `adler2` and `cosmic-theme`'s spacing model work because their whole stack was ground through, one blocker at a time. ### Does the stack have a bottom? If the stacks are drawn from one shared finite pool of language features, then clearing the pool clears every stack at once, and the flat counts above are just what progress looks like before a step change. That is a real possibility and it is testable, so it was tested. Sampling every *file* of the 400 crates rather than only each crate's first blocker — 1,766 blocker observations: | | count | bounded? | |---|---|---| | distinct normalised blocker kinds | **63** | **yes — this is the language-feature pool** | | distinct unsupported std methods | 80 | no — this is `std`'s surface | | distinct unknown functions | 75 | no — these are calls into dependencies | | distinct unsupported macros | 13 | no | So the answer is *both*, split by class: - **Language features do bottom out.** 63 distinct kinds, and each one cleared is cleared for every crate forever. This part is finite and the step-change intuition is correct for it. Associated types produced the first net gain (2 → 3), which is what that dynamic looks like starting. - **The API surface does not.** 80 distinct `std` methods appeared in 1,766 samples of *small* crates; `std` has thousands of items, and each needs a verified Nim equivalent rather than a guess. That is enumerable but it has no bottom you reach by clearing features. - **Dependencies are not a pool at all.** 315 of the 400 crates depend on other crates, which have to be transpiled too, recursively, until the graph ends at `libc`, proc macros or SIMD intrinsics — which it does, and those do not translate. And among the 85 dependency-free crates, 1 of 84 currently transpiles in full. So even with dependencies removed from the picture, features alone are not the only remaining gate. So the ranking above is not a roadmap — it says which feature is most often *first*, which is not the same as which feature finishes a crate. The only honest way to add a crate is to pick it and clear its stack, as was done twice. Generics and associated types are now done. By frequency the next are unsupported types (71), trait objects (35) and `macro_rules!` definitions (32) — but see above before treating that as a plan. ### `cosmic-theme`: what was reachable `tests/cases/032-cosmic-theme-spacing/` transpiles `corner.rs`, `spacing.rs` and `layout.rs` from `cosmic-theme` 1.0.0 — the spacing scale, corner radii and density model a COSMIC-native UI needs to match the desktop — with output byte-identical to rustc's, including the `Density`/`Spacing` and `Roundness`/`CornerRadii` round trips. Those files are the crate's own, with one mechanical change recorded here: the `use serde::{Deserialize, Serialize}` line and the `Serialize, Deserialize` entries in two `derive` lists were removed, because the oracle is plain `rustc` with no dependencies available. Nothing else was touched; rustnim ignores both anyway. The rest of `cosmic-theme` — `theme.rs` (1,830 lines), `color.rs`, `cosmic_palette.rs`, `derivation.rs`, `steps.rs`, `composite.rs` — is colour work built on `palette` (40,874 lines across 122 files, plus a proc-macro crate). `mode.rs` needs `cosmic-config` and its derive macro. Those are dependency walls, not language gaps. ## `bitflags!`, and why it is lowered rather than expanded `bitflags` is the most-depended-on translatable crate in `libcosmic`'s resolved tree — 79 of its 741 crates — so it is the highest-leverage target there. It is also 26 `macro_rules!` definitions across five files, which is the one thing the lowering cannot represent. Expanding the macro does not rescue this. `RUSTC_BOOTSTRAP=1 cargo rustc -- -Zunpretty=expanded` on a single-flag user produces 869 lines that still call `bitflags::{Bits, Flag, Flags, iter::Iter, iter::IterNames, parser::from_str, parser::to_writer}` — items defined by those same macros. The chain does not end in code we could lower. What the macro *means*, though, is small and stable: a newtype over an integer with named constants and set operations. So `src/macros.rs` parses the invocation and the lowering emits that directly. This is a deliberate exception to "a macro whose expansion is not known is rejected", and the argument is that the expansion *is* known here — it is documented, stable, and now pinned by a test. `tests/cases/034-bitflags.rs` is that test, and it is unusual: `//@ extern: bitflags` makes the **oracle** compile against the real crate while rustnim gets no such crate. rustnim has to reproduce bitflags' behaviour without it, and the outputs are compared byte for byte. Two behaviours it pins that a reimplementation would get wrong: - `!x` is complemented and then **masked to `all()`** — `!(READ|WRITE)` is `EXEC`, not `0xFFFFFFFC`. - `from_bits` returns `None` for any bit outside `all()`; `from_bits_truncate` masks instead. plus the `Debug` spelling, which is `Perms(READ | WRITE)` and `Perms(0x0)`. The risk this carries is version drift: a future `bitflags` could change what the macro generates, and the shim would not know. The test is what would catch it, which is why it links the real crate rather than a copy of its documentation. ## `log`, on the same argument `log` is second by dependents in `libcosmic`'s tree (61 of 741), and it has the same shape as `bitflags`: `src/macros.rs` is 20 `macro_rules!`, and that is what the dependents use. `src/lib.rs` is the facade — `Level`, `LevelFilter`, the `Log` trait behind a `&'static dyn`, atomics and `set_logger`. So the macros are lowered directly, against behaviour pinned from the real crate by `tests/cases/035-log.rs` (again `//@ extern: log`, so the oracle links it and rustnim does not). What that test pins: - `max_level()` starts at `Off`. - `log_enabled!` is **false even after `set_max_level`** when no logger is installed, because the facade consults the logger as well as the level. - `Display` for a level is upper-case (`WARN`), `Debug` is not (`Warn`). - `Level::Error as usize` is 1 through `Trace` as 5; `LevelFilter::Off` is 0. A record's arguments are not evaluated when the level is disabled, so the lowering emits `if rsLogEnabled(l): rsLog(l, ..)` rather than computing the message first. **The boundary is deliberate: rustnim models log's *emitting* side, not its installing side.** A transpiled library's `info!` calls work and, with no logger, do nothing — which is exactly Rust's behaviour. Installing a logger is an application's job and is done from Nim: ```nim rsLogSetLogger(proc (level: RsLogLevel, target, msg: string) = echo "[", rsDisplay(level), "] ", msg) rsLogMaxLevel = int(rsLvlTrace) ``` Modelling `impl Log` instead would mean reproducing `Record` and `Metadata`, which is more shim surface for something a Nim application would not write in Rust anyway. The facade's types are emitted as `RsLogLevel` and `RsLogFilter`, not `Level` and `LevelFilter`. The first attempt used Rust's names and broke six existing cases, because a crate's own `Error` type and an enum field named `Error` cannot coexist in one Nim module. ## `libc`: not transpiled, and should not be `libc` is third by dependents in `libcosmic`'s tree (60 of 741), but it is not a crate to translate. Measured: ``` 129,594 lines across 387 files 54,544 `pub const` 7,660 `pub fn`, nearly all inside `extern "C"` blocks -- declarations 1,926 `pub type` 121 actual function bodies in the entire crate ``` There is essentially no code in it. It is a set of declarations binding to the platform's C library, and **Nim reaches those same symbols natively** — the symbols are the same objects, not two implementations of one idea. So what was built is the general capability instead: an `extern "C"` block lowers to Nim `importc` declarations. Both are statements *about* a symbol someone else defines, and both are bound by the C ABI, so the two declarations describe one symbol rather than one being a translation of the other. Raw pointers map too (`*mut T` to `ptr T`), which cleared all 38 raw-pointer blockers in the survey. ### `const` matters at the C level even though it does not at the Rust one Rust emits no C prototype; Nim emits a real one. So declaring `strlen` as taking `*const u8` produces: ``` error: conflicting types for 'strlen'; have 'NU(char *)' note: previous declaration with type 'size_t(const char *)' ``` which is the C compiler catching a declaration that does not match the symbol — a loud failure, and a better outcome than Rust's silence. To make honest declarations expressible, `*const T` maps to a generated const-qualified alias: ```nim type RsConstPtrcchar* {.importc: "const char *", nodecl.} = distinct pointer proc strlen*(s: RsConstPtrcchar): uint {.importc: "strlen", cdecl.} ``` one per element type actually used. Nim's generic form (`importc: "const $1*"`) was tried first and does not work in 2.2.4. A `*const T` whose C spelling we do not know is rejected rather than declared without the `const`. `tests/cases/036-extern-c.rs` calls `abs`, `labs`, `strlen` and `atoi` through this path, byte-identical to rustc. ## Proof of byte-identity for `base16ct` [`PROOF.md`](PROOF.md) sets out what is actually established: exhaustive agreement over every two-byte decode input (65,536), every two-byte encode input (65,536), every single byte through `encode_str` and `HexDisplay`, and every length to 128 — plus a compositional argument extending those to inputs of any length, and 20,000 pseudorandom multi-chunk cases attacking the one step in that argument that is inspection rather than enumeration. Run it with `cargo test --test proof`. It is explicit about the difference between the exhaustive parts and the sampled ones. ## Testing: differential, not golden The bar is **behavioural equivalence with rustc**, not that the output looks plausible. For each case in `tests/cases/`: ``` rustc case.rs && ./case > expected rustnim case.rs -o case.nim && nim c -r case.nim > actual diff expected actual ``` A case only counts as passing when both binaries build *and* produce identical stdout *and* exit with the same status. `tests/differential.rs` implements this, and checks each stage separately so a failure says where it went wrong: `rustnim`, `rustc`, `nim`, or `diff`. Three guards exist specifically because of how the other transpiler failed: - `rustnim` exiting 0 while writing **no output file** is a failure. - `rustnim` exiting 0 while writing an **empty output file** is a failure. - An **empty corpus** is a failure, so the runner cannot pass by finding nothing to do. All three have been verified by deliberately breaking the transpiler and confirming the runner goes red. Cases carry directives in leading `//@` comments: | directive | meaning | |---|---| | `//@ reject: ` | `rustnim` must *fail*, with this in its message | | `//@ skip: ` | not run; reported as skipped | | `//@ args: ` | passed to both binaries | | `//@ stdin: ` | fed to both binaries | | `//@ cfg: feature=` | passed to rustnim, and to rustc as `--cfg feature=""` | | `//@ extern: ` | the **oracle** links this crate; rustnim does not get it | `reject` cases are how the "fail loudly" rule is tested rather than merely stated: `900`–`904` pin the rejections of `i128`, an unmapped standard-library method, a float→int cast, an unimplemented format spec, and a closure. Run one case with `RUSTNIM_CASE=005 cargo test --test differential -- --nocapture`. Nim is found at `.nim-toolchain/bin/nim` in the repository root or any parent, or via `RUSTNIM_NIM`. ## Toolchain - `rustc` / `cargo` 1.98.1 — system. - Nim 2.2.4 — vendored at `.nim-toolchain/` (gitignored; downloaded from nim-lang.org, not installed system-wide). Binary: `.nim-toolchain/bin/nim`. ## Milestone 1 Transpile `base16ct` 1.0.0 — the crate the other transpiler failed on — and have its decoder produce byte-identical output to the Rust original. **Reached.** `tests/cases/026-base16ct-crate/` transpiles **every source file of base16ct 1.0.0** — `error.rs`, `lower.rs`, `upper.rs`, `mixed.rs` and `display.rs`, each byte-for-byte as published on crates.io, verified with `cmp` rather than by eye — together with `lib.rs`'s `decoded_len`, `encoded_len` and `decode_inner` verbatim. The `alloc` half is on, via `--cfg feature=alloc`. Output is byte-identical to rustc's: ``` lower ok abcd1234 len=4 decode: lower, upper, mixed upper-rej err InvalidEncoding ... upper correctly rejects lowercase oddlen err InvalidLength / invalid Base16 length <- Debug and Display encode ok 6162636431323334 len=8 encode, both cases encode_str ok abcd1234 len=8 closure over unsafe, borrowed &str Ok([171, 205, 18, 52]) decode_vec \ abcd1234 encode_string > the alloc half ABCD1234 abcd1234 HexDisplay {:X} {:x} ``` Everything lowers as written: `dst.get_mut(..decoded_len(src)?)`, `src.chunks_exact(2).zip(dst.iter_mut())`, `*dst = byte as u8`, the returned `&'a [u8]` view into the caller's buffer, `encode(src, dst).map(|r| unsafe { core::str::from_utf8_unchecked(r) })`, and `HexDisplay`'s `UpperHex` impl writing once per byte into the formatter. This is the crate whose six files the transpiler in `findings/` emitted empty output for, while exiting 0.