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 ?, generics, saturating_*/checked_*,
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.
How strong is "byte-identical"? PROOF.md answers that precisely:
exhaustive over every two-byte decode input, every two-byte encode input and
every single byte through encode_str/HexDisplay, plus a compositional
argument for longer inputs and 20,000 pseudorandom cases attacking it.
cargo test --test proof runs it — 151,463 cases, 9.1 MB of output, compared
byte for byte.
bitflags! and log
bitflags is the most-depended-on translatable crate in libcosmic's tree (79
of 741), and it is 26 macro_rules! definitions. Expanding the macro does not
help — the expansion still calls into the crate's own macro-defined runtime.
So the macro is lowered directly, and checked against the real crate: the test
links bitflags for the oracle only, and rustnim has to reproduce its
behaviour without it, byte for byte.
log (61 dependents) is the same shape — 20 macro_rules! — and gets the
same treatment. rustnim models log's emitting side: a transpiled library's
info! calls work and, with no logger installed, do nothing, exactly as in
Rust. Installing a logger is done from Nim with rsLogSetLogger.
Does it generalise?
base16ct is the crate this was built toward, so a second one was tried.
adler2 2.0.1 — a stateful checksum with operator-overload trait impls and a
hand-unrolled loop, structurally nothing like base16ct — works, and is
byte-identical across every single byte, every length to 600, and 144
incremental-write splits. It needed ten new features, and it caught a
regression that 33 passing cases had not.
cosmic-theme's spacing scale, corner radii and density model also transpile
byte-identically — the part of it that is not built on palette.
A 400-crate survey says something worth knowing: clearing the single most
common blocker has twice moved the number of fully-working crates by zero,
because each unblocked crate just hits its next one. Blockers are deep, not
wide, and a frequency ranking of first blockers is not a roadmap.
DESIGN.md has the tables.
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 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
|