rustnim
A Rust → Nim transpiler. It parses Rust with syn and emits Nim source that
behaves the same way — same stdout, same exit status — when both are built and
run.
The rule the whole thing is organised around: never approximate a semantic
you cannot represent. If a construct doesn't map, rustnim says so and exits
non-zero. It never writes a file it can't stand behind.
That rule comes from a real failure. We tried an existing transpiler that
advertises Rust support on the base16ct crate; it emitted an empty file and
exited 0. The write-up is in findings/.
Build and run
cargo build --release
./target/release/rustnim input.rs -o out.nim
nim c out.nim
Several inputs are concatenated into one Nim module, in the order given — Nim
has no per-file mod, so items are flattened and names must not collide:
rustnim src/lib.rs src/error.rs --cfg feature=alloc -o crate.nim
What works
Functions and impl methods, structs, enums (C-like and data-carrying),
Option/Result with ?, let/let mut, the integer and float operators at
exact widths, as casts, if/while/loop/for, match including binding
patterns, Vec/slices/arrays, type aliases, function-typed parameters
(impl Fn(A) -> B), multi-file input, #[cfg], and println!/format! with
{}, {:?}, {:x}, {:b}, positional and inline-named arguments, and
zero/space padding.
Also: the formatting trait impls (Display, Debug, LowerHex, UpperHex,
Binary, Octal) and From; and slice iterators — iter, iter_mut,
enumerate, zip, chunks_exact, chunks_exact_mut, windows — resolved
into a single index loop where each binding is an lvalue into the original
container, so *d = v through iter_mut() reaches the caller's slice.
Borrowed slices are views, not copies, including as return types.
Closures and unsafe blocks, &str as a borrowed view rather than an owned
copy, formatting impls whose fmt body writes repeatedly, and the
assert!/assert_eq!/debug_assert* family. Modules: pass the crate root first and each further file after it,
and items are scoped by module, so lower::decode and mixed::decode stay
distinct.
Rejected with a reason, rather than guessed at: i128/u128, generics,
move closures, trait impls other than the ones above, iterator adaptors with
no index-loop equivalent (map, filter, take_while), float→int casts, and
any standard-library method that isn't mapped.
base16ct
It goes through. tests/cases/026-base16ct-crate/ transpiles every source
file of base16ct 1.0.0 — error.rs, lower.rs, upper.rs, mixed.rs,
display.rs — byte-for-byte as published on crates.io, plus lib.rs's
decoded_len, encoded_len and decode_inner verbatim, with the alloc
half enabled. Decoding and encoding are byte-identical to rustc's, including
encode_str (a closure over an unsafe block returning a borrowed &str
view of the bytes just written) and HexDisplay, whose UpperHex impl writes
once per byte into the formatter.
That is the crate the transpiler in findings/ emitted an empty
file for, while exiting 0.
Tests
cargo test
The tests are differential, with rustc as the oracle. Each case in
tests/cases/ is compiled by rustc and run, transpiled and compiled by Nim and
run, and the two stdouts and exit statuses must match. Building anything less
than that — checking the Nim output "looks right", say — is how you end up
shipping the bug in findings/. rustnim exiting 0 with a missing or empty
output file is a hard failure, and so is an empty corpus.
rustc is invoked without -O so debug-profile overflow checks are on, which is
the matching pair for nim c's defaults.
Nim is found at .nim-toolchain/bin/nim in the repo root or any parent, or via
RUSTNIM_NIM. Run a single case with:
RUSTNIM_CASE=005 cargo test --test differential -- --nocapture
Layout
| file | role |
|---|---|
src/ty.rs |
Rust type → Nim type, exact widths, explicit rejections |
src/lower.rs |
items, statements, expressions → Nim |
src/fmt.rs |
println!/format! format strings |
src/prelude.nim |
Option/Result, panic, Display/Debug runtime |
tests/differential.rs |
the runner above |
DESIGN.md has the reasoning: how each construct is mapped, what
was settled by running both compilers rather than by reading docs, and what's
still open.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
|