nandi/rustnimpublic Fork 0
87c9cc836cadb6a4453f18af3eafbe61e2c4f20c
Commits
Clone
git clone https://git.rickub.com/nandi/rustnim.git
git clone ssh://git@rickub.com/nandi/rustnim.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Settle signed-shr and unsigned-wrap semantics against both compilers 87c9cc8 · on 87c9cc836cadb6a4453f18af3eafbe61e2c4f20c · nandi · 6h ago
DESIGN.md · 123 lines · 5.0 KBmarkdown
Blame HistoryOpen raw

rustnim — a Rust → Nim transpiler

Status

Early scaffold. src/ty.rs (type mapping) is written. src/main.rs is
still cargo's default hello-world. Nothing transpiles yet.

Why this exists

We tried 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/
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 —

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 todo
src/fmt.rs println!/format! format-string handling todo
src/prelude.nim Option/Result/panic runtime, embedded in output todo
src/main.rs CLI: rustnim <in.rs> -o <out.nim> todo

Mapping decisions made so far

  • Integers: exact width. i32int32, usizeuint, etc. i128/u128
    rejected.
  • Indexing: both 0-based. Direct.
  • &T → plain value. &mut Tvar 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 +.

Still open

  1. Rust debug builds panic on signed integer overflow; release builds wrap.
    Nim raises OverflowDefect on signed overflow. Item 2 settles the unsigned
    case only. Decide which Rust profile we model, state it in the README, and
    map checked_*/saturating_* explicitly.
  2. char: Rust char is a Unicode scalar; mapped to Rune, which needs
    std/unicode. Confirm round-tripping.

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. tests/ currently has no runner — writing it is the next step, and it
should come before any more of the lowering, so that progress is measured
rather than asserted.

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. It is a
good target: 613 lines, no_std, no dependencies, and its constant-time
integer arithmetic is exactly the kind of thing a sloppy transpiler gets wrong.

  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
# rustnim — a Rust → Nim transpiler

## Status

**Early scaffold.** `src/ty.rs` (type mapping) is written. `src/main.rs` is
still cargo's default hello-world. Nothing transpiles yet.

## 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 | todo |
| `src/fmt.rs` | `println!`/`format!` format-string handling | todo |
| `src/prelude.nim` | `Option`/`Result`/panic runtime, embedded in output | todo |
| `src/main.rs` | CLI: `rustnim <in.rs> -o <out.nim>` | todo |

## 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 `+`.

### Still open

3. Rust debug builds **panic** on signed integer overflow; release builds wrap.
   Nim raises `OverflowDefect` on signed overflow. Item 2 settles the *unsigned*
   case only. Decide which Rust profile we model, state it in the README, and
   map `checked_*`/`saturating_*` explicitly.
4. `char`: Rust `char` is a Unicode scalar; mapped to `Rune`, which needs
   `std/unicode`. Confirm round-tripping.

## 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. `tests/` currently has no runner — writing it is the next step, and it
should come before any more of the lowering, so that progress is measured
rather than asserted.

## 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. It is a
good target: 613 lines, `no_std`, no dependencies, and its constant-time
integer arithmetic is exactly the kind of thing a sloppy transpiler gets wrong.