| Scaffold rustnim: Rust->Nim transpiler, type mapping 1a218c2 nandi 8h ago | 1 | # rustnim — a Rust → Nim transpiler |
| 2 | |
| 3 | ## Status |
| 4 | |
| 5 | **Early scaffold.** `src/ty.rs` (type mapping) is written. `src/main.rs` is |
| 6 | still cargo's default hello-world. Nothing transpiles yet. |
| 7 | |
| 8 | ## Why this exists |
| 9 | |
| 10 | We tried [tarekwasfy01/Code-Transpiler](https://github.com/tarekwasfy01/Code-Transpiler), |
| 11 | which advertises `rust` as a source language, on the `base16ct` crate. It emits |
| 12 | empty files and exits 0. The full investigation is in [`findings/`](findings/) |
| 13 | and is published at |
| 14 | https://rickub.com/nandi/code-transpiler-rust-frontend-findings |
| 15 | |
| 16 | The decisive finding, and the reason this is a new project rather than a patch: |
| 17 | its Universal AST cannot represent Rust. `defaultSemanticTypeContract()` in |
| 18 | `internal/backend/semantic_program.go:85` is hardcoded to |
| 19 | |
| 20 | ``` |
| 21 | numeric: binary64, integer_width: unknown, truth: r_compatible, |
| 22 | ownership: unknown, index_base: 1 |
| 23 | ``` |
| 24 | |
| 25 | and `semantic_document.go:1014` *validates* that every contract equals exactly |
| 26 | that, while `typed_operation.go:46` rejects any value model that is not |
| 27 | `tagged_dynamic_binary64`. There is no integer width and no ownership in the |
| 28 | model at all. Code like `base16ct`'s constant-time decoder — |
| 29 | |
| 30 | ```rust |
| 31 | ret += (((0x2fi16 - byte) & (byte - 0x3a)) >> 8) & (byte - 47); |
| 32 | ``` |
| 33 | |
| 34 | — depends on exact 16-bit signed wrapping and arithmetic shift. Lowering that |
| 35 | into a 1-indexed dynamic float64 model produces silently wrong answers. So the |
| 36 | first rule of this project is the one that codebase broke: |
| 37 | |
| 38 | > **Never approximate a semantic you cannot represent. Fail loudly instead.** |
| 39 | |
| 40 | `src/ty.rs` already does this: `i128`/`u128` are rejected with a reason rather |
| 41 | than widened or truncated. |
| 42 | |
| 43 | ## Architecture |
| 44 | |
| 45 | ``` |
| 46 | Rust source ──syn──> syn AST ──lower──> Nim source ──nim c──> binary |
| 47 | ``` |
| 48 | |
| 49 | **The frontend is `syn`, deliberately.** Hand-rolling a Rust grammar is how the |
| 50 | other project went wrong; a correct parser is not the interesting part of this |
| 51 | problem. The interesting part is the lowering, which is where all the work goes. |
| 52 | |
| 53 | Planned modules: |
| 54 | |
| 55 | | file | role | state | |
| 56 | |---|---|---| |
| 57 | | `src/ty.rs` | Rust type → Nim type, exact widths, explicit rejections | written | |
| 58 | | `src/lower.rs` | items, statements, expressions → Nim | todo | |
| 59 | | `src/fmt.rs` | `println!`/`format!` format-string handling | todo | |
| 60 | | `src/prelude.nim` | `Option`/`Result`/panic runtime, embedded in output | todo | |
| 61 | | `src/main.rs` | CLI: `rustnim <in.rs> -o <out.nim>` | todo | |
| 62 | |
| 63 | ## Mapping decisions made so far |
| 64 | |
| 65 | - **Integers**: exact width. `i32`→`int32`, `usize`→`uint`, etc. `i128`/`u128` |
| 66 | rejected. |
| 67 | - **Indexing**: both 0-based. Direct. |
| 68 | - **`&T`** → plain value. **`&mut T`** → `var T` parameter. |
| 69 | - **`&[T]`** → `openArray[T]` in parameter position, `seq[T]` when owned. |
| 70 | `Nim::owned()` performs that conversion. |
| 71 | - **Ownership/borrowck**: ignored. Nim is GC'd; for safe Rust this is sound. |
| 72 | - **`Option`/`Result`** → object variants in the prelude. |
| 73 | - **`match`** → Nim `case` where the arms are simple, `if`/`elif` when arms have |
| 74 | guards or bindings. |
| 75 | - **Rust's expression-orientation** maps well: Nim `if`/`case` are expressions |
| 76 | too, and a proc's trailing expression is its return value. |
| 77 | |
| Settle signed-shr and unsigned-wrap semantics against both compilers 87c9cc8 nandi 8h ago | 78 | ### Settled empirically (Nim 2.2.4 vs rustc 1.98.1, both run) |
| 79 | |
| 80 | 1. **Nim's `shr` on a signed integer is arithmetic**, matching Rust. |
| 81 | `int16(-256) shr 8` = `-1` in Nim; `(-256i16) >> 8` = `-1` in Rust. |
| 82 | `base16ct`'s decoder depends on this, so it maps directly with no helper. |
| 83 | 2. **Nim's fixed-width unsigned arithmetic wraps silently**, matching Rust's |
| 84 | `wrapping_*`. `uint8(200) + 100` = `44` in Nim; `200u8.wrapping_add(100)` |
| 85 | = `44` in Rust. So `wrapping_add` on an unsigned type is just `+`. |
| 86 | |
| 87 | ### Still open |
| 88 | |
| 89 | 3. Rust debug builds **panic** on signed integer overflow; release builds wrap. |
| 90 | Nim raises `OverflowDefect` on signed overflow. Item 2 settles the *unsigned* |
| 91 | case only. Decide which Rust profile we model, state it in the README, and |
| 92 | map `checked_*`/`saturating_*` explicitly. |
| 93 | 4. `char`: Rust `char` is a Unicode scalar; mapped to `Rune`, which needs |
| Scaffold rustnim: Rust->Nim transpiler, type mapping 1a218c2 nandi 8h ago | 94 | `std/unicode`. Confirm round-tripping. |
| 95 | |
| 96 | ## Testing: differential, not golden |
| 97 | |
| 98 | The bar is **behavioural equivalence with rustc**, not that the output looks |
| 99 | plausible. For each case in `tests/cases/`: |
| 100 | |
| 101 | ``` |
| 102 | rustc case.rs && ./case > expected |
| 103 | rustnim case.rs -o case.nim && nim c -r case.nim > actual |
| 104 | diff expected actual |
| 105 | ``` |
| 106 | |
| 107 | A case only counts as passing when both binaries build *and* produce identical |
| 108 | stdout. `tests/` currently has no runner — writing it is the next step, and it |
| 109 | should come before any more of the lowering, so that progress is measured |
| 110 | rather than asserted. |
| 111 | |
| 112 | ## Toolchain |
| 113 | |
| 114 | - `rustc` / `cargo` 1.98.1 — system. |
| 115 | - Nim 2.2.4 — vendored at `.nim-toolchain/` (gitignored; downloaded from |
| 116 | nim-lang.org, not installed system-wide). Binary: `.nim-toolchain/bin/nim`. |
| 117 | |
| 118 | ## Milestone 1 |
| 119 | |
| 120 | Transpile `base16ct` 1.0.0 — the crate the other transpiler failed on — and |
| 121 | have its decoder produce byte-identical output to the Rust original. It is a |
| 122 | good target: 613 lines, `no_std`, no dependencies, and its constant-time |
| 123 | integer arithmetic is exactly the kind of thing a sloppy transpiler gets wrong. |